Skip to main content

gpui_cn/path_browser/
mod.rs

1//! What the folder picker and the file picker share: the path text and its
2//! `~`, the listings and their pages, the filter, the highlight, and the
3//! list. A picker owns a [`Browser`] and is its [`Host`]; the browser does
4//! the work and the host decides what a row means.
5
6mod create;
7pub(crate) mod path;
8mod style;
9pub(crate) mod view;
10
11use std::{
12    collections::{HashMap, HashSet},
13    rc::Rc,
14    sync::Arc,
15};
16
17use gpui_kit::{
18    AnyWindowHandle, App, AppContext as _, Context, Entity, KeyBinding, ListAlignment, ListState,
19    SharedString, Subscription, Task, Window, actions,
20    base::{
21        actions::{Confirm, SelectDown, SelectUp},
22        input::{InputEvent, InputState},
23    },
24};
25
26use crate::ActiveTheme as _;
27pub use create::CreateFolderError;
28use create::{is_plain_name, validate_folder_name};
29pub(crate) use path::Match;
30use path::{
31    all_visible, collapse_home, is_home_text, join_dir, parent_text, rank, resolve_descend,
32    resolve_dir, split_path,
33};
34pub use style::{ListError, PathStyle, SourcePath};
35
36actions!(
37    gpui_cn_path_picker,
38    [
39        /// Chooses what the picker shows, as its confirm button does.
40        Submit,
41        /// Opens the row that names a new folder.
42        NewFolder,
43        /// Closes the row that names a new folder without making it.
44        CancelNewFolder,
45        /// Makes the folder the open row names.
46        CommitNewFolder
47    ]
48);
49
50/// The shortcut that submits: Cmd+Enter on macOS, Ctrl+Enter elsewhere.
51const SUBMIT_KEY: &str = if cfg!(target_os = "macos") {
52    "cmd-enter"
53} else {
54    "ctrl-enter"
55};
56
57/// The shortcut that opens the new folder row: Cmd+Shift+N on macOS,
58/// Ctrl+Shift+N elsewhere.
59const NEW_FOLDER_KEY: &str = if cfg!(target_os = "macos") {
60    "cmd-shift-n"
61} else {
62    "ctrl-shift-n"
63};
64
65/// The key context of the row that names a new folder. Its Escape binding
66/// is deeper than the input's own, so the row closes before the dialog.
67pub(crate) const NEW_FOLDER_CONTEXT: &str = "GpuiCnNewFolder";
68
69/// The name the new folder row starts with, selected.
70const DEFAULT_FOLDER_NAME: &str = "Untitled folder";
71
72/// Binds the keys of the new folder row, once: Escape closes the row
73/// before it can close the dialog, and Enter makes the folder before it
74/// can enter a row of the list.
75pub(crate) fn init(cx: &mut App) {
76    cx.bind_keys([
77        KeyBinding::new("escape", CancelNewFolder, Some("GpuiCnNewFolder > Input")),
78        KeyBinding::new("enter", CommitNewFolder, Some("GpuiCnNewFolder > Input")),
79    ]);
80}
81
82/// Binds the keys a picker's dialog takes, in its key `context`: Up and
83/// Down move the highlight, Enter confirms the row, the submit shortcut
84/// chooses, and the new folder shortcut opens the row that names one.
85pub(crate) fn bind_keys(context: &'static str, cx: &mut App) {
86    cx.bind_keys([
87        KeyBinding::new(SUBMIT_KEY, Submit, Some(context)),
88        KeyBinding::new(NEW_FOLDER_KEY, NewFolder, Some(context)),
89        KeyBinding::new("up", SelectUp, Some(context)),
90        KeyBinding::new("down", SelectDown, Some(context)),
91        KeyBinding::new("enter", Confirm { secondary: false }, Some(context)),
92    ]);
93}
94
95/// An entry of a listing, as the browser needs to know it. It is public
96/// only so the listing types can name it; applications implement their own
97/// entry types, not this trait.
98pub trait Entry: Clone + Send + Sync + 'static {
99    /// The name, without a path. A name that is empty, is `.` or `..`, or
100    /// has a separator of the source's style or a control character is not
101    /// a name: the pickers drop the entry.
102    fn name(&self) -> &SharedString;
103    /// Whether the entry is hidden.
104    fn hidden(&self) -> bool;
105    /// Whether the entry is a folder, which a picker can go inside.
106    fn is_folder(&self) -> bool;
107}
108
109/// Where the next page of a listing starts. The source makes it and reads
110/// it back, so it can hold anything: an offset, a cursor, a continuation
111/// token. The picker only stores it and hands it back.
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct PageToken(Arc<str>);
114
115impl PageToken {
116    /// A token that holds `value`.
117    pub fn new(value: impl Into<Arc<str>>) -> Self {
118        Self(value.into())
119    }
120
121    /// The value the source put in the token.
122    pub fn as_str(&self) -> &str {
123        &self.0
124    }
125}
126
127/// One page of a listing.
128pub(crate) struct Page<E> {
129    pub(crate) entries: Vec<E>,
130    pub(crate) next: Option<PageToken>,
131}
132
133/// What a browser lists from.
134pub(crate) trait Source<E>: 'static {
135    fn list(
136        &self,
137        dir: &SourcePath,
138        page: Option<PageToken>,
139        cx: &mut App,
140    ) -> Task<Result<Page<E>, ListError>>;
141
142    fn home(&self) -> Option<SourcePath>;
143
144    fn style(&self) -> PathStyle;
145
146    fn env(&self, name: &str) -> Option<String>;
147
148    fn can_create_folders(&self) -> bool;
149
150    fn create_folder(
151        &self,
152        parent: &SourcePath,
153        name: &str,
154        cx: &mut App,
155    ) -> Task<Result<SourcePath, CreateFolderError>>;
156}
157
158/// Where the request for the next page of a listing stands.
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160#[non_exhaustive]
161pub enum MoreState {
162    /// No request is running. Reaching the end of the list starts one.
163    Idle,
164    /// The next page is on its way.
165    Loading,
166    /// The last request failed. The entries loaded stay, and the "Load
167    /// more" row asks again.
168    Failed,
169}
170
171/// The entries of a directory loaded so far, and whether more follow.
172#[derive(Debug)]
173#[non_exhaustive]
174pub struct Loaded<E> {
175    pub(crate) entries: Arc<Vec<E>>,
176    style: PathStyle,
177    names: HashSet<String>,
178    pub(crate) next: Option<PageToken>,
179    pub(crate) more: MoreState,
180    /// Why the last request for a later page failed.
181    more_error: Option<ListError>,
182}
183
184impl<E: Entry> Loaded<E> {
185    fn from_page(page: Page<E>, style: PathStyle) -> Self {
186        let mut loaded = Self {
187            entries: Arc::new(Vec::new()),
188            style,
189            names: HashSet::new(),
190            next: None,
191            more: MoreState::Idle,
192            more_error: None,
193        };
194        loaded.append(page);
195        loaded
196    }
197
198    /// Adds a page: its entries, less the names already loaded, and where
199    /// the following page starts.
200    fn append(&mut self, page: Page<E>) {
201        self.next = page.next;
202        let entries = Arc::make_mut(&mut self.entries);
203        for entry in page.entries {
204            if !is_plain_name(entry.name(), &self.style) {
205                continue;
206            }
207            if self
208                .names
209                .insert(self.style.fold(entry.name()).into_owned())
210            {
211                entries.push(entry);
212            }
213        }
214        self.more = MoreState::Idle;
215        self.more_error = None;
216    }
217}
218
219impl<E: Entry> Loaded<E> {
220    /// The entry named `name`, by the source's own rule for case.
221    pub(crate) fn entry_named(&self, name: &str) -> Option<&E> {
222        let wanted = self.style.fold(name);
223        self.entries
224            .iter()
225            .find(|entry| self.style.fold(entry.name()) == wanted)
226    }
227}
228
229impl<E> Loaded<E> {
230    /// The entries loaded, in the order the source gave them.
231    pub fn entries(&self) -> &[E] {
232        &self.entries
233    }
234
235    /// Where the next page starts, when the source has more.
236    pub fn next(&self) -> Option<&PageToken> {
237        self.next.as_ref()
238    }
239
240    /// How the request for the next page stands.
241    pub fn more(&self) -> MoreState {
242        self.more
243    }
244}
245
246/// What a browser knows about one directory.
247#[derive(Debug)]
248#[non_exhaustive]
249pub enum Listing<E> {
250    /// The source has not answered for the first page yet.
251    Loading,
252    /// The entries loaded so far.
253    Ready(Loaded<E>),
254    /// The source failed for the first page, such as for a path that does
255    /// not exist, with why.
256    Failed(ListError),
257}
258
259/// What the list area shows.
260#[derive(Clone, Copy, PartialEq, Eq)]
261pub(crate) enum Body {
262    Loading,
263    Failed,
264    Empty,
265    NoMatch,
266    List,
267}
268
269/// What `new` added to `old`: the text before it, the text that was added,
270/// and the text after it. A deletion adds nothing.
271fn insertion<'a>(old: &str, new: &'a str) -> (&'a str, &'a str, &'a str) {
272    let start = old
273        .chars()
274        .zip(new.chars())
275        .take_while(|(a, b)| a == b)
276        .map(|(c, _)| c.len_utf8())
277        .sum::<usize>();
278    let room = old.len().min(new.len()) - start;
279    let end = old
280        .chars()
281        .rev()
282        .zip(new.chars().rev())
283        .take_while(|(a, b)| a == b)
284        .map(|(c, _)| c.len_utf8())
285        .sum::<usize>()
286        .min(room);
287    (
288        &new[..start],
289        &new[start..new.len() - end],
290        &new[new.len() - end..],
291    )
292}
293
294/// A separator typed after a query, waiting for the filter of that query
295/// to finish so it knows which folder to go into.
296struct PendingDescend {
297    text: String,
298}
299
300type Visible<E> = Arc<dyn Fn(&E) -> bool + Send + Sync>;
301
302/// What the application does when the user asks to sign in to a source.
303pub(crate) type AuthHandler = Rc<dyn Fn(&mut Window, &mut App)>;
304
305/// The owner of a browser: it gives access to it, and the browser calls
306/// back through the owner's context.
307pub(crate) trait Host<E: Entry>: 'static + Sized {
308    fn browser(&self) -> &Browser<E>;
309    fn browser_mut(&mut self) -> &mut Browser<E>;
310
311    /// The source made the folder at `path`, and the listing is being read
312    /// again. The host reports it and decides what the new folder is.
313    fn folder_created(&mut self, path: SourcePath, window: &mut Window, cx: &mut Context<Self>);
314
315    /// The browser's rows or directory changed after an answer or an edit.
316    fn browser_changed(&mut self, _: &mut Context<Self>) {}
317}
318
319/// The row that names a new folder, open at the top of the list.
320pub(crate) struct NewFolderRow {
321    pub(crate) input: Entity<InputState>,
322    /// Whether the source is making the folder, which the row waits out.
323    pub(crate) creating: bool,
324    /// Why the last try failed, shown under the row.
325    pub(crate) error: Option<SharedString>,
326    _events: Subscription,
327}
328
329/// The path text, the listings, and the filtered rows.
330///
331/// The text is a path. Everything up to and including its last separator
332/// is the directory that is listed; what follows is a fuzzy query over
333/// that directory's entries. A leading `~` stands for the source's home.
334/// Every listing is cached, so going back is instant. An answer for a
335/// directory the path has left is dropped, and a new query cancels the
336/// filter before it.
337pub(crate) struct Browser<E: Entry> {
338    pub(crate) input: Entity<InputState>,
339    source: Rc<dyn Source<E>>,
340    pub(crate) style: PathStyle,
341    window: AnyWindowHandle,
342    pub(crate) dir_text: String,
343    /// The text of the field after the last change the browser took, to
344    /// tell a paste from a keystroke.
345    last_text: String,
346    pub(crate) dir: SourcePath,
347    pub(crate) query: SharedString,
348    pub(crate) listings: HashMap<SourcePath, Listing<E>>,
349    /// The running listing of the first page. Dropping it cancels it.
350    load: Option<Task<()>>,
351    /// The running request for a later page. Dropping it cancels it.
352    load_more: Option<Task<()>>,
353    /// Which directory visit the running requests belong to.
354    load_id: u64,
355    /// Which stay in a directory the browser is in. It changes when the
356    /// browser moves to another directory, and not when the same one is
357    /// listed again.
358    place: u64,
359    pub(crate) shown: Vec<Match>,
360    pub(crate) highlighted: Option<usize>,
361    pub(crate) list: ListState,
362    /// How many items the list was told it has.
363    list_count: usize,
364    /// The running filter. Dropping it cancels it.
365    filter: Option<Task<()>>,
366    filter_id: u64,
367    pending: Option<PendingDescend>,
368    visible: Visible<E>,
369    /// Whether the caller named the start folder, which wins over the
370    /// source's home.
371    pub(crate) explicit_start: bool,
372    auth_handler: Option<AuthHandler>,
373    allow_new_folder: bool,
374    pub(crate) new_folder: Option<NewFolderRow>,
375    /// The running request to make a folder. Dropping it cancels it.
376    creating: Option<Task<()>>,
377    /// A folder to put the highlight on once the listing shows it.
378    reveal: Option<SharedString>,
379}
380
381impl<E: Entry> Browser<E> {
382    /// A browser with an empty path field. The host calls
383    /// [`start`](Self::start) once it holds the browser.
384    pub(crate) fn new<O: Host<E>>(
385        source: Rc<dyn Source<E>>,
386        window: &mut Window,
387        cx: &mut Context<O>,
388    ) -> Self {
389        let input = cx.new(|cx| InputState::new(window, cx).placeholder("Path"));
390        let overdraw = cx.theme().metrics.folder_list_height;
391        let list = ListState::new(0, ListAlignment::Top, overdraw);
392        let style = source.style();
393        Self {
394            input,
395            source,
396            style,
397            window: window.window_handle(),
398            dir_text: String::new(),
399            last_text: String::new(),
400            dir: style.path(""),
401            query: SharedString::default(),
402            listings: HashMap::new(),
403            load: None,
404            load_more: None,
405            load_id: 0,
406            place: 0,
407            shown: Vec::new(),
408            highlighted: None,
409            list,
410            list_count: 0,
411            filter: None,
412            filter_id: 0,
413            pending: None,
414            visible: Arc::new(|_| true),
415            explicit_start: false,
416            auth_handler: None,
417            allow_new_folder: false,
418            new_folder: None,
419            creating: None,
420            reveal: None,
421        }
422    }
423
424    /// Sets what the "Sign in" button of a source that needs a login does.
425    pub(crate) fn set_auth_handler(&mut self, handler: AuthHandler) {
426        self.auth_handler = Some(handler);
427    }
428
429    pub(crate) fn auth_handler(&self) -> Option<AuthHandler> {
430        self.auth_handler.clone()
431    }
432
433    /// Why the listed directory failed, when it did.
434    pub(crate) fn failure(&self) -> Option<&ListError> {
435        match self.listing()? {
436            Listing::Failed(error) => Some(error),
437            _ => None,
438        }
439    }
440
441    /// Lists the directory again: its answer is dropped, the spinner shows,
442    /// and the source is asked for the first page.
443    pub(crate) fn retry<O: Host<E>>(&mut self, cx: &mut Context<O>) {
444        let dir = self.dir.clone();
445        self.listings.remove(&dir);
446        self.enter(dir, cx);
447        self.refilter(false, cx);
448    }
449
450    /// Whether the picker offers to make a folder.
451    pub(crate) fn allows_new_folder(&self) -> bool {
452        self.allow_new_folder
453    }
454
455    /// Sets whether the picker offers the new folder button, before the
456    /// picker is shown.
457    pub(crate) fn set_allow_new_folder_seed(&mut self, allow: bool) {
458        self.allow_new_folder = allow;
459    }
460
461    /// Offers or withdraws the new folder button. Withdrawing closes the
462    /// row that names a folder.
463    pub(crate) fn set_allow_new_folder<O: Host<E>>(&mut self, allow: bool, cx: &mut Context<O>) {
464        if self.allow_new_folder != allow {
465            self.allow_new_folder = allow;
466            if !allow {
467                self.cancel_new_folder(cx);
468            }
469            cx.notify();
470        }
471    }
472
473    /// Whether the picker shows the new folder button: it is on, and the
474    /// source can make folders.
475    pub(crate) fn can_create_folder(&self) -> bool {
476        self.allow_new_folder && self.source.can_create_folders()
477    }
478
479    /// Whether the button works now: the listed directory has loaded.
480    pub(crate) fn new_folder_ready(&self) -> bool {
481        self.can_create_folder() && self.loaded().is_some()
482    }
483
484    /// Opens the row that names a new folder, with the default name
485    /// selected, or puts the caret back in it when it is open. Nothing is
486    /// made until the name is confirmed.
487    pub(crate) fn begin_new_folder<O: Host<E>>(
488        &mut self,
489        name: &str,
490        window: &mut Window,
491        cx: &mut Context<O>,
492    ) {
493        if !self.new_folder_ready() {
494            return;
495        }
496        if let Some(open) = &self.new_folder {
497            let input = open.input.clone();
498            input.update(cx, |input, cx| input.focus(window, cx));
499            return;
500        }
501        let input = cx.new(|cx| InputState::new(window, cx).default_value(name.to_string()));
502        let events = cx.subscribe_in(
503            &input,
504            window,
505            |this: &mut O, _, event: &InputEvent, _, cx| match event {
506                InputEvent::Change => this.browser_mut().clear_new_folder_error(cx),
507                InputEvent::Blur => {
508                    let browser = this.browser_mut();
509                    if browser.new_folder_is_blank(cx) {
510                        browser.cancel_new_folder(cx);
511                    }
512                }
513                InputEvent::Focus | InputEvent::PressEnter { .. } => {}
514            },
515        );
516        input.update(cx, |input, cx| {
517            input.focus(window, cx);
518            input.select_all(window, cx);
519        });
520        self.new_folder = Some(NewFolderRow {
521            input,
522            creating: false,
523            error: None,
524            _events: events,
525        });
526        cx.notify();
527    }
528
529    /// Makes the folder `name` through the row: the row opens with `name`,
530    /// or takes `name` when it is already open, and is committed. It fails
531    /// with [`CreateFolderError::NotReady`] while the directory loads or a
532    /// folder is being made, with [`CreateFolderError::Unsupported`] when
533    /// the picker does not offer new folders, and with what the name check
534    /// finds, which also shows under the row.
535    pub(crate) fn create_folder<O: Host<E>>(
536        &mut self,
537        name: &str,
538        window: &mut Window,
539        cx: &mut Context<O>,
540    ) -> Result<(), CreateFolderError> {
541        if !self.can_create_folder() {
542            return Err(CreateFolderError::Unsupported);
543        }
544        if !self.new_folder_ready() || self.new_folder.as_ref().is_some_and(|row| row.creating) {
545            return Err(CreateFolderError::NotReady);
546        }
547        match &self.new_folder {
548            Some(open) => {
549                let name = name.to_string();
550                open.input
551                    .update(cx, |input, cx| input.set_value(name, window, cx));
552            }
553            None => self.begin_new_folder(name, window, cx),
554        }
555        self.commit_new_folder(cx)
556    }
557
558    /// Opens the row with the default name.
559    pub(crate) fn begin_default_new_folder<O: Host<E>>(
560        &mut self,
561        window: &mut Window,
562        cx: &mut Context<O>,
563    ) {
564        self.begin_new_folder(DEFAULT_FOLDER_NAME, window, cx);
565    }
566
567    fn new_folder_is_blank(&self, cx: &App) -> bool {
568        self.new_folder
569            .as_ref()
570            .is_some_and(|open| !open.creating && open.input.read(cx).value().trim().is_empty())
571    }
572
573    fn clear_new_folder_error<O: Host<E>>(&mut self, cx: &mut Context<O>) {
574        if let Some(open) = &mut self.new_folder
575            && open.error.take().is_some()
576        {
577            cx.notify();
578        }
579    }
580
581    /// Closes the row without making the folder. A folder that the source
582    /// is making is not undone, and the row waits for it.
583    pub(crate) fn cancel_new_folder<O: Host<E>>(&mut self, cx: &mut Context<O>) {
584        if self.new_folder.as_ref().is_some_and(|open| !open.creating) {
585            self.new_folder = None;
586            cx.notify();
587        }
588    }
589
590    /// [`cancel_new_folder`](Self::cancel_new_folder) for a key or a click,
591    /// which puts the caret back in the path field so the dialog still has
592    /// the keyboard.
593    pub(crate) fn dismiss_new_folder<O: Host<E>>(
594        &mut self,
595        window: &mut Window,
596        cx: &mut Context<O>,
597    ) {
598        if self.new_folder.as_ref().is_some_and(|open| !open.creating) {
599            self.cancel_new_folder(cx);
600            self.focus_path(window, cx);
601        }
602    }
603
604    /// Puts the caret in the path field.
605    pub(crate) fn focus_path<O: Host<E>>(&self, window: &mut Window, cx: &mut Context<O>) {
606        self.input.update(cx, |input, cx| input.focus(window, cx));
607    }
608
609    /// Makes the folder the row names: the name is checked here, so a
610    /// name the source would refuse never reaches it, and then the source
611    /// is asked off the UI thread. A refusal stays under the row, which
612    /// stays open to edit.
613    pub(crate) fn commit_new_folder<O: Host<E>>(
614        &mut self,
615        cx: &mut Context<O>,
616    ) -> Result<(), CreateFolderError> {
617        let Some(open) = &self.new_folder else {
618            return Err(CreateFolderError::NotReady);
619        };
620        if open.creating {
621            return Err(CreateFolderError::NotReady);
622        }
623        let typed = open.input.read(cx).value().to_string();
624        let name = match validate_folder_name(&typed, &self.style) {
625            Ok(name) => name,
626            Err(error) => {
627                self.refuse_new_folder(&typed, error.clone(), cx);
628                return Err(error);
629            }
630        };
631        if self
632            .loaded()
633            .is_some_and(|loaded| loaded.entry_named(&name).is_some())
634        {
635            self.refuse_new_folder(&name, CreateFolderError::Exists, cx);
636            return Err(CreateFolderError::Exists);
637        }
638        if !self.source.can_create_folders() {
639            self.refuse_new_folder(&name, CreateFolderError::Unsupported, cx);
640            return Err(CreateFolderError::Unsupported);
641        }
642        if let Some(open) = &mut self.new_folder {
643            open.creating = true;
644            open.error = None;
645        }
646        let dir = self.dir.clone();
647        let handle = self.window;
648        cx.notify();
649        self.creating = Some(cx.spawn(async move |this, cx| {
650            let Ok(answer) = this.update(cx, |this: &mut O, cx| {
651                let source = this.browser().source.clone();
652                source.create_folder(&dir, &name, cx)
653            }) else {
654                return;
655            };
656            let result = answer.await;
657            let made = this
658                .update(cx, |this: &mut O, cx| {
659                    let made = this.browser_mut().created(dir, name, result, cx);
660                    this.browser_changed(cx);
661                    made
662                })
663                .ok()
664                .flatten();
665            if let Some(path) = made {
666                cx.update_window(handle, |_, window, cx| {
667                    this.update(cx, |this: &mut O, cx| this.folder_created(path, window, cx))
668                        .ok();
669                })
670                .ok();
671            }
672        }));
673        Ok(())
674    }
675
676    fn refuse_new_folder<O: Host<E>>(
677        &mut self,
678        name: &str,
679        error: CreateFolderError,
680        cx: &mut Context<O>,
681    ) {
682        if let Some(open) = &mut self.new_folder {
683            open.error = Some(error.message(name));
684            open.creating = false;
685        }
686        cx.notify();
687    }
688
689    /// The source answered for the folder `name` of `dir`. A made folder
690    /// closes the row, reads the listing again, and is handed back so the
691    /// host can report it; a refusal reopens the row for editing.
692    fn created<O: Host<E>>(
693        &mut self,
694        dir: SourcePath,
695        name: String,
696        result: Result<SourcePath, CreateFolderError>,
697        cx: &mut Context<O>,
698    ) -> Option<SourcePath> {
699        self.creating = None;
700        match result {
701            Ok(path) => {
702                self.new_folder = None;
703                self.listings.remove(&dir);
704                if dir == self.dir {
705                    self.reveal = Some(path.file_name().unwrap_or(&name).to_string().into());
706                    self.enter(dir, cx);
707                    self.refilter(false, cx);
708                }
709                cx.notify();
710                Some(path)
711            }
712            Err(error) => {
713                self.refuse_new_folder(&name, error, cx);
714                None
715            }
716        }
717    }
718
719    /// Cancels every request in flight: a running listing is dropped, so
720    /// its answer never arrives, and a directory that was still loading
721    /// is forgotten so it is listed again when the picker shows.
722    pub(crate) fn stop_requests<O: Host<E>>(&mut self, cx: &mut Context<O>) {
723        self.load = None;
724        self.load_more = None;
725        self.filter = None;
726        self.filter_id += 1;
727        self.pending = None;
728        self.creating = None;
729        self.new_folder = None;
730        self.reveal = None;
731        self.load_id += 1;
732        match self.listings.get_mut(&self.dir) {
733            Some(Listing::Loading) => {
734                self.listings.remove(&self.dir);
735            }
736            Some(Listing::Ready(loaded)) if loaded.more == MoreState::Loading => {
737                loaded.more = MoreState::Idle;
738            }
739            _ => {}
740        }
741        cx.notify();
742    }
743
744    /// Lists the directory when no listing of it is known or running, as
745    /// after [`stop_requests`](Self::stop_requests).
746    pub(crate) fn ensure_listed<O: Host<E>>(&mut self, cx: &mut Context<O>) {
747        if self.listings.contains_key(&self.dir) || self.dir_text.is_empty() {
748            return;
749        }
750        let dir = self.dir.clone();
751        self.enter(dir, cx);
752        self.refilter(false, cx);
753    }
754
755    /// The subscription that feeds the path field's edits to the browser.
756    pub(crate) fn watch<O: Host<E>>(
757        &self,
758        window: &mut Window,
759        cx: &mut Context<O>,
760    ) -> Subscription {
761        cx.subscribe_in(
762            &self.input,
763            window,
764            |this: &mut O, input, event: &InputEvent, window, cx| {
765                if matches!(event, InputEvent::Change) {
766                    let text = input.read(cx).value().to_string();
767                    this.browser_mut().text_changed(text, window, cx);
768                    this.browser_changed(cx);
769                }
770            },
771        )
772    }
773
774    /// The text the browser starts with when it was given no start: the
775    /// source's home, or the root.
776    pub(crate) fn default_start(&self) -> String {
777        if self.source.home().is_some() {
778            format!("~{}", self.style.separator())
779        } else {
780            self.style.separator().to_string()
781        }
782    }
783
784    /// The entries that show are those `visible` lets through. A folder
785    /// picker lets every entry through; a file picker lets folders and the
786    /// files its filter accepts.
787    pub(crate) fn set_visible<O: Host<E>>(
788        &mut self,
789        visible: impl Fn(&E) -> bool + Send + Sync + 'static,
790        cx: &mut Context<O>,
791    ) {
792        self.visible = Arc::new(visible);
793        self.refilter(false, cx);
794    }
795
796    /// Lists from `source`: the cached listings are dropped, and the
797    /// browser starts at the new source's home unless the caller named a
798    /// start.
799    pub(crate) fn set_source<O: Host<E>>(
800        &mut self,
801        source: Rc<dyn Source<E>>,
802        window: &mut Window,
803        cx: &mut Context<O>,
804    ) {
805        self.style = source.style();
806        self.source = source;
807        self.listings.clear();
808        self.load = None;
809        self.load_more = None;
810        self.creating = None;
811        self.new_folder = None;
812        self.dir = self.style.path("");
813        let text = if self.explicit_start {
814            self.input.read(cx).value().to_string()
815        } else {
816            self.default_start()
817        };
818        self.set_text(&text, window, cx);
819    }
820
821    /// Starts in `path`, which is listed at once.
822    pub(crate) fn start_in<O: Host<E>>(
823        &mut self,
824        path: &str,
825        window: &mut Window,
826        cx: &mut Context<O>,
827    ) {
828        self.explicit_start = true;
829        let mut text = path.to_string();
830        if !text.ends_with(|c| self.style.is_separator(c)) {
831            text.push(self.style.separator());
832        }
833        self.set_text(&text, window, cx);
834    }
835
836    /// Which visit to a directory the browser is in: it changes every
837    /// time the browser moves to another directory, and when it comes back.
838    pub(crate) fn visit(&self) -> u64 {
839        self.place
840    }
841
842    /// What is known about the directory that is listed.
843    pub(crate) fn listing(&self) -> Option<&Listing<E>> {
844        self.listings.get(&self.dir)
845    }
846
847    /// The loaded entries of the listed directory.
848    pub(crate) fn loaded(&self) -> Option<&Loaded<E>> {
849        match self.listing()? {
850            Listing::Ready(loaded) => Some(loaded),
851            _ => None,
852        }
853    }
854
855    /// How many entries the list shows, not counting the "Load more" row.
856    pub(crate) fn row_count(&self) -> usize {
857        self.shown.len()
858    }
859
860    /// Whether the list ends with a "Load more" row.
861    pub(crate) fn has_more_row(&self) -> bool {
862        matches!(self.listing(), Some(Listing::Ready(loaded)) if loaded.next.is_some())
863    }
864
865    /// The rows of the list: the entries and the "Load more" row.
866    pub(crate) fn item_count(&self) -> usize {
867        self.shown.len() + usize::from(self.has_more_row())
868    }
869
870    /// What the list area shows.
871    pub(crate) fn body(&self) -> Body {
872        match self.listing() {
873            None | Some(Listing::Loading) => Body::Loading,
874            Some(Listing::Failed(_)) => Body::Failed,
875            Some(Listing::Ready(_)) => {
876                if self.has_more_row() || !self.shown.is_empty() {
877                    Body::List
878                } else if self.query.is_empty() {
879                    Body::Empty
880                } else {
881                    Body::NoMatch
882                }
883            }
884        }
885    }
886
887    /// Why the request for the next page failed, when it did.
888    pub(crate) fn more_error(&self) -> Option<&ListError> {
889        match self.listing() {
890            Some(Listing::Ready(loaded)) => loaded.more_error.as_ref(),
891            _ => None,
892        }
893    }
894
895    /// How the request for the next page stands.
896    pub(crate) fn more_state(&self) -> MoreState {
897        match self.listing() {
898            Some(Listing::Ready(loaded)) => loaded.more,
899            _ => MoreState::Idle,
900        }
901    }
902
903    /// The entry shown at `row`, and where the query matched its name.
904    pub(crate) fn entry_at(&self, row: usize) -> Option<(&E, &[std::ops::Range<usize>])> {
905        let loaded = self.loaded()?;
906        let found = self.shown.get(row)?;
907        Some((loaded.entries.get(found.index)?, found.ranges.as_slice()))
908    }
909
910    /// Replaces the path text from code, which the field does not report.
911    pub(crate) fn set_text<O: Host<E>>(
912        &mut self,
913        text: &str,
914        window: &mut Window,
915        cx: &mut Context<O>,
916    ) {
917        self.input.update(cx, |input, cx| {
918            input.set_value(text.to_string(), window, cx)
919        });
920        self.last_text = text.to_string();
921        self.apply_text(text.to_string(), cx);
922    }
923
924    /// Goes to the parent of the listed directory. The root is its own
925    /// parent, and the home's parent is the folder above it.
926    pub(crate) fn go_up<O: Host<E>>(&mut self, window: &mut Window, cx: &mut Context<O>) {
927        let home = self.source.home();
928        let at_home = home.is_some()
929            && is_home_text(&self.dir_text, &self.style)
930            && self
931                .dir_text
932                .trim_end_matches(|c| self.style.is_separator(c))
933                == "~";
934        let text = if at_home {
935            let home = home
936                .clone()
937                .map(|home| home.to_string())
938                .unwrap_or_default();
939            parent_text(&home, &self.style)
940        } else {
941            parent_text(&self.dir_text, &self.style)
942        };
943        let text = collapse_home(&text, home.as_ref(), &self.style);
944        self.set_text(&text, window, cx);
945    }
946
947    /// Goes inside the folder `name` of the listed directory.
948    pub(crate) fn descend<O: Host<E>>(
949        &mut self,
950        name: &str,
951        window: &mut Window,
952        cx: &mut Context<O>,
953    ) {
954        let text = join_dir(&self.dir_text, name, &self.style);
955        let text = collapse_home(&text, self.source.home().as_ref(), &self.style);
956        self.set_text(&text, window, cx);
957    }
958
959    /// Asks the source for the next page of the listed directory, when it
960    /// has one and no request is running. After a failure it asks again.
961    pub(crate) fn load_more<O: Host<E>>(&mut self, cx: &mut Context<O>) {
962        let dir = self.dir.clone();
963        let Some(Listing::Ready(loaded)) = self.listings.get_mut(&dir) else {
964            return;
965        };
966        let Some(token) = loaded.next.clone() else {
967            return;
968        };
969        if loaded.more == MoreState::Loading {
970            return;
971        }
972        loaded.more = MoreState::Loading;
973        let id = self.load_id;
974        cx.notify();
975        self.load_more = Some(cx.spawn(async move |this, cx| {
976            let Ok(answer) = this.update(cx, |this: &mut O, cx| {
977                let source = this.browser().source.clone();
978                source.list(&dir, Some(token), cx)
979            }) else {
980                return;
981            };
982            let result = answer.await;
983            this.update(cx, |this: &mut O, cx| {
984                this.browser_mut().paged(dir, id, result, cx);
985                this.browser_changed(cx);
986            })
987            .ok();
988        }));
989    }
990
991    /// The list reached its last row: loads the next page unless a request
992    /// is running or the last one failed, which the row retries on a
993    /// click.
994    /// The rows moved. Reaching the end of the list loads the next page.
995    pub(crate) fn rows_scrolled<O: Host<E>>(&mut self, cx: &mut Context<O>) {
996        if self.list.is_scrolled_to_end() == Some(true) {
997            self.load_more_on_scroll(cx);
998        }
999    }
1000
1001    fn load_more_on_scroll<O: Host<E>>(&mut self, cx: &mut Context<O>) {
1002        if let Some(Listing::Ready(loaded)) = self.listing()
1003            && loaded.more == MoreState::Idle
1004        {
1005            self.load_more(cx);
1006        }
1007    }
1008
1009    /// The user edited the path text.
1010    fn text_changed<O: Host<E>>(&mut self, text: String, window: &mut Window, cx: &mut Context<O>) {
1011        let (before, added, after) = insertion(&self.last_text, &text);
1012        if added.chars().count() > 1 {
1013            let source = self.source.clone();
1014            let env = move |name: &str| source.env(name);
1015            // A path pasted into an empty field is cleaned whole. One
1016            // pasted into the middle of other text only has its separators
1017            // written the style's way, so what is around it stays.
1018            let clean = if before.is_empty() && after.is_empty() {
1019                self.style.normalize_pasted(&text, &env)
1020            } else {
1021                format!("{before}{}{after}", self.style.normalize_fragment(added))
1022            };
1023            if clean != text {
1024                self.set_text(&clean, window, cx);
1025                return;
1026            }
1027        }
1028        self.last_text = text.clone();
1029        let typed_separator = !self.query.is_empty()
1030            && self
1031                .style
1032                .is_separator(text.chars().last().unwrap_or_default())
1033            && text[..text.len() - 1] == format!("{}{}", self.dir_text, self.query);
1034        if !typed_separator {
1035            self.pending = None;
1036            self.apply_text(text, cx);
1037            return;
1038        }
1039        if self.filter.is_some() {
1040            // The rows still answer an older query. Wait for the filter of
1041            // this one before choosing a folder.
1042            self.pending = Some(PendingDescend { text });
1043        } else {
1044            self.descend_or_keep(text, window, cx);
1045        }
1046    }
1047
1048    /// A separator was typed after the query: goes inside the folder the
1049    /// query resolves to, or keeps the literal text.
1050    fn descend_or_keep<O: Host<E>>(
1051        &mut self,
1052        typed: String,
1053        window: &mut Window,
1054        cx: &mut Context<O>,
1055    ) {
1056        let name = self.loaded().and_then(|loaded| {
1057            resolve_descend(loaded.entries(), &self.shown, &self.query, &self.style)
1058        });
1059        match name {
1060            Some(name) => self.descend(&name, window, cx),
1061            None => self.apply_text(typed, cx),
1062        }
1063    }
1064
1065    /// Splits `text` into the directory and the query, lists the
1066    /// directory when it changed, and filters its entries.
1067    fn apply_text<O: Host<E>>(&mut self, text: String, cx: &mut Context<O>) {
1068        let (dir_text, query) = if text == "~" && self.source.home().is_some() {
1069            (format!("~{}", self.style.separator()), String::new())
1070        } else {
1071            split_path(&text, &self.style)
1072        };
1073        // A query of `..` or `.` names a directory, not a name to match.
1074        let (dir_text, query) = match query.as_str() {
1075            ".." => (
1076                format!("{dir_text}..{}", self.style.separator()),
1077                String::new(),
1078            ),
1079            "." => (dir_text, String::new()),
1080            _ => (dir_text, query),
1081        };
1082        let dir = resolve_dir(&dir_text, self.source.home().as_ref(), &self.style);
1083        self.dir_text = dir_text;
1084        self.query = query.into();
1085        if dir != self.dir {
1086            self.enter(dir, cx);
1087        }
1088        self.refilter(false, cx);
1089    }
1090
1091    /// Moves to `dir`: cancels the requests for the directory left, keeps
1092    /// only the listings that finished, and lists `dir` unless it is
1093    /// cached.
1094    fn enter<O: Host<E>>(&mut self, dir: SourcePath, cx: &mut Context<O>) {
1095        if dir != self.dir {
1096            self.cancel_new_folder(cx);
1097            self.place += 1;
1098            self.reveal = None;
1099        }
1100        self.load = None;
1101        self.load_more = None;
1102        self.load_id += 1;
1103        if let Some(Listing::Ready(loaded)) = self.listings.get_mut(&self.dir)
1104            && loaded.more == MoreState::Loading
1105        {
1106            loaded.more = MoreState::Idle;
1107        }
1108        self.listings
1109            .retain(|_, listing| matches!(listing, Listing::Ready(_)));
1110        self.dir = dir.clone();
1111        if self.listings.contains_key(&dir) {
1112            return;
1113        }
1114        self.listings.insert(dir.clone(), Listing::Loading);
1115        let id = self.load_id;
1116        // The source is asked once the caller's builder chain has ended, so
1117        // a picker that is seeded and given a source asks for the last
1118        // directory only.
1119        self.load = Some(cx.spawn(async move |this, cx| {
1120            let Ok(answer) = this.update(cx, |this: &mut O, cx| {
1121                let source = this.browser().source.clone();
1122                source.list(&dir, None, cx)
1123            }) else {
1124                return;
1125            };
1126            let result = answer.await;
1127            this.update(cx, |this: &mut O, cx| {
1128                this.browser_mut().first_page(dir, id, result, cx);
1129                this.browser_changed(cx);
1130            })
1131            .ok();
1132        }));
1133    }
1134
1135    /// The first page arrived. It is dropped when the path has moved on.
1136    fn first_page<O: Host<E>>(
1137        &mut self,
1138        dir: SourcePath,
1139        id: u64,
1140        result: Result<Page<E>, ListError>,
1141        cx: &mut Context<O>,
1142    ) {
1143        if id != self.load_id || dir != self.dir {
1144            return;
1145        }
1146        self.load = None;
1147        let listing = match result {
1148            Ok(page) => Listing::Ready(Loaded::from_page(page, self.style)),
1149            Err(error) => Listing::Failed(error),
1150        };
1151        self.listings.insert(dir, listing);
1152        self.refilter(false, cx);
1153    }
1154
1155    /// A later page arrived. It is dropped when the path has moved on.
1156    fn paged<O: Host<E>>(
1157        &mut self,
1158        dir: SourcePath,
1159        id: u64,
1160        result: Result<Page<E>, ListError>,
1161        cx: &mut Context<O>,
1162    ) {
1163        if id != self.load_id || dir != self.dir {
1164            return;
1165        }
1166        self.load_more = None;
1167        let Some(Listing::Ready(loaded)) = self.listings.get_mut(&dir) else {
1168            return;
1169        };
1170        match result {
1171            Ok(page) => loaded.append(page),
1172            Err(error) => {
1173                loaded.more = MoreState::Failed;
1174                loaded.more_error = Some(error);
1175            }
1176        }
1177        self.refilter(true, cx);
1178    }
1179
1180    /// Filters the listed entries for the query: at once for an empty
1181    /// query, and on a background task for any other, which the next call
1182    /// cancels. `keep_place` keeps the scroll position and the highlight,
1183    /// for a page appended to the list.
1184    pub(crate) fn refilter<O: Host<E>>(&mut self, keep_place: bool, cx: &mut Context<O>) {
1185        self.filter = None;
1186        self.filter_id += 1;
1187        let id = self.filter_id;
1188        let Some(Listing::Ready(loaded)) = self.listing() else {
1189            self.show(Vec::new(), false, cx);
1190            return;
1191        };
1192        if self.query.is_empty() {
1193            let shown = all_visible(&loaded.entries, &*self.visible);
1194            self.show(shown, keep_place, cx);
1195            return;
1196        }
1197        let entries = loaded.entries.clone();
1198        let visible = self.visible.clone();
1199        let query = self.query.to_string();
1200        let window = self.window;
1201        self.filter = Some(cx.spawn(async move |this, cx| {
1202            let matches = cx
1203                .background_spawn(async move { rank(&entries, &*visible, &query) })
1204                .await;
1205            let pending = this
1206                .update(cx, |this: &mut O, cx| {
1207                    let browser = this.browser_mut();
1208                    if browser.filter_id != id {
1209                        return None;
1210                    }
1211                    browser.filter = None;
1212                    browser.show(matches, keep_place, cx);
1213                    let pending = browser.pending.take();
1214                    this.browser_changed(cx);
1215                    pending
1216                })
1217                .ok()
1218                .flatten();
1219            if let Some(pending) = pending {
1220                // A separator was typed while the filter ran, and the
1221                // field needs the window to take the folder's name.
1222                cx.update_window(window, |_, window, cx| {
1223                    this.update(cx, |this: &mut O, cx| {
1224                        this.browser_mut().descend_or_keep(pending.text, window, cx)
1225                    })
1226                    .ok();
1227                })
1228                .ok();
1229            }
1230        }));
1231        cx.notify();
1232    }
1233
1234    fn show<O: Host<E>>(&mut self, shown: Vec<Match>, keep_place: bool, cx: &mut Context<O>) {
1235        let kept = keep_place.then(|| {
1236            let was_more = self.highlighted == Some(self.shown.len());
1237            let name = self
1238                .highlighted
1239                .and_then(|row| self.entry_at(row))
1240                .map(|(entry, _)| entry.name().clone());
1241            (
1242                self.list.logical_scroll_top(),
1243                name,
1244                was_more,
1245                self.shown.len(),
1246            )
1247        });
1248        self.shown = shown;
1249        let count = self.item_count();
1250        if let Some((top, name, was_more, previous)) = kept {
1251            // Rows that did not change stay out of the replaced range, and the
1252            // place the list was scrolled to is put back.
1253            let common = if self.query.is_empty() {
1254                previous.min(self.shown.len())
1255            } else {
1256                0
1257            };
1258            self.list
1259                .splice(common..self.list_count, count.saturating_sub(common));
1260            self.list.scroll_to(top);
1261            self.highlighted = match (name, self.loaded()) {
1262                (Some(name), Some(loaded)) => {
1263                    let wanted = self.style.fold(&name);
1264                    self.shown.iter().position(|found| {
1265                        loaded
1266                            .entries
1267                            .get(found.index)
1268                            .is_some_and(|entry| self.style.fold(entry.name()) == wanted)
1269                    })
1270                }
1271                _ => None,
1272            };
1273            if was_more {
1274                // The highlight was on the Load more row: it stays on the
1275                // row, or moves to the first row the page added.
1276                self.highlighted = if self.has_more_row() {
1277                    Some(self.shown.len())
1278                } else {
1279                    (previous < self.shown.len()).then_some(previous)
1280                };
1281            }
1282        } else {
1283            self.list.reset(count);
1284            self.highlighted = (!self.shown.is_empty()).then_some(0);
1285        }
1286        self.list_count = count;
1287        self.reveal_new_folder();
1288        cx.notify();
1289    }
1290
1291    /// Puts the highlight on the folder that was just made, once the
1292    /// listing shows it, and scrolls to it.
1293    fn reveal_new_folder(&mut self) {
1294        let Some(name) = self.reveal.clone() else {
1295            return;
1296        };
1297        let Some(loaded) = self.loaded() else {
1298            return;
1299        };
1300        let wanted = self.style.fold(&name);
1301        let row = self.shown.iter().position(|found| {
1302            loaded
1303                .entries
1304                .get(found.index)
1305                .is_some_and(|entry| self.style.fold(entry.name()) == wanted)
1306        });
1307        if let Some(row) = row {
1308            self.reveal = None;
1309            self.highlighted = Some(row);
1310            self.list.scroll_to_reveal_item(row);
1311        }
1312    }
1313
1314    /// Moves the highlight one row up or down, wrapping at the ends.
1315    pub(crate) fn move_highlight<O: Host<E>>(&mut self, delta: isize, cx: &mut Context<O>) {
1316        let count = self.item_count() as isize;
1317        if count == 0 {
1318            return;
1319        }
1320        let row = match self.highlighted {
1321            Some(row) => (row as isize + delta).rem_euclid(count),
1322            None if delta > 0 => 0,
1323            None => count - 1,
1324        } as usize;
1325        self.highlighted = Some(row);
1326        self.list.scroll_to_reveal_item(row);
1327        cx.notify();
1328    }
1329}