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