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.primary_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.primary_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(
830 format!(
831 "[directory{}]filename.ext",
832 self.path_style.primary_separator()
833 )
834 .as_str(),
835 )
836 }
837
838 fn separators_after_indices(&self) -> Vec<usize> {
839 let Some(m) = self.string_matches.first() else {
840 return Vec::new();
841 };
842 if m.string == self.current_dir() {
843 vec![0]
844 } else {
845 Vec::new()
846 }
847 }
848}
849
850fn path_candidates(
851 parent_path_is_root: bool,
852 mut children: Vec<DirectoryItem>,
853) -> Vec<CandidateInfo> {
854 if parent_path_is_root {
855 children.push(DirectoryItem {
856 is_dir: true,
857 path: PathBuf::default(),
858 });
859 }
860
861 children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
862 children
863 .iter()
864 .enumerate()
865 .map(|(ix, item)| CandidateInfo {
866 path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
867 is_dir: item.is_dir,
868 })
869 .collect()
870}
871
872#[cfg(target_os = "windows")]
873fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
874 let last_item = Path::new(&query)
875 .file_name()
876 .unwrap_or_default()
877 .to_string_lossy();
878 let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
879 (dir.to_string(), last_item.into_owned())
880 } else {
881 (query.to_string(), String::new())
882 };
883 match path_style {
884 PathStyle::Posix => {
885 if dir.is_empty() {
886 dir = "/".to_string();
887 }
888 }
889 PathStyle::Windows => {
890 if dir.len() < 3 {
891 dir = "C:\\".to_string();
892 }
893 }
894 }
895 (dir, suffix)
896}
897
898#[cfg(not(target_os = "windows"))]
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 (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
914 (query[..index].to_string(), query[index + 1..].to_string())
915 } else {
916 (query, String::new())
917 };
918 if dir.len() < 3 {
919 dir = "C:\\".to_string();
920 }
921 if !dir.ends_with('\\') {
922 dir.push('\\');
923 }
924 (dir, suffix)
925 }
926 }
927}
928
929#[cfg(test)]
930mod tests {
931 use util::paths::PathStyle;
932
933 use crate::open_path_prompt::get_dir_and_suffix;
934
935 #[test]
936 fn test_get_dir_and_suffix_with_windows_style() {
937 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
938 assert_eq!(dir, "C:\\");
939 assert_eq!(suffix, "");
940
941 let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
942 assert_eq!(dir, "C:\\");
943 assert_eq!(suffix, "");
944
945 let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
946 assert_eq!(dir, "C:\\");
947 assert_eq!(suffix, "");
948
949 let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
950 assert_eq!(dir, "C:\\");
951 assert_eq!(suffix, "Use");
952
953 let (dir, suffix) =
954 get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
955 assert_eq!(dir, "C:\\Users\\Junkui\\");
956 assert_eq!(suffix, "Docum");
957
958 let (dir, suffix) =
959 get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
960 assert_eq!(dir, "C:\\Users\\Junkui\\");
961 assert_eq!(suffix, "Documents");
962
963 let (dir, suffix) =
964 get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
965 assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
966 assert_eq!(suffix, "");
967 }
968
969 #[test]
970 fn test_get_dir_and_suffix_with_posix_style() {
971 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
972 assert_eq!(dir, "/");
973 assert_eq!(suffix, "");
974
975 let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
976 assert_eq!(dir, "/");
977 assert_eq!(suffix, "");
978
979 let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
980 assert_eq!(dir, "/");
981 assert_eq!(suffix, "Use");
982
983 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
984 assert_eq!(dir, "/Users/Junkui/");
985 assert_eq!(suffix, "Docum");
986
987 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
988 assert_eq!(dir, "/Users/Junkui/");
989 assert_eq!(suffix, "Documents");
990
991 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
992 assert_eq!(dir, "/Users/Junkui/Documents/");
993 assert_eq!(suffix, "");
994 }
995}