file_finder.rs

  1#[cfg(test)]
  2mod file_finder_tests;
  3
  4use collections::HashMap;
  5use editor::{scroll::Autoscroll, Bias, Editor};
  6use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
  7use gpui::{
  8    actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
  9    ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
 10};
 11use picker::{Picker, PickerDelegate};
 12use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
 13use std::{
 14    cmp,
 15    path::{Path, PathBuf},
 16    sync::{
 17        atomic::{self, AtomicBool},
 18        Arc,
 19    },
 20};
 21use text::Point;
 22use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
 23use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
 24use workspace::{ModalView, Workspace};
 25
 26actions!(file_finder, [Toggle]);
 27
 28impl ModalView for FileFinder {}
 29
 30pub struct FileFinder {
 31    picker: View<Picker<FileFinderDelegate>>,
 32}
 33
 34pub fn init(cx: &mut AppContext) {
 35    cx.observe_new_views(FileFinder::register).detach();
 36}
 37
 38impl FileFinder {
 39    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 40        workspace.register_action(|workspace, _: &Toggle, cx| {
 41            let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
 42                Self::open(workspace, cx);
 43                return;
 44            };
 45
 46            file_finder.update(cx, |file_finder, cx| {
 47                file_finder
 48                    .picker
 49                    .update(cx, |picker, cx| picker.cycle_selection(cx))
 50            });
 51        });
 52    }
 53
 54    fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 55        let project = workspace.project().read(cx);
 56
 57        let currently_opened_path = workspace
 58            .active_item(cx)
 59            .and_then(|item| item.project_path(cx))
 60            .map(|project_path| {
 61                let abs_path = project
 62                    .worktree_for_id(project_path.worktree_id, cx)
 63                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
 64                FoundPath::new(project_path, abs_path)
 65            });
 66
 67        // if exists, bubble the currently opened path to the top
 68        let history_items = currently_opened_path
 69            .clone()
 70            .into_iter()
 71            .chain(
 72                workspace
 73                    .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
 74                    .into_iter()
 75                    .filter(|(history_path, _)| {
 76                        Some(history_path)
 77                            != currently_opened_path
 78                                .as_ref()
 79                                .map(|found_path| &found_path.project)
 80                    })
 81                    .filter(|(_, history_abs_path)| {
 82                        history_abs_path.as_ref()
 83                            != currently_opened_path
 84                                .as_ref()
 85                                .and_then(|found_path| found_path.absolute.as_ref())
 86                    })
 87                    .filter(|(_, history_abs_path)| match history_abs_path {
 88                        Some(abs_path) => history_file_exists(abs_path),
 89                        None => true,
 90                    })
 91                    .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
 92            )
 93            .collect();
 94
 95        let project = workspace.project().clone();
 96        let weak_workspace = cx.view().downgrade();
 97        workspace.toggle_modal(cx, |cx| {
 98            let delegate = FileFinderDelegate::new(
 99                cx.view().downgrade(),
100                weak_workspace,
101                project,
102                currently_opened_path,
103                history_items,
104                cx,
105            );
106
107            FileFinder::new(delegate, cx)
108        });
109    }
110
111    fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
112        Self {
113            picker: cx.new_view(|cx| Picker::new(delegate, cx)),
114        }
115    }
116}
117
118impl EventEmitter<DismissEvent> for FileFinder {}
119
120impl FocusableView for FileFinder {
121    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
122        self.picker.focus_handle(cx)
123    }
124}
125
126impl Render for FileFinder {
127    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
128        v_flex().w(rems(34.)).child(self.picker.clone())
129    }
130}
131
132pub struct FileFinderDelegate {
133    file_finder: WeakView<FileFinder>,
134    workspace: WeakView<Workspace>,
135    project: Model<Project>,
136    search_count: usize,
137    latest_search_id: usize,
138    latest_search_did_cancel: bool,
139    latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
140    currently_opened_path: Option<FoundPath>,
141    matches: Matches,
142    selected_index: Option<usize>,
143    cancel_flag: Arc<AtomicBool>,
144    history_items: Vec<FoundPath>,
145}
146
147/// Use a custom ordering for file finder: the regular one
148/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:
149/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`
150///
151/// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:
152/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`
153/// as the files are shown in the project panel lists.
154#[derive(Debug, Clone, PartialEq, Eq)]
155struct ProjectPanelOrdMatch(PathMatch);
156
157impl Ord for ProjectPanelOrdMatch {
158    fn cmp(&self, other: &Self) -> cmp::Ordering {
159        self.0
160            .score
161            .partial_cmp(&other.0.score)
162            .unwrap_or(cmp::Ordering::Equal)
163            .then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id))
164            .then_with(|| {
165                other
166                    .0
167                    .distance_to_relative_ancestor
168                    .cmp(&self.0.distance_to_relative_ancestor)
169            })
170            .then_with(|| self.0.path.cmp(&other.0.path).reverse())
171    }
172}
173
174impl PartialOrd for ProjectPanelOrdMatch {
175    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
176        Some(self.cmp(other))
177    }
178}
179
180#[derive(Debug, Default)]
181struct Matches {
182    history: Vec<(FoundPath, Option<ProjectPanelOrdMatch>)>,
183    search: Vec<ProjectPanelOrdMatch>,
184}
185
186#[derive(Debug)]
187enum Match<'a> {
188    History(&'a FoundPath, Option<&'a ProjectPanelOrdMatch>),
189    Search(&'a ProjectPanelOrdMatch),
190}
191
192impl Matches {
193    fn len(&self) -> usize {
194        self.history.len() + self.search.len()
195    }
196
197    fn get(&self, index: usize) -> Option<Match<'_>> {
198        if index < self.history.len() {
199            self.history
200                .get(index)
201                .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
202        } else {
203            self.search
204                .get(index - self.history.len())
205                .map(Match::Search)
206        }
207    }
208
209    fn push_new_matches(
210        &mut self,
211        history_items: &Vec<FoundPath>,
212        query: &PathLikeWithPosition<FileSearchQuery>,
213        new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
214        extend_old_matches: bool,
215    ) {
216        let matching_history_paths = matching_history_item_paths(history_items, query);
217        let new_search_matches = new_search_matches
218            .filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
219        let history_items_to_show = history_items.iter().filter_map(|history_item| {
220            Some((
221                history_item.clone(),
222                Some(
223                    matching_history_paths
224                        .get(&history_item.project.path)?
225                        .clone(),
226                ),
227            ))
228        });
229        self.history.clear();
230        util::extend_sorted(
231            &mut self.history,
232            history_items_to_show,
233            100,
234            |(_, a), (_, b)| b.cmp(a),
235        );
236
237        if extend_old_matches {
238            self.search
239                .retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
240        } else {
241            self.search.clear();
242        }
243        util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a));
244    }
245}
246
247fn matching_history_item_paths(
248    history_items: &Vec<FoundPath>,
249    query: &PathLikeWithPosition<FileSearchQuery>,
250) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
251    let history_items_by_worktrees = history_items
252        .iter()
253        .filter_map(|found_path| {
254            let candidate = PathMatchCandidate {
255                path: &found_path.project.path,
256                // Only match history items names, otherwise their paths may match too many queries, producing false positives.
257                // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
258                // it would be shown first always, despite the latter being a better match.
259                char_bag: CharBag::from_iter(
260                    found_path
261                        .project
262                        .path
263                        .file_name()?
264                        .to_string_lossy()
265                        .to_lowercase()
266                        .chars(),
267                ),
268            };
269            Some((found_path.project.worktree_id, candidate))
270        })
271        .fold(
272            HashMap::default(),
273            |mut candidates, (worktree_id, new_candidate)| {
274                candidates
275                    .entry(worktree_id)
276                    .or_insert_with(Vec::new)
277                    .push(new_candidate);
278                candidates
279            },
280        );
281    let mut matching_history_paths = HashMap::default();
282    for (worktree, candidates) in history_items_by_worktrees {
283        let max_results = candidates.len() + 1;
284        matching_history_paths.extend(
285            fuzzy::match_fixed_path_set(
286                candidates,
287                worktree.to_usize(),
288                query.path_like.path_query(),
289                false,
290                max_results,
291            )
292            .into_iter()
293            .map(|path_match| {
294                (
295                    Arc::clone(&path_match.path),
296                    ProjectPanelOrdMatch(path_match),
297                )
298            }),
299        );
300    }
301    matching_history_paths
302}
303
304#[derive(Debug, Clone, PartialEq, Eq)]
305struct FoundPath {
306    project: ProjectPath,
307    absolute: Option<PathBuf>,
308}
309
310impl FoundPath {
311    fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
312        Self { project, absolute }
313    }
314}
315
316const MAX_RECENT_SELECTIONS: usize = 20;
317
318#[cfg(not(test))]
319fn history_file_exists(abs_path: &PathBuf) -> bool {
320    abs_path.exists()
321}
322
323#[cfg(test)]
324fn history_file_exists(abs_path: &PathBuf) -> bool {
325    !abs_path.ends_with("nonexistent.rs")
326}
327
328pub enum Event {
329    Selected(ProjectPath),
330    Dismissed,
331}
332
333#[derive(Debug, Clone)]
334struct FileSearchQuery {
335    raw_query: String,
336    file_query_end: Option<usize>,
337}
338
339impl FileSearchQuery {
340    fn path_query(&self) -> &str {
341        match self.file_query_end {
342            Some(file_path_end) => &self.raw_query[..file_path_end],
343            None => &self.raw_query,
344        }
345    }
346}
347
348impl FileFinderDelegate {
349    fn new(
350        file_finder: WeakView<FileFinder>,
351        workspace: WeakView<Workspace>,
352        project: Model<Project>,
353        currently_opened_path: Option<FoundPath>,
354        history_items: Vec<FoundPath>,
355        cx: &mut ViewContext<FileFinder>,
356    ) -> Self {
357        cx.observe(&project, |file_finder, _, cx| {
358            //todo We should probably not re-render on every project anything
359            file_finder
360                .picker
361                .update(cx, |picker, cx| picker.refresh(cx))
362        })
363        .detach();
364
365        Self {
366            file_finder,
367            workspace,
368            project,
369            search_count: 0,
370            latest_search_id: 0,
371            latest_search_did_cancel: false,
372            latest_search_query: None,
373            currently_opened_path,
374            matches: Matches::default(),
375            selected_index: None,
376            cancel_flag: Arc::new(AtomicBool::new(false)),
377            history_items,
378        }
379    }
380
381    fn spawn_search(
382        &mut self,
383        query: PathLikeWithPosition<FileSearchQuery>,
384        cx: &mut ViewContext<Picker<Self>>,
385    ) -> Task<()> {
386        let relative_to = self
387            .currently_opened_path
388            .as_ref()
389            .map(|found_path| Arc::clone(&found_path.project.path));
390        let worktrees = self
391            .project
392            .read(cx)
393            .visible_worktrees(cx)
394            .collect::<Vec<_>>();
395        let include_root_name = worktrees.len() > 1;
396        let candidate_sets = worktrees
397            .into_iter()
398            .map(|worktree| {
399                let worktree = worktree.read(cx);
400                PathMatchCandidateSet {
401                    snapshot: worktree.snapshot(),
402                    include_ignored: worktree
403                        .root_entry()
404                        .map_or(false, |entry| entry.is_ignored),
405                    include_root_name,
406                }
407            })
408            .collect::<Vec<_>>();
409
410        let search_id = util::post_inc(&mut self.search_count);
411        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
412        self.cancel_flag = Arc::new(AtomicBool::new(false));
413        let cancel_flag = self.cancel_flag.clone();
414        cx.spawn(|picker, mut cx| async move {
415            let matches = fuzzy::match_path_sets(
416                candidate_sets.as_slice(),
417                query.path_like.path_query(),
418                relative_to,
419                false,
420                100,
421                &cancel_flag,
422                cx.background_executor().clone(),
423            )
424            .await
425            .into_iter()
426            .map(ProjectPanelOrdMatch);
427            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
428            picker
429                .update(&mut cx, |picker, cx| {
430                    picker.delegate.selected_index.take();
431                    picker
432                        .delegate
433                        .set_search_matches(search_id, did_cancel, query, matches, cx)
434                })
435                .log_err();
436        })
437    }
438
439    fn set_search_matches(
440        &mut self,
441        search_id: usize,
442        did_cancel: bool,
443        query: PathLikeWithPosition<FileSearchQuery>,
444        matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
445        cx: &mut ViewContext<Picker<Self>>,
446    ) {
447        if search_id >= self.latest_search_id {
448            self.latest_search_id = search_id;
449            let extend_old_matches = self.latest_search_did_cancel
450                && Some(query.path_like.path_query())
451                    == self
452                        .latest_search_query
453                        .as_ref()
454                        .map(|query| query.path_like.path_query());
455            self.matches.push_new_matches(
456                &self.history_items,
457                &query,
458                matches.into_iter(),
459                extend_old_matches,
460            );
461            self.latest_search_query = Some(query);
462            self.latest_search_did_cancel = did_cancel;
463            cx.notify();
464        }
465    }
466
467    fn labels_for_match(
468        &self,
469        path_match: Match,
470        cx: &AppContext,
471        ix: usize,
472    ) -> (String, Vec<usize>, String, Vec<usize>) {
473        let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
474            Match::History(found_path, found_path_match) => {
475                let worktree_id = found_path.project.worktree_id;
476                let project_relative_path = &found_path.project.path;
477                let has_worktree = self
478                    .project
479                    .read(cx)
480                    .worktree_for_id(worktree_id, cx)
481                    .is_some();
482
483                if !has_worktree {
484                    if let Some(absolute_path) = &found_path.absolute {
485                        return (
486                            absolute_path
487                                .file_name()
488                                .map_or_else(
489                                    || project_relative_path.to_string_lossy(),
490                                    |file_name| file_name.to_string_lossy(),
491                                )
492                                .to_string(),
493                            Vec::new(),
494                            absolute_path.to_string_lossy().to_string(),
495                            Vec::new(),
496                        );
497                    }
498                }
499
500                let mut path = Arc::clone(project_relative_path);
501                if project_relative_path.as_ref() == Path::new("") {
502                    if let Some(absolute_path) = &found_path.absolute {
503                        path = Arc::from(absolute_path.as_path());
504                    }
505                }
506
507                let mut path_match = PathMatch {
508                    score: ix as f64,
509                    positions: Vec::new(),
510                    worktree_id: worktree_id.to_usize(),
511                    path,
512                    path_prefix: "".into(),
513                    distance_to_relative_ancestor: usize::MAX,
514                };
515                if let Some(found_path_match) = found_path_match {
516                    path_match
517                        .positions
518                        .extend(found_path_match.0.positions.iter())
519                }
520
521                self.labels_for_path_match(&path_match)
522            }
523            Match::Search(path_match) => self.labels_for_path_match(&path_match.0),
524        };
525
526        if file_name_positions.is_empty() {
527            if let Some(user_home_path) = std::env::var("HOME").ok() {
528                let user_home_path = user_home_path.trim();
529                if !user_home_path.is_empty() {
530                    if (&full_path).starts_with(user_home_path) {
531                        return (
532                            file_name,
533                            file_name_positions,
534                            full_path.replace(user_home_path, "~"),
535                            full_path_positions,
536                        );
537                    }
538                }
539            }
540        }
541
542        (
543            file_name,
544            file_name_positions,
545            full_path,
546            full_path_positions,
547        )
548    }
549
550    fn labels_for_path_match(
551        &self,
552        path_match: &PathMatch,
553    ) -> (String, Vec<usize>, String, Vec<usize>) {
554        let path = &path_match.path;
555        let path_string = path.to_string_lossy();
556        let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
557        let path_positions = path_match.positions.clone();
558
559        let file_name = path.file_name().map_or_else(
560            || path_match.path_prefix.to_string(),
561            |file_name| file_name.to_string_lossy().to_string(),
562        );
563        let file_name_start = path_match.path_prefix.len() + path_string.len() - file_name.len();
564        let file_name_positions = path_positions
565            .iter()
566            .filter_map(|pos| {
567                if pos >= &file_name_start {
568                    Some(pos - file_name_start)
569                } else {
570                    None
571                }
572            })
573            .collect();
574
575        (file_name, file_name_positions, full_path, path_positions)
576    }
577
578    fn lookup_absolute_path(
579        &self,
580        query: PathLikeWithPosition<FileSearchQuery>,
581        cx: &mut ViewContext<'_, Picker<Self>>,
582    ) -> Task<()> {
583        cx.spawn(|picker, mut cx| async move {
584            let Some((project, fs)) = picker
585                .update(&mut cx, |picker, cx| {
586                    let fs = Arc::clone(&picker.delegate.project.read(cx).fs());
587                    (picker.delegate.project.clone(), fs)
588                })
589                .log_err()
590            else {
591                return;
592            };
593
594            let query_path = Path::new(query.path_like.path_query());
595            let mut path_matches = Vec::new();
596            match fs.metadata(query_path).await.log_err() {
597                Some(Some(_metadata)) => {
598                    let update_result = project
599                        .update(&mut cx, |project, cx| {
600                            if let Some((worktree, relative_path)) =
601                                project.find_local_worktree(query_path, cx)
602                            {
603                                path_matches.push(ProjectPanelOrdMatch(PathMatch {
604                                    score: 1.0,
605                                    positions: Vec::new(),
606                                    worktree_id: worktree.read(cx).id().to_usize(),
607                                    path: Arc::from(relative_path),
608                                    path_prefix: "".into(),
609                                    distance_to_relative_ancestor: usize::MAX,
610                                }));
611                            }
612                        })
613                        .log_err();
614                    if update_result.is_none() {
615                        return;
616                    }
617                }
618                Some(None) => {}
619                None => return,
620            }
621
622            picker
623                .update(&mut cx, |picker, cx| {
624                    let picker_delegate = &mut picker.delegate;
625                    let search_id = util::post_inc(&mut picker_delegate.search_count);
626                    picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
627
628                    anyhow::Ok(())
629                })
630                .log_err();
631        })
632    }
633}
634
635impl PickerDelegate for FileFinderDelegate {
636    type ListItem = ListItem;
637
638    fn placeholder_text(&self) -> Arc<str> {
639        "Search project files...".into()
640    }
641
642    fn match_count(&self) -> usize {
643        self.matches.len()
644    }
645
646    fn selected_index(&self) -> usize {
647        self.selected_index.unwrap_or(0)
648    }
649
650    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
651        self.selected_index = Some(ix);
652        cx.notify();
653    }
654
655    fn separators_after_indices(&self) -> Vec<usize> {
656        let history_items = self.matches.history.len();
657        if history_items == 0 || self.matches.search.is_empty() {
658            Vec::new()
659        } else {
660            vec![history_items - 1]
661        }
662    }
663
664    fn update_matches(
665        &mut self,
666        raw_query: String,
667        cx: &mut ViewContext<Picker<Self>>,
668    ) -> Task<()> {
669        let raw_query = raw_query.trim();
670        if raw_query.is_empty() {
671            let project = self.project.read(cx);
672            self.latest_search_id = post_inc(&mut self.search_count);
673            self.selected_index.take();
674            self.matches = Matches {
675                history: self
676                    .history_items
677                    .iter()
678                    .filter(|history_item| {
679                        project
680                            .worktree_for_id(history_item.project.worktree_id, cx)
681                            .is_some()
682                            || (project.is_local() && history_item.absolute.is_some())
683                    })
684                    .cloned()
685                    .map(|p| (p, None))
686                    .collect(),
687                search: Vec::new(),
688            };
689            cx.notify();
690            Task::ready(())
691        } else {
692            let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
693                Ok::<_, std::convert::Infallible>(FileSearchQuery {
694                    raw_query: raw_query.to_owned(),
695                    file_query_end: if path_like_str == raw_query {
696                        None
697                    } else {
698                        Some(path_like_str.len())
699                    },
700                })
701            })
702            .expect("infallible");
703
704            if Path::new(query.path_like.path_query()).is_absolute() {
705                self.lookup_absolute_path(query, cx)
706            } else {
707                self.spawn_search(query, cx)
708            }
709        }
710    }
711
712    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
713        if let Some(m) = self.matches.get(self.selected_index()) {
714            if let Some(workspace) = self.workspace.upgrade() {
715                let open_task = workspace.update(cx, move |workspace, cx| {
716                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
717                        if secondary {
718                            workspace.split_path(project_path, cx)
719                        } else {
720                            workspace.open_path(project_path, None, true, cx)
721                        }
722                    };
723                    match m {
724                        Match::History(history_match, _) => {
725                            let worktree_id = history_match.project.worktree_id;
726                            if workspace
727                                .project()
728                                .read(cx)
729                                .worktree_for_id(worktree_id, cx)
730                                .is_some()
731                            {
732                                split_or_open(
733                                    workspace,
734                                    ProjectPath {
735                                        worktree_id,
736                                        path: Arc::clone(&history_match.project.path),
737                                    },
738                                    cx,
739                                )
740                            } else {
741                                match history_match.absolute.as_ref() {
742                                    Some(abs_path) => {
743                                        if secondary {
744                                            workspace.split_abs_path(
745                                                abs_path.to_path_buf(),
746                                                false,
747                                                cx,
748                                            )
749                                        } else {
750                                            workspace.open_abs_path(
751                                                abs_path.to_path_buf(),
752                                                false,
753                                                cx,
754                                            )
755                                        }
756                                    }
757                                    None => split_or_open(
758                                        workspace,
759                                        ProjectPath {
760                                            worktree_id,
761                                            path: Arc::clone(&history_match.project.path),
762                                        },
763                                        cx,
764                                    ),
765                                }
766                            }
767                        }
768                        Match::Search(m) => split_or_open(
769                            workspace,
770                            ProjectPath {
771                                worktree_id: WorktreeId::from_usize(m.0.worktree_id),
772                                path: m.0.path.clone(),
773                            },
774                            cx,
775                        ),
776                    }
777                });
778
779                let row = self
780                    .latest_search_query
781                    .as_ref()
782                    .and_then(|query| query.row)
783                    .map(|row| row.saturating_sub(1));
784                let col = self
785                    .latest_search_query
786                    .as_ref()
787                    .and_then(|query| query.column)
788                    .unwrap_or(0)
789                    .saturating_sub(1);
790                let finder = self.file_finder.clone();
791
792                cx.spawn(|_, mut cx| async move {
793                    let item = open_task.await.log_err()?;
794                    if let Some(row) = row {
795                        if let Some(active_editor) = item.downcast::<Editor>() {
796                            active_editor
797                                .downgrade()
798                                .update(&mut cx, |editor, cx| {
799                                    let snapshot = editor.snapshot(cx).display_snapshot;
800                                    let point = snapshot
801                                        .buffer_snapshot
802                                        .clip_point(Point::new(row, col), Bias::Left);
803                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
804                                        s.select_ranges([point..point])
805                                    });
806                                })
807                                .log_err();
808                        }
809                    }
810                    finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
811
812                    Some(())
813                })
814                .detach();
815            }
816        }
817    }
818
819    fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
820        self.file_finder
821            .update(cx, |_, cx| cx.emit(DismissEvent))
822            .log_err();
823    }
824
825    fn render_match(
826        &self,
827        ix: usize,
828        selected: bool,
829        cx: &mut ViewContext<Picker<Self>>,
830    ) -> Option<Self::ListItem> {
831        let path_match = self
832            .matches
833            .get(ix)
834            .expect("Invalid matches state: no element for index {ix}");
835
836        let (file_name, file_name_positions, full_path, full_path_positions) =
837            self.labels_for_match(path_match, cx, ix);
838
839        Some(
840            ListItem::new(ix)
841                .spacing(ListItemSpacing::Sparse)
842                .inset(true)
843                .selected(selected)
844                .child(
845                    v_flex()
846                        .child(HighlightedLabel::new(file_name, file_name_positions))
847                        .child(HighlightedLabel::new(full_path, full_path_positions)),
848                ),
849        )
850    }
851}
852
853#[cfg(test)]
854mod tests {
855    use super::*;
856
857    #[test]
858    fn test_custom_project_search_ordering_in_file_finder() {
859        let mut file_finder_sorted_output = vec![
860            ProjectPanelOrdMatch(PathMatch {
861                score: 0.5,
862                positions: Vec::new(),
863                worktree_id: 0,
864                path: Arc::from(Path::new("b0.5")),
865                path_prefix: Arc::from(""),
866                distance_to_relative_ancestor: 0,
867            }),
868            ProjectPanelOrdMatch(PathMatch {
869                score: 1.0,
870                positions: Vec::new(),
871                worktree_id: 0,
872                path: Arc::from(Path::new("c1.0")),
873                path_prefix: Arc::from(""),
874                distance_to_relative_ancestor: 0,
875            }),
876            ProjectPanelOrdMatch(PathMatch {
877                score: 1.0,
878                positions: Vec::new(),
879                worktree_id: 0,
880                path: Arc::from(Path::new("a1.0")),
881                path_prefix: Arc::from(""),
882                distance_to_relative_ancestor: 0,
883            }),
884            ProjectPanelOrdMatch(PathMatch {
885                score: 0.5,
886                positions: Vec::new(),
887                worktree_id: 0,
888                path: Arc::from(Path::new("a0.5")),
889                path_prefix: Arc::from(""),
890                distance_to_relative_ancestor: 0,
891            }),
892            ProjectPanelOrdMatch(PathMatch {
893                score: 1.0,
894                positions: Vec::new(),
895                worktree_id: 0,
896                path: Arc::from(Path::new("b1.0")),
897                path_prefix: Arc::from(""),
898                distance_to_relative_ancestor: 0,
899            }),
900        ];
901        file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
902
903        assert_eq!(
904            file_finder_sorted_output,
905            vec![
906                ProjectPanelOrdMatch(PathMatch {
907                    score: 1.0,
908                    positions: Vec::new(),
909                    worktree_id: 0,
910                    path: Arc::from(Path::new("a1.0")),
911                    path_prefix: Arc::from(""),
912                    distance_to_relative_ancestor: 0,
913                }),
914                ProjectPanelOrdMatch(PathMatch {
915                    score: 1.0,
916                    positions: Vec::new(),
917                    worktree_id: 0,
918                    path: Arc::from(Path::new("b1.0")),
919                    path_prefix: Arc::from(""),
920                    distance_to_relative_ancestor: 0,
921                }),
922                ProjectPanelOrdMatch(PathMatch {
923                    score: 1.0,
924                    positions: Vec::new(),
925                    worktree_id: 0,
926                    path: Arc::from(Path::new("c1.0")),
927                    path_prefix: Arc::from(""),
928                    distance_to_relative_ancestor: 0,
929                }),
930                ProjectPanelOrdMatch(PathMatch {
931                    score: 0.5,
932                    positions: Vec::new(),
933                    worktree_id: 0,
934                    path: Arc::from(Path::new("a0.5")),
935                    path_prefix: Arc::from(""),
936                    distance_to_relative_ancestor: 0,
937                }),
938                ProjectPanelOrdMatch(PathMatch {
939                    score: 0.5,
940                    positions: Vec::new(),
941                    worktree_id: 0,
942                    path: Arc::from(Path::new("b0.5")),
943                    path_prefix: Arc::from(""),
944                    distance_to_relative_ancestor: 0,
945                }),
946            ]
947        );
948    }
949}