Skip to main content

gpui_cn/folder_picker/
source.rs

1//! Where a folder picker gets its folders: a trait any file system,
2//! remote machine, or API can implement, and the local disk as the
3//! default.
4
5use std::{cell::RefCell, collections::HashMap, rc::Rc};
6
7use gpui_kit::{App, AppContext as _, SharedString, Task};
8
9use crate::path_browser::{
10    CreateFolderError, Entry, ListError, Page, PageToken, PathStyle, Source, SourcePath,
11};
12use smol::stream::StreamExt as _;
13
14/// A folder in a listing.
15#[derive(Clone, Debug, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct FolderEntry {
18    name: SharedString,
19    hidden: bool,
20}
21
22impl FolderEntry {
23    /// A folder with a name. A name that starts with a dot is hidden.
24    pub fn new(name: impl Into<SharedString>) -> Self {
25        let name = name.into();
26        let hidden = name.starts_with('.');
27        Self { name, hidden }
28    }
29
30    /// Whether the folder is hidden, which sorts it after the others.
31    pub fn with_hidden(mut self, hidden: bool) -> Self {
32        self.hidden = hidden;
33        self
34    }
35
36    /// The folder's name, without a path.
37    pub fn name(&self) -> &SharedString {
38        &self.name
39    }
40
41    /// Whether the folder is hidden.
42    pub fn hidden(&self) -> bool {
43        self.hidden
44    }
45}
46
47/// One page of a listing: folders, and where the next page starts.
48#[derive(Clone, Debug, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct FolderPage {
51    entries: Vec<FolderEntry>,
52    next: Option<PageToken>,
53}
54
55impl FolderPage {
56    /// The last page of a listing, with `entries` in the order to show.
57    pub fn new(entries: impl IntoIterator<Item = FolderEntry>) -> Self {
58        Self {
59            entries: entries.into_iter().collect(),
60            next: None,
61        }
62    }
63
64    /// Says that more folders follow, from `token`. The picker shows a
65    /// "Load more" row and asks for that page when the user reaches it.
66    pub fn with_next(mut self, token: PageToken) -> Self {
67        self.next = Some(token);
68        self
69    }
70
71    /// The folders of the page.
72    pub fn entries(&self) -> &[FolderEntry] {
73        &self.entries
74    }
75
76    /// Where the next page starts, when there is one.
77    pub fn next(&self) -> Option<&PageToken> {
78        self.next.as_ref()
79    }
80}
81
82/// Where a [`FolderPicker`](super::FolderPicker) lists folders from: the
83/// local disk, an SSH session, a bucket, an API. The picker never waits
84/// for a source. It calls `list` and awaits the task, and it drops the
85/// task to cancel it.
86///
87/// A source lists one directory a page at a time. It should give entries
88/// in the order to show: the picker keeps that order and sorts nothing,
89/// so a source that pages must sort across its pages itself. Folders
90/// that repeat across pages, by name, show once.
91///
92/// ```
93/// use gpui_cn::{FolderEntry, FolderPage, FolderSource, ListError, PageToken, SourcePath};
94/// use gpui_kit::{App, AppContext as _, Task};
95///
96/// /// A source over a list of names, ten to a page.
97/// struct Names(Vec<String>);
98///
99/// impl FolderSource for Names {
100///     fn list(
101///         &self,
102///         _dir: &SourcePath,
103///         page: Option<PageToken>,
104///         cx: &mut App,
105///     ) -> Task<Result<FolderPage, ListError>> {
106///         let start = page.and_then(|t| t.as_str().parse().ok()).unwrap_or(0);
107///         let end = (start + 10).min(self.0.len());
108///         let names = self.0[start..end].to_vec();
109///         let more = (end < self.0.len()).then(|| PageToken::new(end.to_string()));
110///         // Any work goes on a task: a request, a query, a disk read.
111///         cx.background_spawn(async move {
112///             let page = FolderPage::new(names.into_iter().map(FolderEntry::new));
113///             Ok(match more {
114///                 Some(token) => page.with_next(token),
115///                 None => page,
116///             })
117///         })
118///     }
119/// }
120/// ```
121pub trait FolderSource: 'static {
122    /// Lists the folders of `dir`, from the start when `page` is `None`,
123    /// or else from the page `token` a previous page named. A folder whose
124    /// name is empty, `.` or `..`, or has a separator of the source's style
125    /// or a control character is dropped by the picker.
126    fn list(
127        &self,
128        dir: &SourcePath,
129        page: Option<PageToken>,
130        cx: &mut App,
131    ) -> Task<Result<FolderPage, ListError>>;
132
133    /// The source's home folder, which a leading `~` in the typed path
134    /// stands for and where a picker with no start of its own begins. The
135    /// default is `None`: a source with no home does not expand `~`, so a
136    /// typed `~` is an ordinary name, and the picker starts at the root.
137    /// A remote or virtual source gives its own, made with
138    /// [`path_style`](Self::path_style).
139    fn home(&self) -> Option<SourcePath> {
140        None
141    }
142
143    /// How the source writes its paths. The default is the style of the
144    /// machine the application runs on. A source for another machine
145    /// names its own: a Linux server is [`PathStyle::posix`] whatever
146    /// the client is.
147    fn path_style(&self) -> PathStyle {
148        PathStyle::host()
149    }
150
151    /// The value of the variable `name`, for `%NAME%` in a Windows-style
152    /// path that is pasted. The default knows none: a remote source never
153    /// reads the host's environment.
154    fn env(&self, name: &str) -> Option<String> {
155        let _ = name;
156        None
157    }
158
159    /// Whether the source can make folders. The default is `false`, which
160    /// hides the picker's "New folder" button.
161    fn can_create_folders(&self) -> bool {
162        false
163    }
164
165    /// Makes the folder `name` inside `parent` and answers with its path.
166    /// The picker has checked `name` already: it is not empty, has no
167    /// separator, and is not `.` or `..`. Answer
168    /// [`CreateFolderError::Exists`] when something has that name. The
169    /// picker lists the directory again afterward, so the new folder
170    /// shows in the order the source lists. The default answers
171    /// [`CreateFolderError::Unsupported`].
172    fn create_folder(
173        &self,
174        parent: &SourcePath,
175        name: &str,
176        cx: &mut App,
177    ) -> Task<Result<SourcePath, CreateFolderError>> {
178        let _ = (parent, name, cx);
179        Task::ready(Err(CreateFolderError::Unsupported))
180    }
181}
182
183/// The local file system, in one page. It reads on a background task,
184/// counts a symlink to a folder as a folder, and puts the visible folders
185/// first and the hidden ones after, each in alphabetical order without
186/// regard to case.
187#[derive(Clone, Copy, Debug, Default)]
188pub struct LocalFolders;
189
190impl FolderSource for LocalFolders {
191    /// The home directory of the user running the application.
192    fn home(&self) -> Option<SourcePath> {
193        std::env::home_dir().map(|home| PathStyle::host().path(home.to_string_lossy()))
194    }
195
196    fn env(&self, name: &str) -> Option<String> {
197        std::env::var(name).ok()
198    }
199
200    fn can_create_folders(&self) -> bool {
201        true
202    }
203
204    fn create_folder(
205        &self,
206        parent: &SourcePath,
207        name: &str,
208        cx: &mut App,
209    ) -> Task<Result<SourcePath, CreateFolderError>> {
210        let path = parent.join(name);
211        let made = path.to_path_buf();
212        cx.background_spawn(async move {
213            smol::fs::create_dir(&made).await?;
214            Ok(path)
215        })
216    }
217
218    fn list(
219        &self,
220        dir: &SourcePath,
221        _page: Option<PageToken>,
222        cx: &mut App,
223    ) -> Task<Result<FolderPage, ListError>> {
224        let dir = dir.to_path_buf();
225        cx.background_spawn(async move { Ok(list_local(dir).await?) })
226    }
227}
228
229async fn list_local(dir: std::path::PathBuf) -> std::io::Result<FolderPage> {
230    let mut read = smol::fs::read_dir(&dir).await?;
231    let mut folders = Vec::new();
232    while let Some(entry) = read.next().await {
233        let Ok(entry) = entry else { continue };
234        let is_folder = match entry.file_type().await {
235            Ok(kind) if kind.is_dir() => true,
236            Ok(kind) if kind.is_symlink() => smol::fs::metadata(entry.path())
237                .await
238                .is_ok_and(|meta| meta.is_dir()),
239            _ => false,
240        };
241        if is_folder {
242            folders.push(FolderEntry::new(
243                entry.file_name().to_string_lossy().into_owned(),
244            ));
245        }
246    }
247    let style = PathStyle::host();
248    folders.sort_by(|a, b| {
249        a.hidden()
250            .cmp(&b.hidden())
251            .then_with(|| style.compare_names(a.name(), b.name()))
252    });
253    Ok(FolderPage::new(folders))
254}
255
256impl Entry for FolderEntry {
257    fn name(&self) -> &SharedString {
258        &self.name
259    }
260
261    fn hidden(&self) -> bool {
262        self.hidden
263    }
264
265    fn is_folder(&self) -> bool {
266        true
267    }
268}
269
270/// A folder source seen as the browser's source.
271pub(super) struct Adapter(pub(super) Rc<dyn FolderSource>);
272
273impl Source<FolderEntry> for Adapter {
274    fn list(
275        &self,
276        dir: &SourcePath,
277        page: Option<PageToken>,
278        cx: &mut App,
279    ) -> Task<Result<Page<FolderEntry>, ListError>> {
280        let task = self.0.list(dir, page, cx);
281        cx.spawn(async move |_| {
282            let page = task.await?;
283            Ok(Page {
284                entries: page.entries,
285                next: page.next,
286            })
287        })
288    }
289
290    fn home(&self) -> Option<SourcePath> {
291        self.0.home()
292    }
293
294    fn style(&self) -> PathStyle {
295        self.0.path_style()
296    }
297
298    fn env(&self, name: &str) -> Option<String> {
299        self.0.env(name)
300    }
301
302    fn can_create_folders(&self) -> bool {
303        self.0.can_create_folders()
304    }
305
306    fn create_folder(
307        &self,
308        parent: &SourcePath,
309        name: &str,
310        cx: &mut App,
311    ) -> Task<Result<SourcePath, CreateFolderError>> {
312        self.0.create_folder(parent, name, cx)
313    }
314}
315
316/// Folders held in memory, for fixtures, demos, and tests. It lists a
317/// directory the application named, visible folders first and hidden ones
318/// after, fails with [`ListError::NotFound`] for any other, and makes
319/// folders: a new one shows in its parent's next listing and is an empty
320/// directory itself. Clones share the folders.
321#[derive(Clone, Debug, Default)]
322pub struct MemoryFolders {
323    style: PathStyle,
324    home: Option<SourcePath>,
325    dirs: Rc<RefCell<HashMap<SourcePath, Vec<FolderEntry>>>>,
326}
327
328impl MemoryFolders {
329    /// A file system with no directories.
330    pub fn new() -> Self {
331        Self::default()
332    }
333
334    /// Writes the paths in `style`. Set it before the home and the
335    /// directories. The default is the host's style.
336    pub fn with_style(mut self, style: PathStyle) -> Self {
337        self.style = style;
338        self
339    }
340
341    /// Sets the home folder, which `~` stands for.
342    pub fn with_home(mut self, home: impl AsRef<str>) -> Self {
343        self.home = Some(self.style.path(home));
344        self
345    }
346
347    /// Adds the directory `path` with the folders `names`.
348    pub fn with_dir<S: Into<SharedString>>(
349        self,
350        path: impl AsRef<str>,
351        names: impl IntoIterator<Item = S>,
352    ) -> Self {
353        let style = self.style;
354        let mut entries: Vec<FolderEntry> = names
355            .into_iter()
356            .map(|name| {
357                let name = name.into();
358                let hidden = style.is_hidden_name(&name);
359                FolderEntry::new(name).with_hidden(hidden)
360            })
361            .collect();
362        sort_folders(&mut entries, &style);
363        self.dirs.borrow_mut().insert(style.path(path), entries);
364        self
365    }
366}
367
368fn sort_folders(entries: &mut [FolderEntry], style: &PathStyle) {
369    entries.sort_by(|a, b| {
370        a.hidden()
371            .cmp(&b.hidden())
372            .then_with(|| style.compare_names(a.name(), b.name()))
373    });
374}
375
376impl FolderSource for MemoryFolders {
377    fn home(&self) -> Option<SourcePath> {
378        self.home.clone()
379    }
380
381    fn path_style(&self) -> PathStyle {
382        self.style
383    }
384
385    fn list(
386        &self,
387        dir: &SourcePath,
388        _page: Option<PageToken>,
389        cx: &mut App,
390    ) -> Task<Result<FolderPage, ListError>> {
391        let found = self.dirs.borrow().get(dir).cloned();
392        cx.background_spawn(async move { found.map(FolderPage::new).ok_or(ListError::NotFound) })
393    }
394
395    fn can_create_folders(&self) -> bool {
396        true
397    }
398
399    fn create_folder(
400        &self,
401        parent: &SourcePath,
402        name: &str,
403        _: &mut App,
404    ) -> Task<Result<SourcePath, CreateFolderError>> {
405        let mut dirs = self.dirs.borrow_mut();
406        let style = self.style;
407        let Some(entries) = dirs.get_mut(parent) else {
408            return Task::ready(Err(CreateFolderError::Other(
409                "That directory does not exist".into(),
410            )));
411        };
412        if entries
413            .iter()
414            .any(|entry| style.fold(entry.name()) == style.fold(name))
415        {
416            return Task::ready(Err(CreateFolderError::Exists));
417        }
418        let hidden = style.is_hidden_name(name);
419        entries.push(FolderEntry::new(name.to_string()).with_hidden(hidden));
420        sort_folders(entries, &style);
421        let path = parent.join(name);
422        dirs.insert(path.clone(), Vec::new());
423        Task::ready(Ok(path))
424    }
425}