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 && 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 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, .. } => Some(
714 ListItem::new(ix)
715 .spacing(ListItemSpacing::Sparse)
716 .start_slot::<Icon>(file_icon)
717 .inset(true)
718 .toggle_state(selected)
719 .child(HighlightedLabel::new(
720 if parent_path == &self.prompt_root {
721 format!("{}{}", self.prompt_root, candidate.path.string)
722 } else if is_current_dir_candidate {
723 "open this directory".to_string()
724 } else {
725 candidate.path.string
726 },
727 match_positions,
728 )),
729 ),
730 DirectoryState::Create {
731 parent_path,
732 user_input,
733 ..
734 } => {
735 let (label, delta) = if parent_path == &self.prompt_root {
736 (
737 format!("{}{}", self.prompt_root, candidate.path.string),
738 self.prompt_root.len(),
739 )
740 } else {
741 (candidate.path.string.clone(), 0)
742 };
743 let label_len = label.len();
744
745 let label_with_highlights = match user_input {
746 Some(user_input) => {
747 if user_input.file.string == candidate.path.string {
748 if user_input.exists {
749 let label = if user_input.is_dir {
750 label
751 } else {
752 format!("{label} (replace)")
753 };
754 StyledText::new(label)
755 .with_default_highlights(
756 &window.text_style(),
757 vec![(
758 delta..delta + label_len,
759 HighlightStyle::color(Color::Conflict.color(cx)),
760 )],
761 )
762 .into_any_element()
763 } else {
764 StyledText::new(format!("{label} (create)"))
765 .with_default_highlights(
766 &window.text_style(),
767 vec![(
768 delta..delta + label_len,
769 HighlightStyle::color(Color::Created.color(cx)),
770 )],
771 )
772 .into_any_element()
773 }
774 } else {
775 let mut highlight_positions = match_positions;
776 highlight_positions.iter_mut().for_each(|position| {
777 *position += delta;
778 });
779 HighlightedLabel::new(label, highlight_positions).into_any_element()
780 }
781 }
782 None => {
783 let mut highlight_positions = match_positions;
784 highlight_positions.iter_mut().for_each(|position| {
785 *position += delta;
786 });
787 HighlightedLabel::new(label, highlight_positions).into_any_element()
788 }
789 };
790
791 Some(
792 ListItem::new(ix)
793 .spacing(ListItemSpacing::Sparse)
794 .start_slot::<Icon>(file_icon)
795 .inset(true)
796 .toggle_state(selected)
797 .child(LabelLike::new().child(label_with_highlights)),
798 )
799 }
800 DirectoryState::None { .. } => None,
801 }
802 }
803
804 fn render_footer(
805 &self,
806 window: &mut Window,
807 cx: &mut Context<Picker<Self>>,
808 ) -> Option<AnyElement> {
809 (self.render_footer)(window, cx)
810 }
811
812 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
813 Some(match &self.directory_state {
814 DirectoryState::Create { .. } => SharedString::from("Type a path…"),
815 DirectoryState::List {
816 error: Some(error), ..
817 } => error.clone(),
818 DirectoryState::List { .. } | DirectoryState::None { .. } => {
819 SharedString::from("No such file or directory")
820 }
821 })
822 }
823
824 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
825 Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
826 }
827
828 fn separators_after_indices(&self) -> Vec<usize> {
829 let Some(m) = self.string_matches.first() else {
830 return Vec::new();
831 };
832 if m.string == self.current_dir() {
833 vec![0]
834 } else {
835 Vec::new()
836 }
837 }
838}
839
840fn path_candidates(
841 parent_path_is_root: bool,
842 mut children: Vec<DirectoryItem>,
843) -> Vec<CandidateInfo> {
844 if parent_path_is_root {
845 children.push(DirectoryItem {
846 is_dir: true,
847 path: PathBuf::default(),
848 });
849 }
850
851 children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
852 children
853 .iter()
854 .enumerate()
855 .map(|(ix, item)| CandidateInfo {
856 path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
857 is_dir: item.is_dir,
858 })
859 .collect()
860}
861
862#[cfg(target_os = "windows")]
863fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
864 let last_item = Path::new(&query)
865 .file_name()
866 .unwrap_or_default()
867 .to_string_lossy();
868 let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
869 (dir.to_string(), last_item.into_owned())
870 } else {
871 (query.to_string(), String::new())
872 };
873 match path_style {
874 PathStyle::Posix => {
875 if dir.is_empty() {
876 dir = "/".to_string();
877 }
878 }
879 PathStyle::Windows => {
880 if dir.len() < 3 {
881 dir = "C:\\".to_string();
882 }
883 }
884 }
885 (dir, suffix)
886}
887
888#[cfg(not(target_os = "windows"))]
889fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
890 match path_style {
891 PathStyle::Posix => {
892 let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
893 (query[..index].to_string(), query[index + 1..].to_string())
894 } else {
895 (query, String::new())
896 };
897 if !dir.ends_with('/') {
898 dir.push('/');
899 }
900 (dir, suffix)
901 }
902 PathStyle::Windows => {
903 let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
904 (query[..index].to_string(), query[index + 1..].to_string())
905 } else {
906 (query, String::new())
907 };
908 if dir.len() < 3 {
909 dir = "C:\\".to_string();
910 }
911 if !dir.ends_with('\\') {
912 dir.push('\\');
913 }
914 (dir, suffix)
915 }
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 use util::paths::PathStyle;
922
923 use crate::open_path_prompt::get_dir_and_suffix;
924
925 #[test]
926 fn test_get_dir_and_suffix_with_windows_style() {
927 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
928 assert_eq!(dir, "C:\\");
929 assert_eq!(suffix, "");
930
931 let (dir, suffix) = get_dir_and_suffix("C:".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:\\Use".into(), PathStyle::Windows);
940 assert_eq!(dir, "C:\\");
941 assert_eq!(suffix, "Use");
942
943 let (dir, suffix) =
944 get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
945 assert_eq!(dir, "C:\\Users\\Junkui\\");
946 assert_eq!(suffix, "Docum");
947
948 let (dir, suffix) =
949 get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
950 assert_eq!(dir, "C:\\Users\\Junkui\\");
951 assert_eq!(suffix, "Documents");
952
953 let (dir, suffix) =
954 get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
955 assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
956 assert_eq!(suffix, "");
957 }
958
959 #[test]
960 fn test_get_dir_and_suffix_with_posix_style() {
961 let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
962 assert_eq!(dir, "/");
963 assert_eq!(suffix, "");
964
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("/Use".into(), PathStyle::Posix);
970 assert_eq!(dir, "/");
971 assert_eq!(suffix, "Use");
972
973 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
974 assert_eq!(dir, "/Users/Junkui/");
975 assert_eq!(suffix, "Docum");
976
977 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
978 assert_eq!(dir, "/Users/Junkui/");
979 assert_eq!(suffix, "Documents");
980
981 let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
982 assert_eq!(dir, "/Users/Junkui/Documents/");
983 assert_eq!(suffix, "");
984 }
985}