open_path_prompt.rs

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