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