file_finder.rs

   1#[cfg(test)]
   2mod file_finder_tests;
   3
   4use futures::future::join_all;
   5pub use open_path_prompt::OpenPathDelegate;
   6
   7use channel::ChannelStore;
   8use client::ChannelId;
   9use collections::HashMap;
  10use editor::Editor;
  11use file_icons::FileIcons;
  12use fuzzy::{StringMatch, StringMatchCandidate};
  13use fuzzy_nucleo::{PathMatch, PathMatchCandidate};
  14use gpui::{
  15    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
  16    KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render,
  17    StatefulInteractiveElement, Styled, Task, WeakEntity, Window, actions, rems,
  18};
  19use open_path_prompt::{
  20    OpenPathPrompt,
  21    file_finder_settings::{FileFinderSettings, FileFinderWidth},
  22};
  23use picker::{Picker, PickerDelegate};
  24use project::{
  25    PathMatchCandidateSet, Project, ProjectPath, WorktreeId, worktree_store::WorktreeStore,
  26};
  27use project_panel::project_panel_settings::ProjectPanelSettings;
  28use settings::Settings;
  29use std::{
  30    borrow::Cow,
  31    cmp,
  32    ops::Range,
  33    path::{Component, Path, PathBuf},
  34    sync::{
  35        Arc,
  36        atomic::{self, AtomicBool},
  37    },
  38};
  39use ui::{
  40    ButtonLike, CommonAnimationExt, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem,
  41    ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
  42};
  43use ui_input::ErasedEditor;
  44use util::{
  45    ResultExt, maybe,
  46    paths::{PathStyle, PathWithPosition},
  47    post_inc,
  48    rel_path::RelPath,
  49};
  50use workspace::{
  51    ModalView, OpenChannelNotesById, OpenOptions, OpenVisible, SplitDirection, Workspace,
  52    item::PreviewTabsSettings, notifications::NotifyResultExt, pane,
  53};
  54use zed_actions::search::ToggleIncludeIgnored;
  55
  56actions!(
  57    file_finder,
  58    [
  59        /// Selects the previous item in the file finder.
  60        SelectPrevious,
  61        /// Toggles the file filter menu.
  62        ToggleFilterMenu,
  63        /// Toggles the split direction menu.
  64        ToggleSplitMenu
  65    ]
  66);
  67
  68impl ModalView for FileFinder {
  69    fn on_before_dismiss(
  70        &mut self,
  71        window: &mut Window,
  72        cx: &mut Context<Self>,
  73    ) -> workspace::DismissDecision {
  74        let submenu_focused = self.picker.update(cx, |picker, cx| {
  75            picker
  76                .delegate
  77                .filter_popover_menu_handle
  78                .is_focused(window, cx)
  79                || picker
  80                    .delegate
  81                    .split_popover_menu_handle
  82                    .is_focused(window, cx)
  83        });
  84        workspace::DismissDecision::Dismiss(!submenu_focused)
  85    }
  86}
  87
  88pub struct FileFinder {
  89    picker: Entity<Picker<FileFinderDelegate>>,
  90    picker_focus_handle: FocusHandle,
  91    init_modifiers: Option<Modifiers>,
  92}
  93
  94pub fn init(cx: &mut App) {
  95    cx.observe_new(FileFinder::register).detach();
  96    cx.observe_new(OpenPathPrompt::register).detach();
  97    cx.observe_new(OpenPathPrompt::register_new_path).detach();
  98}
  99
 100impl FileFinder {
 101    fn register(
 102        workspace: &mut Workspace,
 103        _window: Option<&mut Window>,
 104        _: &mut Context<Workspace>,
 105    ) {
 106        workspace.register_action(
 107            |workspace, action: &workspace::ToggleFileFinder, window, cx| {
 108                let Some(file_finder) = workspace.active_modal::<Self>(cx) else {
 109                    Self::open(workspace, action.separate_history, window, cx).detach();
 110                    return;
 111                };
 112
 113                file_finder.update(cx, |file_finder, cx| {
 114                    file_finder.init_modifiers = Some(window.modifiers());
 115                    file_finder.picker.update(cx, |picker, cx| {
 116                        picker.cycle_selection(window, cx);
 117                    });
 118                });
 119            },
 120        );
 121    }
 122
 123    fn open(
 124        workspace: &mut Workspace,
 125        separate_history: bool,
 126        window: &mut Window,
 127        cx: &mut Context<Workspace>,
 128    ) -> Task<()> {
 129        let project = workspace.project().read(cx);
 130        let fs = project.fs();
 131
 132        let currently_opened_path = workspace.active_item(cx).and_then(|item| {
 133            let project_path = item.project_path(cx)?;
 134            let abs_path = project
 135                .worktree_for_id(project_path.worktree_id, cx)?
 136                .read(cx)
 137                .absolutize(&project_path.path);
 138            Some(FoundPath::new(project_path, abs_path))
 139        });
 140
 141        let history_items = workspace
 142            .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
 143            .into_iter()
 144            .filter_map(|(project_path, abs_path)| {
 145                if project.entry_for_path(&project_path, cx).is_some() {
 146                    return Some(Task::ready(Some(FoundPath::new(project_path, abs_path?))));
 147                }
 148                let abs_path = abs_path?;
 149                if project.is_local() {
 150                    let fs = fs.clone();
 151                    Some(cx.background_spawn(async move {
 152                        if fs.is_file(&abs_path).await {
 153                            Some(FoundPath::new(project_path, abs_path))
 154                        } else {
 155                            None
 156                        }
 157                    }))
 158                } else {
 159                    Some(Task::ready(Some(FoundPath::new(project_path, abs_path))))
 160                }
 161            })
 162            .collect::<Vec<_>>();
 163        cx.spawn_in(window, async move |workspace, cx| {
 164            let history_items = join_all(history_items).await.into_iter().flatten();
 165
 166            workspace
 167                .update_in(cx, |workspace, window, cx| {
 168                    let project = workspace.project().clone();
 169                    let weak_workspace = cx.entity().downgrade();
 170                    workspace.toggle_modal(window, cx, |window, cx| {
 171                        let delegate = FileFinderDelegate::new(
 172                            cx.entity().downgrade(),
 173                            weak_workspace,
 174                            project,
 175                            currently_opened_path,
 176                            history_items.collect(),
 177                            separate_history,
 178                            window,
 179                            cx,
 180                        );
 181
 182                        FileFinder::new(delegate, window, cx)
 183                    });
 184                })
 185                .ok();
 186        })
 187    }
 188
 189    fn new(delegate: FileFinderDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
 190        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
 191        let picker_focus_handle = picker.focus_handle(cx);
 192        picker.update(cx, |picker, _| {
 193            picker.delegate.focus_handle = picker_focus_handle.clone();
 194        });
 195        Self {
 196            picker,
 197            picker_focus_handle,
 198            init_modifiers: window.modifiers().modified().then_some(window.modifiers()),
 199        }
 200    }
 201
 202    fn handle_modifiers_changed(
 203        &mut self,
 204        event: &ModifiersChangedEvent,
 205        window: &mut Window,
 206        cx: &mut Context<Self>,
 207    ) {
 208        let Some(init_modifiers) = self.init_modifiers.take() else {
 209            return;
 210        };
 211        if self.picker.read(cx).delegate.has_changed_selected_index
 212            && (!event.modified() || !init_modifiers.is_subset_of(event))
 213        {
 214            self.init_modifiers = None;
 215            window.dispatch_action(menu::Confirm.boxed_clone(), cx);
 216        }
 217    }
 218
 219    fn handle_select_prev(
 220        &mut self,
 221        _: &SelectPrevious,
 222        window: &mut Window,
 223        cx: &mut Context<Self>,
 224    ) {
 225        self.init_modifiers = Some(window.modifiers());
 226        window.dispatch_action(Box::new(menu::SelectPrevious), cx);
 227    }
 228
 229    fn handle_filter_toggle_menu(
 230        &mut self,
 231        _: &ToggleFilterMenu,
 232        window: &mut Window,
 233        cx: &mut Context<Self>,
 234    ) {
 235        self.picker.update(cx, |picker, cx| {
 236            let menu_handle = &picker.delegate.filter_popover_menu_handle;
 237            if menu_handle.is_deployed() {
 238                menu_handle.hide(cx);
 239            } else {
 240                menu_handle.show(window, cx);
 241            }
 242        });
 243    }
 244
 245    fn handle_split_toggle_menu(
 246        &mut self,
 247        _: &ToggleSplitMenu,
 248        window: &mut Window,
 249        cx: &mut Context<Self>,
 250    ) {
 251        self.picker.update(cx, |picker, cx| {
 252            let menu_handle = &picker.delegate.split_popover_menu_handle;
 253            if menu_handle.is_deployed() {
 254                menu_handle.hide(cx);
 255            } else {
 256                menu_handle.show(window, cx);
 257            }
 258        });
 259    }
 260
 261    fn handle_toggle_ignored(
 262        &mut self,
 263        _: &ToggleIncludeIgnored,
 264        window: &mut Window,
 265        cx: &mut Context<Self>,
 266    ) {
 267        self.picker.update(cx, |picker, cx| {
 268            picker.delegate.include_ignored = match picker.delegate.include_ignored {
 269                Some(true) => FileFinderSettings::get_global(cx)
 270                    .include_ignored
 271                    .map(|_| false),
 272                Some(false) => Some(true),
 273                None => Some(true),
 274            };
 275            picker.delegate.include_ignored_refresh =
 276                picker.delegate.update_matches(picker.query(cx), window, cx);
 277        });
 278    }
 279
 280    fn go_to_file_split_left(
 281        &mut self,
 282        _: &pane::SplitLeft,
 283        window: &mut Window,
 284        cx: &mut Context<Self>,
 285    ) {
 286        self.go_to_file_split_inner(SplitDirection::Left, window, cx)
 287    }
 288
 289    fn go_to_file_split_right(
 290        &mut self,
 291        _: &pane::SplitRight,
 292        window: &mut Window,
 293        cx: &mut Context<Self>,
 294    ) {
 295        self.go_to_file_split_inner(SplitDirection::Right, window, cx)
 296    }
 297
 298    fn go_to_file_split_up(
 299        &mut self,
 300        _: &pane::SplitUp,
 301        window: &mut Window,
 302        cx: &mut Context<Self>,
 303    ) {
 304        self.go_to_file_split_inner(SplitDirection::Up, window, cx)
 305    }
 306
 307    fn go_to_file_split_down(
 308        &mut self,
 309        _: &pane::SplitDown,
 310        window: &mut Window,
 311        cx: &mut Context<Self>,
 312    ) {
 313        self.go_to_file_split_inner(SplitDirection::Down, window, cx)
 314    }
 315
 316    fn go_to_file_split_inner(
 317        &mut self,
 318        split_direction: SplitDirection,
 319        window: &mut Window,
 320        cx: &mut Context<Self>,
 321    ) {
 322        self.picker.update(cx, |picker, cx| {
 323            let delegate = &mut picker.delegate;
 324            if let Some(workspace) = delegate.workspace.upgrade()
 325                && let Some(m) = delegate.matches.get(delegate.selected_index())
 326            {
 327                let path = match m {
 328                    Match::History { path, .. } => {
 329                        let worktree_id = path.project.worktree_id;
 330                        ProjectPath {
 331                            worktree_id,
 332                            path: Arc::clone(&path.project.path),
 333                        }
 334                    }
 335                    Match::Search(m) => ProjectPath {
 336                        worktree_id: WorktreeId::from_usize(m.0.worktree_id),
 337                        path: m.0.path.clone(),
 338                    },
 339                    Match::CreateNew(p) => p.clone(),
 340                    Match::Channel { .. } => return,
 341                };
 342                let open_task = workspace.update(cx, move |workspace, cx| {
 343                    workspace.split_path_preview(path, false, Some(split_direction), window, cx)
 344                });
 345                open_task.detach_and_log_err(cx);
 346            }
 347        })
 348    }
 349
 350    pub fn modal_max_width(width_setting: FileFinderWidth, window: &mut Window) -> Pixels {
 351        let window_width = window.viewport_size().width;
 352        let small_width = rems(34.).to_pixels(window.rem_size());
 353
 354        match width_setting {
 355            FileFinderWidth::Small => small_width,
 356            FileFinderWidth::Full => window_width,
 357            FileFinderWidth::XLarge => (window_width - px(512.)).max(small_width),
 358            FileFinderWidth::Large => (window_width - px(768.)).max(small_width),
 359            FileFinderWidth::Medium => (window_width - px(1024.)).max(small_width),
 360        }
 361    }
 362}
 363
 364impl EventEmitter<DismissEvent> for FileFinder {}
 365
 366impl Focusable for FileFinder {
 367    fn focus_handle(&self, _: &App) -> FocusHandle {
 368        self.picker_focus_handle.clone()
 369    }
 370}
 371
 372impl Render for FileFinder {
 373    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 374        let key_context = self.picker.read(cx).delegate.key_context(window, cx);
 375
 376        let file_finder_settings = FileFinderSettings::get_global(cx);
 377        let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, window);
 378
 379        v_flex()
 380            .key_context(key_context)
 381            .w(modal_max_width)
 382            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
 383            .on_action(cx.listener(Self::handle_select_prev))
 384            .on_action(cx.listener(Self::handle_filter_toggle_menu))
 385            .on_action(cx.listener(Self::handle_split_toggle_menu))
 386            .on_action(cx.listener(Self::handle_toggle_ignored))
 387            .on_action(cx.listener(Self::go_to_file_split_left))
 388            .on_action(cx.listener(Self::go_to_file_split_right))
 389            .on_action(cx.listener(Self::go_to_file_split_up))
 390            .on_action(cx.listener(Self::go_to_file_split_down))
 391            .child(self.picker.clone())
 392    }
 393}
 394
 395pub struct FileFinderDelegate {
 396    file_finder: WeakEntity<FileFinder>,
 397    workspace: WeakEntity<Workspace>,
 398    project: Entity<Project>,
 399    channel_store: Option<Entity<ChannelStore>>,
 400    search_count: usize,
 401    latest_search_id: usize,
 402    latest_search_did_cancel: bool,
 403    latest_search_query: Option<FileSearchQuery>,
 404    currently_opened_path: Option<FoundPath>,
 405    matches: Matches,
 406    selected_index: usize,
 407    has_changed_selected_index: bool,
 408    cancel_flag: Arc<AtomicBool>,
 409    history_items: Vec<FoundPath>,
 410    separate_history: bool,
 411    first_update: bool,
 412    filter_popover_menu_handle: PopoverMenuHandle<ContextMenu>,
 413    split_popover_menu_handle: PopoverMenuHandle<ContextMenu>,
 414    focus_handle: FocusHandle,
 415    include_ignored: Option<bool>,
 416    include_ignored_refresh: Task<()>,
 417}
 418
 419/// Use a custom ordering for file finder: the regular one
 420/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:
 421/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`
 422///
 423/// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:
 424/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`
 425/// as the files are shown in the project panel lists.
 426#[derive(Debug, Clone, PartialEq, Eq)]
 427struct ProjectPanelOrdMatch(PathMatch);
 428
 429impl Ord for ProjectPanelOrdMatch {
 430    fn cmp(&self, other: &Self) -> cmp::Ordering {
 431        self.0
 432            .score
 433            .partial_cmp(&other.0.score)
 434            .unwrap_or(cmp::Ordering::Equal)
 435            .then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id))
 436            .then_with(|| {
 437                other
 438                    .0
 439                    .distance_to_relative_ancestor
 440                    .cmp(&self.0.distance_to_relative_ancestor)
 441            })
 442            .then_with(|| self.0.path.cmp(&other.0.path).reverse())
 443    }
 444}
 445
 446impl PartialOrd for ProjectPanelOrdMatch {
 447    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
 448        Some(self.cmp(other))
 449    }
 450}
 451
 452#[derive(Debug, Default)]
 453struct Matches {
 454    separate_history: bool,
 455    matches: Vec<Match>,
 456}
 457
 458#[derive(Debug, Clone)]
 459enum Match {
 460    History {
 461        path: FoundPath,
 462        panel_match: Option<ProjectPanelOrdMatch>,
 463    },
 464    Search(ProjectPanelOrdMatch),
 465    Channel {
 466        channel_id: ChannelId,
 467        channel_name: SharedString,
 468        string_match: StringMatch,
 469    },
 470    CreateNew(ProjectPath),
 471}
 472
 473impl Match {
 474    fn relative_path(&self) -> Option<&Arc<RelPath>> {
 475        match self {
 476            Match::History { path, .. } => Some(&path.project.path),
 477            Match::Search(panel_match) => Some(&panel_match.0.path),
 478            Match::Channel { .. } | Match::CreateNew(_) => None,
 479        }
 480    }
 481
 482    fn abs_path(&self, project: &Entity<Project>, cx: &App) -> Option<PathBuf> {
 483        match self {
 484            Match::History { path, .. } => Some(path.absolute.clone()),
 485            Match::Search(ProjectPanelOrdMatch(path_match)) => Some(
 486                project
 487                    .read(cx)
 488                    .worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?
 489                    .read(cx)
 490                    .absolutize(&path_match.path),
 491            ),
 492            Match::Channel { .. } | Match::CreateNew(_) => None,
 493        }
 494    }
 495
 496    fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> {
 497        match self {
 498            Match::History { panel_match, .. } => panel_match.as_ref(),
 499            Match::Search(panel_match) => Some(panel_match),
 500            Match::Channel { .. } | Match::CreateNew(_) => None,
 501        }
 502    }
 503}
 504
 505impl Matches {
 506    fn len(&self) -> usize {
 507        self.matches.len()
 508    }
 509
 510    fn get(&self, index: usize) -> Option<&Match> {
 511        self.matches.get(index)
 512    }
 513
 514    fn position(
 515        &self,
 516        entry: &Match,
 517        currently_opened: Option<&FoundPath>,
 518    ) -> Result<usize, usize> {
 519        if let Match::History {
 520            path,
 521            panel_match: None,
 522        } = entry
 523        {
 524            // Slow case: linear search by path. Should not happen actually,
 525            // since we call `position` only if matches set changed, but the query has not changed.
 526            // And History entries do not have panel_match if query is empty, so there's no
 527            // reason for the matches set to change.
 528            self.matches
 529                .iter()
 530                .position(|m| match m.relative_path() {
 531                    Some(p) => path.project.path == *p,
 532                    None => false,
 533                })
 534                .ok_or(0)
 535        } else {
 536            self.matches.binary_search_by(|m| {
 537                // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b.
 538                // And we want the better entries go first.
 539                Self::cmp_matches(self.separate_history, currently_opened, m, entry).reverse()
 540            })
 541        }
 542    }
 543
 544    fn push_new_matches<'a>(
 545        &'a mut self,
 546        worktree_store: Entity<WorktreeStore>,
 547        cx: &'a App,
 548        history_items: impl IntoIterator<Item = &'a FoundPath> + Clone,
 549        currently_opened: Option<&'a FoundPath>,
 550        query: Option<&FileSearchQuery>,
 551        new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
 552        extend_old_matches: bool,
 553        path_style: PathStyle,
 554    ) {
 555        let Some(query) = query else {
 556            // assuming that if there's no query, then there's no search matches.
 557            self.matches.clear();
 558            let path_to_entry = |found_path: &FoundPath| Match::History {
 559                path: found_path.clone(),
 560                panel_match: None,
 561            };
 562
 563            self.matches
 564                .extend(history_items.into_iter().map(path_to_entry));
 565            return;
 566        };
 567
 568        let worktree_name_by_id = if should_hide_root_in_entry_path(&worktree_store, cx) {
 569            None
 570        } else {
 571            Some(
 572                worktree_store
 573                    .read(cx)
 574                    .worktrees()
 575                    .map(|worktree| {
 576                        let snapshot = worktree.read(cx).snapshot();
 577                        (snapshot.id(), snapshot.root_name().into())
 578                    })
 579                    .collect(),
 580            )
 581        };
 582        let new_history_matches = matching_history_items(
 583            history_items,
 584            currently_opened,
 585            worktree_name_by_id,
 586            query,
 587            path_style,
 588        );
 589        let new_search_matches: Vec<Match> = new_search_matches
 590            .filter(|path_match| {
 591                !new_history_matches.contains_key(&ProjectPath {
 592                    path: path_match.0.path.clone(),
 593                    worktree_id: WorktreeId::from_usize(path_match.0.worktree_id),
 594                })
 595            })
 596            .map(Match::Search)
 597            .collect();
 598
 599        if extend_old_matches {
 600            // since we take history matches instead of new search matches
 601            // and history matches has not changed(since the query has not changed and we do not extend old matches otherwise),
 602            // old matches can't contain paths present in history_matches as well.
 603            self.matches.retain(|m| matches!(m, Match::Search(_)));
 604        } else {
 605            self.matches.clear();
 606        }
 607
 608        // At this point we have an unsorted set of new history matches, an unsorted set of new search matches
 609        // and a sorted set of old search matches.
 610        // It is possible that the new search matches' paths contain some of the old search matches' paths.
 611        // History matches' paths are unique, since store in a HashMap by path.
 612        // We build a sorted Vec<Match>, eliminating duplicate search matches.
 613        // Search matches with the same paths should have equal `ProjectPanelOrdMatch`, so we should
 614        // not have any duplicates after building the final list.
 615        for new_match in new_history_matches
 616            .into_values()
 617            .chain(new_search_matches.into_iter())
 618        {
 619            match self.position(&new_match, currently_opened) {
 620                Ok(_duplicate) => continue,
 621                Err(i) => {
 622                    self.matches.insert(i, new_match);
 623                    if self.matches.len() == 100 {
 624                        break;
 625                    }
 626                }
 627            }
 628        }
 629    }
 630
 631    /// If a < b, then a is a worse match, aligning with the `ProjectPanelOrdMatch` ordering.
 632    fn cmp_matches(
 633        separate_history: bool,
 634        currently_opened: Option<&FoundPath>,
 635        a: &Match,
 636        b: &Match,
 637    ) -> cmp::Ordering {
 638        // Handle CreateNew variant - always put it at the end
 639        match (a, b) {
 640            (Match::CreateNew(_), _) => return cmp::Ordering::Less,
 641            (_, Match::CreateNew(_)) => return cmp::Ordering::Greater,
 642            _ => {}
 643        }
 644
 645        match (&a, &b) {
 646            // bubble currently opened files to the top
 647            (Match::History { path, .. }, _) if Some(path) == currently_opened => {
 648                return cmp::Ordering::Greater;
 649            }
 650            (_, Match::History { path, .. }) if Some(path) == currently_opened => {
 651                return cmp::Ordering::Less;
 652            }
 653
 654            _ => {}
 655        }
 656
 657        if separate_history {
 658            match (a, b) {
 659                (Match::History { .. }, Match::Search(_)) => return cmp::Ordering::Greater,
 660                (Match::Search(_), Match::History { .. }) => return cmp::Ordering::Less,
 661
 662                _ => {}
 663            }
 664        }
 665
 666        // For file-vs-file matches, use the existing detailed comparison.
 667        if let (Some(a_panel), Some(b_panel)) = (a.panel_match(), b.panel_match()) {
 668            return a_panel.cmp(b_panel);
 669        }
 670
 671        let a_score = Self::match_score(a);
 672        let b_score = Self::match_score(b);
 673        // When at least one side is a channel, compare by raw score.
 674        a_score
 675            .partial_cmp(&b_score)
 676            .unwrap_or(cmp::Ordering::Equal)
 677    }
 678
 679    fn match_score(m: &Match) -> f64 {
 680        match m {
 681            Match::History { panel_match, .. } => panel_match.as_ref().map_or(0.0, |pm| pm.0.score),
 682            Match::Search(pm) => pm.0.score,
 683            Match::Channel { string_match, .. } => string_match.score,
 684            Match::CreateNew(_) => 0.0,
 685        }
 686    }
 687}
 688
 689fn matching_history_items<'a>(
 690    history_items: impl IntoIterator<Item = &'a FoundPath>,
 691    currently_opened: Option<&'a FoundPath>,
 692    worktree_name_by_id: Option<HashMap<WorktreeId, Arc<RelPath>>>,
 693    query: &FileSearchQuery,
 694    path_style: PathStyle,
 695) -> HashMap<ProjectPath, Match> {
 696    let mut candidates_paths = HashMap::default();
 697
 698    let history_items_by_worktrees = history_items
 699        .into_iter()
 700        .chain(currently_opened)
 701        .map(|found_path| {
 702            // Only match history items names, otherwise their paths may match too many queries,
 703            // producing false positives. E.g. `foo` would match both `something/foo/bar.rs` and
 704            // `something/foo/foo.rs` and if the former is a history item, it would be shown first
 705            // always, despite the latter being a better match.
 706            let candidate = PathMatchCandidate::new(
 707                &found_path.project.path,
 708                false,
 709                worktree_name_by_id
 710                    .as_ref()
 711                    .and_then(|m| m.get(&found_path.project.worktree_id))
 712                    .map(|prefix| prefix.as_ref()),
 713            );
 714            candidates_paths.insert(&found_path.project, found_path);
 715            (found_path.project.worktree_id, candidate)
 716        })
 717        .fold(
 718            HashMap::default(),
 719            |mut candidates, (worktree_id, new_candidate)| {
 720                candidates
 721                    .entry(worktree_id)
 722                    .or_insert_with(Vec::new)
 723                    .push(new_candidate);
 724                candidates
 725            },
 726        );
 727    let mut matching_history_paths = HashMap::default();
 728    for (worktree, candidates) in history_items_by_worktrees {
 729        let max_results = candidates.len() + 1;
 730        let worktree_root_name = worktree_name_by_id
 731            .as_ref()
 732            .and_then(|w| w.get(&worktree).cloned());
 733
 734        matching_history_paths.extend(
 735            fuzzy_nucleo::match_fixed_path_set(
 736                candidates,
 737                worktree.to_usize(),
 738                worktree_root_name,
 739                query.path_query(),
 740                fuzzy_nucleo::Case::Ignore,
 741                max_results,
 742                path_style,
 743            )
 744            .into_iter()
 745            // filter matches where at least one matched position is in filename portion, to prevent directory matches, nucleo scores them higher as history items are matched against their full path
 746            .filter(|path_match| {
 747                if let Some(filename) = path_match.path.file_name() {
 748                    let filename_start = path_match.path.as_unix_str().len() - filename.len();
 749                    path_match
 750                        .positions
 751                        .iter()
 752                        .any(|&pos| pos >= filename_start)
 753                } else {
 754                    true
 755                }
 756            })
 757            .filter_map(|path_match| {
 758                candidates_paths
 759                    .remove_entry(&ProjectPath {
 760                        worktree_id: WorktreeId::from_usize(path_match.worktree_id),
 761                        path: Arc::clone(&path_match.path),
 762                    })
 763                    .map(|(project_path, found_path)| {
 764                        (
 765                            project_path.clone(),
 766                            Match::History {
 767                                path: found_path.clone(),
 768                                panel_match: Some(ProjectPanelOrdMatch(path_match)),
 769                            },
 770                        )
 771                    })
 772            }),
 773        );
 774    }
 775    matching_history_paths
 776}
 777
 778fn should_hide_root_in_entry_path(worktree_store: &Entity<WorktreeStore>, cx: &App) -> bool {
 779    let multiple_worktrees = worktree_store
 780        .read(cx)
 781        .visible_worktrees(cx)
 782        .filter(|worktree| !worktree.read(cx).is_single_file())
 783        .nth(1)
 784        .is_some();
 785    ProjectPanelSettings::get_global(cx).hide_root && !multiple_worktrees
 786}
 787
 788#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 789struct FoundPath {
 790    project: ProjectPath,
 791    absolute: PathBuf,
 792}
 793
 794impl FoundPath {
 795    fn new(project: ProjectPath, absolute: PathBuf) -> Self {
 796        Self { project, absolute }
 797    }
 798}
 799
 800const MAX_RECENT_SELECTIONS: usize = 20;
 801
 802pub enum Event {
 803    Selected(ProjectPath),
 804    Dismissed,
 805}
 806
 807#[derive(Debug, Clone)]
 808struct FileSearchQuery {
 809    raw_query: String,
 810    file_query_end: Option<usize>,
 811    path_position: PathWithPosition,
 812}
 813
 814impl FileSearchQuery {
 815    fn path_query(&self) -> &str {
 816        match self.file_query_end {
 817            Some(file_path_end) => &self.raw_query[..file_path_end],
 818            None => &self.raw_query,
 819        }
 820    }
 821}
 822
 823impl FileFinderDelegate {
 824    fn new(
 825        file_finder: WeakEntity<FileFinder>,
 826        workspace: WeakEntity<Workspace>,
 827        project: Entity<Project>,
 828        currently_opened_path: Option<FoundPath>,
 829        history_items: Vec<FoundPath>,
 830        separate_history: bool,
 831        window: &mut Window,
 832        cx: &mut Context<FileFinder>,
 833    ) -> Self {
 834        Self::subscribe_to_updates(&project, window, cx);
 835        let channel_store = if FileFinderSettings::get_global(cx).include_channels {
 836            ChannelStore::try_global(cx)
 837        } else {
 838            None
 839        };
 840        Self {
 841            file_finder,
 842            workspace,
 843            project,
 844            channel_store,
 845            search_count: 0,
 846            latest_search_id: 0,
 847            latest_search_did_cancel: false,
 848            latest_search_query: None,
 849            currently_opened_path,
 850            matches: Matches::default(),
 851            has_changed_selected_index: false,
 852            selected_index: 0,
 853            cancel_flag: Arc::new(AtomicBool::new(false)),
 854            history_items,
 855            separate_history,
 856            first_update: true,
 857            filter_popover_menu_handle: PopoverMenuHandle::default(),
 858            split_popover_menu_handle: PopoverMenuHandle::default(),
 859            focus_handle: cx.focus_handle(),
 860            include_ignored: FileFinderSettings::get_global(cx).include_ignored,
 861            include_ignored_refresh: Task::ready(()),
 862        }
 863    }
 864
 865    fn subscribe_to_updates(
 866        project: &Entity<Project>,
 867        window: &mut Window,
 868        cx: &mut Context<FileFinder>,
 869    ) {
 870        cx.subscribe_in(project, window, |file_finder, _, event, window, cx| {
 871            match event {
 872                project::Event::WorktreeUpdatedEntries(_, _)
 873                | project::Event::WorktreeAdded(_)
 874                | project::Event::WorktreeRemoved(_) => file_finder
 875                    .picker
 876                    .update(cx, |picker, cx| picker.refresh(window, cx)),
 877                _ => {}
 878            };
 879        })
 880        .detach();
 881    }
 882
 883    fn spawn_search(
 884        &mut self,
 885        query: FileSearchQuery,
 886        window: &mut Window,
 887        cx: &mut Context<Picker<Self>>,
 888    ) -> Task<()> {
 889        let relative_to = self
 890            .currently_opened_path
 891            .as_ref()
 892            .map(|found_path| Arc::clone(&found_path.project.path));
 893        let worktree_store = self.project.read(cx).worktree_store();
 894        let worktrees = worktree_store
 895            .read(cx)
 896            .visible_worktrees_and_single_files(cx)
 897            .collect::<Vec<_>>();
 898        let include_root_name = !should_hide_root_in_entry_path(&worktree_store, cx);
 899        let candidate_sets = worktrees
 900            .into_iter()
 901            .map(|worktree| {
 902                let worktree = worktree.read(cx);
 903                PathMatchCandidateSet {
 904                    snapshot: worktree.snapshot(),
 905                    include_ignored: self.include_ignored.unwrap_or_else(|| {
 906                        worktree.root_entry().is_some_and(|entry| entry.is_ignored)
 907                    }),
 908                    include_root_name,
 909                    candidates: project::Candidates::Files,
 910                }
 911            })
 912            .collect::<Vec<_>>();
 913
 914        let search_id = util::post_inc(&mut self.search_count);
 915        self.cancel_flag.store(true, atomic::Ordering::Release);
 916        self.cancel_flag = Arc::new(AtomicBool::new(false));
 917        let cancel_flag = self.cancel_flag.clone();
 918        cx.spawn_in(window, async move |picker, cx| {
 919            let matches = fuzzy_nucleo::match_path_sets(
 920                candidate_sets.as_slice(),
 921                query.path_query(),
 922                &relative_to,
 923                fuzzy_nucleo::Case::Ignore,
 924                100,
 925                &cancel_flag,
 926                cx.background_executor().clone(),
 927            )
 928            .await
 929            .into_iter()
 930            .map(ProjectPanelOrdMatch);
 931            let did_cancel = cancel_flag.load(atomic::Ordering::Acquire);
 932            picker
 933                .update(cx, |picker, cx| {
 934                    picker
 935                        .delegate
 936                        .set_search_matches(search_id, did_cancel, query, matches, cx)
 937                })
 938                .log_err();
 939        })
 940    }
 941
 942    fn set_search_matches(
 943        &mut self,
 944        search_id: usize,
 945        did_cancel: bool,
 946        query: FileSearchQuery,
 947        matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
 948        cx: &mut Context<Picker<Self>>,
 949    ) {
 950        if search_id >= self.latest_search_id {
 951            self.latest_search_id = search_id;
 952            let query_changed = Some(query.path_query())
 953                != self
 954                    .latest_search_query
 955                    .as_ref()
 956                    .map(|query| query.path_query());
 957            let extend_old_matches = self.latest_search_did_cancel && !query_changed;
 958
 959            let selected_match = if query_changed {
 960                None
 961            } else {
 962                self.matches.get(self.selected_index).cloned()
 963            };
 964
 965            let path_style = self.project.read(cx).path_style(cx);
 966            self.matches.push_new_matches(
 967                self.project.read(cx).worktree_store(),
 968                cx,
 969                &self.history_items,
 970                self.currently_opened_path.as_ref(),
 971                Some(&query),
 972                matches.into_iter(),
 973                extend_old_matches,
 974                path_style,
 975            );
 976
 977            // Add channel matches
 978            if let Some(channel_store) = &self.channel_store {
 979                let channel_store = channel_store.read(cx);
 980                let channels: Vec<_> = channel_store.channels().cloned().collect();
 981                if !channels.is_empty() {
 982                    let candidates = channels
 983                        .iter()
 984                        .enumerate()
 985                        .map(|(id, channel)| StringMatchCandidate::new(id, &channel.name));
 986                    let channel_query = query.path_query();
 987                    let query_lower = channel_query.to_lowercase();
 988                    let mut channel_matches = Vec::new();
 989                    for candidate in candidates {
 990                        let channel_name = candidate.string;
 991                        let name_lower = channel_name.to_lowercase();
 992
 993                        let mut positions = Vec::new();
 994                        let mut query_idx = 0;
 995                        for (name_idx, name_char) in name_lower.char_indices() {
 996                            if query_idx < query_lower.len() {
 997                                let query_char =
 998                                    query_lower[query_idx..].chars().next().unwrap_or_default();
 999                                if name_char == query_char {
1000                                    positions.push(name_idx);
1001                                    query_idx += query_char.len_utf8();
1002                                }
1003                            }
1004                        }
1005
1006                        if query_idx == query_lower.len() {
1007                            let channel = &channels[candidate.id];
1008                            let score = if name_lower == query_lower {
1009                                1.0
1010                            } else if name_lower.starts_with(&query_lower) {
1011                                0.8
1012                            } else {
1013                                0.5 * (query_lower.len() as f64 / name_lower.len() as f64)
1014                            };
1015                            channel_matches.push(Match::Channel {
1016                                channel_id: channel.id,
1017                                channel_name: channel.name.clone(),
1018                                string_match: StringMatch {
1019                                    candidate_id: candidate.id,
1020                                    score,
1021                                    positions,
1022                                    string: channel_name,
1023                                },
1024                            });
1025                        }
1026                    }
1027                    for channel_match in channel_matches {
1028                        match self
1029                            .matches
1030                            .position(&channel_match, self.currently_opened_path.as_ref())
1031                        {
1032                            Ok(_duplicate) => {}
1033                            Err(ix) => self.matches.matches.insert(ix, channel_match),
1034                        }
1035                    }
1036                }
1037            }
1038
1039            let query_path = query.raw_query.as_str();
1040            if let Ok(mut query_path) = RelPath::new(Path::new(query_path), path_style) {
1041                let available_worktree = self
1042                    .project
1043                    .read(cx)
1044                    .visible_worktrees(cx)
1045                    .filter(|worktree| !worktree.read(cx).is_single_file())
1046                    .collect::<Vec<_>>();
1047                let worktree_count = available_worktree.len();
1048                let mut expect_worktree = available_worktree.first().cloned();
1049                for worktree in &available_worktree {
1050                    let worktree_root = worktree.read(cx).root_name();
1051                    if worktree_count > 1 {
1052                        if let Ok(suffix) = query_path.strip_prefix(worktree_root) {
1053                            query_path = Cow::Owned(suffix.to_owned());
1054                            expect_worktree = Some(worktree.clone());
1055                            break;
1056                        }
1057                    }
1058                }
1059
1060                if let Some(FoundPath { ref project, .. }) = self.currently_opened_path {
1061                    let worktree_id = project.worktree_id;
1062                    let focused_file_in_available_worktree = available_worktree
1063                        .iter()
1064                        .any(|wt| wt.read(cx).id() == worktree_id);
1065
1066                    if focused_file_in_available_worktree {
1067                        expect_worktree = self.project.read(cx).worktree_for_id(worktree_id, cx);
1068                    }
1069                }
1070
1071                if let Some(worktree) = expect_worktree {
1072                    let worktree = worktree.read(cx);
1073                    if worktree.entry_for_path(&query_path).is_none()
1074                        && !query.raw_query.ends_with("/")
1075                        && !(path_style.is_windows() && query.raw_query.ends_with("\\"))
1076                    {
1077                        self.matches.matches.push(Match::CreateNew(ProjectPath {
1078                            worktree_id: worktree.id(),
1079                            path: query_path.into_arc(),
1080                        }));
1081                    }
1082                }
1083            }
1084
1085            self.selected_index = selected_match.map_or_else(
1086                || self.calculate_selected_index(cx),
1087                |m| {
1088                    self.matches
1089                        .position(&m, self.currently_opened_path.as_ref())
1090                        .unwrap_or(0)
1091                },
1092            );
1093
1094            self.latest_search_query = Some(query);
1095            self.latest_search_did_cancel = did_cancel;
1096
1097            cx.notify();
1098        }
1099    }
1100
1101    fn labels_for_match(
1102        &self,
1103        path_match: &Match,
1104        window: &mut Window,
1105        cx: &App,
1106    ) -> (HighlightedLabel, HighlightedLabel) {
1107        let path_style = self.project.read(cx).path_style(cx);
1108        let (file_name, file_name_positions, mut full_path, mut full_path_positions) =
1109            match &path_match {
1110                Match::History {
1111                    path: entry_path,
1112                    panel_match,
1113                } => {
1114                    let worktree_id = entry_path.project.worktree_id;
1115                    let worktree = self
1116                        .project
1117                        .read(cx)
1118                        .worktree_for_id(worktree_id, cx)
1119                        .filter(|worktree| worktree.read(cx).is_visible());
1120
1121                    if let Some(panel_match) = panel_match {
1122                        self.labels_for_path_match(&panel_match.0, path_style)
1123                    } else if let Some(worktree) = worktree {
1124                        let worktree_store = self.project.read(cx).worktree_store();
1125                        let full_path = if should_hide_root_in_entry_path(&worktree_store, cx) {
1126                            entry_path.project.path.clone()
1127                        } else {
1128                            worktree.read(cx).root_name().join(&entry_path.project.path)
1129                        };
1130                        let mut components = full_path.components();
1131                        let filename = components.next_back().unwrap_or("");
1132                        let prefix = components.rest();
1133                        (
1134                            filename.to_string(),
1135                            Vec::new(),
1136                            prefix.display(path_style).to_string() + path_style.primary_separator(),
1137                            Vec::new(),
1138                        )
1139                    } else {
1140                        (
1141                            entry_path
1142                                .absolute
1143                                .file_name()
1144                                .map_or(String::new(), |f| f.to_string_lossy().into_owned()),
1145                            Vec::new(),
1146                            entry_path.absolute.parent().map_or(String::new(), |path| {
1147                                path.to_string_lossy().into_owned() + path_style.primary_separator()
1148                            }),
1149                            Vec::new(),
1150                        )
1151                    }
1152                }
1153                Match::Search(path_match) => self.labels_for_path_match(&path_match.0, path_style),
1154                Match::Channel {
1155                    channel_name,
1156                    string_match,
1157                    ..
1158                } => (
1159                    channel_name.to_string(),
1160                    string_match.positions.clone(),
1161                    "Channel Notes".to_string(),
1162                    vec![],
1163                ),
1164                Match::CreateNew(project_path) => (
1165                    format!("Create file: {}", project_path.path.display(path_style)),
1166                    vec![],
1167                    String::from(""),
1168                    vec![],
1169                ),
1170            };
1171
1172        if file_name_positions.is_empty() {
1173            let user_home_path = util::paths::home_dir().to_string_lossy();
1174            if !user_home_path.is_empty() && full_path.starts_with(&*user_home_path) {
1175                full_path.replace_range(0..user_home_path.len(), "~");
1176                full_path_positions.retain_mut(|pos| {
1177                    if *pos >= user_home_path.len() {
1178                        *pos -= user_home_path.len();
1179                        *pos += 1;
1180                        true
1181                    } else {
1182                        false
1183                    }
1184                })
1185            }
1186        }
1187
1188        if full_path.is_ascii() {
1189            let file_finder_settings = FileFinderSettings::get_global(cx);
1190            let max_width =
1191                FileFinder::modal_max_width(file_finder_settings.modal_max_width, window);
1192            let (normal_em, small_em) = {
1193                let style = window.text_style();
1194                let font_id = window.text_system().resolve_font(&style.font());
1195                let font_size = TextSize::Default.rems(cx).to_pixels(window.rem_size());
1196                let normal = cx
1197                    .text_system()
1198                    .em_width(font_id, font_size)
1199                    .unwrap_or(px(16.));
1200                let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
1201                let small = cx
1202                    .text_system()
1203                    .em_width(font_id, font_size)
1204                    .unwrap_or(px(10.));
1205                (normal, small)
1206            };
1207            let budget = full_path_budget(&file_name, normal_em, small_em, max_width);
1208            // If the computed budget is zero, we certainly won't be able to achieve it,
1209            // so no point trying to elide the path.
1210            if budget > 0 && full_path.len() > budget {
1211                let components = PathComponentSlice::new(&full_path);
1212                if let Some(elided_range) =
1213                    components.elision_range(budget - 1, &full_path_positions)
1214                {
1215                    let elided_len = elided_range.end - elided_range.start;
1216                    let placeholder = "";
1217                    full_path_positions.retain_mut(|mat| {
1218                        if *mat >= elided_range.end {
1219                            *mat -= elided_len;
1220                            *mat += placeholder.len();
1221                        } else if *mat >= elided_range.start {
1222                            return false;
1223                        }
1224                        true
1225                    });
1226                    full_path.replace_range(elided_range, placeholder);
1227                }
1228            }
1229        }
1230
1231        (
1232            HighlightedLabel::new(file_name, file_name_positions),
1233            HighlightedLabel::new(full_path, full_path_positions)
1234                .size(LabelSize::Small)
1235                .color(Color::Muted),
1236        )
1237    }
1238
1239    fn labels_for_path_match(
1240        &self,
1241        path_match: &PathMatch,
1242        path_style: PathStyle,
1243    ) -> (String, Vec<usize>, String, Vec<usize>) {
1244        let full_path = path_match.path_prefix.join(&path_match.path);
1245        let mut path_positions = path_match.positions.clone();
1246
1247        let file_name = full_path.file_name().unwrap_or("");
1248        let file_name_start = full_path.as_unix_str().len() - file_name.len();
1249        let file_name_positions = path_positions
1250            .iter()
1251            .filter_map(|pos| {
1252                if pos >= &file_name_start {
1253                    Some(pos - file_name_start)
1254                } else {
1255                    None
1256                }
1257            })
1258            .collect::<Vec<_>>();
1259
1260        let full_path = full_path
1261            .display(path_style)
1262            .trim_end_matches(&file_name)
1263            .to_string();
1264        path_positions.retain(|idx| *idx < full_path.len());
1265
1266        debug_assert!(
1267            file_name_positions
1268                .iter()
1269                .all(|ix| file_name[*ix..].chars().next().is_some()),
1270            "invalid file name positions {file_name:?} {file_name_positions:?}"
1271        );
1272        debug_assert!(
1273            path_positions
1274                .iter()
1275                .all(|ix| full_path[*ix..].chars().next().is_some()),
1276            "invalid path positions {full_path:?} {path_positions:?}"
1277        );
1278
1279        (
1280            file_name.to_string(),
1281            file_name_positions,
1282            full_path,
1283            path_positions,
1284        )
1285    }
1286
1287    /// Attempts to resolve an absolute file path and update the search matches if found.
1288    ///
1289    /// If the query path resolves to an absolute file that exists in the project,
1290    /// this method will find the corresponding worktree and relative path, create a
1291    /// match for it, and update the picker's search results.
1292    ///
1293    /// Returns `true` if the absolute path exists, otherwise returns `false`.
1294    fn lookup_absolute_path(
1295        &self,
1296        query: FileSearchQuery,
1297        window: &mut Window,
1298        cx: &mut Context<Picker<Self>>,
1299    ) -> Task<bool> {
1300        cx.spawn_in(window, async move |picker, cx| {
1301            let Some(project) = picker
1302                .read_with(cx, |picker, _| picker.delegate.project.clone())
1303                .log_err()
1304            else {
1305                return false;
1306            };
1307
1308            let query_path = Path::new(query.path_query());
1309            let mut path_matches = Vec::new();
1310
1311            let abs_file_exists = project
1312                .update(cx, |this, cx| {
1313                    this.resolve_abs_file_path(query.path_query(), cx)
1314                })
1315                .await
1316                .is_some();
1317
1318            if abs_file_exists {
1319                project.update(cx, |project, cx| {
1320                    if let Some((worktree, relative_path)) = project.find_worktree(query_path, cx) {
1321                        path_matches.push(ProjectPanelOrdMatch(PathMatch {
1322                            score: 1.0,
1323                            positions: Vec::new(),
1324                            worktree_id: worktree.read(cx).id().to_usize(),
1325                            path: relative_path,
1326                            path_prefix: RelPath::empty().into(),
1327                            is_dir: false, // File finder doesn't support directories
1328                            distance_to_relative_ancestor: usize::MAX,
1329                        }));
1330                    }
1331                });
1332            }
1333
1334            picker
1335                .update_in(cx, |picker, _, cx| {
1336                    let picker_delegate = &mut picker.delegate;
1337                    let search_id = util::post_inc(&mut picker_delegate.search_count);
1338                    picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
1339
1340                    anyhow::Ok(())
1341                })
1342                .log_err();
1343            abs_file_exists
1344        })
1345    }
1346
1347    /// Skips first history match (that is displayed topmost) if it's currently opened.
1348    fn calculate_selected_index(&self, cx: &mut Context<Picker<Self>>) -> usize {
1349        if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search
1350            && let Some(Match::History { path, .. }) = self.matches.get(0)
1351            && Some(path) == self.currently_opened_path.as_ref()
1352        {
1353            let elements_after_first = self.matches.len() - 1;
1354            if elements_after_first > 0 {
1355                return 1;
1356            }
1357        }
1358
1359        0
1360    }
1361
1362    fn key_context(&self, window: &Window, cx: &App) -> KeyContext {
1363        let mut key_context = KeyContext::new_with_defaults();
1364        key_context.add("FileFinder");
1365
1366        if self.filter_popover_menu_handle.is_focused(window, cx) {
1367            key_context.add("filter_menu_open");
1368        }
1369
1370        if self.split_popover_menu_handle.is_focused(window, cx) {
1371            key_context.add("split_menu_open");
1372        }
1373        key_context
1374    }
1375}
1376
1377fn full_path_budget(
1378    file_name: &str,
1379    normal_em: Pixels,
1380    small_em: Pixels,
1381    max_width: Pixels,
1382) -> usize {
1383    (((max_width / 0.8) - file_name.len() * normal_em) / small_em) as usize
1384}
1385
1386impl PickerDelegate for FileFinderDelegate {
1387    type ListItem = ListItem;
1388
1389    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1390        "Search project files...".into()
1391    }
1392
1393    fn match_count(&self) -> usize {
1394        self.matches.len()
1395    }
1396
1397    fn selected_index(&self) -> usize {
1398        self.selected_index
1399    }
1400
1401    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
1402        self.has_changed_selected_index = true;
1403        self.selected_index = ix;
1404        cx.notify();
1405    }
1406
1407    fn separators_after_indices(&self) -> Vec<usize> {
1408        if self.separate_history {
1409            let first_non_history_index = self
1410                .matches
1411                .matches
1412                .iter()
1413                .enumerate()
1414                .find(|(_, m)| !matches!(m, Match::History { .. }))
1415                .map(|(i, _)| i);
1416            if let Some(first_non_history_index) = first_non_history_index
1417                && first_non_history_index > 0
1418            {
1419                return vec![first_non_history_index - 1];
1420            }
1421        }
1422        Vec::new()
1423    }
1424
1425    fn update_matches(
1426        &mut self,
1427        raw_query: String,
1428        window: &mut Window,
1429        cx: &mut Context<Picker<Self>>,
1430    ) -> Task<()> {
1431        let raw_query = raw_query.trim();
1432
1433        let raw_query = match &raw_query.get(0..2) {
1434            Some(".\\" | "./") => &raw_query[2..],
1435            Some(prefix @ ("a\\" | "a/" | "b\\" | "b/")) => {
1436                if self
1437                    .workspace
1438                    .upgrade()
1439                    .into_iter()
1440                    .flat_map(|workspace| workspace.read(cx).worktrees(cx))
1441                    .all(|worktree| {
1442                        worktree
1443                            .read(cx)
1444                            .entry_for_path(RelPath::unix(prefix.split_at(1).0).unwrap())
1445                            .is_none_or(|entry| !entry.is_dir())
1446                    })
1447                {
1448                    &raw_query[2..]
1449                } else {
1450                    raw_query
1451                }
1452            }
1453            _ => raw_query,
1454        };
1455
1456        if raw_query.is_empty() {
1457            // if there was no query before, and we already have some (history) matches
1458            // there's no need to update anything, since nothing has changed.
1459            // We also want to populate matches set from history entries on the first update.
1460            if self.latest_search_query.is_some() || self.first_update {
1461                let project = self.project.read(cx);
1462
1463                self.latest_search_id = post_inc(&mut self.search_count);
1464                self.latest_search_query = None;
1465                self.matches = Matches {
1466                    separate_history: self.separate_history,
1467                    ..Matches::default()
1468                };
1469                let path_style = self.project.read(cx).path_style(cx);
1470
1471                self.matches.push_new_matches(
1472                    project.worktree_store(),
1473                    cx,
1474                    self.history_items.iter().filter(|history_item| {
1475                        project
1476                            .worktree_for_id(history_item.project.worktree_id, cx)
1477                            .is_some()
1478                            || project.is_local()
1479                            || project.is_via_remote_server()
1480                    }),
1481                    self.currently_opened_path.as_ref(),
1482                    None,
1483                    None.into_iter(),
1484                    false,
1485                    path_style,
1486                );
1487
1488                self.first_update = false;
1489                self.selected_index = 0;
1490            }
1491            cx.notify();
1492            Task::ready(())
1493        } else {
1494            let path_position = PathWithPosition::parse_str(raw_query);
1495            let raw_query = raw_query.trim().trim_end_matches(':').to_owned();
1496            let path = path_position.path.clone();
1497            let path_str = path_position.path.to_str();
1498            let path_trimmed = path_str.unwrap_or(&raw_query).trim_end_matches(':');
1499            let file_query_end = if path_trimmed == raw_query {
1500                None
1501            } else {
1502                // Safe to unwrap as we won't get here when the unwrap in if fails
1503                Some(path_str.unwrap().len())
1504            };
1505
1506            let query = FileSearchQuery {
1507                raw_query,
1508                file_query_end,
1509                path_position,
1510            };
1511
1512            cx.spawn_in(window, async move |this, cx| {
1513                let _ = maybe!(async move {
1514                    let is_absolute_path = path.is_absolute();
1515                    let did_resolve_abs_path = is_absolute_path
1516                        && this
1517                            .update_in(cx, |this, window, cx| {
1518                                this.delegate
1519                                    .lookup_absolute_path(query.clone(), window, cx)
1520                            })?
1521                            .await;
1522
1523                    // Only check for relative paths if no absolute paths were
1524                    // found.
1525                    if !did_resolve_abs_path {
1526                        this.update_in(cx, |this, window, cx| {
1527                            this.delegate.spawn_search(query, window, cx)
1528                        })?
1529                        .await;
1530                    }
1531                    anyhow::Ok(())
1532                })
1533                .await;
1534            })
1535        }
1536    }
1537
1538    fn confirm(
1539        &mut self,
1540        secondary: bool,
1541        window: &mut Window,
1542        cx: &mut Context<Picker<FileFinderDelegate>>,
1543    ) {
1544        if let Some(m) = self.matches.get(self.selected_index())
1545            && let Some(workspace) = self.workspace.upgrade()
1546        {
1547            // Channel matches are handled separately since they dispatch an action
1548            // rather than directly opening a file path.
1549            if let Match::Channel { channel_id, .. } = m {
1550                let channel_id = channel_id.0;
1551                let finder = self.file_finder.clone();
1552                window.dispatch_action(OpenChannelNotesById { channel_id }.boxed_clone(), cx);
1553                finder.update(cx, |_, cx| cx.emit(DismissEvent)).log_err();
1554                return;
1555            }
1556
1557            let open_task = workspace.update(cx, |workspace, cx| {
1558                let split_or_open =
1559                    |workspace: &mut Workspace,
1560                     project_path,
1561                     window: &mut Window,
1562                     cx: &mut Context<Workspace>| {
1563                        let allow_preview =
1564                            PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
1565                        if secondary {
1566                            workspace.split_path_preview(
1567                                project_path,
1568                                allow_preview,
1569                                None,
1570                                window,
1571                                cx,
1572                            )
1573                        } else {
1574                            workspace.open_path_preview(
1575                                project_path,
1576                                None,
1577                                true,
1578                                allow_preview,
1579                                true,
1580                                window,
1581                                cx,
1582                            )
1583                        }
1584                    };
1585                match &m {
1586                    Match::CreateNew(project_path) => {
1587                        // Create a new file with the given filename
1588                        if secondary {
1589                            workspace.split_path_preview(
1590                                project_path.clone(),
1591                                false,
1592                                None,
1593                                window,
1594                                cx,
1595                            )
1596                        } else {
1597                            workspace.open_path_preview(
1598                                project_path.clone(),
1599                                None,
1600                                true,
1601                                false,
1602                                true,
1603                                window,
1604                                cx,
1605                            )
1606                        }
1607                    }
1608
1609                    Match::History { path, .. } => {
1610                        let worktree_id = path.project.worktree_id;
1611                        if workspace
1612                            .project()
1613                            .read(cx)
1614                            .worktree_for_id(worktree_id, cx)
1615                            .is_some()
1616                        {
1617                            split_or_open(
1618                                workspace,
1619                                ProjectPath {
1620                                    worktree_id,
1621                                    path: Arc::clone(&path.project.path),
1622                                },
1623                                window,
1624                                cx,
1625                            )
1626                        } else if secondary {
1627                            workspace.split_abs_path(path.absolute.clone(), false, window, cx)
1628                        } else {
1629                            workspace.open_abs_path(
1630                                path.absolute.clone(),
1631                                OpenOptions {
1632                                    visible: Some(OpenVisible::None),
1633                                    ..Default::default()
1634                                },
1635                                window,
1636                                cx,
1637                            )
1638                        }
1639                    }
1640                    Match::Search(m) => split_or_open(
1641                        workspace,
1642                        ProjectPath {
1643                            worktree_id: WorktreeId::from_usize(m.0.worktree_id),
1644                            path: m.0.path.clone(),
1645                        },
1646                        window,
1647                        cx,
1648                    ),
1649                    Match::Channel { .. } => unreachable!("handled above"),
1650                }
1651            });
1652
1653            let row = self
1654                .latest_search_query
1655                .as_ref()
1656                .and_then(|query| query.path_position.row)
1657                .map(|row| row.saturating_sub(1));
1658            let col = self
1659                .latest_search_query
1660                .as_ref()
1661                .and_then(|query| query.path_position.column)
1662                .unwrap_or(0)
1663                .saturating_sub(1);
1664            let finder = self.file_finder.clone();
1665            let workspace = self.workspace.clone();
1666
1667            cx.spawn_in(window, async move |_, mut cx| {
1668                let item = open_task
1669                    .await
1670                    .notify_workspace_async_err(workspace, &mut cx)?;
1671                if let Some(row) = row
1672                    && let Some(active_editor) = item.downcast::<Editor>()
1673                {
1674                    active_editor
1675                        .downgrade()
1676                        .update_in(cx, |editor, window, cx| {
1677                            let Some(buffer) = editor.buffer().read(cx).as_singleton() else {
1678                                return;
1679                            };
1680                            let buffer_snapshot = buffer.read(cx).snapshot();
1681                            let point = buffer_snapshot.point_from_external_input(row, col);
1682                            editor.go_to_singleton_buffer_point(point, window, cx);
1683                        })
1684                        .log_err();
1685                }
1686                finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?;
1687
1688                Some(())
1689            })
1690            .detach();
1691        }
1692    }
1693
1694    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<FileFinderDelegate>>) {
1695        self.file_finder
1696            .update(cx, |_, cx| cx.emit(DismissEvent))
1697            .log_err();
1698    }
1699
1700    fn render_match(
1701        &self,
1702        ix: usize,
1703        selected: bool,
1704        window: &mut Window,
1705        cx: &mut Context<Picker<Self>>,
1706    ) -> Option<Self::ListItem> {
1707        let settings = FileFinderSettings::get_global(cx);
1708
1709        let path_match = self.matches.get(ix)?;
1710
1711        let end_icon = match path_match {
1712            Match::History { .. } => Icon::new(IconName::HistoryRerun)
1713                .color(Color::Muted)
1714                .size(IconSize::Small)
1715                .into_any_element(),
1716            Match::Search(_) => v_flex()
1717                .flex_none()
1718                .size(IconSize::Small.rems())
1719                .into_any_element(),
1720            Match::Channel { .. } => v_flex()
1721                .flex_none()
1722                .size(IconSize::Small.rems())
1723                .into_any_element(),
1724            Match::CreateNew(_) => Icon::new(IconName::Plus)
1725                .color(Color::Muted)
1726                .size(IconSize::Small)
1727                .into_any_element(),
1728        };
1729        let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx);
1730
1731        let file_icon = match path_match {
1732            Match::Channel { .. } => Some(Icon::new(IconName::Hash).color(Color::Muted)),
1733            _ => maybe!({
1734                if !settings.file_icons {
1735                    return None;
1736                }
1737                let abs_path = path_match.abs_path(&self.project, cx)?;
1738                let file_name = abs_path.file_name()?;
1739                let icon = FileIcons::get_icon(file_name.as_ref(), cx)?;
1740                Some(Icon::from_path(icon).color(Color::Muted))
1741            }),
1742        };
1743
1744        Some(
1745            ListItem::new(ix)
1746                .spacing(ListItemSpacing::Sparse)
1747                .start_slot::<Icon>(file_icon)
1748                .end_slot::<AnyElement>(end_icon)
1749                .inset(true)
1750                .toggle_state(selected)
1751                .child(
1752                    h_flex()
1753                        .gap_2()
1754                        .py_px()
1755                        .child(file_name_label)
1756                        .child(full_path_label),
1757                ),
1758        )
1759    }
1760
1761    fn render_editor(
1762        &self,
1763        editor: &Arc<dyn ErasedEditor>,
1764        window: &mut Window,
1765        cx: &mut Context<Picker<Self>>,
1766    ) -> Div {
1767        let has_search_query = self.latest_search_query.is_some();
1768        let is_project_scan_running = {
1769            let worktree_store = self.project.read(cx).worktree_store();
1770            !worktree_store.read(cx).initial_scan_completed()
1771        };
1772
1773        h_flex()
1774            .flex_none()
1775            .h_9()
1776            .px_2p5()
1777            .justify_between()
1778            .border_b_1()
1779            .border_color(cx.theme().colors().border_variant)
1780            .child(editor.render(window, cx))
1781            .when(is_project_scan_running && has_search_query, |this| {
1782                this.child(
1783                    h_flex()
1784                        .id("project-scan-indicator")
1785                        .tooltip(Tooltip::text("Project Scan in Progress…"))
1786                        .child(
1787                            Icon::new(IconName::LoadCircle)
1788                                .color(Color::Accent)
1789                                .size(IconSize::Small)
1790                                .with_rotate_animation(2),
1791                        ),
1792                )
1793            })
1794    }
1795
1796    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
1797        let focus_handle = self.focus_handle.clone();
1798
1799        Some(
1800            h_flex()
1801                .w_full()
1802                .p_1p5()
1803                .justify_between()
1804                .border_t_1()
1805                .border_color(cx.theme().colors().border_variant)
1806                .child(
1807                    PopoverMenu::new("filter-menu-popover")
1808                        .with_handle(self.filter_popover_menu_handle.clone())
1809                        .attach(gpui::Anchor::BottomRight)
1810                        .anchor(gpui::Anchor::BottomLeft)
1811                        .offset(gpui::Point {
1812                            x: px(1.0),
1813                            y: px(1.0),
1814                        })
1815                        .trigger_with_tooltip(
1816                            IconButton::new("filter-trigger", IconName::Sliders)
1817                                .icon_size(IconSize::Small)
1818                                .icon_size(IconSize::Small)
1819                                .toggle_state(self.include_ignored.unwrap_or(false))
1820                                .when(self.include_ignored.is_some(), |this| {
1821                                    this.indicator(Indicator::dot().color(Color::Info))
1822                                }),
1823                            {
1824                                let focus_handle = focus_handle.clone();
1825                                move |_window, cx| {
1826                                    Tooltip::for_action_in(
1827                                        "Filter Options",
1828                                        &ToggleFilterMenu,
1829                                        &focus_handle,
1830                                        cx,
1831                                    )
1832                                }
1833                            },
1834                        )
1835                        .menu({
1836                            let focus_handle = focus_handle.clone();
1837                            let include_ignored = self.include_ignored;
1838
1839                            move |window, cx| {
1840                                Some(ContextMenu::build(window, cx, {
1841                                    let focus_handle = focus_handle.clone();
1842                                    move |menu, _, _| {
1843                                        menu.context(focus_handle.clone())
1844                                            .header("Filter Options")
1845                                            .toggleable_entry(
1846                                                "Include Ignored Files",
1847                                                include_ignored.unwrap_or(false),
1848                                                ui::IconPosition::End,
1849                                                Some(ToggleIncludeIgnored.boxed_clone()),
1850                                                move |window, cx| {
1851                                                    window.focus(&focus_handle, cx);
1852                                                    window.dispatch_action(
1853                                                        ToggleIncludeIgnored.boxed_clone(),
1854                                                        cx,
1855                                                    );
1856                                                },
1857                                            )
1858                                    }
1859                                }))
1860                            }
1861                        }),
1862                )
1863                .child(
1864                    h_flex()
1865                        .gap_0p5()
1866                        .child(
1867                            PopoverMenu::new("split-menu-popover")
1868                                .with_handle(self.split_popover_menu_handle.clone())
1869                                .attach(gpui::Anchor::BottomRight)
1870                                .anchor(gpui::Anchor::BottomLeft)
1871                                .offset(gpui::Point {
1872                                    x: px(1.0),
1873                                    y: px(1.0),
1874                                })
1875                                .trigger(
1876                                    ButtonLike::new("split-trigger")
1877                                        .child(Label::new("Split…"))
1878                                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1879                                        .child(
1880                                            KeyBinding::for_action_in(
1881                                                &ToggleSplitMenu,
1882                                                &focus_handle,
1883                                                cx,
1884                                            )
1885                                            .size(rems_from_px(12.)),
1886                                        ),
1887                                )
1888                                .menu({
1889                                    let focus_handle = focus_handle.clone();
1890
1891                                    move |window, cx| {
1892                                        Some(ContextMenu::build(window, cx, {
1893                                            let focus_handle = focus_handle.clone();
1894                                            move |menu, _, _| {
1895                                                menu.context(focus_handle)
1896                                                    .action(
1897                                                        "Split Left",
1898                                                        pane::SplitLeft::default().boxed_clone(),
1899                                                    )
1900                                                    .action(
1901                                                        "Split Right",
1902                                                        pane::SplitRight::default().boxed_clone(),
1903                                                    )
1904                                                    .action(
1905                                                        "Split Up",
1906                                                        pane::SplitUp::default().boxed_clone(),
1907                                                    )
1908                                                    .action(
1909                                                        "Split Down",
1910                                                        pane::SplitDown::default().boxed_clone(),
1911                                                    )
1912                                            }
1913                                        }))
1914                                    }
1915                                }),
1916                        )
1917                        .child(
1918                            Button::new("open-selection", "Open")
1919                                .key_binding(
1920                                    KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1921                                        .map(|kb| kb.size(rems_from_px(12.))),
1922                                )
1923                                .on_click(|_, window, cx| {
1924                                    window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1925                                }),
1926                        ),
1927                )
1928                .into_any(),
1929        )
1930    }
1931}
1932
1933#[derive(Clone, Debug, PartialEq, Eq)]
1934struct PathComponentSlice<'a> {
1935    path: Cow<'a, Path>,
1936    path_str: Cow<'a, str>,
1937    component_ranges: Vec<(Component<'a>, Range<usize>)>,
1938}
1939
1940impl<'a> PathComponentSlice<'a> {
1941    fn new(path: &'a str) -> Self {
1942        let trimmed_path = Path::new(path).components().as_path().as_os_str();
1943        let mut component_ranges = Vec::new();
1944        let mut components = Path::new(trimmed_path).components();
1945        let len = trimmed_path.as_encoded_bytes().len();
1946        let mut pos = 0;
1947        while let Some(component) = components.next() {
1948            component_ranges.push((component, pos..0));
1949            pos = len - components.as_path().as_os_str().as_encoded_bytes().len();
1950        }
1951        for ((_, range), ancestor) in component_ranges
1952            .iter_mut()
1953            .rev()
1954            .zip(Path::new(trimmed_path).ancestors())
1955        {
1956            range.end = ancestor.as_os_str().as_encoded_bytes().len();
1957        }
1958        Self {
1959            path: Cow::Borrowed(Path::new(path)),
1960            path_str: Cow::Borrowed(path),
1961            component_ranges,
1962        }
1963    }
1964
1965    fn elision_range(&self, budget: usize, matches: &[usize]) -> Option<Range<usize>> {
1966        let eligible_range = {
1967            assert!(matches.windows(2).all(|w| w[0] <= w[1]));
1968            let mut matches = matches.iter().copied().peekable();
1969            let mut longest: Option<Range<usize>> = None;
1970            let mut cur = 0..0;
1971            let mut seen_normal = false;
1972            for (i, (component, range)) in self.component_ranges.iter().enumerate() {
1973                let is_normal = matches!(component, Component::Normal(_));
1974                let is_first_normal = is_normal && !seen_normal;
1975                seen_normal |= is_normal;
1976                let is_last = i == self.component_ranges.len() - 1;
1977                let contains_match = matches.peek().is_some_and(|mat| range.contains(mat));
1978                if contains_match {
1979                    matches.next();
1980                }
1981                if is_first_normal || is_last || !is_normal || contains_match {
1982                    if longest
1983                        .as_ref()
1984                        .is_none_or(|old| old.end - old.start <= cur.end - cur.start)
1985                    {
1986                        longest = Some(cur);
1987                    }
1988                    cur = i + 1..i + 1;
1989                } else {
1990                    cur.end = i + 1;
1991                }
1992            }
1993            if longest
1994                .as_ref()
1995                .is_none_or(|old| old.end - old.start <= cur.end - cur.start)
1996            {
1997                longest = Some(cur);
1998            }
1999            longest
2000        };
2001
2002        let eligible_range = eligible_range?;
2003        assert!(eligible_range.start <= eligible_range.end);
2004        if eligible_range.is_empty() {
2005            return None;
2006        }
2007
2008        let elided_range: Range<usize> = {
2009            let byte_range = self.component_ranges[eligible_range.start].1.start
2010                ..self.component_ranges[eligible_range.end - 1].1.end;
2011            let midpoint = self.path_str.len() / 2;
2012            let distance_from_start = byte_range.start.abs_diff(midpoint);
2013            let distance_from_end = byte_range.end.abs_diff(midpoint);
2014            let pick_from_end = distance_from_start > distance_from_end;
2015            let mut len_with_elision = self.path_str.len();
2016            let mut i = eligible_range.start;
2017            while i < eligible_range.end {
2018                let x = if pick_from_end {
2019                    eligible_range.end - i + eligible_range.start - 1
2020                } else {
2021                    i
2022                };
2023                len_with_elision -= self.component_ranges[x]
2024                    .0
2025                    .as_os_str()
2026                    .as_encoded_bytes()
2027                    .len()
2028                    + 1;
2029                if len_with_elision <= budget {
2030                    break;
2031                }
2032                i += 1;
2033            }
2034            if len_with_elision > budget {
2035                return None;
2036            } else if pick_from_end {
2037                let x = eligible_range.end - i + eligible_range.start - 1;
2038                x..eligible_range.end
2039            } else {
2040                let x = i;
2041                eligible_range.start..x + 1
2042            }
2043        };
2044
2045        let byte_range = self.component_ranges[elided_range.start].1.start
2046            ..self.component_ranges[elided_range.end - 1].1.end;
2047        Some(byte_range)
2048    }
2049}