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 =
229 OpenPathDelegate::new(tx, lister.clone(), creating_path, cx).show_hidden();
230 let picker = Picker::uniform_list(delegate, window, cx).width(rems(34.));
231 let mut query = lister.default_query(cx);
232 if let Some(suggested_name) = suggested_name {
233 query.push_str(&suggested_name);
234 }
235 picker.set_query(&query, window, cx);
236 picker
237 });
238 }
239
240 fn prompt_for_new_path(
241 workspace: &mut Workspace,
242 lister: DirectoryLister,
243 suggested_name: Option<String>,
244 tx: oneshot::Sender<Option<Vec<PathBuf>>>,
245 window: &mut Window,
246 cx: &mut Context<Workspace>,
247 ) {
248 Self::prompt_for_open_path(workspace, lister, true, suggested_name, tx, window, cx);
249 }
250}
251
252impl PickerDelegate for OpenPathDelegate {
253 type ListItem = ui::ListItem;
254
255 fn match_count(&self) -> usize {
256 let user_input = if let DirectoryState::Create { user_input, .. } = &self.directory_state {
257 user_input
258 .as_ref()
259 .filter(|input| !input.exists || !input.is_dir)
260 .into_iter()
261 .count()
262 } else {
263 0
264 };
265 self.string_matches.len() + user_input
266 }
267
268 fn selected_index(&self) -> usize {
269 self.selected_index
270 }
271
272 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
273 self.selected_index = ix;
274 cx.notify();
275 }
276
277 fn update_matches(
278 &mut self,
279 query: String,
280 window: &mut Window,
281 cx: &mut Context<Picker<Self>>,
282 ) -> Task<()> {
283 let lister = &self.lister;
284 let input_is_empty = query.is_empty();
285 let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
286
287 let query = match &self.directory_state {
288 DirectoryState::List { parent_path, .. } => {
289 if parent_path == &dir {
290 None
291 } else {
292 Some(lister.list_directory(dir.clone(), cx))
293 }
294 }
295 DirectoryState::Create {
296 parent_path,
297 user_input,
298 ..
299 } => {
300 if parent_path == &dir
301 && user_input.as_ref().map(|input| &input.file.string) == Some(&suffix)
302 {
303 None
304 } else {
305 Some(lister.list_directory(dir.clone(), cx))
306 }
307 }
308 DirectoryState::None { .. } => Some(lister.list_directory(dir.clone(), cx)),
309 };
310 self.cancel_flag.store(true, atomic::Ordering::Release);
311 self.cancel_flag = Arc::new(AtomicBool::new(false));
312 let cancel_flag = self.cancel_flag.clone();
313 let hidden_entries = self.hidden_entries;
314 let parent_path_is_root = self.prompt_root == dir;
315 let current_dir = self.current_dir();
316 cx.spawn_in(window, async move |this, cx| {
317 if let Some(query) = query {
318 let paths = query.await;
319 if cancel_flag.load(atomic::Ordering::Acquire) {
320 return;
321 }
322
323 if this
324 .update(cx, |this, _| {
325 let new_state = match &this.delegate.directory_state {
326 DirectoryState::None { create: false }
327 | DirectoryState::List { .. } => match paths {
328 Ok(paths) => DirectoryState::List {
329 entries: path_candidates(parent_path_is_root, paths),
330 parent_path: dir.clone(),
331 error: None,
332 },
333 Err(e) => DirectoryState::List {
334 entries: Vec::new(),
335 parent_path: dir.clone(),
336 error: Some(SharedString::from(e.to_string())),
337 },
338 },
339 DirectoryState::None { create: true }
340 | DirectoryState::Create { .. } => match paths {
341 Ok(paths) => {
342 let mut entries = path_candidates(parent_path_is_root, paths);
343 let mut exists = false;
344 let mut is_dir = false;
345 let mut new_id = None;
346 entries.retain(|entry| {
347 new_id = new_id.max(Some(entry.path.id));
348 if entry.path.string == suffix {
349 exists = true;
350 is_dir = entry.is_dir;
351 }
352 !exists || is_dir
353 });
354
355 let new_id = new_id.map(|id| id + 1).unwrap_or(0);
356 let user_input = if suffix.is_empty() {
357 None
358 } else {
359 Some(UserInput {
360 file: StringMatchCandidate::new(new_id, &suffix),
361 exists,
362 is_dir,
363 })
364 };
365 DirectoryState::Create {
366 entries,
367 parent_path: dir.clone(),
368 user_input,
369 }
370 }
371 Err(_) => DirectoryState::Create {
372 entries: Vec::new(),
373 parent_path: dir.clone(),
374 user_input: Some(UserInput {
375 exists: false,
376 is_dir: false,
377 file: StringMatchCandidate::new(0, &suffix),
378 }),
379 },
380 },
381 };
382 this.delegate.directory_state = new_state;
383 })
384 .is_err()
385 {
386 return;
387 }
388 }
389
390 let Ok(mut new_entries) =
391 this.update(cx, |this, _| match &this.delegate.directory_state {
392 DirectoryState::List {
393 entries,
394 error: None,
395 ..
396 }
397 | DirectoryState::Create { entries, .. } => entries.clone(),
398 DirectoryState::List { error: Some(_), .. } | DirectoryState::None { .. } => {
399 Vec::new()
400 }
401 })
402 else {
403 return;
404 };
405
406 if !hidden_entries {
407 new_entries.retain(|entry| !entry.path.string.starts_with('.'));
408 }
409
410 let max_id = new_entries
411 .iter()
412 .map(|entry| entry.path.id)
413 .max()
414 .unwrap_or(0);
415
416 if suffix.is_empty() {
417 let should_prepend_with_current_dir = this
418 .read_with(cx, |picker, _| {
419 !input_is_empty
420 && match &picker.delegate.directory_state {
421 DirectoryState::List { error, .. } => error.is_none(),
422 DirectoryState::Create { .. } => false,
423 DirectoryState::None { .. } => false,
424 }
425 })
426 .unwrap_or(false);
427
428 let current_dir_in_new_entries = new_entries
429 .iter()
430 .any(|entry| &entry.path.string == current_dir);
431
432 if should_prepend_with_current_dir && !current_dir_in_new_entries {
433 new_entries.insert(
434 0,
435 CandidateInfo {
436 path: StringMatchCandidate {
437 id: max_id + 1,
438 string: current_dir.to_string(),
439 char_bag: CharBag::from(current_dir),
440 },
441 is_dir: true,
442 },
443 );
444 }
445
446 this.update(cx, |this, cx| {
447 this.delegate.selected_index = 0;
448 this.delegate.string_matches = new_entries
449 .iter()
450 .map(|m| StringMatch {
451 candidate_id: m.path.id,
452 score: 0.0,
453 positions: Vec::new(),
454 string: m.path.string.clone(),
455 })
456 .collect();
457 this.delegate.directory_state =
458 match &this.delegate.directory_state {
459 DirectoryState::None { create: false }
460 | DirectoryState::List { .. } => DirectoryState::List {
461 parent_path: dir.clone(),
462 entries: new_entries,
463 error: None,
464 },
465 DirectoryState::None { create: true }
466 | DirectoryState::Create { .. } => DirectoryState::Create {
467 parent_path: dir.clone(),
468 user_input: None,
469 entries: new_entries,
470 },
471 };
472 cx.notify();
473 })
474 .ok();
475 return;
476 }
477
478 let Ok(is_create_state) =
479 this.update(cx, |this, _| match &this.delegate.directory_state {
480 DirectoryState::Create { .. } => true,
481 DirectoryState::List { .. } => false,
482 DirectoryState::None { create } => *create,
483 })
484 else {
485 return;
486 };
487
488 let candidates = new_entries
489 .iter()
490 .filter_map(|entry| {
491 if is_create_state && !entry.is_dir && Some(&suffix) == Some(&entry.path.string)
492 {
493 None
494 } else if !suffix.is_empty() && entry.path.string == current_dir {
495 None
496 } else {
497 Some(&entry.path)
498 }
499 })
500 .collect::<Vec<_>>();
501
502 let matches = fuzzy::match_strings(
503 candidates.as_slice(),
504 &suffix,
505 false,
506 true,
507 100,
508 &cancel_flag,
509 cx.background_executor().clone(),
510 )
511 .await;
512 if cancel_flag.load(atomic::Ordering::Acquire) {
513 return;
514 }
515
516 this.update(cx, |this, cx| {
517 this.delegate.selected_index = 0;
518 this.delegate.string_matches = matches.clone();
519 this.delegate.string_matches.sort_by_key(|m| {
520 (
521 new_entries
522 .iter()
523 .find(|entry| entry.path.id == m.candidate_id)
524 .map(|entry| &entry.path)
525 .map(|candidate| !candidate.string.starts_with(&suffix)),
526 m.candidate_id,
527 )
528 });
529 this.delegate.directory_state = match &this.delegate.directory_state {
530 DirectoryState::None { create: false } | DirectoryState::List { .. } => {
531 DirectoryState::List {
532 entries: new_entries,
533 parent_path: dir.clone(),
534 error: None,
535 }
536 }
537 DirectoryState::None { create: true } => DirectoryState::Create {
538 entries: new_entries,
539 parent_path: dir.clone(),
540 user_input: Some(UserInput {
541 file: StringMatchCandidate::new(0, &suffix),
542 exists: false,
543 is_dir: false,
544 }),
545 },
546 DirectoryState::Create { user_input, .. } => {
547 let (new_id, exists, is_dir) = user_input
548 .as_ref()
549 .map(|input| (input.file.id, input.exists, input.is_dir))
550 .unwrap_or_else(|| (0, false, false));
551 DirectoryState::Create {
552 entries: new_entries,
553 parent_path: dir.clone(),
554 user_input: Some(UserInput {
555 file: StringMatchCandidate::new(new_id, &suffix),
556 exists,
557 is_dir,
558 }),
559 }
560 }
561 };
562
563 cx.notify();
564 })
565 .ok();
566 })
567 }
568
569 fn confirm_completion(
570 &mut self,
571 query: String,
572 _window: &mut Window,
573 _: &mut Context<Picker<Self>>,
574 ) -> Option<String> {
575 let candidate = self.get_entry(self.selected_index)?;
576 if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() {
577 return None;
578 }
579
580 let path_style = self.path_style;
581 Some(
582 maybe!({
583 match &self.directory_state {
584 DirectoryState::Create { parent_path, .. } => Some(format!(
585 "{}{}{}",
586 parent_path,
587 candidate.path.string,
588 if candidate.is_dir {
589 path_style.primary_separator()
590 } else {
591 ""
592 }
593 )),
594 DirectoryState::List { parent_path, .. } => Some(format!(
595 "{}{}{}",
596 parent_path,
597 candidate.path.string,
598 if candidate.is_dir {
599 path_style.primary_separator()
600 } else {
601 ""
602 }
603 )),
604 DirectoryState::None { .. } => return None,
605 }
606 })
607 .unwrap_or(query),
608 )
609 }
610
611 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
612 let Some(candidate) = self.get_entry(self.selected_index) else {
613 return;
614 };
615
616 match &self.directory_state {
617 DirectoryState::None { .. } => return,
618 DirectoryState::List { parent_path, .. } => {
619 let confirmed_path =
620 if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
621 PathBuf::from(&self.prompt_root)
622 } else {
623 Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
624 .join(&candidate.path.string)
625 };
626 if let Some(tx) = self.tx.take() {
627 tx.send(Some(vec![confirmed_path])).ok();
628 }
629 }
630 DirectoryState::Create {
631 parent_path,
632 user_input,
633 ..
634 } => match user_input {
635 None => return,
636 Some(user_input) => {
637 if user_input.is_dir {
638 return;
639 }
640 let prompted_path =
641 if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
642 PathBuf::from(&self.prompt_root)
643 } else {
644 Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
645 .join(&user_input.file.string)
646 };
647 if user_input.exists {
648 self.should_dismiss = false;
649 let answer = window.prompt(
650 gpui::PromptLevel::Critical,
651 &format!("{prompted_path:?} already exists. Do you want to replace it?"),
652 Some(
653 "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
654 ),
655 &["Replace", "Cancel"],
656 cx
657 );
658 self.replace_prompt = cx.spawn_in(window, async move |picker, cx| {
659 let answer = answer.await.ok();
660 picker
661 .update(cx, |picker, cx| {
662 picker.delegate.should_dismiss = true;
663 if answer != Some(0) {
664 return;
665 }
666 if let Some(tx) = picker.delegate.tx.take() {
667 tx.send(Some(vec![prompted_path])).ok();
668 }
669 cx.emit(gpui::DismissEvent);
670 })
671 .ok();
672 });
673 return;
674 } else if let Some(tx) = self.tx.take() {
675 tx.send(Some(vec![prompted_path])).ok();
676 }
677 }
678 },
679 }
680
681 cx.emit(gpui::DismissEvent);
682 }
683
684 fn should_dismiss(&self) -> bool {
685 self.should_dismiss
686 }
687
688 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
689 if let Some(tx) = self.tx.take() {
690 tx.send(None).ok();
691 }
692 cx.emit(gpui::DismissEvent)
693 }
694
695 fn render_match(
696 &self,
697 ix: usize,
698 selected: bool,
699 window: &mut Window,
700 cx: &mut Context<Picker<Self>>,
701 ) -> Option<Self::ListItem> {
702 let settings = FileFinderSettings::get_global(cx);
703 let candidate = self.get_entry(ix)?;
704 let mut match_positions = match &self.directory_state {
705 DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
706 DirectoryState::Create { user_input, .. } => {
707 if let Some(user_input) = user_input {
708 if !user_input.exists || !user_input.is_dir {
709 if ix == 0 {
710 Vec::new()
711 } else {
712 self.string_matches.get(ix - 1)?.positions.clone()
713 }
714 } else {
715 self.string_matches.get(ix)?.positions.clone()
716 }
717 } else {
718 self.string_matches.get(ix)?.positions.clone()
719 }
720 }
721 DirectoryState::None { .. } => Vec::new(),
722 };
723
724 let is_current_dir_candidate = candidate.path.string == self.current_dir();
725
726 let file_icon = maybe!({
727 if !settings.file_icons {
728 return None;
729 }
730
731 let path = path::Path::new(&candidate.path.string);
732 let icon = if candidate.is_dir {
733 if is_current_dir_candidate {
734 return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
735 } else {
736 FileIcons::get_folder_icon(false, path, cx)?
737 }
738 } else {
739 FileIcons::get_icon(path, cx)?
740 };
741 Some(Icon::from_path(icon).color(Color::Muted))
742 });
743
744 match &self.directory_state {
745 DirectoryState::List { parent_path, .. } => {
746 let (label, indices) = if is_current_dir_candidate {
747 ("open this directory".to_string(), vec![])
748 } else if *parent_path == self.prompt_root {
749 match_positions.iter_mut().for_each(|position| {
750 *position += self.prompt_root.len();
751 });
752 (
753 format!("{}{}", self.prompt_root, candidate.path.string),
754 match_positions,
755 )
756 } else {
757 (candidate.path.string, match_positions)
758 };
759 Some(
760 ListItem::new(ix)
761 .spacing(ListItemSpacing::Sparse)
762 .start_slot::<Icon>(file_icon)
763 .inset(true)
764 .toggle_state(selected)
765 .child(HighlightedLabel::new(label, indices)),
766 )
767 }
768 DirectoryState::Create {
769 parent_path,
770 user_input,
771 ..
772 } => {
773 let (label, delta) = if *parent_path == self.prompt_root {
774 match_positions.iter_mut().for_each(|position| {
775 *position += self.prompt_root.len();
776 });
777 (
778 format!("{}{}", self.prompt_root, candidate.path.string),
779 self.prompt_root.len(),
780 )
781 } else {
782 (candidate.path.string.clone(), 0)
783 };
784
785 let label_with_highlights = match user_input {
786 Some(user_input) => {
787 let label_len = label.len();
788 if user_input.file.string == candidate.path.string {
789 if user_input.exists {
790 let label = if user_input.is_dir {
791 label
792 } else {
793 format!("{label} (replace)")
794 };
795 StyledText::new(label)
796 .with_default_highlights(
797 &window.text_style(),
798 vec![(
799 delta..label_len,
800 HighlightStyle::color(Color::Conflict.color(cx)),
801 )],
802 )
803 .into_any_element()
804 } else {
805 StyledText::new(format!("{label} (create)"))
806 .with_default_highlights(
807 &window.text_style(),
808 vec![(
809 delta..label_len,
810 HighlightStyle::color(Color::Created.color(cx)),
811 )],
812 )
813 .into_any_element()
814 }
815 } else {
816 HighlightedLabel::new(label, match_positions).into_any_element()
817 }
818 }
819 None => HighlightedLabel::new(label, match_positions).into_any_element(),
820 };
821
822 Some(
823 ListItem::new(ix)
824 .spacing(ListItemSpacing::Sparse)
825 .start_slot::<Icon>(file_icon)
826 .inset(true)
827 .toggle_state(selected)
828 .child(LabelLike::new().child(label_with_highlights)),
829 )
830 }
831 DirectoryState::None { .. } => None,
832 }
833 }
834
835 fn render_footer(
836 &self,
837 window: &mut Window,
838 cx: &mut Context<Picker<Self>>,
839 ) -> Option<AnyElement> {
840 (self.render_footer)(window, cx)
841 }
842
843 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
844 Some(match &self.directory_state {
845 DirectoryState::Create { .. } => SharedString::from("Type a path…"),
846 DirectoryState::List {
847 error: Some(error), ..
848 } => error.clone(),
849 DirectoryState::List { .. } | DirectoryState::None { .. } => {
850 SharedString::from("No such file or directory")
851 }
852 })
853 }
854
855 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
856 Arc::from(
857 format!(
858 "[directory{}]filename.ext",
859 self.path_style.primary_separator()
860 )
861 .as_str(),
862 )
863 }
864
865 fn separators_after_indices(&self) -> Vec<usize> {
866 let Some(m) = self.string_matches.first() else {
867 return Vec::new();
868 };
869 if m.string == self.current_dir() {
870 vec![0]
871 } else {
872 Vec::new()
873 }
874 }
875}
876
877fn path_candidates(
878 parent_path_is_root: bool,
879 mut children: Vec<DirectoryItem>,
880) -> Vec<CandidateInfo> {
881 if parent_path_is_root {
882 children.push(DirectoryItem {
883 is_dir: true,
884 path: PathBuf::default(),
885 });
886 }
887
888 children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
889 children
890 .iter()
891 .enumerate()
892 .map(|(ix, item)| CandidateInfo {
893 path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
894 is_dir: item.is_dir,
895 })
896 .collect()
897}
898
899fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
900 match path_style {
901 PathStyle::Posix => {
902 let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
903 (query[..index].to_string(), query[index + 1..].to_string())
904 } else {
905 (query, String::new())
906 };
907 if !dir.ends_with('/') {
908 dir.push('/');
909 }
910 (dir, suffix)
911 }
912 PathStyle::Windows => {
913 let last_sep = query.rfind('\\').into_iter().chain(query.rfind('/')).max();
914 let (mut dir, suffix) = if let Some(index) = last_sep {
915 (
916 query[..index + 1].to_string(),
917 query[index + 1..].to_string(),
918 )
919 } else {
920 (query, String::new())
921 };
922 if dir.len() < 3 {
923 dir = "C:\\".to_string();
924 }
925 (dir, suffix)
926 }
927 }
928}
929
930#[cfg(test)]
931mod tests {
932 use util::paths::PathStyle;
933
934 use super::get_dir_and_suffix;
935
936 #[test]
937 fn test_get_dir_and_suffix_with_windows_style() {
938 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
939 assert_eq!(dir, "C:\\");
940 assert_eq!(suffix, "");
941
942 let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
943 assert_eq!(dir, "C:\\");
944 assert_eq!(suffix, "");
945
946 let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
947 assert_eq!(dir, "C:\\");
948 assert_eq!(suffix, "");
949
950 let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
951 assert_eq!(dir, "C:\\");
952 assert_eq!(suffix, "Use");
953
954 let (dir, suffix) =
955 get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
956 assert_eq!(dir, "C:\\Users\\Junkui\\");
957 assert_eq!(suffix, "Docum");
958
959 let (dir, suffix) =
960 get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
961 assert_eq!(dir, "C:\\Users\\Junkui\\");
962 assert_eq!(suffix, "Documents");
963
964 let (dir, suffix) =
965 get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
966 assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
967 assert_eq!(suffix, "");
968
969 let (dir, suffix) = get_dir_and_suffix("C:\\root\\.".into(), PathStyle::Windows);
970 assert_eq!(dir, "C:\\root\\");
971 assert_eq!(suffix, ".");
972
973 let (dir, suffix) = get_dir_and_suffix("C:\\root\\..".into(), PathStyle::Windows);
974 assert_eq!(dir, "C:\\root\\");
975 assert_eq!(suffix, "..");
976
977 let (dir, suffix) = get_dir_and_suffix("C:\\root\\.hidden".into(), PathStyle::Windows);
978 assert_eq!(dir, "C:\\root\\");
979 assert_eq!(suffix, ".hidden");
980
981 let (dir, suffix) = get_dir_and_suffix("C:/root/".into(), PathStyle::Windows);
982 assert_eq!(dir, "C:/root/");
983 assert_eq!(suffix, "");
984
985 let (dir, suffix) = get_dir_and_suffix("C:/root/Use".into(), PathStyle::Windows);
986 assert_eq!(dir, "C:/root/");
987 assert_eq!(suffix, "Use");
988
989 let (dir, suffix) = get_dir_and_suffix("C:\\root/Use".into(), PathStyle::Windows);
990 assert_eq!(dir, "C:\\root/");
991 assert_eq!(suffix, "Use");
992
993 let (dir, suffix) = get_dir_and_suffix("C:/root\\.hidden".into(), PathStyle::Windows);
994 assert_eq!(dir, "C:/root\\");
995 assert_eq!(suffix, ".hidden");
996 }
997
998 #[test]
999 fn test_get_dir_and_suffix_with_posix_style() {
1000 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
1001 assert_eq!(dir, "/");
1002 assert_eq!(suffix, "");
1003
1004 let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
1005 assert_eq!(dir, "/");
1006 assert_eq!(suffix, "");
1007
1008 let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
1009 assert_eq!(dir, "/");
1010 assert_eq!(suffix, "Use");
1011
1012 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
1013 assert_eq!(dir, "/Users/Junkui/");
1014 assert_eq!(suffix, "Docum");
1015
1016 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
1017 assert_eq!(dir, "/Users/Junkui/");
1018 assert_eq!(suffix, "Documents");
1019
1020 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
1021 assert_eq!(dir, "/Users/Junkui/Documents/");
1022 assert_eq!(suffix, "");
1023
1024 let (dir, suffix) = get_dir_and_suffix("/root/.".into(), PathStyle::Posix);
1025 assert_eq!(dir, "/root/");
1026 assert_eq!(suffix, ".");
1027
1028 let (dir, suffix) = get_dir_and_suffix("/root/..".into(), PathStyle::Posix);
1029 assert_eq!(dir, "/root/");
1030 assert_eq!(suffix, "..");
1031
1032 let (dir, suffix) = get_dir_and_suffix("/root/.hidden".into(), PathStyle::Posix);
1033 assert_eq!(dir, "/root/");
1034 assert_eq!(suffix, ".hidden");
1035 }
1036}