1pub mod file_finder_settings;
2
3#[cfg(test)]
4mod open_path_prompt_tests;
5
6use file_finder_settings::FileFinderSettings;
7use file_icons::FileIcons;
8use futures::channel::oneshot;
9use fuzzy::{CharBag, StringMatch, StringMatchCandidate};
10use gpui::{HighlightStyle, StyledText, Task};
11use picker::{Picker, PickerDelegate};
12use project::{DirectoryItem, DirectoryLister};
13use settings::Settings;
14use std::{
15 path::{self, Path, PathBuf},
16 sync::{
17 Arc,
18 atomic::{self, AtomicBool},
19 },
20};
21use ui::{Context, LabelLike, ListItem, Window};
22use ui::{HighlightedLabel, ListItemSpacing, prelude::*};
23use util::{
24 maybe,
25 paths::{PathStyle, compare_paths},
26};
27use workspace::Workspace;
28
29pub struct OpenPathPrompt;
30
31pub struct OpenPathDelegate {
32 tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
33 lister: DirectoryLister,
34 selected_index: usize,
35 directory_state: DirectoryState,
36 string_matches: Vec<StringMatch>,
37 cancel_flag: Arc<AtomicBool>,
38 should_dismiss: bool,
39 prompt_root: String,
40 path_style: PathStyle,
41 replace_prompt: Task<()>,
42 render_footer:
43 Arc<dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static>,
44 hidden_entries: bool,
45}
46
47impl OpenPathDelegate {
48 pub fn new(
49 tx: oneshot::Sender<Option<Vec<PathBuf>>>,
50 lister: DirectoryLister,
51 creating_path: bool,
52 cx: &App,
53 ) -> Self {
54 let path_style = lister.path_style(cx);
55 Self {
56 tx: Some(tx),
57 lister,
58 selected_index: 0,
59 directory_state: DirectoryState::None {
60 create: creating_path,
61 },
62 string_matches: Vec::new(),
63 cancel_flag: Arc::new(AtomicBool::new(false)),
64 should_dismiss: true,
65 prompt_root: match path_style {
66 PathStyle::Posix => "/".to_string(),
67 PathStyle::Windows => "C:\\".to_string(),
68 },
69 path_style,
70 replace_prompt: Task::ready(()),
71 render_footer: Arc::new(|_, _| None),
72 hidden_entries: false,
73 }
74 }
75
76 pub fn with_footer(
77 mut self,
78 footer: Arc<
79 dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static,
80 >,
81 ) -> Self {
82 self.render_footer = footer;
83 self
84 }
85
86 pub fn show_hidden(mut self) -> Self {
87 self.hidden_entries = true;
88 self
89 }
90 fn get_entry(&self, selected_match_index: usize) -> Option<CandidateInfo> {
91 match &self.directory_state {
92 DirectoryState::List { entries, .. } => {
93 let id = self.string_matches.get(selected_match_index)?.candidate_id;
94 entries.iter().find(|entry| entry.path.id == id).cloned()
95 }
96 DirectoryState::Create {
97 user_input,
98 entries,
99 ..
100 } => {
101 let mut i = selected_match_index;
102 if let Some(user_input) = user_input
103 && (!user_input.exists || !user_input.is_dir)
104 {
105 if i == 0 {
106 return Some(CandidateInfo {
107 path: user_input.file.clone(),
108 is_dir: false,
109 });
110 } else {
111 i -= 1;
112 }
113 }
114 let id = self.string_matches.get(i)?.candidate_id;
115 entries.iter().find(|entry| entry.path.id == id).cloned()
116 }
117 DirectoryState::None { .. } => None,
118 }
119 }
120
121 #[cfg(any(test, feature = "test-support"))]
122 pub fn collect_match_candidates(&self) -> Vec<String> {
123 match &self.directory_state {
124 DirectoryState::List { entries, .. } => self
125 .string_matches
126 .iter()
127 .filter_map(|string_match| {
128 entries
129 .iter()
130 .find(|entry| entry.path.id == string_match.candidate_id)
131 .map(|candidate| candidate.path.string.clone())
132 })
133 .collect(),
134 DirectoryState::Create {
135 user_input,
136 entries,
137 ..
138 } => user_input
139 .iter()
140 .filter(|user_input| !user_input.exists || !user_input.is_dir)
141 .map(|user_input| user_input.file.string.clone())
142 .chain(self.string_matches.iter().filter_map(|string_match| {
143 entries
144 .iter()
145 .find(|entry| entry.path.id == string_match.candidate_id)
146 .map(|candidate| candidate.path.string.clone())
147 }))
148 .collect(),
149 DirectoryState::None { .. } => Vec::new(),
150 }
151 }
152
153 fn current_dir(&self) -> &'static str {
154 match self.path_style {
155 PathStyle::Posix => "./",
156 PathStyle::Windows => ".\\",
157 }
158 }
159}
160
161#[derive(Debug)]
162enum DirectoryState {
163 List {
164 parent_path: String,
165 entries: Vec<CandidateInfo>,
166 error: Option<SharedString>,
167 },
168 Create {
169 parent_path: String,
170 user_input: Option<UserInput>,
171 entries: Vec<CandidateInfo>,
172 },
173 None {
174 create: bool,
175 },
176}
177
178#[derive(Debug, Clone)]
179struct UserInput {
180 file: StringMatchCandidate,
181 exists: bool,
182 is_dir: bool,
183}
184
185#[derive(Debug, Clone)]
186struct CandidateInfo {
187 path: StringMatchCandidate,
188 is_dir: bool,
189}
190
191impl OpenPathPrompt {
192 pub fn register(
193 workspace: &mut Workspace,
194 _window: Option<&mut Window>,
195 _: &mut Context<Workspace>,
196 ) {
197 workspace.set_prompt_for_open_path(Box::new(|workspace, lister, window, cx| {
198 let (tx, rx) = futures::channel::oneshot::channel();
199 Self::prompt_for_open_path(workspace, lister, false, None, tx, window, cx);
200 rx
201 }));
202 }
203
204 pub fn register_new_path(
205 workspace: &mut Workspace,
206 _window: Option<&mut Window>,
207 _: &mut Context<Workspace>,
208 ) {
209 workspace.set_prompt_for_new_path(Box::new(
210 |workspace, lister, suggested_name, window, cx| {
211 let (tx, rx) = futures::channel::oneshot::channel();
212 Self::prompt_for_new_path(workspace, lister, suggested_name, tx, window, cx);
213 rx
214 },
215 ));
216 }
217
218 fn prompt_for_open_path(
219 workspace: &mut Workspace,
220 lister: DirectoryLister,
221 creating_path: bool,
222 suggested_name: Option<String>,
223 tx: oneshot::Sender<Option<Vec<PathBuf>>>,
224 window: &mut Window,
225 cx: &mut Context<Workspace>,
226 ) {
227 workspace.toggle_modal(window, cx, |window, cx| {
228 let delegate = OpenPathDelegate::new(tx, lister.clone(), creating_path, cx);
229 let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
230 let mut query = lister.default_query(cx);
231 if let Some(suggested_name) = suggested_name {
232 query.push_str(&suggested_name);
233 }
234 picker.set_query(&query, window, cx);
235 picker
236 });
237 }
238
239 fn prompt_for_new_path(
240 workspace: &mut Workspace,
241 lister: DirectoryLister,
242 suggested_name: Option<String>,
243 tx: oneshot::Sender<Option<Vec<PathBuf>>>,
244 window: &mut Window,
245 cx: &mut Context<Workspace>,
246 ) {
247 Self::prompt_for_open_path(workspace, lister, true, suggested_name, tx, window, cx);
248 }
249}
250
251impl PickerDelegate for OpenPathDelegate {
252 type ListItem = ui::ListItem;
253
254 fn match_count(&self) -> usize {
255 let user_input = if let DirectoryState::Create { user_input, .. } = &self.directory_state {
256 user_input
257 .as_ref()
258 .filter(|input| !input.exists || !input.is_dir)
259 .into_iter()
260 .count()
261 } else {
262 0
263 };
264 self.string_matches.len() + user_input
265 }
266
267 fn selected_index(&self) -> usize {
268 self.selected_index
269 }
270
271 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
272 self.selected_index = ix;
273 cx.notify();
274 }
275
276 fn update_matches(
277 &mut self,
278 query: String,
279 window: &mut Window,
280 cx: &mut Context<Picker<Self>>,
281 ) -> Task<()> {
282 let lister = &self.lister;
283 let input_is_empty = query.is_empty();
284 let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
285
286 let query = match &self.directory_state {
287 DirectoryState::List { parent_path, .. } => {
288 if parent_path == &dir {
289 None
290 } else {
291 Some(lister.list_directory(dir.clone(), cx))
292 }
293 }
294 DirectoryState::Create {
295 parent_path,
296 user_input,
297 ..
298 } => {
299 if parent_path == &dir
300 && user_input.as_ref().map(|input| &input.file.string) == Some(&suffix)
301 {
302 None
303 } else {
304 Some(lister.list_directory(dir.clone(), cx))
305 }
306 }
307 DirectoryState::None { .. } => Some(lister.list_directory(dir.clone(), cx)),
308 };
309 self.cancel_flag.store(true, atomic::Ordering::Release);
310 self.cancel_flag = Arc::new(AtomicBool::new(false));
311 let cancel_flag = self.cancel_flag.clone();
312 let hidden_entries = self.hidden_entries;
313 let parent_path_is_root = self.prompt_root == dir;
314 let current_dir = self.current_dir();
315 cx.spawn_in(window, async move |this, cx| {
316 if let Some(query) = query {
317 let paths = query.await;
318 if cancel_flag.load(atomic::Ordering::Acquire) {
319 return;
320 }
321
322 if this
323 .update(cx, |this, _| {
324 let new_state = match &this.delegate.directory_state {
325 DirectoryState::None { create: false }
326 | DirectoryState::List { .. } => match paths {
327 Ok(paths) => DirectoryState::List {
328 entries: path_candidates(parent_path_is_root, paths),
329 parent_path: dir.clone(),
330 error: None,
331 },
332 Err(e) => DirectoryState::List {
333 entries: Vec::new(),
334 parent_path: dir.clone(),
335 error: Some(SharedString::from(e.to_string())),
336 },
337 },
338 DirectoryState::None { create: true }
339 | DirectoryState::Create { .. } => match paths {
340 Ok(paths) => {
341 let mut entries = path_candidates(parent_path_is_root, paths);
342 let mut exists = false;
343 let mut is_dir = false;
344 let mut new_id = None;
345 entries.retain(|entry| {
346 new_id = new_id.max(Some(entry.path.id));
347 if entry.path.string == suffix {
348 exists = true;
349 is_dir = entry.is_dir;
350 }
351 !exists || is_dir
352 });
353
354 let new_id = new_id.map(|id| id + 1).unwrap_or(0);
355 let user_input = if suffix.is_empty() {
356 None
357 } else {
358 Some(UserInput {
359 file: StringMatchCandidate::new(new_id, &suffix),
360 exists,
361 is_dir,
362 })
363 };
364 DirectoryState::Create {
365 entries,
366 parent_path: dir.clone(),
367 user_input,
368 }
369 }
370 Err(_) => DirectoryState::Create {
371 entries: Vec::new(),
372 parent_path: dir.clone(),
373 user_input: Some(UserInput {
374 exists: false,
375 is_dir: false,
376 file: StringMatchCandidate::new(0, &suffix),
377 }),
378 },
379 },
380 };
381 this.delegate.directory_state = new_state;
382 })
383 .is_err()
384 {
385 return;
386 }
387 }
388
389 let Ok(mut new_entries) =
390 this.update(cx, |this, _| match &this.delegate.directory_state {
391 DirectoryState::List {
392 entries,
393 error: None,
394 ..
395 }
396 | DirectoryState::Create { entries, .. } => entries.clone(),
397 DirectoryState::List { error: Some(_), .. } | DirectoryState::None { .. } => {
398 Vec::new()
399 }
400 })
401 else {
402 return;
403 };
404
405 let max_id = new_entries
406 .iter()
407 .map(|entry| entry.path.id)
408 .max()
409 .unwrap_or(0);
410 if !suffix.starts_with('.') && !hidden_entries {
411 new_entries.retain(|entry| !entry.path.string.starts_with('.'));
412 }
413
414 if suffix.is_empty() {
415 let should_prepend_with_current_dir = this
416 .read_with(cx, |picker, _| {
417 !input_is_empty
418 && match &picker.delegate.directory_state {
419 DirectoryState::List { error, .. } => error.is_none(),
420 DirectoryState::Create { .. } => false,
421 DirectoryState::None { .. } => false,
422 }
423 })
424 .unwrap_or(false);
425
426 let current_dir_in_new_entries = new_entries
427 .iter()
428 .any(|entry| &entry.path.string == current_dir);
429
430 if should_prepend_with_current_dir && !current_dir_in_new_entries {
431 new_entries.insert(
432 0,
433 CandidateInfo {
434 path: StringMatchCandidate {
435 id: max_id + 1,
436 string: current_dir.to_string(),
437 char_bag: CharBag::from(current_dir),
438 },
439 is_dir: true,
440 },
441 );
442 }
443
444 this.update(cx, |this, cx| {
445 this.delegate.selected_index = 0;
446 this.delegate.string_matches = new_entries
447 .iter()
448 .map(|m| StringMatch {
449 candidate_id: m.path.id,
450 score: 0.0,
451 positions: Vec::new(),
452 string: m.path.string.clone(),
453 })
454 .collect();
455 this.delegate.directory_state =
456 match &this.delegate.directory_state {
457 DirectoryState::None { create: false }
458 | DirectoryState::List { .. } => DirectoryState::List {
459 parent_path: dir.clone(),
460 entries: new_entries,
461 error: None,
462 },
463 DirectoryState::None { create: true }
464 | DirectoryState::Create { .. } => DirectoryState::Create {
465 parent_path: dir.clone(),
466 user_input: None,
467 entries: new_entries,
468 },
469 };
470 cx.notify();
471 })
472 .ok();
473 return;
474 }
475
476 let Ok(is_create_state) =
477 this.update(cx, |this, _| match &this.delegate.directory_state {
478 DirectoryState::Create { .. } => true,
479 DirectoryState::List { .. } => false,
480 DirectoryState::None { create } => *create,
481 })
482 else {
483 return;
484 };
485
486 let candidates = new_entries
487 .iter()
488 .filter_map(|entry| {
489 if is_create_state && !entry.is_dir && Some(&suffix) == Some(&entry.path.string)
490 {
491 None
492 } else {
493 Some(&entry.path)
494 }
495 })
496 .collect::<Vec<_>>();
497
498 let matches = fuzzy::match_strings(
499 candidates.as_slice(),
500 &suffix,
501 false,
502 true,
503 100,
504 &cancel_flag,
505 cx.background_executor().clone(),
506 )
507 .await;
508 if cancel_flag.load(atomic::Ordering::Acquire) {
509 return;
510 }
511
512 this.update(cx, |this, cx| {
513 this.delegate.selected_index = 0;
514 this.delegate.string_matches = matches.clone();
515 this.delegate.string_matches.sort_by_key(|m| {
516 (
517 new_entries
518 .iter()
519 .find(|entry| entry.path.id == m.candidate_id)
520 .map(|entry| &entry.path)
521 .map(|candidate| !candidate.string.starts_with(&suffix)),
522 m.candidate_id,
523 )
524 });
525 this.delegate.directory_state = match &this.delegate.directory_state {
526 DirectoryState::None { create: false } | DirectoryState::List { .. } => {
527 DirectoryState::List {
528 entries: new_entries,
529 parent_path: dir.clone(),
530 error: None,
531 }
532 }
533 DirectoryState::None { create: true } => DirectoryState::Create {
534 entries: new_entries,
535 parent_path: dir.clone(),
536 user_input: Some(UserInput {
537 file: StringMatchCandidate::new(0, &suffix),
538 exists: false,
539 is_dir: false,
540 }),
541 },
542 DirectoryState::Create { user_input, .. } => {
543 let (new_id, exists, is_dir) = user_input
544 .as_ref()
545 .map(|input| (input.file.id, input.exists, input.is_dir))
546 .unwrap_or_else(|| (0, false, false));
547 DirectoryState::Create {
548 entries: new_entries,
549 parent_path: dir.clone(),
550 user_input: Some(UserInput {
551 file: StringMatchCandidate::new(new_id, &suffix),
552 exists,
553 is_dir,
554 }),
555 }
556 }
557 };
558
559 cx.notify();
560 })
561 .ok();
562 })
563 }
564
565 fn confirm_completion(
566 &mut self,
567 query: String,
568 _window: &mut Window,
569 _: &mut Context<Picker<Self>>,
570 ) -> Option<String> {
571 let candidate = self.get_entry(self.selected_index)?;
572 if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() {
573 return None;
574 }
575
576 let path_style = self.path_style;
577 Some(
578 maybe!({
579 match &self.directory_state {
580 DirectoryState::Create { parent_path, .. } => Some(format!(
581 "{}{}{}",
582 parent_path,
583 candidate.path.string,
584 if candidate.is_dir {
585 path_style.primary_separator()
586 } else {
587 ""
588 }
589 )),
590 DirectoryState::List { parent_path, .. } => Some(format!(
591 "{}{}{}",
592 parent_path,
593 candidate.path.string,
594 if candidate.is_dir {
595 path_style.primary_separator()
596 } else {
597 ""
598 }
599 )),
600 DirectoryState::None { .. } => return None,
601 }
602 })
603 .unwrap_or(query),
604 )
605 }
606
607 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
608 let Some(candidate) = self.get_entry(self.selected_index) else {
609 return;
610 };
611
612 match &self.directory_state {
613 DirectoryState::None { .. } => return,
614 DirectoryState::List { parent_path, .. } => {
615 let confirmed_path =
616 if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
617 PathBuf::from(&self.prompt_root)
618 } else {
619 Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
620 .join(&candidate.path.string)
621 };
622 if let Some(tx) = self.tx.take() {
623 tx.send(Some(vec![confirmed_path])).ok();
624 }
625 }
626 DirectoryState::Create {
627 parent_path,
628 user_input,
629 ..
630 } => match user_input {
631 None => return,
632 Some(user_input) => {
633 if user_input.is_dir {
634 return;
635 }
636 let prompted_path =
637 if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
638 PathBuf::from(&self.prompt_root)
639 } else {
640 Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
641 .join(&user_input.file.string)
642 };
643 if user_input.exists {
644 self.should_dismiss = false;
645 let answer = window.prompt(
646 gpui::PromptLevel::Critical,
647 &format!("{prompted_path:?} already exists. Do you want to replace it?"),
648 Some(
649 "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
650 ),
651 &["Replace", "Cancel"],
652 cx
653 );
654 self.replace_prompt = cx.spawn_in(window, async move |picker, cx| {
655 let answer = answer.await.ok();
656 picker
657 .update(cx, |picker, cx| {
658 picker.delegate.should_dismiss = true;
659 if answer != Some(0) {
660 return;
661 }
662 if let Some(tx) = picker.delegate.tx.take() {
663 tx.send(Some(vec![prompted_path])).ok();
664 }
665 cx.emit(gpui::DismissEvent);
666 })
667 .ok();
668 });
669 return;
670 } else if let Some(tx) = self.tx.take() {
671 tx.send(Some(vec![prompted_path])).ok();
672 }
673 }
674 },
675 }
676
677 cx.emit(gpui::DismissEvent);
678 }
679
680 fn should_dismiss(&self) -> bool {
681 self.should_dismiss
682 }
683
684 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
685 if let Some(tx) = self.tx.take() {
686 tx.send(None).ok();
687 }
688 cx.emit(gpui::DismissEvent)
689 }
690
691 fn render_match(
692 &self,
693 ix: usize,
694 selected: bool,
695 window: &mut Window,
696 cx: &mut Context<Picker<Self>>,
697 ) -> Option<Self::ListItem> {
698 let settings = FileFinderSettings::get_global(cx);
699 let candidate = self.get_entry(ix)?;
700 let mut match_positions = match &self.directory_state {
701 DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
702 DirectoryState::Create { user_input, .. } => {
703 if let Some(user_input) = user_input {
704 if !user_input.exists || !user_input.is_dir {
705 if ix == 0 {
706 Vec::new()
707 } else {
708 self.string_matches.get(ix - 1)?.positions.clone()
709 }
710 } else {
711 self.string_matches.get(ix)?.positions.clone()
712 }
713 } else {
714 self.string_matches.get(ix)?.positions.clone()
715 }
716 }
717 DirectoryState::None { .. } => Vec::new(),
718 };
719
720 let is_current_dir_candidate = candidate.path.string == self.current_dir();
721
722 let file_icon = maybe!({
723 if !settings.file_icons {
724 return None;
725 }
726
727 let path = path::Path::new(&candidate.path.string);
728 let icon = if candidate.is_dir {
729 if is_current_dir_candidate {
730 return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
731 } else {
732 FileIcons::get_folder_icon(false, path, cx)?
733 }
734 } else {
735 FileIcons::get_icon(path, cx)?
736 };
737 Some(Icon::from_path(icon).color(Color::Muted))
738 });
739
740 match &self.directory_state {
741 DirectoryState::List { parent_path, .. } => {
742 let (label, indices) = if is_current_dir_candidate {
743 ("open this directory".to_string(), vec![])
744 } else if *parent_path == self.prompt_root {
745 match_positions.iter_mut().for_each(|position| {
746 *position += self.prompt_root.len();
747 });
748 (
749 format!("{}{}", self.prompt_root, candidate.path.string),
750 match_positions,
751 )
752 } else {
753 (candidate.path.string, match_positions)
754 };
755 Some(
756 ListItem::new(ix)
757 .spacing(ListItemSpacing::Sparse)
758 .start_slot::<Icon>(file_icon)
759 .inset(true)
760 .toggle_state(selected)
761 .child(HighlightedLabel::new(label, indices)),
762 )
763 }
764 DirectoryState::Create {
765 parent_path,
766 user_input,
767 ..
768 } => {
769 let (label, delta) = if *parent_path == self.prompt_root {
770 match_positions.iter_mut().for_each(|position| {
771 *position += self.prompt_root.len();
772 });
773 (
774 format!("{}{}", self.prompt_root, candidate.path.string),
775 self.prompt_root.len(),
776 )
777 } else {
778 (candidate.path.string.clone(), 0)
779 };
780
781 let label_with_highlights = match user_input {
782 Some(user_input) => {
783 let label_len = label.len();
784 if user_input.file.string == candidate.path.string {
785 if user_input.exists {
786 let label = if user_input.is_dir {
787 label
788 } else {
789 format!("{label} (replace)")
790 };
791 StyledText::new(label)
792 .with_default_highlights(
793 &window.text_style(),
794 vec![(
795 delta..label_len,
796 HighlightStyle::color(Color::Conflict.color(cx)),
797 )],
798 )
799 .into_any_element()
800 } else {
801 StyledText::new(format!("{label} (create)"))
802 .with_default_highlights(
803 &window.text_style(),
804 vec![(
805 delta..label_len,
806 HighlightStyle::color(Color::Created.color(cx)),
807 )],
808 )
809 .into_any_element()
810 }
811 } else {
812 HighlightedLabel::new(label, match_positions).into_any_element()
813 }
814 }
815 None => HighlightedLabel::new(label, match_positions).into_any_element(),
816 };
817
818 Some(
819 ListItem::new(ix)
820 .spacing(ListItemSpacing::Sparse)
821 .start_slot::<Icon>(file_icon)
822 .inset(true)
823 .toggle_state(selected)
824 .child(LabelLike::new().child(label_with_highlights)),
825 )
826 }
827 DirectoryState::None { .. } => None,
828 }
829 }
830
831 fn render_footer(
832 &self,
833 window: &mut Window,
834 cx: &mut Context<Picker<Self>>,
835 ) -> Option<AnyElement> {
836 (self.render_footer)(window, cx)
837 }
838
839 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
840 Some(match &self.directory_state {
841 DirectoryState::Create { .. } => SharedString::from("Type a path…"),
842 DirectoryState::List {
843 error: Some(error), ..
844 } => error.clone(),
845 DirectoryState::List { .. } | DirectoryState::None { .. } => {
846 SharedString::from("No such file or directory")
847 }
848 })
849 }
850
851 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
852 Arc::from(
853 format!(
854 "[directory{}]filename.ext",
855 self.path_style.primary_separator()
856 )
857 .as_str(),
858 )
859 }
860
861 fn separators_after_indices(&self) -> Vec<usize> {
862 let Some(m) = self.string_matches.first() else {
863 return Vec::new();
864 };
865 if m.string == self.current_dir() {
866 vec![0]
867 } else {
868 Vec::new()
869 }
870 }
871}
872
873fn path_candidates(
874 parent_path_is_root: bool,
875 mut children: Vec<DirectoryItem>,
876) -> Vec<CandidateInfo> {
877 if parent_path_is_root {
878 children.push(DirectoryItem {
879 is_dir: true,
880 path: PathBuf::default(),
881 });
882 }
883
884 children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
885 children
886 .iter()
887 .enumerate()
888 .map(|(ix, item)| CandidateInfo {
889 path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
890 is_dir: item.is_dir,
891 })
892 .collect()
893}
894
895#[cfg(target_os = "windows")]
896fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
897 let last_item = Path::new(&query)
898 .file_name()
899 .unwrap_or_default()
900 .to_string_lossy();
901 let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
902 (dir.to_string(), last_item.into_owned())
903 } else {
904 (query.to_string(), String::new())
905 };
906 match path_style {
907 PathStyle::Posix => {
908 if dir.is_empty() {
909 dir = "/".to_string();
910 }
911 }
912 PathStyle::Windows => {
913 if dir.len() < 3 {
914 dir = "C:\\".to_string();
915 }
916 }
917 }
918 (dir, suffix)
919}
920
921#[cfg(not(target_os = "windows"))]
922fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
923 match path_style {
924 PathStyle::Posix => {
925 let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
926 (query[..index].to_string(), query[index + 1..].to_string())
927 } else {
928 (query, String::new())
929 };
930 if !dir.ends_with('/') {
931 dir.push('/');
932 }
933 (dir, suffix)
934 }
935 PathStyle::Windows => {
936 let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
937 (query[..index].to_string(), query[index + 1..].to_string())
938 } else {
939 (query, String::new())
940 };
941 if dir.len() < 3 {
942 dir = "C:\\".to_string();
943 }
944 if !dir.ends_with('\\') {
945 dir.push('\\');
946 }
947 (dir, suffix)
948 }
949 }
950}
951
952#[cfg(test)]
953mod tests {
954 use util::paths::PathStyle;
955
956 use super::get_dir_and_suffix;
957
958 #[test]
959 fn test_get_dir_and_suffix_with_windows_style() {
960 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
961 assert_eq!(dir, "C:\\");
962 assert_eq!(suffix, "");
963
964 let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
965 assert_eq!(dir, "C:\\");
966 assert_eq!(suffix, "");
967
968 let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
969 assert_eq!(dir, "C:\\");
970 assert_eq!(suffix, "");
971
972 let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
973 assert_eq!(dir, "C:\\");
974 assert_eq!(suffix, "Use");
975
976 let (dir, suffix) =
977 get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
978 assert_eq!(dir, "C:\\Users\\Junkui\\");
979 assert_eq!(suffix, "Docum");
980
981 let (dir, suffix) =
982 get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
983 assert_eq!(dir, "C:\\Users\\Junkui\\");
984 assert_eq!(suffix, "Documents");
985
986 let (dir, suffix) =
987 get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
988 assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
989 assert_eq!(suffix, "");
990 }
991
992 #[test]
993 fn test_get_dir_and_suffix_with_posix_style() {
994 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
995 assert_eq!(dir, "/");
996 assert_eq!(suffix, "");
997
998 let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
999 assert_eq!(dir, "/");
1000 assert_eq!(suffix, "");
1001
1002 let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
1003 assert_eq!(dir, "/");
1004 assert_eq!(suffix, "Use");
1005
1006 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
1007 assert_eq!(dir, "/Users/Junkui/");
1008 assert_eq!(suffix, "Docum");
1009
1010 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
1011 assert_eq!(dir, "/Users/Junkui/");
1012 assert_eq!(suffix, "Documents");
1013
1014 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
1015 assert_eq!(dir, "/Users/Junkui/Documents/");
1016 assert_eq!(suffix, "");
1017 }
1018}