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, 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::local());
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 && match &picker.delegate.directory_state {
396 DirectoryState::List { error, .. } => error.is_none(),
397 DirectoryState::Create { .. } => false,
398 DirectoryState::None { .. } => false,
399 }
400 })
401 .unwrap_or(false);
402 if should_prepend_with_current_dir {
403 new_entries.insert(
404 0,
405 CandidateInfo {
406 path: StringMatchCandidate {
407 id: max_id + 1,
408 string: current_dir.to_string(),
409 char_bag: CharBag::from(current_dir),
410 },
411 is_dir: true,
412 },
413 );
414 }
415
416 this.update(cx, |this, cx| {
417 this.delegate.selected_index = 0;
418 this.delegate.string_matches = new_entries
419 .iter()
420 .map(|m| StringMatch {
421 candidate_id: m.path.id,
422 score: 0.0,
423 positions: Vec::new(),
424 string: m.path.string.clone(),
425 })
426 .collect();
427 this.delegate.directory_state =
428 match &this.delegate.directory_state {
429 DirectoryState::None { create: false }
430 | DirectoryState::List { .. } => DirectoryState::List {
431 parent_path: dir.clone(),
432 entries: new_entries,
433 error: None,
434 },
435 DirectoryState::None { create: true }
436 | DirectoryState::Create { .. } => DirectoryState::Create {
437 parent_path: dir.clone(),
438 user_input: None,
439 entries: new_entries,
440 },
441 };
442 cx.notify();
443 })
444 .ok();
445 return;
446 }
447
448 let Ok(is_create_state) =
449 this.update(cx, |this, _| match &this.delegate.directory_state {
450 DirectoryState::Create { .. } => true,
451 DirectoryState::List { .. } => false,
452 DirectoryState::None { create } => *create,
453 })
454 else {
455 return;
456 };
457
458 let candidates = new_entries
459 .iter()
460 .filter_map(|entry| {
461 if is_create_state && !entry.is_dir && Some(&suffix) == Some(&entry.path.string)
462 {
463 None
464 } else {
465 Some(&entry.path)
466 }
467 })
468 .collect::<Vec<_>>();
469
470 let matches = fuzzy::match_strings(
471 candidates.as_slice(),
472 &suffix,
473 false,
474 true,
475 100,
476 &cancel_flag,
477 cx.background_executor().clone(),
478 )
479 .await;
480 if cancel_flag.load(atomic::Ordering::Acquire) {
481 return;
482 }
483
484 this.update(cx, |this, cx| {
485 this.delegate.selected_index = 0;
486 this.delegate.string_matches = matches.clone();
487 this.delegate.string_matches.sort_by_key(|m| {
488 (
489 new_entries
490 .iter()
491 .find(|entry| entry.path.id == m.candidate_id)
492 .map(|entry| &entry.path)
493 .map(|candidate| !candidate.string.starts_with(&suffix)),
494 m.candidate_id,
495 )
496 });
497 this.delegate.directory_state = match &this.delegate.directory_state {
498 DirectoryState::None { create: false } | DirectoryState::List { .. } => {
499 DirectoryState::List {
500 entries: new_entries,
501 parent_path: dir.clone(),
502 error: None,
503 }
504 }
505 DirectoryState::None { create: true } => DirectoryState::Create {
506 entries: new_entries,
507 parent_path: dir.clone(),
508 user_input: Some(UserInput {
509 file: StringMatchCandidate::new(0, &suffix),
510 exists: false,
511 is_dir: false,
512 }),
513 },
514 DirectoryState::Create { user_input, .. } => {
515 let (new_id, exists, is_dir) = user_input
516 .as_ref()
517 .map(|input| (input.file.id, input.exists, input.is_dir))
518 .unwrap_or_else(|| (0, false, false));
519 DirectoryState::Create {
520 entries: new_entries,
521 parent_path: dir.clone(),
522 user_input: Some(UserInput {
523 file: StringMatchCandidate::new(new_id, &suffix),
524 exists,
525 is_dir,
526 }),
527 }
528 }
529 };
530
531 cx.notify();
532 })
533 .ok();
534 })
535 }
536
537 fn confirm_completion(
538 &mut self,
539 query: String,
540 _window: &mut Window,
541 _: &mut Context<Picker<Self>>,
542 ) -> Option<String> {
543 let candidate = self.get_entry(self.selected_index)?;
544 if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() {
545 return None;
546 }
547
548 let path_style = self.path_style;
549 Some(
550 maybe!({
551 match &self.directory_state {
552 DirectoryState::Create { parent_path, .. } => Some(format!(
553 "{}{}{}",
554 parent_path,
555 candidate.path.string,
556 if candidate.is_dir {
557 path_style.separator()
558 } else {
559 ""
560 }
561 )),
562 DirectoryState::List { parent_path, .. } => Some(format!(
563 "{}{}{}",
564 parent_path,
565 candidate.path.string,
566 if candidate.is_dir {
567 path_style.separator()
568 } else {
569 ""
570 }
571 )),
572 DirectoryState::None { .. } => return None,
573 }
574 })
575 .unwrap_or(query),
576 )
577 }
578
579 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
580 let Some(candidate) = self.get_entry(self.selected_index) else {
581 return;
582 };
583
584 match &self.directory_state {
585 DirectoryState::None { .. } => return,
586 DirectoryState::List { parent_path, .. } => {
587 let confirmed_path =
588 if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
589 PathBuf::from(&self.prompt_root)
590 } else {
591 Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
592 .join(&candidate.path.string)
593 };
594 if let Some(tx) = self.tx.take() {
595 tx.send(Some(vec![confirmed_path])).ok();
596 }
597 }
598 DirectoryState::Create {
599 parent_path,
600 user_input,
601 ..
602 } => match user_input {
603 None => return,
604 Some(user_input) => {
605 if user_input.is_dir {
606 return;
607 }
608 let prompted_path =
609 if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
610 PathBuf::from(&self.prompt_root)
611 } else {
612 Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
613 .join(&user_input.file.string)
614 };
615 if user_input.exists {
616 self.should_dismiss = false;
617 let answer = window.prompt(
618 gpui::PromptLevel::Critical,
619 &format!("{prompted_path:?} already exists. Do you want to replace it?"),
620 Some(
621 "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
622 ),
623 &["Replace", "Cancel"],
624 cx
625 );
626 self.replace_prompt = cx.spawn_in(window, async move |picker, cx| {
627 let answer = answer.await.ok();
628 picker
629 .update(cx, |picker, cx| {
630 picker.delegate.should_dismiss = true;
631 if answer != Some(0) {
632 return;
633 }
634 if let Some(tx) = picker.delegate.tx.take() {
635 tx.send(Some(vec![prompted_path])).ok();
636 }
637 cx.emit(gpui::DismissEvent);
638 })
639 .ok();
640 });
641 return;
642 } else if let Some(tx) = self.tx.take() {
643 tx.send(Some(vec![prompted_path])).ok();
644 }
645 }
646 },
647 }
648
649 cx.emit(gpui::DismissEvent);
650 }
651
652 fn should_dismiss(&self) -> bool {
653 self.should_dismiss
654 }
655
656 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
657 if let Some(tx) = self.tx.take() {
658 tx.send(None).ok();
659 }
660 cx.emit(gpui::DismissEvent)
661 }
662
663 fn render_match(
664 &self,
665 ix: usize,
666 selected: bool,
667 window: &mut Window,
668 cx: &mut Context<Picker<Self>>,
669 ) -> Option<Self::ListItem> {
670 let settings = FileFinderSettings::get_global(cx);
671 let candidate = self.get_entry(ix)?;
672 let mut match_positions = match &self.directory_state {
673 DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
674 DirectoryState::Create { user_input, .. } => {
675 if let Some(user_input) = user_input {
676 if !user_input.exists || !user_input.is_dir {
677 if ix == 0 {
678 Vec::new()
679 } else {
680 self.string_matches.get(ix - 1)?.positions.clone()
681 }
682 } else {
683 self.string_matches.get(ix)?.positions.clone()
684 }
685 } else {
686 self.string_matches.get(ix)?.positions.clone()
687 }
688 }
689 DirectoryState::None { .. } => Vec::new(),
690 };
691
692 let is_current_dir_candidate = candidate.path.string == self.current_dir();
693
694 let file_icon = maybe!({
695 if !settings.file_icons {
696 return None;
697 }
698
699 let path = path::Path::new(&candidate.path.string);
700 let icon = if candidate.is_dir {
701 if is_current_dir_candidate {
702 return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
703 } else {
704 FileIcons::get_folder_icon(false, path, cx)?
705 }
706 } else {
707 FileIcons::get_icon(path, cx)?
708 };
709 Some(Icon::from_path(icon).color(Color::Muted))
710 });
711
712 match &self.directory_state {
713 DirectoryState::List { parent_path, .. } => {
714 let (label, indices) = if is_current_dir_candidate {
715 ("open this directory".to_string(), vec![])
716 } else if *parent_path == self.prompt_root {
717 match_positions.iter_mut().for_each(|position| {
718 *position += self.prompt_root.len();
719 });
720 (
721 format!("{}{}", self.prompt_root, candidate.path.string),
722 match_positions,
723 )
724 } else {
725 (candidate.path.string, match_positions)
726 };
727 Some(
728 ListItem::new(ix)
729 .spacing(ListItemSpacing::Sparse)
730 .start_slot::<Icon>(file_icon)
731 .inset(true)
732 .toggle_state(selected)
733 .child(HighlightedLabel::new(label, indices)),
734 )
735 }
736 DirectoryState::Create {
737 parent_path,
738 user_input,
739 ..
740 } => {
741 let (label, delta) = if *parent_path == self.prompt_root {
742 match_positions.iter_mut().for_each(|position| {
743 *position += self.prompt_root.len();
744 });
745 (
746 format!("{}{}", self.prompt_root, candidate.path.string),
747 self.prompt_root.len(),
748 )
749 } else {
750 (candidate.path.string.clone(), 0)
751 };
752
753 let label_with_highlights = match user_input {
754 Some(user_input) => {
755 let label_len = label.len();
756 if user_input.file.string == candidate.path.string {
757 if user_input.exists {
758 let label = if user_input.is_dir {
759 label
760 } else {
761 format!("{label} (replace)")
762 };
763 StyledText::new(label)
764 .with_default_highlights(
765 &window.text_style(),
766 vec![(
767 delta..label_len,
768 HighlightStyle::color(Color::Conflict.color(cx)),
769 )],
770 )
771 .into_any_element()
772 } else {
773 StyledText::new(format!("{label} (create)"))
774 .with_default_highlights(
775 &window.text_style(),
776 vec![(
777 delta..label_len,
778 HighlightStyle::color(Color::Created.color(cx)),
779 )],
780 )
781 .into_any_element()
782 }
783 } else {
784 HighlightedLabel::new(label, match_positions).into_any_element()
785 }
786 }
787 None => HighlightedLabel::new(label, match_positions).into_any_element(),
788 };
789
790 Some(
791 ListItem::new(ix)
792 .spacing(ListItemSpacing::Sparse)
793 .start_slot::<Icon>(file_icon)
794 .inset(true)
795 .toggle_state(selected)
796 .child(LabelLike::new().child(label_with_highlights)),
797 )
798 }
799 DirectoryState::None { .. } => None,
800 }
801 }
802
803 fn render_footer(
804 &self,
805 window: &mut Window,
806 cx: &mut Context<Picker<Self>>,
807 ) -> Option<AnyElement> {
808 (self.render_footer)(window, cx)
809 }
810
811 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
812 Some(match &self.directory_state {
813 DirectoryState::Create { .. } => SharedString::from("Type a path…"),
814 DirectoryState::List {
815 error: Some(error), ..
816 } => error.clone(),
817 DirectoryState::List { .. } | DirectoryState::None { .. } => {
818 SharedString::from("No such file or directory")
819 }
820 })
821 }
822
823 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
824 Arc::from(format!("[directory{}]filename.ext", self.path_style.separator()).as_str())
825 }
826
827 fn separators_after_indices(&self) -> Vec<usize> {
828 let Some(m) = self.string_matches.first() else {
829 return Vec::new();
830 };
831 if m.string == self.current_dir() {
832 vec![0]
833 } else {
834 Vec::new()
835 }
836 }
837}
838
839fn path_candidates(
840 parent_path_is_root: bool,
841 mut children: Vec<DirectoryItem>,
842) -> Vec<CandidateInfo> {
843 if parent_path_is_root {
844 children.push(DirectoryItem {
845 is_dir: true,
846 path: PathBuf::default(),
847 });
848 }
849
850 children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
851 children
852 .iter()
853 .enumerate()
854 .map(|(ix, item)| CandidateInfo {
855 path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
856 is_dir: item.is_dir,
857 })
858 .collect()
859}
860
861#[cfg(target_os = "windows")]
862fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
863 let last_item = Path::new(&query)
864 .file_name()
865 .unwrap_or_default()
866 .to_string_lossy();
867 let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
868 (dir.to_string(), last_item.into_owned())
869 } else {
870 (query.to_string(), String::new())
871 };
872 match path_style {
873 PathStyle::Posix => {
874 if dir.is_empty() {
875 dir = "/".to_string();
876 }
877 }
878 PathStyle::Windows => {
879 if dir.len() < 3 {
880 dir = "C:\\".to_string();
881 }
882 }
883 }
884 (dir, suffix)
885}
886
887#[cfg(not(target_os = "windows"))]
888fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
889 match path_style {
890 PathStyle::Posix => {
891 let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
892 (query[..index].to_string(), query[index + 1..].to_string())
893 } else {
894 (query, String::new())
895 };
896 if !dir.ends_with('/') {
897 dir.push('/');
898 }
899 (dir, suffix)
900 }
901 PathStyle::Windows => {
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.len() < 3 {
908 dir = "C:\\".to_string();
909 }
910 if !dir.ends_with('\\') {
911 dir.push('\\');
912 }
913 (dir, suffix)
914 }
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use util::paths::PathStyle;
921
922 use crate::open_path_prompt::get_dir_and_suffix;
923
924 #[test]
925 fn test_get_dir_and_suffix_with_windows_style() {
926 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
927 assert_eq!(dir, "C:\\");
928 assert_eq!(suffix, "");
929
930 let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
931 assert_eq!(dir, "C:\\");
932 assert_eq!(suffix, "");
933
934 let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
935 assert_eq!(dir, "C:\\");
936 assert_eq!(suffix, "");
937
938 let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
939 assert_eq!(dir, "C:\\");
940 assert_eq!(suffix, "Use");
941
942 let (dir, suffix) =
943 get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
944 assert_eq!(dir, "C:\\Users\\Junkui\\");
945 assert_eq!(suffix, "Docum");
946
947 let (dir, suffix) =
948 get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
949 assert_eq!(dir, "C:\\Users\\Junkui\\");
950 assert_eq!(suffix, "Documents");
951
952 let (dir, suffix) =
953 get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
954 assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
955 assert_eq!(suffix, "");
956 }
957
958 #[test]
959 fn test_get_dir_and_suffix_with_posix_style() {
960 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
961 assert_eq!(dir, "/");
962 assert_eq!(suffix, "");
963
964 let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
965 assert_eq!(dir, "/");
966 assert_eq!(suffix, "");
967
968 let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
969 assert_eq!(dir, "/");
970 assert_eq!(suffix, "Use");
971
972 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
973 assert_eq!(dir, "/Users/Junkui/");
974 assert_eq!(suffix, "Docum");
975
976 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
977 assert_eq!(dir, "/Users/Junkui/");
978 assert_eq!(suffix, "Documents");
979
980 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
981 assert_eq!(dir, "/Users/Junkui/Documents/");
982 assert_eq!(suffix, "");
983 }
984}