1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
//! Pickers over a machine that is not this one.
//!
//! [`FolderPicker`](crate::FolderPicker) and [`FilePicker`](crate::FilePicker)
//! never touch the disk themselves. They ask a source, [`FolderSource`](crate::FolderSource) or
//! [`FileSource`](crate::FileSource), and a source can be an SSH session, an SFTP server, an HTTP
//! API, a container, or a bucket. The crate has no network code and no login
//! code: the application brings both.
//!
//! # What a source decides
//!
//! - **The path style.** [`path_style`](crate::FileSource::path_style) names how the
//! machine writes paths: [`PathStyle::posix`](crate::PathStyle::posix) for a Linux server,
//! [`PathStyle::windows`](crate::PathStyle::windows) for a Windows one, whatever the host is. The
//! pickers parse, join, compare, sort and cache with that style, never
//! with the host's. A Windows style takes `/` and `\` when a path is typed
//! or pasted, cleans quotes, `file://` URLs and a lone drive, and ignores
//! case, so `C:/Users/Me` and `c:\users\me` are one folder. A POSIX style
//! keeps a backslash as a name character. Paths are a [`SourcePath`](crate::SourcePath), which
//! compares by that style and is only convertible to a host
//! `PathBuf` for a source that lists the local disk.
//! - **The home.** [`home`](crate::FileSource::home) is what a leading `~` means and
//! where the picker starts. The host's home is never used for a remote
//! source.
//! - **The environment.** [`env`](crate::FileSource::env) answers `%NAME%` in a
//! pasted Windows path, and by default knows nothing.
//! - **Folders.** [`can_create_folders`](crate::FileSource::can_create_folders) and
//! [`create_folder`](crate::FileSource::create_folder) turn on the "New folder"
//! button, which the application enables with `allow_new_folder`.
//!
//! # Time and failure
//!
//! Every call returns a task. The picker awaits it off the UI thread, drops
//! it to cancel (when the user moves to another folder, closes the dialog, or
//! switches source), and shows the list's own loading and error states in a
//! box that never changes size. A source answers a failed listing with a typed
//! [`ListError`](crate::ListError):
//!
//! - [`ListError::Disconnected`](crate::ListError::Disconnected) and [`ListError::Other`](crate::ListError::Other) show their message
//! with a Retry button, which lists the folder again.
//! - [`ListError::AuthRequired`](crate::ListError::AuthRequired) shows its message with a "Sign in" button,
//! which calls the handler the application gave with
//! [`on_auth_required`](crate::FilePickerState::on_auth_required). The
//! application signs the user in however it likes and calls
//! [`retry`](crate::FilePickerState::retry) when it is done.
//! - [`ListError::NotFound`](crate::ListError::NotFound) and [`ListError::PermissionDenied`](crate::ListError::PermissionDenied) show their
//! message and nothing else.
//!
//! # A remote source
//!
//! This source is a server held in memory. Its answers would come from a
//! request instead, awaited on a background task, and it pages, asks for a
//! login, and makes folders. Make it with [`PathStyle::posix`](crate::PathStyle::posix) or
//! [`PathStyle::windows`](crate::PathStyle::windows) and the picker behaves the same.
//!
//! ```
//! use std::{
//! cell::{Cell, RefCell},
//! collections::HashMap,
//! rc::Rc,
//! };
//!
//! use gpui_cn::{
//! CreateFolderError, FileEntry, FilePage, FilePickerState, FileSource, ListError, PageToken,
//! PathStyle, SourcePath,
//! };
//! use gpui_kit::{App, AppContext as _, Context, Task, Window};
//!
//! /// The folders of a machine, by directory.
//! #[derive(Clone)]
//! struct Server {
//! style: PathStyle,
//! home: &'static str,
//! signed_in: Rc<Cell<bool>>,
//! dirs: Rc<RefCell<HashMap<SourcePath, Vec<String>>>>,
//! }
//!
//! impl Server {
//! fn new(style: PathStyle, home: &'static str) -> Self {
//! let names = |names: &[&str]| names.iter().map(|name| name.to_string()).collect();
//! let dirs = HashMap::from([(style.path(home), names(&["code", "docs"]))]);
//! Self {
//! style,
//! home,
//! signed_in: Rc::default(),
//! dirs: Rc::new(RefCell::new(dirs)),
//! }
//! }
//!
//! fn posix() -> Self {
//! Self::new(PathStyle::posix(), "/home/me")
//! }
//!
//! fn windows() -> Self {
//! Self::new(PathStyle::windows(), "C:\\Users\\me")
//! }
//! }
//!
//! impl FileSource for Server {
//! fn path_style(&self) -> PathStyle {
//! self.style
//! }
//!
//! fn home(&self) -> Option<SourcePath> {
//! Some(self.style.path(self.home))
//! }
//!
//! fn list(
//! &self,
//! dir: &SourcePath,
//! page: Option<PageToken>,
//! cx: &mut App,
//! ) -> Task<Result<FilePage, ListError>> {
//! if !self.signed_in.get() {
//! return Task::ready(Err(ListError::AuthRequired(
//! "Sign in to the server to browse it".into(),
//! )));
//! }
//! let Some(names) = self.dirs.borrow().get(dir).cloned() else {
//! return Task::ready(Err(ListError::NotFound));
//! };
//! // Ten to a page: the token is whatever the source needs to resume.
//! let start = page.and_then(|t| t.as_str().parse().ok()).unwrap_or(0);
//! let end = (start + 10).min(names.len());
//! let next = (end < names.len()).then(|| PageToken::new(end.to_string()));
//! // A real source awaits its request here, on a background task.
//! cx.background_spawn(async move {
//! let page = FilePage::new(names[start..end].iter().cloned().map(FileEntry::folder));
//! Ok(match next {
//! Some(token) => page.with_next(token),
//! None => page,
//! })
//! })
//! }
//!
//! fn can_create_folders(&self) -> bool {
//! true
//! }
//!
//! fn create_folder(
//! &self,
//! parent: &SourcePath,
//! name: &str,
//! _: &mut App,
//! ) -> Task<Result<SourcePath, CreateFolderError>> {
//! let mut dirs = self.dirs.borrow_mut();
//! let style = self.style;
//! let Some(names) = dirs.get_mut(parent) else {
//! return Task::ready(Err(CreateFolderError::PermissionDenied));
//! };
//! // Names compare by the style's case rule.
//! if names.iter().any(|known| style.fold(known) == style.fold(name)) {
//! return Task::ready(Err(CreateFolderError::Exists));
//! }
//! names.push(name.to_string());
//! let path = parent.join(name);
//! dirs.insert(path.clone(), Vec::new());
//! Task::ready(Ok(path))
//! }
//! }
//!
//! /// A picker over the server, with a login the application owns.
//! fn picker(server: Server, window: &mut Window, cx: &mut Context<FilePickerState>) -> FilePickerState {
//! let signed_in = server.signed_in.clone();
//! let this = cx.weak_entity();
//! FilePickerState::new(window, cx)
//! .with_source(server, window, cx)
//! .allow_new_folder(true)
//! .on_auth_required(move |_, cx| {
//! // The application's own sign-in goes here, then the list again.
//! signed_in.set(true);
//! this.update(cx, |picker, cx| picker.retry(cx)).ok();
//! })
//! }
//!
//! let _ = (Server::posix(), Server::windows(), picker);
//! ```
//!
//! The same source serves a [`FolderPicker`](crate::FolderPicker) by
//! implementing [`FolderSource`](crate::FolderSource) with the same methods and
//! [`FolderPage`](crate::FolderPage) in place of [`FilePage`](crate::FilePage).
//!
//! The gallery's "Remote (simulated)" switch on the file picker and folder
//! picker pages runs a source like this one, with latency, a folder that
//! drops its connection once, and a folder that needs a login.