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