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                            && 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 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, .. } => Some(
714                ListItem::new(ix)
715                    .spacing(ListItemSpacing::Sparse)
716                    .start_slot::<Icon>(file_icon)
717                    .inset(true)
718                    .toggle_state(selected)
719                    .child(HighlightedLabel::new(
720                        if parent_path == &self.prompt_root {
721                            format!("{}{}", self.prompt_root, candidate.path.string)
722                        } else if is_current_dir_candidate {
723                            "open this directory".to_string()
724                        } else {
725                            candidate.path.string
726                        },
727                        match_positions,
728                    )),
729            ),
730            DirectoryState::Create {
731                parent_path,
732                user_input,
733                ..
734            } => {
735                let (label, delta) = if parent_path == &self.prompt_root {
736                    (
737                        format!("{}{}", self.prompt_root, candidate.path.string),
738                        self.prompt_root.len(),
739                    )
740                } else {
741                    (candidate.path.string.clone(), 0)
742                };
743                let label_len = label.len();
744
745                let label_with_highlights = match user_input {
746                    Some(user_input) => {
747                        if user_input.file.string == candidate.path.string {
748                            if user_input.exists {
749                                let label = if user_input.is_dir {
750                                    label
751                                } else {
752                                    format!("{label} (replace)")
753                                };
754                                StyledText::new(label)
755                                    .with_default_highlights(
756                                        &window.text_style(),
757                                        vec![(
758                                            delta..delta + label_len,
759                                            HighlightStyle::color(Color::Conflict.color(cx)),
760                                        )],
761                                    )
762                                    .into_any_element()
763                            } else {
764                                StyledText::new(format!("{label} (create)"))
765                                    .with_default_highlights(
766                                        &window.text_style(),
767                                        vec![(
768                                            delta..delta + label_len,
769                                            HighlightStyle::color(Color::Created.color(cx)),
770                                        )],
771                                    )
772                                    .into_any_element()
773                            }
774                        } else {
775                            let mut highlight_positions = match_positions;
776                            highlight_positions.iter_mut().for_each(|position| {
777                                *position += delta;
778                            });
779                            HighlightedLabel::new(label, highlight_positions).into_any_element()
780                        }
781                    }
782                    None => {
783                        let mut highlight_positions = match_positions;
784                        highlight_positions.iter_mut().for_each(|position| {
785                            *position += delta;
786                        });
787                        HighlightedLabel::new(label, highlight_positions).into_any_element()
788                    }
789                };
790
791                Some(
792                    ListItem::new(ix)
793                        .spacing(ListItemSpacing::Sparse)
794                        .start_slot::<Icon>(file_icon)
795                        .inset(true)
796                        .toggle_state(selected)
797                        .child(LabelLike::new().child(label_with_highlights)),
798                )
799            }
800            DirectoryState::None { .. } => None,
801        }
802    }
803
804    fn render_footer(
805        &self,
806        window: &mut Window,
807        cx: &mut Context<Picker<Self>>,
808    ) -> Option<AnyElement> {
809        (self.render_footer)(window, cx)
810    }
811
812    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
813        Some(match &self.directory_state {
814            DirectoryState::Create { .. } => SharedString::from("Type a path…"),
815            DirectoryState::List {
816                error: Some(error), ..
817            } => error.clone(),
818            DirectoryState::List { .. } | DirectoryState::None { .. } => {
819                SharedString::from("No such file or directory")
820            }
821        })
822    }
823
824    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
825        Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
826    }
827
828    fn separators_after_indices(&self) -> Vec<usize> {
829        let Some(m) = self.string_matches.first() else {
830            return Vec::new();
831        };
832        if m.string == self.current_dir() {
833            vec![0]
834        } else {
835            Vec::new()
836        }
837    }
838}
839
840fn path_candidates(
841    parent_path_is_root: bool,
842    mut children: Vec<DirectoryItem>,
843) -> Vec<CandidateInfo> {
844    if parent_path_is_root {
845        children.push(DirectoryItem {
846            is_dir: true,
847            path: PathBuf::default(),
848        });
849    }
850
851    children.sort_by(|a, b| compare_paths((&a.path, true), (&b.path, true)));
852    children
853        .iter()
854        .enumerate()
855        .map(|(ix, item)| CandidateInfo {
856            path: StringMatchCandidate::new(ix, &item.path.to_string_lossy()),
857            is_dir: item.is_dir,
858        })
859        .collect()
860}
861
862#[cfg(target_os = "windows")]
863fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
864    let last_item = Path::new(&query)
865        .file_name()
866        .unwrap_or_default()
867        .to_string_lossy();
868    let (mut dir, suffix) = if let Some(dir) = query.strip_suffix(last_item.as_ref()) {
869        (dir.to_string(), last_item.into_owned())
870    } else {
871        (query.to_string(), String::new())
872    };
873    match path_style {
874        PathStyle::Posix => {
875            if dir.is_empty() {
876                dir = "/".to_string();
877            }
878        }
879        PathStyle::Windows => {
880            if dir.len() < 3 {
881                dir = "C:\\".to_string();
882            }
883        }
884    }
885    (dir, suffix)
886}
887
888#[cfg(not(target_os = "windows"))]
889fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
890    match path_style {
891        PathStyle::Posix => {
892            let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
893                (query[..index].to_string(), query[index + 1..].to_string())
894            } else {
895                (query, String::new())
896            };
897            if !dir.ends_with('/') {
898                dir.push('/');
899            }
900            (dir, suffix)
901        }
902        PathStyle::Windows => {
903            let (mut dir, suffix) = if let Some(index) = query.rfind('\\') {
904                (query[..index].to_string(), query[index + 1..].to_string())
905            } else {
906                (query, String::new())
907            };
908            if dir.len() < 3 {
909                dir = "C:\\".to_string();
910            }
911            if !dir.ends_with('\\') {
912                dir.push('\\');
913            }
914            (dir, suffix)
915        }
916    }
917}
918
919#[cfg(test)]
920mod tests {
921    use util::paths::PathStyle;
922
923    use crate::open_path_prompt::get_dir_and_suffix;
924
925    #[test]
926    fn test_get_dir_and_suffix_with_windows_style() {
927        let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Windows);
928        assert_eq!(dir, "C:\\");
929        assert_eq!(suffix, "");
930
931        let (dir, suffix) = get_dir_and_suffix("C:".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:\\Use".into(), PathStyle::Windows);
940        assert_eq!(dir, "C:\\");
941        assert_eq!(suffix, "Use");
942
943        let (dir, suffix) =
944            get_dir_and_suffix("C:\\Users\\Junkui\\Docum".into(), PathStyle::Windows);
945        assert_eq!(dir, "C:\\Users\\Junkui\\");
946        assert_eq!(suffix, "Docum");
947
948        let (dir, suffix) =
949            get_dir_and_suffix("C:\\Users\\Junkui\\Documents".into(), PathStyle::Windows);
950        assert_eq!(dir, "C:\\Users\\Junkui\\");
951        assert_eq!(suffix, "Documents");
952
953        let (dir, suffix) =
954            get_dir_and_suffix("C:\\Users\\Junkui\\Documents\\".into(), PathStyle::Windows);
955        assert_eq!(dir, "C:\\Users\\Junkui\\Documents\\");
956        assert_eq!(suffix, "");
957    }
958
959    #[test]
960    fn test_get_dir_and_suffix_with_posix_style() {
961        let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
962        assert_eq!(dir, "/");
963        assert_eq!(suffix, "");
964
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("/Use".into(), PathStyle::Posix);
970        assert_eq!(dir, "/");
971        assert_eq!(suffix, "Use");
972
973        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
974        assert_eq!(dir, "/Users/Junkui/");
975        assert_eq!(suffix, "Docum");
976
977        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
978        assert_eq!(dir, "/Users/Junkui/");
979        assert_eq!(suffix, "Documents");
980
981        let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
982        assert_eq!(dir, "/Users/Junkui/Documents/");
983        assert_eq!(suffix, "");
984    }
985}