1use std::{cell::RefCell, collections::HashMap, rc::Rc, time::SystemTime};
6
7use gpui_kit::{App, AppContext as _, SharedString, Task};
8use smol::stream::StreamExt as _;
9
10use crate::path_browser::{CreateFolderError, Entry, ListError, PageToken, PathStyle, SourcePath};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum FileKind {
16 Folder,
18 File,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24#[non_exhaustive]
25pub struct FileEntry {
26 name: SharedString,
27 kind: FileKind,
28 hidden: bool,
29 size: Option<u64>,
30 modified: Option<SystemTime>,
31}
32
33impl FileEntry {
34 fn of(name: impl Into<SharedString>, kind: FileKind) -> Self {
35 let name = name.into();
36 let hidden = name.starts_with('.');
37 Self {
38 name,
39 kind,
40 hidden,
41 size: None,
42 modified: None,
43 }
44 }
45
46 pub fn file(name: impl Into<SharedString>) -> Self {
48 Self::of(name, FileKind::File)
49 }
50
51 pub fn folder(name: impl Into<SharedString>) -> Self {
53 Self::of(name, FileKind::Folder)
54 }
55
56 pub fn with_hidden(mut self, hidden: bool) -> Self {
58 self.hidden = hidden;
59 self
60 }
61
62 pub fn with_size(mut self, size: u64) -> Self {
64 self.size = Some(size);
65 self
66 }
67
68 pub fn with_modified(mut self, modified: SystemTime) -> Self {
70 self.modified = Some(modified);
71 self
72 }
73
74 pub fn name(&self) -> &SharedString {
76 &self.name
77 }
78
79 pub fn kind(&self) -> FileKind {
81 self.kind
82 }
83
84 pub fn is_folder(&self) -> bool {
86 self.kind == FileKind::Folder
87 }
88
89 pub fn hidden(&self) -> bool {
91 self.hidden
92 }
93
94 pub fn size(&self) -> Option<u64> {
96 self.size
97 }
98
99 pub fn modified(&self) -> Option<SystemTime> {
101 self.modified
102 }
103
104 pub fn extension(&self) -> Option<String> {
106 if self.is_folder() {
107 return None;
108 }
109 let (stem, extension) = self.name.rsplit_once('.')?;
110 (!stem.is_empty() && !extension.is_empty()).then(|| extension.to_lowercase())
111 }
112}
113
114impl Entry for FileEntry {
115 fn name(&self) -> &SharedString {
116 &self.name
117 }
118
119 fn hidden(&self) -> bool {
120 self.hidden
121 }
122
123 fn is_folder(&self) -> bool {
124 self.kind == FileKind::Folder
125 }
126}
127
128pub(crate) fn sort_entries(entries: &mut [FileEntry], style: &PathStyle) {
132 entries.sort_by(|a, b| {
133 b.is_folder()
134 .cmp(&a.is_folder())
135 .then_with(|| a.hidden.cmp(&b.hidden))
136 .then_with(|| style.compare_names(&a.name, &b.name))
137 });
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
142#[non_exhaustive]
143pub struct FilePage {
144 pub(super) entries: Vec<FileEntry>,
145 pub(super) next: Option<PageToken>,
146}
147
148impl FilePage {
149 pub fn new(entries: impl IntoIterator<Item = FileEntry>) -> Self {
151 Self {
152 entries: entries.into_iter().collect(),
153 next: None,
154 }
155 }
156
157 pub fn with_next(mut self, token: PageToken) -> Self {
160 self.next = Some(token);
161 self
162 }
163
164 pub fn entries(&self) -> &[FileEntry] {
166 &self.entries
167 }
168
169 pub fn next(&self) -> Option<&PageToken> {
171 self.next.as_ref()
172 }
173}
174
175pub trait FileSource: 'static {
210 fn list(
215 &self,
216 dir: &SourcePath,
217 page: Option<PageToken>,
218 cx: &mut App,
219 ) -> Task<Result<FilePage, ListError>>;
220
221 fn home(&self) -> Option<SourcePath> {
227 None
228 }
229
230 fn path_style(&self) -> PathStyle {
235 PathStyle::host()
236 }
237
238 fn env(&self, name: &str) -> Option<String> {
242 let _ = name;
243 None
244 }
245
246 fn can_create_folders(&self) -> bool {
249 false
250 }
251
252 fn create_folder(
260 &self,
261 parent: &SourcePath,
262 name: &str,
263 cx: &mut App,
264 ) -> Task<Result<SourcePath, CreateFolderError>> {
265 let _ = (parent, name, cx);
266 Task::ready(Err(CreateFolderError::Unsupported))
267 }
268}
269
270#[derive(Clone, Copy, Debug, Default)]
274pub struct LocalFiles;
275
276impl FileSource for LocalFiles {
277 fn home(&self) -> Option<SourcePath> {
279 std::env::home_dir().map(|home| PathStyle::host().path(home.to_string_lossy()))
280 }
281
282 fn env(&self, name: &str) -> Option<String> {
283 std::env::var(name).ok()
284 }
285
286 fn can_create_folders(&self) -> bool {
287 true
288 }
289
290 fn create_folder(
291 &self,
292 parent: &SourcePath,
293 name: &str,
294 cx: &mut App,
295 ) -> Task<Result<SourcePath, CreateFolderError>> {
296 let path = parent.join(name);
297 let made = path.to_path_buf();
298 cx.background_spawn(async move {
299 smol::fs::create_dir(&made).await?;
300 Ok(path)
301 })
302 }
303
304 fn list(
305 &self,
306 dir: &SourcePath,
307 _page: Option<PageToken>,
308 cx: &mut App,
309 ) -> Task<Result<FilePage, ListError>> {
310 let dir = dir.to_path_buf();
311 cx.background_spawn(async move { Ok(list_local(dir).await?) })
312 }
313}
314
315async fn list_local(dir: std::path::PathBuf) -> std::io::Result<FilePage> {
316 let mut read = smol::fs::read_dir(&dir).await?;
317 let mut entries = Vec::new();
318 while let Some(entry) = read.next().await {
319 let Ok(entry) = entry else { continue };
320 let name = entry.file_name().to_string_lossy().into_owned();
321 let Ok(kind) = entry.file_type().await else {
322 continue;
323 };
324 let meta = if kind.is_symlink() {
325 smol::fs::metadata(entry.path()).await.ok()
326 } else {
327 entry.metadata().await.ok()
328 };
329 let mut found = match &meta {
330 Some(meta) if meta.is_dir() => FileEntry::folder(name),
331 _ => FileEntry::file(name),
332 };
333 if let Some(meta) = meta {
334 if !meta.is_dir() {
335 found = found.with_size(meta.len());
336 }
337 if let Ok(modified) = meta.modified() {
338 found = found.with_modified(modified);
339 }
340 }
341 entries.push(found);
342 }
343 sort_entries(&mut entries, &PathStyle::host());
344 Ok(FilePage::new(entries))
345}
346
347#[derive(Clone, Debug, Default)]
353pub struct MemoryFiles {
354 style: PathStyle,
355 home: Option<SourcePath>,
356 dirs: Rc<RefCell<HashMap<SourcePath, Vec<FileEntry>>>>,
357}
358
359impl MemoryFiles {
360 pub fn new() -> Self {
362 Self::default()
363 }
364
365 pub fn with_style(mut self, style: PathStyle) -> Self {
368 self.style = style;
369 self
370 }
371
372 pub fn with_home(mut self, home: impl AsRef<str>) -> Self {
374 self.home = Some(self.style.path(home));
375 self
376 }
377
378 pub fn with_dir(
380 self,
381 path: impl AsRef<str>,
382 entries: impl IntoIterator<Item = FileEntry>,
383 ) -> Self {
384 let mut entries: Vec<FileEntry> = entries.into_iter().collect();
385 sort_entries(&mut entries, &self.style);
386 self.dirs
387 .borrow_mut()
388 .insert(self.style.path(path), entries);
389 self
390 }
391}
392
393impl FileSource for MemoryFiles {
394 fn home(&self) -> Option<SourcePath> {
395 self.home.clone()
396 }
397
398 fn path_style(&self) -> PathStyle {
399 self.style
400 }
401
402 fn list(
403 &self,
404 dir: &SourcePath,
405 _page: Option<PageToken>,
406 cx: &mut App,
407 ) -> Task<Result<FilePage, ListError>> {
408 let found = self.dirs.borrow().get(dir).cloned();
409 cx.background_spawn(async move { found.map(FilePage::new).ok_or(ListError::NotFound) })
410 }
411
412 fn can_create_folders(&self) -> bool {
413 true
414 }
415
416 fn create_folder(
417 &self,
418 parent: &SourcePath,
419 name: &str,
420 _: &mut App,
421 ) -> Task<Result<SourcePath, CreateFolderError>> {
422 let mut dirs = self.dirs.borrow_mut();
423 let style = self.style;
424 let Some(entries) = dirs.get_mut(parent) else {
425 return Task::ready(Err(CreateFolderError::Other(
426 "That directory does not exist".into(),
427 )));
428 };
429 if entries
430 .iter()
431 .any(|entry| style.fold(entry.name()) == style.fold(name))
432 {
433 return Task::ready(Err(CreateFolderError::Exists));
434 }
435 let hidden = style.is_hidden_name(name);
436 entries.push(FileEntry::folder(name.to_string()).with_hidden(hidden));
437 sort_entries(entries, &style);
438 let path = parent.join(name);
439 dirs.insert(path.clone(), Vec::new());
440 Task::ready(Ok(path))
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 fn names(entries: &[FileEntry]) -> Vec<&str> {
449 entries.iter().map(|e| e.name().as_ref()).collect()
450 }
451
452 #[test]
453 fn folders_come_first_then_files_each_by_name_without_case() {
454 let mut entries = vec![
455 FileEntry::file("beta.txt"),
456 FileEntry::folder("src"),
457 FileEntry::file("Alpha.txt"),
458 FileEntry::folder(".git"),
459 FileEntry::file(".env"),
460 FileEntry::folder("Docs"),
461 ];
462 sort_entries(&mut entries, &PathStyle::posix());
463 assert_eq!(
464 names(&entries),
465 ["Docs", "src", ".git", "Alpha.txt", "beta.txt", ".env"]
466 );
467 }
468
469 #[test]
470 fn a_leading_dot_hides_an_entry_and_can_be_overridden() {
471 assert!(FileEntry::file(".env").hidden());
472 assert!(!FileEntry::file(".env").with_hidden(false).hidden());
473 assert!(FileEntry::folder("src").with_hidden(true).hidden());
474 }
475
476 #[test]
477 fn the_extension_is_lower_case_and_needs_a_stem() {
478 assert_eq!(
479 FileEntry::file("Main.RS").extension().as_deref(),
480 Some("rs")
481 );
482 assert_eq!(
483 FileEntry::file("a.tar.gz").extension().as_deref(),
484 Some("gz")
485 );
486 assert_eq!(FileEntry::file(".env").extension(), None);
487 assert_eq!(FileEntry::file("Makefile").extension(), None);
488 assert_eq!(FileEntry::folder("v1.2").extension(), None);
489 }
490}