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                {
 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}