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.replace(" ", "");
670        let raw_query = raw_query.trim();
671        if raw_query.is_empty() {
672            let project = self.project.read(cx);
673            self.latest_search_id = post_inc(&mut self.search_count);
674            self.selected_index.take();
675            self.matches = Matches {
676                history: self
677                    .history_items
678                    .iter()
679                    .filter(|history_item| {
680                        project
681                            .worktree_for_id(history_item.project.worktree_id, cx)
682                            .is_some()
683                            || (project.is_local() && history_item.absolute.is_some())
684                    })
685                    .cloned()
686                    .map(|p| (p, None))
687                    .collect(),
688                search: Vec::new(),
689            };
690            cx.notify();
691            Task::ready(())
692        } else {
693            let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
694                Ok::<_, std::convert::Infallible>(FileSearchQuery {
695                    raw_query: raw_query.to_owned(),
696                    file_query_end: if path_like_str == raw_query {
697                        None
698                    } else {
699                        Some(path_like_str.len())
700                    },
701                })
702            })
703            .expect("infallible");
704
705            if Path::new(query.path_like.path_query()).is_absolute() {
706                self.lookup_absolute_path(query, cx)
707            } else {
708                self.spawn_search(query, cx)
709            }
710        }
711    }
712
713    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
714        if let Some(m) = self.matches.get(self.selected_index()) {
715            if let Some(workspace) = self.workspace.upgrade() {
716                let open_task = workspace.update(cx, move |workspace, cx| {
717                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
718                        if secondary {
719                            workspace.split_path(project_path, cx)
720                        } else {
721                            workspace.open_path(project_path, None, true, cx)
722                        }
723                    };
724                    match m {
725                        Match::History(history_match, _) => {
726                            let worktree_id = history_match.project.worktree_id;
727                            if workspace
728                                .project()
729                                .read(cx)
730                                .worktree_for_id(worktree_id, cx)
731                                .is_some()
732                            {
733                                split_or_open(
734                                    workspace,
735                                    ProjectPath {
736                                        worktree_id,
737                                        path: Arc::clone(&history_match.project.path),
738                                    },
739                                    cx,
740                                )
741                            } else {
742                                match history_match.absolute.as_ref() {
743                                    Some(abs_path) => {
744                                        if secondary {
745                                            workspace.split_abs_path(
746                                                abs_path.to_path_buf(),
747                                                false,
748                                                cx,
749                                            )
750                                        } else {
751                                            workspace.open_abs_path(
752                                                abs_path.to_path_buf(),
753                                                false,
754                                                cx,
755                                            )
756                                        }
757                                    }
758                                    None => split_or_open(
759                                        workspace,
760                                        ProjectPath {
761                                            worktree_id,
762                                            path: Arc::clone(&history_match.project.path),
763                                        },
764                                        cx,
765                                    ),
766                                }
767                            }
768                        }
769                        Match::Search(m) => split_or_open(
770                            workspace,
771                            ProjectPath {
772                                worktree_id: WorktreeId::from_usize(m.0.worktree_id),
773                                path: m.0.path.clone(),
774                            },
775                            cx,
776                        ),
777                    }
778                });
779
780                let row = self
781                    .latest_search_query
782                    .as_ref()
783                    .and_then(|query| query.row)
784                    .map(|row| row.saturating_sub(1));
785                let col = self
786                    .latest_search_query
787                    .as_ref()
788                    .and_then(|query| query.column)
789                    .unwrap_or(0)
790                    .saturating_sub(1);
791                let finder = self.file_finder.clone();
792
793                cx.spawn(|_, mut cx| async move {
794                    let item = open_task.await.log_err()?;
795                    if let Some(row) = row {
796                        if let Some(active_editor) = item.downcast::<Editor>() {
797                            active_editor
798                                .downgrade()
799                                .update(&mut cx, |editor, cx| {
800                                    let snapshot = editor.snapshot(cx).display_snapshot;
801                                    let point = snapshot
802                                        .buffer_snapshot
803                                        .clip_point(Point::new(row, col), Bias::Left);
804                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
805                                        s.select_ranges([point..point])
806                                    });
807                                })
808                                .log_err();
809                        }
810                    }
811                    finder.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok()?;
812
813                    Some(())
814                })
815                .detach();
816            }
817        }
818    }
819
820    fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
821        self.file_finder
822            .update(cx, |_, cx| cx.emit(DismissEvent))
823            .log_err();
824    }
825
826    fn render_match(
827        &self,
828        ix: usize,
829        selected: bool,
830        cx: &mut ViewContext<Picker<Self>>,
831    ) -> Option<Self::ListItem> {
832        let path_match = self
833            .matches
834            .get(ix)
835            .expect("Invalid matches state: no element for index {ix}");
836
837        let (file_name, file_name_positions, full_path, full_path_positions) =
838            self.labels_for_match(path_match, cx, ix);
839
840        Some(
841            ListItem::new(ix)
842                .spacing(ListItemSpacing::Sparse)
843                .inset(true)
844                .selected(selected)
845                .child(
846                    v_flex()
847                        .child(HighlightedLabel::new(file_name, file_name_positions))
848                        .child(HighlightedLabel::new(full_path, full_path_positions)),
849                ),
850        )
851    }
852}
853
854#[cfg(test)]
855mod tests {
856    use super::*;
857
858    #[test]
859    fn test_custom_project_search_ordering_in_file_finder() {
860        let mut file_finder_sorted_output = vec![
861            ProjectPanelOrdMatch(PathMatch {
862                score: 0.5,
863                positions: Vec::new(),
864                worktree_id: 0,
865                path: Arc::from(Path::new("b0.5")),
866                path_prefix: Arc::from(""),
867                distance_to_relative_ancestor: 0,
868            }),
869            ProjectPanelOrdMatch(PathMatch {
870                score: 1.0,
871                positions: Vec::new(),
872                worktree_id: 0,
873                path: Arc::from(Path::new("c1.0")),
874                path_prefix: Arc::from(""),
875                distance_to_relative_ancestor: 0,
876            }),
877            ProjectPanelOrdMatch(PathMatch {
878                score: 1.0,
879                positions: Vec::new(),
880                worktree_id: 0,
881                path: Arc::from(Path::new("a1.0")),
882                path_prefix: Arc::from(""),
883                distance_to_relative_ancestor: 0,
884            }),
885            ProjectPanelOrdMatch(PathMatch {
886                score: 0.5,
887                positions: Vec::new(),
888                worktree_id: 0,
889                path: Arc::from(Path::new("a0.5")),
890                path_prefix: Arc::from(""),
891                distance_to_relative_ancestor: 0,
892            }),
893            ProjectPanelOrdMatch(PathMatch {
894                score: 1.0,
895                positions: Vec::new(),
896                worktree_id: 0,
897                path: Arc::from(Path::new("b1.0")),
898                path_prefix: Arc::from(""),
899                distance_to_relative_ancestor: 0,
900            }),
901        ];
902        file_finder_sorted_output.sort_by(|a, b| b.cmp(a));
903
904        assert_eq!(
905            file_finder_sorted_output,
906            vec![
907                ProjectPanelOrdMatch(PathMatch {
908                    score: 1.0,
909                    positions: Vec::new(),
910                    worktree_id: 0,
911                    path: Arc::from(Path::new("a1.0")),
912                    path_prefix: Arc::from(""),
913                    distance_to_relative_ancestor: 0,
914                }),
915                ProjectPanelOrdMatch(PathMatch {
916                    score: 1.0,
917                    positions: Vec::new(),
918                    worktree_id: 0,
919                    path: Arc::from(Path::new("b1.0")),
920                    path_prefix: Arc::from(""),
921                    distance_to_relative_ancestor: 0,
922                }),
923                ProjectPanelOrdMatch(PathMatch {
924                    score: 1.0,
925                    positions: Vec::new(),
926                    worktree_id: 0,
927                    path: Arc::from(Path::new("c1.0")),
928                    path_prefix: Arc::from(""),
929                    distance_to_relative_ancestor: 0,
930                }),
931                ProjectPanelOrdMatch(PathMatch {
932                    score: 0.5,
933                    positions: Vec::new(),
934                    worktree_id: 0,
935                    path: Arc::from(Path::new("a0.5")),
936                    path_prefix: Arc::from(""),
937                    distance_to_relative_ancestor: 0,
938                }),
939                ProjectPanelOrdMatch(PathMatch {
940                    score: 0.5,
941                    positions: Vec::new(),
942                    worktree_id: 0,
943                    path: Arc::from(Path::new("b0.5")),
944                    path_prefix: Arc::from(""),
945                    distance_to_relative_ancestor: 0,
946                }),
947            ]
948        );
949    }
950}