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