file_finder.rs

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