open_path_prompt.rs

  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}