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