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, 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::local());
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                            && match &picker.delegate.directory_state {
396                                DirectoryState::List { error, .. } => error.is_none(),
397                                DirectoryState::Create { .. } => false,
398                                DirectoryState::None { .. } => false,
399                            }
400                    })
401                    .unwrap_or(false);
402
403                let current_dir_in_new_entries = new_entries
404                    .iter()
405                    .any(|entry| &entry.path.string == current_dir);
406
407                if should_prepend_with_current_dir && !current_dir_in_new_entries {
408                    new_entries.insert(
409                        0,
410                        CandidateInfo {
411                            path: StringMatchCandidate {
412                                id: max_id + 1,
413                                string: current_dir.to_string(),
414                                char_bag: CharBag::from(current_dir),
415                            },
416                            is_dir: true,
417                        },
418                    );
419                }
420
421                this.update(cx, |this, cx| {
422                    this.delegate.selected_index = 0;
423                    this.delegate.string_matches = new_entries
424                        .iter()
425                        .map(|m| StringMatch {
426                            candidate_id: m.path.id,
427                            score: 0.0,
428                            positions: Vec::new(),
429                            string: m.path.string.clone(),
430                        })
431                        .collect();
432                    this.delegate.directory_state =
433                        match &this.delegate.directory_state {
434                            DirectoryState::None { create: false }
435                            | DirectoryState::List { .. } => DirectoryState::List {
436                                parent_path: dir.clone(),
437                                entries: new_entries,
438                                error: None,
439                            },
440                            DirectoryState::None { create: true }
441                            | DirectoryState::Create { .. } => DirectoryState::Create {
442                                parent_path: dir.clone(),
443                                user_input: None,
444                                entries: new_entries,
445                            },
446                        };
447                    cx.notify();
448                })
449                .ok();
450                return;
451            }
452
453            let Ok(is_create_state) =
454                this.update(cx, |this, _| match &this.delegate.directory_state {
455                    DirectoryState::Create { .. } => true,
456                    DirectoryState::List { .. } => false,
457                    DirectoryState::None { create } => *create,
458                })
459            else {
460                return;
461            };
462
463            let candidates = new_entries
464                .iter()
465                .filter_map(|entry| {
466                    if is_create_state && !entry.is_dir && Some(&suffix) == Some(&entry.path.string)
467                    {
468                        None
469                    } else {
470                        Some(&entry.path)
471                    }
472                })
473                .collect::<Vec<_>>();
474
475            let matches = fuzzy::match_strings(
476                candidates.as_slice(),
477                &suffix,
478                false,
479                true,
480                100,
481                &cancel_flag,
482                cx.background_executor().clone(),
483            )
484            .await;
485            if cancel_flag.load(atomic::Ordering::Acquire) {
486                return;
487            }
488
489            this.update(cx, |this, cx| {
490                this.delegate.selected_index = 0;
491                this.delegate.string_matches = matches.clone();
492                this.delegate.string_matches.sort_by_key(|m| {
493                    (
494                        new_entries
495                            .iter()
496                            .find(|entry| entry.path.id == m.candidate_id)
497                            .map(|entry| &entry.path)
498                            .map(|candidate| !candidate.string.starts_with(&suffix)),
499                        m.candidate_id,
500                    )
501                });
502                this.delegate.directory_state = match &this.delegate.directory_state {
503                    DirectoryState::None { create: false } | DirectoryState::List { .. } => {
504                        DirectoryState::List {
505                            entries: new_entries,
506                            parent_path: dir.clone(),
507                            error: None,
508                        }
509                    }
510                    DirectoryState::None { create: true } => DirectoryState::Create {
511                        entries: new_entries,
512                        parent_path: dir.clone(),
513                        user_input: Some(UserInput {
514                            file: StringMatchCandidate::new(0, &suffix),
515                            exists: false,
516                            is_dir: false,
517                        }),
518                    },
519                    DirectoryState::Create { user_input, .. } => {
520                        let (new_id, exists, is_dir) = user_input
521                            .as_ref()
522                            .map(|input| (input.file.id, input.exists, input.is_dir))
523                            .unwrap_or_else(|| (0, false, false));
524                        DirectoryState::Create {
525                            entries: new_entries,
526                            parent_path: dir.clone(),
527                            user_input: Some(UserInput {
528                                file: StringMatchCandidate::new(new_id, &suffix),
529                                exists,
530                                is_dir,
531                            }),
532                        }
533                    }
534                };
535
536                cx.notify();
537            })
538            .ok();
539        })
540    }
541
542    fn confirm_completion(
543        &mut self,
544        query: String,
545        _window: &mut Window,
546        _: &mut Context<Picker<Self>>,
547    ) -> Option<String> {
548        let candidate = self.get_entry(self.selected_index)?;
549        if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() {
550            return None;
551        }
552
553        let path_style = self.path_style;
554        Some(
555            maybe!({
556                match &self.directory_state {
557                    DirectoryState::Create { parent_path, .. } => Some(format!(
558                        "{}{}{}",
559                        parent_path,
560                        candidate.path.string,
561                        if candidate.is_dir {
562                            path_style.separator()
563                        } else {
564                            ""
565                        }
566                    )),
567                    DirectoryState::List { parent_path, .. } => Some(format!(
568                        "{}{}{}",
569                        parent_path,
570                        candidate.path.string,
571                        if candidate.is_dir {
572                            path_style.separator()
573                        } else {
574                            ""
575                        }
576                    )),
577                    DirectoryState::None { .. } => return None,
578                }
579            })
580            .unwrap_or(query),
581        )
582    }
583
584    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
585        let Some(candidate) = self.get_entry(self.selected_index) else {
586            return;
587        };
588
589        match &self.directory_state {
590            DirectoryState::None { .. } => return,
591            DirectoryState::List { parent_path, .. } => {
592                let confirmed_path =
593                    if parent_path == &self.prompt_root && candidate.path.string.is_empty() {
594                        PathBuf::from(&self.prompt_root)
595                    } else {
596                        Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
597                            .join(&candidate.path.string)
598                    };
599                if let Some(tx) = self.tx.take() {
600                    tx.send(Some(vec![confirmed_path])).ok();
601                }
602            }
603            DirectoryState::Create {
604                parent_path,
605                user_input,
606                ..
607            } => match user_input {
608                None => return,
609                Some(user_input) => {
610                    if user_input.is_dir {
611                        return;
612                    }
613                    let prompted_path =
614                        if parent_path == &self.prompt_root && user_input.file.string.is_empty() {
615                            PathBuf::from(&self.prompt_root)
616                        } else {
617                            Path::new(self.lister.resolve_tilde(parent_path, cx).as_ref())
618                                .join(&user_input.file.string)
619                        };
620                    if user_input.exists {
621                        self.should_dismiss = false;
622                        let answer = window.prompt(
623                            gpui::PromptLevel::Critical,
624                            &format!("{prompted_path:?} already exists. Do you want to replace it?"),
625                            Some(
626                                "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",
627                            ),
628                            &["Replace", "Cancel"],
629                            cx
630                        );
631                        self.replace_prompt = cx.spawn_in(window, async move |picker, cx| {
632                            let answer = answer.await.ok();
633                            picker
634                                .update(cx, |picker, cx| {
635                                    picker.delegate.should_dismiss = true;
636                                    if answer != Some(0) {
637                                        return;
638                                    }
639                                    if let Some(tx) = picker.delegate.tx.take() {
640                                        tx.send(Some(vec![prompted_path])).ok();
641                                    }
642                                    cx.emit(gpui::DismissEvent);
643                                })
644                                .ok();
645                        });
646                        return;
647                    } else if let Some(tx) = self.tx.take() {
648                        tx.send(Some(vec![prompted_path])).ok();
649                    }
650                }
651            },
652        }
653
654        cx.emit(gpui::DismissEvent);
655    }
656
657    fn should_dismiss(&self) -> bool {
658        self.should_dismiss
659    }
660
661    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
662        if let Some(tx) = self.tx.take() {
663            tx.send(None).ok();
664        }
665        cx.emit(gpui::DismissEvent)
666    }
667
668    fn render_match(
669        &self,
670        ix: usize,
671        selected: bool,
672        window: &mut Window,
673        cx: &mut Context<Picker<Self>>,
674    ) -> Option<Self::ListItem> {
675        let settings = FileFinderSettings::get_global(cx);
676        let candidate = self.get_entry(ix)?;
677        let mut match_positions = match &self.directory_state {
678            DirectoryState::List { .. } => self.string_matches.get(ix)?.positions.clone(),
679            DirectoryState::Create { user_input, .. } => {
680                if let Some(user_input) = user_input {
681                    if !user_input.exists || !user_input.is_dir {
682                        if ix == 0 {
683                            Vec::new()
684                        } else {
685                            self.string_matches.get(ix - 1)?.positions.clone()
686                        }
687                    } else {
688                        self.string_matches.get(ix)?.positions.clone()
689                    }
690                } else {
691                    self.string_matches.get(ix)?.positions.clone()
692                }
693            }
694            DirectoryState::None { .. } => Vec::new(),
695        };
696
697        let is_current_dir_candidate = candidate.path.string == self.current_dir();
698
699        let file_icon = maybe!({
700            if !settings.file_icons {
701                return None;
702            }
703
704            let path = path::Path::new(&candidate.path.string);
705            let icon = if candidate.is_dir {
706                if is_current_dir_candidate {
707                    return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
708                } else {
709                    FileIcons::get_folder_icon(false, path, cx)?
710                }
711            } else {
712                FileIcons::get_icon(path, cx)?
713            };
714            Some(Icon::from_path(icon).color(Color::Muted))
715        });
716
717        match &self.directory_state {
718            DirectoryState::List { parent_path, .. } => {
719                let (label, indices) = if is_current_dir_candidate {
720                    ("open this directory".to_string(), vec![])
721                } else if *parent_path == self.prompt_root {
722                    match_positions.iter_mut().for_each(|position| {
723                        *position += self.prompt_root.len();
724                    });
725                    (
726                        format!("{}{}", self.prompt_root, candidate.path.string),
727                        match_positions,
728                    )
729                } else {
730                    (candidate.path.string, match_positions)
731                };
732                Some(
733                    ListItem::new(ix)
734                        .spacing(ListItemSpacing::Sparse)
735                        .start_slot::<Icon>(file_icon)
736                        .inset(true)
737                        .toggle_state(selected)
738                        .child(HighlightedLabel::new(label, indices)),
739                )
740            }
741            DirectoryState::Create {
742                parent_path,
743                user_input,
744                ..
745            } => {
746                let (label, delta) = if *parent_path == self.prompt_root {
747                    match_positions.iter_mut().for_each(|position| {
748                        *position += self.prompt_root.len();
749                    });
750                    (
751                        format!("{}{}", self.prompt_root, candidate.path.string),
752                        self.prompt_root.len(),
753                    )
754                } else {
755                    (candidate.path.string.clone(), 0)
756                };
757
758                let label_with_highlights = match user_input {
759                    Some(user_input) => {
760                        let label_len = label.len();
761                        if user_input.file.string == candidate.path.string {
762                            if user_input.exists {
763                                let label = if user_input.is_dir {
764                                    label
765                                } else {
766                                    format!("{label} (replace)")
767                                };
768                                StyledText::new(label)
769                                    .with_default_highlights(
770                                        &window.text_style(),
771                                        vec![(
772                                            delta..label_len,
773                                            HighlightStyle::color(Color::Conflict.color(cx)),
774                                        )],
775                                    )
776                                    .into_any_element()
777                            } else {
778                                StyledText::new(format!("{label} (create)"))
779                                    .with_default_highlights(
780                                        &window.text_style(),
781                                        vec![(
782                                            delta..label_len,
783                                            HighlightStyle::color(Color::Created.color(cx)),
784                                        )],
785                                    )
786                                    .into_any_element()
787                            }
788                        } else {
789                            HighlightedLabel::new(label, match_positions).into_any_element()
790                        }
791                    }
792                    None => HighlightedLabel::new(label, match_positions).into_any_element(),
793                };
794
795                Some(
796                    ListItem::new(ix)
797                        .spacing(ListItemSpacing::Sparse)
798                        .start_slot::<Icon>(file_icon)
799                        .inset(true)
800                        .toggle_state(selected)
801                        .child(LabelLike::new().child(label_with_highlights)),
802                )
803            }
804            DirectoryState::None { .. } => None,
805        }
806    }
807
808    fn render_footer(
809        &self,
810        window: &mut Window,
811        cx: &mut Context<Picker<Self>>,
812    ) -> Option<AnyElement> {
813        (self.render_footer)(window, cx)
814    }
815
816    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
817        Some(match &self.directory_state {
818            DirectoryState::Create { .. } => SharedString::from("Type a path…"),
819            DirectoryState::List {
820                error: Some(error), ..
821            } => error.clone(),
822            DirectoryState::List { .. } | DirectoryState::None { .. } => {
823                SharedString::from("No such file or directory")
824            }
825        })
826    }
827
828    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
829        Arc::from(format!("[directory{}]filename.ext", self.path_style.separator()).as_str())
830    }
831
832    fn separators_after_indices(&self) -> Vec<usize> {
833        let Some(m) = self.string_matches.first() else {
834            return Vec::new();
835        };
836        if m.string == self.current_dir() {
837            vec![0]
838        } else {
839            Vec::new()
840        }
841    }
842}
843
844fn path_candidates(
845    parent_path_is_root: bool,
846    mut children: Vec<DirectoryItem>,
847) -> Vec<CandidateInfo> {
848    if parent_path_is_root {
849        children.push(DirectoryItem {
850            is_dir: true,
851            path: PathBuf::default(),
852        });
853    }
854
855    children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
856    children
857        .iter()
858        .enumerate()
859        .map(|(ix, item)| CandidateInfo {
860            path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
861            is_dir: item.is_dir,
862        })
863        .collect()
864}
865
866#[cfg(target_os = "windows")]
867fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
868    let last_item = Path::new(&query)
869        .file_name()
870        .unwrap_or_default()
871        .to_string_lossy();
872    let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
873        (dir.to_string(), last_item.into_owned())
874    } else {
875        (query.to_string(), String::new())
876    };
877    match path_style {
878        PathStyle::Posix => {
879            if dir.is_empty() {
880                dir = "/".to_string();
881            }
882        }
883        PathStyle::Windows => {
884            if dir.len() < 3 {
885                dir = "C:\\".to_string();
886            }
887        }
888    }
889    (dir, suffix)
890}
891
892#[cfg(not(target_os = "windows"))]
893fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
894    match path_style {
895        PathStyle::Posix => {
896            let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
897                (query[..index].to_string(), query[index + 1..].to_string())
898            } else {
899                (query, String::new())
900            };
901            if !dir.ends_with('/') {
902                dir.push('/');
903            }
904            (dir, suffix)
905        }
906        PathStyle::Windows => {
907            let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
908                (query[..index].to_string(), query[index + 1..].to_string())
909            } else {
910                (query, String::new())
911            };
912            if dir.len() < 3 {
913                dir = "C:\\".to_string();
914            }
915            if !dir.ends_with('\\') {
916                dir.push('\\');
917            }
918            (dir, suffix)
919        }
920    }
921}
922
923#[cfg(test)]
924mod tests {
925    use util::paths::PathStyle;
926
927    use crate::open_path_prompt::get_dir_and_suffix;
928
929    #[test]
930    fn test_get_dir_and_suffix_with_windows_style() {
931        let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
932        assert_eq!(dir, "C:\\");
933        assert_eq!(suffix, "");
934
935        let (dir, suffix) = get_dir_and_suffix("C:".into(), PathStyle::Windows);
936        assert_eq!(dir, "C:\\");
937        assert_eq!(suffix, "");
938
939        let (dir, suffix) = get_dir_and_suffix("C:\\".into(), PathStyle::Windows);
940        assert_eq!(dir, "C:\\");
941        assert_eq!(suffix, "");
942
943        let (dir, suffix) = get_dir_and_suffix("C:\\Use".into(), PathStyle::Windows);
944        assert_eq!(dir, "C:\\");
945        assert_eq!(suffix, "Use");
946
947        let (dir, suffix) =
948            get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
949        assert_eq!(dir, "C:\\Users\\Junkui\\");
950        assert_eq!(suffix, "Docum");
951
952        let (dir, suffix) =
953            get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
954        assert_eq!(dir, "C:\\Users\\Junkui\\");
955        assert_eq!(suffix, "Documents");
956
957        let (dir, suffix) =
958            get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
959        assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
960        assert_eq!(suffix, "");
961    }
962
963    #[test]
964    fn test_get_dir_and_suffix_with_posix_style() {
965        let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
966        assert_eq!(dir, "/");
967        assert_eq!(suffix, "");
968
969        let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
970        assert_eq!(dir, "/");
971        assert_eq!(suffix, "");
972
973        let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
974        assert_eq!(dir, "/");
975        assert_eq!(suffix, "Use");
976
977        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
978        assert_eq!(dir, "/Users/Junkui/");
979        assert_eq!(suffix, "Docum");
980
981        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
982        assert_eq!(dir, "/Users/Junkui/");
983        assert_eq!(suffix, "Documents");
984
985        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
986        assert_eq!(dir, "/Users/Junkui/Documents/");
987        assert_eq!(suffix, "");
988    }
989}