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