1use 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#[derive(Clone, Debug, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct FolderEntry {
18 name: SharedString,
19 hidden: bool,
20}
21
22impl FolderEntry {
23 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 pub fn with_hidden(mut self, hidden: bool) -> Self {
32 self.hidden = hidden;
33 self
34 }
35
36 pub fn name(&self) -> &SharedString {
38 &self.name
39 }
40
41 pub fn hidden(&self) -> bool {
43 self.hidden
44 }
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct FolderPage {
51 entries: Vec<FolderEntry>,
52 next: Option<PageToken>,
53}
54
55impl FolderPage {
56 pub fn new(entries: impl IntoIterator<Item = FolderEntry>) -> Self {
58 Self {
59 entries: entries.into_iter().collect(),
60 next: None,
61 }
62 }
63
64 pub fn with_next(mut self, token: PageToken) -> Self {
67 self.next = Some(token);
68 self
69 }
70
71 pub fn entries(&self) -> &[FolderEntry] {
73 &self.entries
74 }
75
76 pub fn next(&self) -> Option<&PageToken> {
78 self.next.as_ref()
79 }
80}
81
82pub trait FolderSource: 'static {
122 fn list(
127 &self,
128 dir: &SourcePath,
129 page: Option<PageToken>,
130 cx: &mut App,
131 ) -> Task<Result<FolderPage, ListError>>;
132
133 fn home(&self) -> Option<SourcePath> {
140 None
141 }
142
143 fn path_style(&self) -> PathStyle {
148 PathStyle::host()
149 }
150
151 fn env(&self, name: &str) -> Option<String> {
155 let _ = name;
156 None
157 }
158
159 fn can_create_folders(&self) -> bool {
162 false
163 }
164
165 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#[derive(Clone, Copy, Debug, Default)]
188pub struct LocalFolders;
189
190impl FolderSource for LocalFolders {
191 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
270pub(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#[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 pub fn new() -> Self {
331 Self::default()
332 }
333
334 pub fn with_style(mut self, style: PathStyle) -> Self {
337 self.style = style;
338 self
339 }
340
341 pub fn with_home(mut self, home: impl AsRef<str>) -> Self {
343 self.home = Some(self.style.path(home));
344 self
345 }
346
347 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}