1mod 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 Submit,
41 NewFolder,
43 CancelNewFolder,
45 CommitNewFolder
47 ]
48);
49
50const SUBMIT_KEY: &str = if cfg!(target_os = "macos") {
52 "cmd-enter"
53} else {
54 "ctrl-enter"
55};
56
57const NEW_FOLDER_KEY: &str = if cfg!(target_os = "macos") {
60 "cmd-shift-n"
61} else {
62 "ctrl-shift-n"
63};
64
65pub(crate) const NEW_FOLDER_CONTEXT: &str = "GpuiCnNewFolder";
68
69const DEFAULT_FOLDER_NAME: &str = "Untitled folder";
71
72pub(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
82pub(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
95pub trait Entry: Clone + Send + Sync + 'static {
99 fn name(&self) -> &SharedString;
103 fn hidden(&self) -> bool;
105 fn is_folder(&self) -> bool;
107}
108
109#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct PageToken(Arc<str>);
114
115impl PageToken {
116 pub fn new(value: impl Into<Arc<str>>) -> Self {
118 Self(value.into())
119 }
120
121 pub fn as_str(&self) -> &str {
123 &self.0
124 }
125}
126
127pub(crate) struct Page<E> {
129 pub(crate) entries: Vec<E>,
130 pub(crate) next: Option<PageToken>,
131}
132
133pub(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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160#[non_exhaustive]
161pub enum MoreState {
162 Idle,
164 Loading,
166 Failed,
169}
170
171#[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 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 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 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 pub fn entries(&self) -> &[E] {
232 &self.entries
233 }
234
235 pub fn next(&self) -> Option<&PageToken> {
237 self.next.as_ref()
238 }
239
240 pub fn more(&self) -> MoreState {
242 self.more
243 }
244}
245
246#[derive(Debug)]
248#[non_exhaustive]
249pub enum Listing<E> {
250 Loading,
252 Ready(Loaded<E>),
254 Failed(ListError),
257}
258
259#[derive(Clone, Copy, PartialEq, Eq)]
261pub(crate) enum Body {
262 Loading,
263 Failed,
264 Empty,
265 NoMatch,
266 List,
267}
268
269fn 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
294struct PendingDescend {
297 text: String,
298}
299
300type Visible<E> = Arc<dyn Fn(&E) -> bool + Send + Sync>;
301
302pub(crate) type AuthHandler = Rc<dyn Fn(&mut Window, &mut App)>;
304
305pub(crate) trait Host<E: Entry>: 'static + Sized {
308 fn browser(&self) -> &Browser<E>;
309 fn browser_mut(&mut self) -> &mut Browser<E>;
310
311 fn folder_created(&mut self, path: SourcePath, window: &mut Window, cx: &mut Context<Self>);
314
315 fn browser_changed(&mut self, _: &mut Context<Self>) {}
317}
318
319pub(crate) struct NewFolderRow {
321 pub(crate) input: Entity<InputState>,
322 pub(crate) creating: bool,
324 pub(crate) error: Option<SharedString>,
326 _events: Subscription,
327}
328
329pub(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 last_text: String,
346 pub(crate) dir: SourcePath,
347 pub(crate) query: SharedString,
348 pub(crate) listings: HashMap<SourcePath, Listing<E>>,
349 load: Option<Task<()>>,
351 load_more: Option<Task<()>>,
353 load_id: u64,
355 place: u64,
359 pub(crate) shown: Vec<Match>,
360 pub(crate) highlighted: Option<usize>,
361 pub(crate) list: ListState,
362 list_count: usize,
364 filter: Option<Task<()>>,
366 filter_id: u64,
367 pending: Option<PendingDescend>,
368 visible: Visible<E>,
369 pub(crate) explicit_start: bool,
372 auth_handler: Option<AuthHandler>,
373 allow_new_folder: bool,
374 pub(crate) new_folder: Option<NewFolderRow>,
375 creating: Option<Task<()>>,
377 reveal: Option<SharedString>,
379}
380
381impl<E: Entry> Browser<E> {
382 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 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 pub(crate) fn failure(&self) -> Option<&ListError> {
435 match self.listing()? {
436 Listing::Failed(error) => Some(error),
437 _ => None,
438 }
439 }
440
441 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 pub(crate) fn allows_new_folder(&self) -> bool {
452 self.allow_new_folder
453 }
454
455 pub(crate) fn set_allow_new_folder_seed(&mut self, allow: bool) {
458 self.allow_new_folder = allow;
459 }
460
461 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 pub(crate) fn can_create_folder(&self) -> bool {
476 self.allow_new_folder && self.source.can_create_folders()
477 }
478
479 pub(crate) fn new_folder_ready(&self) -> bool {
481 self.can_create_folder() && self.loaded().is_some()
482 }
483
484 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub(crate) fn visit(&self) -> u64 {
839 self.place
840 }
841
842 pub(crate) fn listing(&self) -> Option<&Listing<E>> {
844 self.listings.get(&self.dir)
845 }
846
847 pub(crate) fn loaded(&self) -> Option<&Loaded<E>> {
849 match self.listing()? {
850 Listing::Ready(loaded) => Some(loaded),
851 _ => None,
852 }
853 }
854
855 pub(crate) fn row_count(&self) -> usize {
857 self.shown.len()
858 }
859
860 pub(crate) fn has_more_row(&self) -> bool {
862 matches!(self.listing(), Some(Listing::Ready(loaded)) if loaded.next.is_some())
863 }
864
865 pub(crate) fn item_count(&self) -> usize {
867 self.shown.len() + usize::from(self.has_more_row())
868 }
869
870 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 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 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 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 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 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 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 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 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 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 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 self.pending = Some(PendingDescend { text });
1043 } else {
1044 self.descend_or_keep(text, window, cx);
1045 }
1046 }
1047
1048 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 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 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 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 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 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 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 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 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 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 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 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 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}