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