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            let candidate = PathMatchCandidate {
 702                is_dir: false, // You can't open directories as project items
 703                path: &found_path.project.path,
 704                // Only match history items names, otherwise their paths may match too many queries, producing false positives.
 705                // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
 706                // it would be shown first always, despite the latter being a better match.
 707            };
 708            candidates_paths.insert(&found_path.project, found_path);
 709            (found_path.project.worktree_id, candidate)
 710        })
 711        .fold(
 712            HashMap::default(),
 713            |mut candidates, (worktree_id, new_candidate)| {
 714                candidates
 715                    .entry(worktree_id)
 716                    .or_insert_with(Vec::new)
 717                    .push(new_candidate);
 718                candidates
 719            },
 720        );
 721    let mut matching_history_paths = HashMap::default();
 722    for (worktree, candidates) in history_items_by_worktrees {
 723        let max_results = candidates.len() + 1;
 724        let worktree_root_name = worktree_name_by_id
 725            .as_ref()
 726            .and_then(|w| w.get(&worktree).cloned());
 727
 728        matching_history_paths.extend(
 729            fuzzy_nucleo::match_fixed_path_set(
 730                candidates,
 731                worktree.to_usize(),
 732                worktree_root_name,
 733                query.path_query(),
 734                false,
 735                max_results,
 736                path_style,
 737            )
 738            .into_iter()
 739            // 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
 740            .filter(|path_match| {
 741                if let Some(filename) = path_match.path.file_name() {
 742                    let filename_start = path_match.path.as_unix_str().len() - filename.len();
 743                    path_match
 744                        .positions
 745                        .iter()
 746                        .any(|&pos| pos >= filename_start)
 747                } else {
 748                    true
 749                }
 750            })
 751            .filter_map(|path_match| {
 752                candidates_paths
 753                    .remove_entry(&ProjectPath {
 754                        worktree_id: WorktreeId::from_usize(path_match.worktree_id),
 755                        path: Arc::clone(&path_match.path),
 756                    })
 757                    .map(|(project_path, found_path)| {
 758                        (
 759                            project_path.clone(),
 760                            Match::History {
 761                                path: found_path.clone(),
 762                                panel_match: Some(ProjectPanelOrdMatch(path_match)),
 763                            },
 764                        )
 765                    })
 766            }),
 767        );
 768    }
 769    matching_history_paths
 770}
 771
 772fn should_hide_root_in_entry_path(worktree_store: &Entity<WorktreeStore>, cx: &App) -> bool {
 773    let multiple_worktrees = worktree_store
 774        .read(cx)
 775        .visible_worktrees(cx)
 776        .filter(|worktree| !worktree.read(cx).is_single_file())
 777        .nth(1)
 778        .is_some();
 779    ProjectPanelSettings::get_global(cx).hide_root && !multiple_worktrees
 780}
 781
 782#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
 783struct FoundPath {
 784    project: ProjectPath,
 785    absolute: PathBuf,
 786}
 787
 788impl FoundPath {
 789    fn new(project: ProjectPath, absolute: PathBuf) -> Self {
 790        Self { project, absolute }
 791    }
 792}
 793
 794const MAX_RECENT_SELECTIONS: usize = 20;
 795
 796pub enum Event {
 797    Selected(ProjectPath),
 798    Dismissed,
 799}
 800
 801#[derive(Debug, Clone)]
 802struct FileSearchQuery {
 803    raw_query: String,
 804    file_query_end: Option<usize>,
 805    path_position: PathWithPosition,
 806}
 807
 808impl FileSearchQuery {
 809    fn path_query(&self) -> &str {
 810        match self.file_query_end {
 811            Some(file_path_end) => &self.raw_query[..file_path_end],
 812            None => &self.raw_query,
 813        }
 814    }
 815}
 816
 817impl FileFinderDelegate {
 818    fn new(
 819        file_finder: WeakEntity<FileFinder>,
 820        workspace: WeakEntity<Workspace>,
 821        project: Entity<Project>,
 822        currently_opened_path: Option<FoundPath>,
 823        history_items: Vec<FoundPath>,
 824        separate_history: bool,
 825        window: &mut Window,
 826        cx: &mut Context<FileFinder>,
 827    ) -> Self {
 828        Self::subscribe_to_updates(&project, window, cx);
 829        let channel_store = if FileFinderSettings::get_global(cx).include_channels {
 830            ChannelStore::try_global(cx)
 831        } else {
 832            None
 833        };
 834        Self {
 835            file_finder,
 836            workspace,
 837            project,
 838            channel_store,
 839            search_count: 0,
 840            latest_search_id: 0,
 841            latest_search_did_cancel: false,
 842            latest_search_query: None,
 843            currently_opened_path,
 844            matches: Matches::default(),
 845            has_changed_selected_index: false,
 846            selected_index: 0,
 847            cancel_flag: Arc::new(AtomicBool::new(false)),
 848            history_items,
 849            separate_history,
 850            first_update: true,
 851            filter_popover_menu_handle: PopoverMenuHandle::default(),
 852            split_popover_menu_handle: PopoverMenuHandle::default(),
 853            focus_handle: cx.focus_handle(),
 854            include_ignored: FileFinderSettings::get_global(cx).include_ignored,
 855            include_ignored_refresh: Task::ready(()),
 856        }
 857    }
 858
 859    fn subscribe_to_updates(
 860        project: &Entity<Project>,
 861        window: &mut Window,
 862        cx: &mut Context<FileFinder>,
 863    ) {
 864        cx.subscribe_in(project, window, |file_finder, _, event, window, cx| {
 865            match event {
 866                project::Event::WorktreeUpdatedEntries(_, _)
 867                | project::Event::WorktreeAdded(_)
 868                | project::Event::WorktreeRemoved(_) => file_finder
 869                    .picker
 870                    .update(cx, |picker, cx| picker.refresh(window, cx)),
 871                _ => {}
 872            };
 873        })
 874        .detach();
 875    }
 876
 877    fn spawn_search(
 878        &mut self,
 879        query: FileSearchQuery,
 880        window: &mut Window,
 881        cx: &mut Context<Picker<Self>>,
 882    ) -> Task<()> {
 883        let relative_to = self
 884            .currently_opened_path
 885            .as_ref()
 886            .map(|found_path| Arc::clone(&found_path.project.path));
 887        let worktree_store = self.project.read(cx).worktree_store();
 888        let worktrees = worktree_store
 889            .read(cx)
 890            .visible_worktrees_and_single_files(cx)
 891            .collect::<Vec<_>>();
 892        let include_root_name = !should_hide_root_in_entry_path(&worktree_store, cx);
 893        let candidate_sets = worktrees
 894            .into_iter()
 895            .map(|worktree| {
 896                let worktree = worktree.read(cx);
 897                PathMatchCandidateSet {
 898                    snapshot: worktree.snapshot(),
 899                    include_ignored: self.include_ignored.unwrap_or_else(|| {
 900                        worktree.root_entry().is_some_and(|entry| entry.is_ignored)
 901                    }),
 902                    include_root_name,
 903                    candidates: project::Candidates::Files,
 904                }
 905            })
 906            .collect::<Vec<_>>();
 907
 908        let search_id = util::post_inc(&mut self.search_count);
 909        self.cancel_flag.store(true, atomic::Ordering::Release);
 910        self.cancel_flag = Arc::new(AtomicBool::new(false));
 911        let cancel_flag = self.cancel_flag.clone();
 912        cx.spawn_in(window, async move |picker, cx| {
 913            let matches = fuzzy_nucleo::match_path_sets(
 914                candidate_sets.as_slice(),
 915                query.path_query(),
 916                &relative_to,
 917                false,
 918                100,
 919                &cancel_flag,
 920                cx.background_executor().clone(),
 921            )
 922            .await
 923            .into_iter()
 924            .map(ProjectPanelOrdMatch);
 925            let did_cancel = cancel_flag.load(atomic::Ordering::Acquire);
 926            picker
 927                .update(cx, |picker, cx| {
 928                    picker
 929                        .delegate
 930                        .set_search_matches(search_id, did_cancel, query, matches, cx)
 931                })
 932                .log_err();
 933        })
 934    }
 935
 936    fn set_search_matches(
 937        &mut self,
 938        search_id: usize,
 939        did_cancel: bool,
 940        query: FileSearchQuery,
 941        matches: impl IntoIterator<Item = ProjectPanelOrdMatch>,
 942        cx: &mut Context<Picker<Self>>,
 943    ) {
 944        if search_id >= self.latest_search_id {
 945            self.latest_search_id = search_id;
 946            let query_changed = Some(query.path_query())
 947                != self
 948                    .latest_search_query
 949                    .as_ref()
 950                    .map(|query| query.path_query());
 951            let extend_old_matches = self.latest_search_did_cancel && !query_changed;
 952
 953            let selected_match = if query_changed {
 954                None
 955            } else {
 956                self.matches.get(self.selected_index).cloned()
 957            };
 958
 959            let path_style = self.project.read(cx).path_style(cx);
 960            self.matches.push_new_matches(
 961                self.project.read(cx).worktree_store(),
 962                cx,
 963                &self.history_items,
 964                self.currently_opened_path.as_ref(),
 965                Some(&query),
 966                matches.into_iter(),
 967                extend_old_matches,
 968                path_style,
 969            );
 970
 971            // Add channel matches
 972            if let Some(channel_store) = &self.channel_store {
 973                let channel_store = channel_store.read(cx);
 974                let channels: Vec<_> = channel_store.channels().cloned().collect();
 975                if !channels.is_empty() {
 976                    let candidates = channels
 977                        .iter()
 978                        .enumerate()
 979                        .map(|(id, channel)| StringMatchCandidate::new(id, &channel.name));
 980                    let channel_query = query.path_query();
 981                    let query_lower = channel_query.to_lowercase();
 982                    let mut channel_matches = Vec::new();
 983                    for candidate in candidates {
 984                        let channel_name = candidate.string;
 985                        let name_lower = channel_name.to_lowercase();
 986
 987                        let mut positions = Vec::new();
 988                        let mut query_idx = 0;
 989                        for (name_idx, name_char) in name_lower.char_indices() {
 990                            if query_idx < query_lower.len() {
 991                                let query_char =
 992                                    query_lower[query_idx..].chars().next().unwrap_or_default();
 993                                if name_char == query_char {
 994                                    positions.push(name_idx);
 995                                    query_idx += query_char.len_utf8();
 996                                }
 997                            }
 998                        }
 999
1000                        if query_idx == query_lower.len() {
1001                            let channel = &channels[candidate.id];
1002                            let score = if name_lower == query_lower {
1003                                1.0
1004                            } else if name_lower.starts_with(&query_lower) {
1005                                0.8
1006                            } else {
1007                                0.5 * (query_lower.len() as f64 / name_lower.len() as f64)
1008                            };
1009                            channel_matches.push(Match::Channel {
1010                                channel_id: channel.id,
1011                                channel_name: channel.name.clone(),
1012                                string_match: StringMatch {
1013                                    candidate_id: candidate.id,
1014                                    score,
1015                                    positions,
1016                                    string: channel_name,
1017                                },
1018                            });
1019                        }
1020                    }
1021                    for channel_match in channel_matches {
1022                        match self
1023                            .matches
1024                            .position(&channel_match, self.currently_opened_path.as_ref())
1025                        {
1026                            Ok(_duplicate) => {}
1027                            Err(ix) => self.matches.matches.insert(ix, channel_match),
1028                        }
1029                    }
1030                }
1031            }
1032
1033            let query_path = query.raw_query.as_str();
1034            if let Ok(mut query_path) = RelPath::new(Path::new(query_path), path_style) {
1035                let available_worktree = self
1036                    .project
1037                    .read(cx)
1038                    .visible_worktrees(cx)
1039                    .filter(|worktree| !worktree.read(cx).is_single_file())
1040                    .collect::<Vec<_>>();
1041                let worktree_count = available_worktree.len();
1042                let mut expect_worktree = available_worktree.first().cloned();
1043                for worktree in &available_worktree {
1044                    let worktree_root = worktree.read(cx).root_name();
1045                    if worktree_count > 1 {
1046                        if let Ok(suffix) = query_path.strip_prefix(worktree_root) {
1047                            query_path = Cow::Owned(suffix.to_owned());
1048                            expect_worktree = Some(worktree.clone());
1049                            break;
1050                        }
1051                    }
1052                }
1053
1054                if let Some(FoundPath { ref project, .. }) = self.currently_opened_path {
1055                    let worktree_id = project.worktree_id;
1056                    let focused_file_in_available_worktree = available_worktree
1057                        .iter()
1058                        .any(|wt| wt.read(cx).id() == worktree_id);
1059
1060                    if focused_file_in_available_worktree {
1061                        expect_worktree = self.project.read(cx).worktree_for_id(worktree_id, cx);
1062                    }
1063                }
1064
1065                if let Some(worktree) = expect_worktree {
1066                    let worktree = worktree.read(cx);
1067                    if worktree.entry_for_path(&query_path).is_none()
1068                        && !query.raw_query.ends_with("/")
1069                        && !(path_style.is_windows() && query.raw_query.ends_with("\\"))
1070                    {
1071                        self.matches.matches.push(Match::CreateNew(ProjectPath {
1072                            worktree_id: worktree.id(),
1073                            path: query_path.into_arc(),
1074                        }));
1075                    }
1076                }
1077            }
1078
1079            self.selected_index = selected_match.map_or_else(
1080                || self.calculate_selected_index(cx),
1081                |m| {
1082                    self.matches
1083                        .position(&m, self.currently_opened_path.as_ref())
1084                        .unwrap_or(0)
1085                },
1086            );
1087
1088            self.latest_search_query = Some(query);
1089            self.latest_search_did_cancel = did_cancel;
1090
1091            cx.notify();
1092        }
1093    }
1094
1095    fn labels_for_match(
1096        &self,
1097        path_match: &Match,
1098        window: &mut Window,
1099        cx: &App,
1100    ) -> (HighlightedLabel, HighlightedLabel) {
1101        let path_style = self.project.read(cx).path_style(cx);
1102        let (file_name, file_name_positions, mut full_path, mut full_path_positions) =
1103            match &path_match {
1104                Match::History {
1105                    path: entry_path,
1106                    panel_match,
1107                } => {
1108                    let worktree_id = entry_path.project.worktree_id;
1109                    let worktree = self
1110                        .project
1111                        .read(cx)
1112                        .worktree_for_id(worktree_id, cx)
1113                        .filter(|worktree| worktree.read(cx).is_visible());
1114
1115                    if let Some(panel_match) = panel_match {
1116                        self.labels_for_path_match(&panel_match.0, path_style)
1117                    } else if let Some(worktree) = worktree {
1118                        let worktree_store = self.project.read(cx).worktree_store();
1119                        let full_path = if should_hide_root_in_entry_path(&worktree_store, cx) {
1120                            entry_path.project.path.clone()
1121                        } else {
1122                            worktree.read(cx).root_name().join(&entry_path.project.path)
1123                        };
1124                        let mut components = full_path.components();
1125                        let filename = components.next_back().unwrap_or("");
1126                        let prefix = components.rest();
1127                        (
1128                            filename.to_string(),
1129                            Vec::new(),
1130                            prefix.display(path_style).to_string() + path_style.primary_separator(),
1131                            Vec::new(),
1132                        )
1133                    } else {
1134                        (
1135                            entry_path
1136                                .absolute
1137                                .file_name()
1138                                .map_or(String::new(), |f| f.to_string_lossy().into_owned()),
1139                            Vec::new(),
1140                            entry_path.absolute.parent().map_or(String::new(), |path| {
1141                                path.to_string_lossy().into_owned() + path_style.primary_separator()
1142                            }),
1143                            Vec::new(),
1144                        )
1145                    }
1146                }
1147                Match::Search(path_match) => self.labels_for_path_match(&path_match.0, path_style),
1148                Match::Channel {
1149                    channel_name,
1150                    string_match,
1151                    ..
1152                } => (
1153                    channel_name.to_string(),
1154                    string_match.positions.clone(),
1155                    "Channel Notes".to_string(),
1156                    vec![],
1157                ),
1158                Match::CreateNew(project_path) => (
1159                    format!("Create file: {}", project_path.path.display(path_style)),
1160                    vec![],
1161                    String::from(""),
1162                    vec![],
1163                ),
1164            };
1165
1166        if file_name_positions.is_empty() {
1167            let user_home_path = util::paths::home_dir().to_string_lossy();
1168            if !user_home_path.is_empty() && full_path.starts_with(&*user_home_path) {
1169                full_path.replace_range(0..user_home_path.len(), "~");
1170                full_path_positions.retain_mut(|pos| {
1171                    if *pos >= user_home_path.len() {
1172                        *pos -= user_home_path.len();
1173                        *pos += 1;
1174                        true
1175                    } else {
1176                        false
1177                    }
1178                })
1179            }
1180        }
1181
1182        if full_path.is_ascii() {
1183            let file_finder_settings = FileFinderSettings::get_global(cx);
1184            let max_width =
1185                FileFinder::modal_max_width(file_finder_settings.modal_max_width, window);
1186            let (normal_em, small_em) = {
1187                let style = window.text_style();
1188                let font_id = window.text_system().resolve_font(&style.font());
1189                let font_size = TextSize::Default.rems(cx).to_pixels(window.rem_size());
1190                let normal = cx
1191                    .text_system()
1192                    .em_width(font_id, font_size)
1193                    .unwrap_or(px(16.));
1194                let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
1195                let small = cx
1196                    .text_system()
1197                    .em_width(font_id, font_size)
1198                    .unwrap_or(px(10.));
1199                (normal, small)
1200            };
1201            let budget = full_path_budget(&file_name, normal_em, small_em, max_width);
1202            // If the computed budget is zero, we certainly won't be able to achieve it,
1203            // so no point trying to elide the path.
1204            if budget > 0 && full_path.len() > budget {
1205                let components = PathComponentSlice::new(&full_path);
1206                if let Some(elided_range) =
1207                    components.elision_range(budget - 1, &full_path_positions)
1208                {
1209                    let elided_len = elided_range.end - elided_range.start;
1210                    let placeholder = "";
1211                    full_path_positions.retain_mut(|mat| {
1212                        if *mat >= elided_range.end {
1213                            *mat -= elided_len;
1214                            *mat += placeholder.len();
1215                        } else if *mat >= elided_range.start {
1216                            return false;
1217                        }
1218                        true
1219                    });
1220                    full_path.replace_range(elided_range, placeholder);
1221                }
1222            }
1223        }
1224
1225        (
1226            HighlightedLabel::new(file_name, file_name_positions),
1227            HighlightedLabel::new(full_path, full_path_positions)
1228                .size(LabelSize::Small)
1229                .color(Color::Muted),
1230        )
1231    }
1232
1233    fn labels_for_path_match(
1234        &self,
1235        path_match: &PathMatch,
1236        path_style: PathStyle,
1237    ) -> (String, Vec<usize>, String, Vec<usize>) {
1238        let full_path = path_match.path_prefix.join(&path_match.path);
1239        let mut path_positions = path_match.positions.clone();
1240
1241        let file_name = full_path.file_name().unwrap_or("");
1242        let file_name_start = full_path.as_unix_str().len() - file_name.len();
1243        let file_name_positions = path_positions
1244            .iter()
1245            .filter_map(|pos| {
1246                if pos >= &file_name_start {
1247                    Some(pos - file_name_start)
1248                } else {
1249                    None
1250                }
1251            })
1252            .collect::<Vec<_>>();
1253
1254        let full_path = full_path
1255            .display(path_style)
1256            .trim_end_matches(&file_name)
1257            .to_string();
1258        path_positions.retain(|idx| *idx < full_path.len());
1259
1260        debug_assert!(
1261            file_name_positions
1262                .iter()
1263                .all(|ix| file_name[*ix..].chars().next().is_some()),
1264            "invalid file name positions {file_name:?} {file_name_positions:?}"
1265        );
1266        debug_assert!(
1267            path_positions
1268                .iter()
1269                .all(|ix| full_path[*ix..].chars().next().is_some()),
1270            "invalid path positions {full_path:?} {path_positions:?}"
1271        );
1272
1273        (
1274            file_name.to_string(),
1275            file_name_positions,
1276            full_path,
1277            path_positions,
1278        )
1279    }
1280
1281    /// Attempts to resolve an absolute file path and update the search matches if found.
1282    ///
1283    /// If the query path resolves to an absolute file that exists in the project,
1284    /// this method will find the corresponding worktree and relative path, create a
1285    /// match for it, and update the picker's search results.
1286    ///
1287    /// Returns `true` if the absolute path exists, otherwise returns `false`.
1288    fn lookup_absolute_path(
1289        &self,
1290        query: FileSearchQuery,
1291        window: &mut Window,
1292        cx: &mut Context<Picker<Self>>,
1293    ) -> Task<bool> {
1294        cx.spawn_in(window, async move |picker, cx| {
1295            let Some(project) = picker
1296                .read_with(cx, |picker, _| picker.delegate.project.clone())
1297                .log_err()
1298            else {
1299                return false;
1300            };
1301
1302            let query_path = Path::new(query.path_query());
1303            let mut path_matches = Vec::new();
1304
1305            let abs_file_exists = project
1306                .update(cx, |this, cx| {
1307                    this.resolve_abs_file_path(query.path_query(), cx)
1308                })
1309                .await
1310                .is_some();
1311
1312            if abs_file_exists {
1313                project.update(cx, |project, cx| {
1314                    if let Some((worktree, relative_path)) = project.find_worktree(query_path, cx) {
1315                        path_matches.push(ProjectPanelOrdMatch(PathMatch {
1316                            score: 1.0,
1317                            positions: Vec::new(),
1318                            worktree_id: worktree.read(cx).id().to_usize(),
1319                            path: relative_path,
1320                            path_prefix: RelPath::empty().into(),
1321                            is_dir: false, // File finder doesn't support directories
1322                            distance_to_relative_ancestor: usize::MAX,
1323                        }));
1324                    }
1325                });
1326            }
1327
1328            picker
1329                .update_in(cx, |picker, _, cx| {
1330                    let picker_delegate = &mut picker.delegate;
1331                    let search_id = util::post_inc(&mut picker_delegate.search_count);
1332                    picker_delegate.set_search_matches(search_id, false, query, path_matches, cx);
1333
1334                    anyhow::Ok(())
1335                })
1336                .log_err();
1337            abs_file_exists
1338        })
1339    }
1340
1341    /// Skips first history match (that is displayed topmost) if it's currently opened.
1342    fn calculate_selected_index(&self, cx: &mut Context<Picker<Self>>) -> usize {
1343        if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search
1344            && let Some(Match::History { path, .. }) = self.matches.get(0)
1345            && Some(path) == self.currently_opened_path.as_ref()
1346        {
1347            let elements_after_first = self.matches.len() - 1;
1348            if elements_after_first > 0 {
1349                return 1;
1350            }
1351        }
1352
1353        0
1354    }
1355
1356    fn key_context(&self, window: &Window, cx: &App) -> KeyContext {
1357        let mut key_context = KeyContext::new_with_defaults();
1358        key_context.add("FileFinder");
1359
1360        if self.filter_popover_menu_handle.is_focused(window, cx) {
1361            key_context.add("filter_menu_open");
1362        }
1363
1364        if self.split_popover_menu_handle.is_focused(window, cx) {
1365            key_context.add("split_menu_open");
1366        }
1367        key_context
1368    }
1369}
1370
1371fn full_path_budget(
1372    file_name: &str,
1373    normal_em: Pixels,
1374    small_em: Pixels,
1375    max_width: Pixels,
1376) -> usize {
1377    (((max_width / 0.8) - file_name.len() * normal_em) / small_em) as usize
1378}
1379
1380impl PickerDelegate for FileFinderDelegate {
1381    type ListItem = ListItem;
1382
1383    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1384        "Search project files...".into()
1385    }
1386
1387    fn match_count(&self) -> usize {
1388        self.matches.len()
1389    }
1390
1391    fn selected_index(&self) -> usize {
1392        self.selected_index
1393    }
1394
1395    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
1396        self.has_changed_selected_index = true;
1397        self.selected_index = ix;
1398        cx.notify();
1399    }
1400
1401    fn separators_after_indices(&self) -> Vec<usize> {
1402        if self.separate_history {
1403            let first_non_history_index = self
1404                .matches
1405                .matches
1406                .iter()
1407                .enumerate()
1408                .find(|(_, m)| !matches!(m, Match::History { .. }))
1409                .map(|(i, _)| i);
1410            if let Some(first_non_history_index) = first_non_history_index
1411                && first_non_history_index > 0
1412            {
1413                return vec![first_non_history_index - 1];
1414            }
1415        }
1416        Vec::new()
1417    }
1418
1419    fn update_matches(
1420        &mut self,
1421        raw_query: String,
1422        window: &mut Window,
1423        cx: &mut Context<Picker<Self>>,
1424    ) -> Task<()> {
1425        let raw_query = raw_query.trim();
1426
1427        let raw_query = match &raw_query.get(0..2) {
1428            Some(".\\" | "./") => &raw_query[2..],
1429            Some(prefix @ ("a\\" | "a/" | "b\\" | "b/")) => {
1430                if self
1431                    .workspace
1432                    .upgrade()
1433                    .into_iter()
1434                    .flat_map(|workspace| workspace.read(cx).worktrees(cx))
1435                    .all(|worktree| {
1436                        worktree
1437                            .read(cx)
1438                            .entry_for_path(RelPath::unix(prefix.split_at(1).0).unwrap())
1439                            .is_none_or(|entry| !entry.is_dir())
1440                    })
1441                {
1442                    &raw_query[2..]
1443                } else {
1444                    raw_query
1445                }
1446            }
1447            _ => raw_query,
1448        };
1449
1450        if raw_query.is_empty() {
1451            // if there was no query before, and we already have some (history) matches
1452            // there's no need to update anything, since nothing has changed.
1453            // We also want to populate matches set from history entries on the first update.
1454            if self.latest_search_query.is_some() || self.first_update {
1455                let project = self.project.read(cx);
1456
1457                self.latest_search_id = post_inc(&mut self.search_count);
1458                self.latest_search_query = None;
1459                self.matches = Matches {
1460                    separate_history: self.separate_history,
1461                    ..Matches::default()
1462                };
1463                let path_style = self.project.read(cx).path_style(cx);
1464
1465                self.matches.push_new_matches(
1466                    project.worktree_store(),
1467                    cx,
1468                    self.history_items.iter().filter(|history_item| {
1469                        project
1470                            .worktree_for_id(history_item.project.worktree_id, cx)
1471                            .is_some()
1472                            || project.is_local()
1473                            || project.is_via_remote_server()
1474                    }),
1475                    self.currently_opened_path.as_ref(),
1476                    None,
1477                    None.into_iter(),
1478                    false,
1479                    path_style,
1480                );
1481
1482                self.first_update = false;
1483                self.selected_index = 0;
1484            }
1485            cx.notify();
1486            Task::ready(())
1487        } else {
1488            let path_position = PathWithPosition::parse_str(raw_query);
1489            let raw_query = raw_query.trim().trim_end_matches(':').to_owned();
1490            let path = path_position.path.clone();
1491            let path_str = path_position.path.to_str();
1492            let path_trimmed = path_str.unwrap_or(&raw_query).trim_end_matches(':');
1493            let file_query_end = if path_trimmed == raw_query {
1494                None
1495            } else {
1496                // Safe to unwrap as we won't get here when the unwrap in if fails
1497                Some(path_str.unwrap().len())
1498            };
1499
1500            let query = FileSearchQuery {
1501                raw_query,
1502                file_query_end,
1503                path_position,
1504            };
1505
1506            cx.spawn_in(window, async move |this, cx| {
1507                let _ = maybe!(async move {
1508                    let is_absolute_path = path.is_absolute();
1509                    let did_resolve_abs_path = is_absolute_path
1510                        && this
1511                            .update_in(cx, |this, window, cx| {
1512                                this.delegate
1513                                    .lookup_absolute_path(query.clone(), window, cx)
1514                            })?
1515                            .await;
1516
1517                    // Only check for relative paths if no absolute paths were
1518                    // found.
1519                    if !did_resolve_abs_path {
1520                        this.update_in(cx, |this, window, cx| {
1521                            this.delegate.spawn_search(query, window, cx)
1522                        })?
1523                        .await;
1524                    }
1525                    anyhow::Ok(())
1526                })
1527                .await;
1528            })
1529        }
1530    }
1531
1532    fn confirm(
1533        &mut self,
1534        secondary: bool,
1535        window: &mut Window,
1536        cx: &mut Context<Picker<FileFinderDelegate>>,
1537    ) {
1538        if let Some(m) = self.matches.get(self.selected_index())
1539            && let Some(workspace) = self.workspace.upgrade()
1540        {
1541            // Channel matches are handled separately since they dispatch an action
1542            // rather than directly opening a file path.
1543            if let Match::Channel { channel_id, .. } = m {
1544                let channel_id = channel_id.0;
1545                let finder = self.file_finder.clone();
1546                window.dispatch_action(OpenChannelNotesById { channel_id }.boxed_clone(), cx);
1547                finder.update(cx, |_, cx| cx.emit(DismissEvent)).log_err();
1548                return;
1549            }
1550
1551            let open_task = workspace.update(cx, |workspace, cx| {
1552                let split_or_open =
1553                    |workspace: &mut Workspace,
1554                     project_path,
1555                     window: &mut Window,
1556                     cx: &mut Context<Workspace>| {
1557                        let allow_preview =
1558                            PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
1559                        if secondary {
1560                            workspace.split_path_preview(
1561                                project_path,
1562                                allow_preview,
1563                                None,
1564                                window,
1565                                cx,
1566                            )
1567                        } else {
1568                            workspace.open_path_preview(
1569                                project_path,
1570                                None,
1571                                true,
1572                                allow_preview,
1573                                true,
1574                                window,
1575                                cx,
1576                            )
1577                        }
1578                    };
1579                match &m {
1580                    Match::CreateNew(project_path) => {
1581                        // Create a new file with the given filename
1582                        if secondary {
1583                            workspace.split_path_preview(
1584                                project_path.clone(),
1585                                false,
1586                                None,
1587                                window,
1588                                cx,
1589                            )
1590                        } else {
1591                            workspace.open_path_preview(
1592                                project_path.clone(),
1593                                None,
1594                                true,
1595                                false,
1596                                true,
1597                                window,
1598                                cx,
1599                            )
1600                        }
1601                    }
1602
1603                    Match::History { path, .. } => {
1604                        let worktree_id = path.project.worktree_id;
1605                        if workspace
1606                            .project()
1607                            .read(cx)
1608                            .worktree_for_id(worktree_id, cx)
1609                            .is_some()
1610                        {
1611                            split_or_open(
1612                                workspace,
1613                                ProjectPath {
1614                                    worktree_id,
1615                                    path: Arc::clone(&path.project.path),
1616                                },
1617                                window,
1618                                cx,
1619                            )
1620                        } else if secondary {
1621                            workspace.split_abs_path(path.absolute.clone(), false, window, cx)
1622                        } else {
1623                            workspace.open_abs_path(
1624                                path.absolute.clone(),
1625                                OpenOptions {
1626                                    visible: Some(OpenVisible::None),
1627                                    ..Default::default()
1628                                },
1629                                window,
1630                                cx,
1631                            )
1632                        }
1633                    }
1634                    Match::Search(m) => split_or_open(
1635                        workspace,
1636                        ProjectPath {
1637                            worktree_id: WorktreeId::from_usize(m.0.worktree_id),
1638                            path: m.0.path.clone(),
1639                        },
1640                        window,
1641                        cx,
1642                    ),
1643                    Match::Channel { .. } => unreachable!("handled above"),
1644                }
1645            });
1646
1647            let row = self
1648                .latest_search_query
1649                .as_ref()
1650                .and_then(|query| query.path_position.row)
1651                .map(|row| row.saturating_sub(1));
1652            let col = self
1653                .latest_search_query
1654                .as_ref()
1655                .and_then(|query| query.path_position.column)
1656                .unwrap_or(0)
1657                .saturating_sub(1);
1658            let finder = self.file_finder.clone();
1659            let workspace = self.workspace.clone();
1660
1661            cx.spawn_in(window, async move |_, mut cx| {
1662                let item = open_task
1663                    .await
1664                    .notify_workspace_async_err(workspace, &mut cx)?;
1665                if let Some(row) = row
1666                    && let Some(active_editor) = item.downcast::<Editor>()
1667                {
1668                    active_editor
1669                        .downgrade()
1670                        .update_in(cx, |editor, window, cx| {
1671                            let Some(buffer) = editor.buffer().read(cx).as_singleton() else {
1672                                return;
1673                            };
1674                            let buffer_snapshot = buffer.read(cx).snapshot();
1675                            let point = buffer_snapshot.point_from_external_input(row, col);
1676                            editor.go_to_singleton_buffer_point(point, window, cx);
1677                        })
1678                        .log_err();
1679                }
1680                finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?;
1681
1682                Some(())
1683            })
1684            .detach();
1685        }
1686    }
1687
1688    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<FileFinderDelegate>>) {
1689        self.file_finder
1690            .update(cx, |_, cx| cx.emit(DismissEvent))
1691            .log_err();
1692    }
1693
1694    fn render_match(
1695        &self,
1696        ix: usize,
1697        selected: bool,
1698        window: &mut Window,
1699        cx: &mut Context<Picker<Self>>,
1700    ) -> Option<Self::ListItem> {
1701        let settings = FileFinderSettings::get_global(cx);
1702
1703        let path_match = self.matches.get(ix)?;
1704
1705        let end_icon = match path_match {
1706            Match::History { .. } => Icon::new(IconName::HistoryRerun)
1707                .color(Color::Muted)
1708                .size(IconSize::Small)
1709                .into_any_element(),
1710            Match::Search(_) => v_flex()
1711                .flex_none()
1712                .size(IconSize::Small.rems())
1713                .into_any_element(),
1714            Match::Channel { .. } => v_flex()
1715                .flex_none()
1716                .size(IconSize::Small.rems())
1717                .into_any_element(),
1718            Match::CreateNew(_) => Icon::new(IconName::Plus)
1719                .color(Color::Muted)
1720                .size(IconSize::Small)
1721                .into_any_element(),
1722        };
1723        let (file_name_label, full_path_label) = self.labels_for_match(path_match, window, cx);
1724
1725        let file_icon = match path_match {
1726            Match::Channel { .. } => Some(Icon::new(IconName::Hash).color(Color::Muted)),
1727            _ => maybe!({
1728                if !settings.file_icons {
1729                    return None;
1730                }
1731                let abs_path = path_match.abs_path(&self.project, cx)?;
1732                let file_name = abs_path.file_name()?;
1733                let icon = FileIcons::get_icon(file_name.as_ref(), cx)?;
1734                Some(Icon::from_path(icon).color(Color::Muted))
1735            }),
1736        };
1737
1738        Some(
1739            ListItem::new(ix)
1740                .spacing(ListItemSpacing::Sparse)
1741                .start_slot::<Icon>(file_icon)
1742                .end_slot::<AnyElement>(end_icon)
1743                .inset(true)
1744                .toggle_state(selected)
1745                .child(
1746                    h_flex()
1747                        .gap_2()
1748                        .py_px()
1749                        .child(file_name_label)
1750                        .child(full_path_label),
1751                ),
1752        )
1753    }
1754
1755    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
1756        let focus_handle = self.focus_handle.clone();
1757
1758        Some(
1759            h_flex()
1760                .w_full()
1761                .p_1p5()
1762                .justify_between()
1763                .border_t_1()
1764                .border_color(cx.theme().colors().border_variant)
1765                .child(
1766                    PopoverMenu::new("filter-menu-popover")
1767                        .with_handle(self.filter_popover_menu_handle.clone())
1768                        .attach(gpui::Corner::BottomRight)
1769                        .anchor(gpui::Corner::BottomLeft)
1770                        .offset(gpui::Point {
1771                            x: px(1.0),
1772                            y: px(1.0),
1773                        })
1774                        .trigger_with_tooltip(
1775                            IconButton::new("filter-trigger", IconName::Sliders)
1776                                .icon_size(IconSize::Small)
1777                                .icon_size(IconSize::Small)
1778                                .toggle_state(self.include_ignored.unwrap_or(false))
1779                                .when(self.include_ignored.is_some(), |this| {
1780                                    this.indicator(Indicator::dot().color(Color::Info))
1781                                }),
1782                            {
1783                                let focus_handle = focus_handle.clone();
1784                                move |_window, cx| {
1785                                    Tooltip::for_action_in(
1786                                        "Filter Options",
1787                                        &ToggleFilterMenu,
1788                                        &focus_handle,
1789                                        cx,
1790                                    )
1791                                }
1792                            },
1793                        )
1794                        .menu({
1795                            let focus_handle = focus_handle.clone();
1796                            let include_ignored = self.include_ignored;
1797
1798                            move |window, cx| {
1799                                Some(ContextMenu::build(window, cx, {
1800                                    let focus_handle = focus_handle.clone();
1801                                    move |menu, _, _| {
1802                                        menu.context(focus_handle.clone())
1803                                            .header("Filter Options")
1804                                            .toggleable_entry(
1805                                                "Include Ignored Files",
1806                                                include_ignored.unwrap_or(false),
1807                                                ui::IconPosition::End,
1808                                                Some(ToggleIncludeIgnored.boxed_clone()),
1809                                                move |window, cx| {
1810                                                    window.focus(&focus_handle, cx);
1811                                                    window.dispatch_action(
1812                                                        ToggleIncludeIgnored.boxed_clone(),
1813                                                        cx,
1814                                                    );
1815                                                },
1816                                            )
1817                                    }
1818                                }))
1819                            }
1820                        }),
1821                )
1822                .child(
1823                    h_flex()
1824                        .gap_0p5()
1825                        .child(
1826                            PopoverMenu::new("split-menu-popover")
1827                                .with_handle(self.split_popover_menu_handle.clone())
1828                                .attach(gpui::Corner::BottomRight)
1829                                .anchor(gpui::Corner::BottomLeft)
1830                                .offset(gpui::Point {
1831                                    x: px(1.0),
1832                                    y: px(1.0),
1833                                })
1834                                .trigger(
1835                                    ButtonLike::new("split-trigger")
1836                                        .child(Label::new("Split…"))
1837                                        .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1838                                        .child(
1839                                            KeyBinding::for_action_in(
1840                                                &ToggleSplitMenu,
1841                                                &focus_handle,
1842                                                cx,
1843                                            )
1844                                            .size(rems_from_px(12.)),
1845                                        ),
1846                                )
1847                                .menu({
1848                                    let focus_handle = focus_handle.clone();
1849
1850                                    move |window, cx| {
1851                                        Some(ContextMenu::build(window, cx, {
1852                                            let focus_handle = focus_handle.clone();
1853                                            move |menu, _, _| {
1854                                                menu.context(focus_handle)
1855                                                    .action(
1856                                                        "Split Left",
1857                                                        pane::SplitLeft::default().boxed_clone(),
1858                                                    )
1859                                                    .action(
1860                                                        "Split Right",
1861                                                        pane::SplitRight::default().boxed_clone(),
1862                                                    )
1863                                                    .action(
1864                                                        "Split Up",
1865                                                        pane::SplitUp::default().boxed_clone(),
1866                                                    )
1867                                                    .action(
1868                                                        "Split Down",
1869                                                        pane::SplitDown::default().boxed_clone(),
1870                                                    )
1871                                            }
1872                                        }))
1873                                    }
1874                                }),
1875                        )
1876                        .child(
1877                            Button::new("open-selection", "Open")
1878                                .key_binding(
1879                                    KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1880                                        .map(|kb| kb.size(rems_from_px(12.))),
1881                                )
1882                                .on_click(|_, window, cx| {
1883                                    window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1884                                }),
1885                        ),
1886                )
1887                .into_any(),
1888        )
1889    }
1890}
1891
1892#[derive(Clone, Debug, PartialEq, Eq)]
1893struct PathComponentSlice<'a> {
1894    path: Cow<'a, Path>,
1895    path_str: Cow<'a, str>,
1896    component_ranges: Vec<(Component<'a>, Range<usize>)>,
1897}
1898
1899impl<'a> PathComponentSlice<'a> {
1900    fn new(path: &'a str) -> Self {
1901        let trimmed_path = Path::new(path).components().as_path().as_os_str();
1902        let mut component_ranges = Vec::new();
1903        let mut components = Path::new(trimmed_path).components();
1904        let len = trimmed_path.as_encoded_bytes().len();
1905        let mut pos = 0;
1906        while let Some(component) = components.next() {
1907            component_ranges.push((component, pos..0));
1908            pos = len - components.as_path().as_os_str().as_encoded_bytes().len();
1909        }
1910        for ((_, range), ancestor) in component_ranges
1911            .iter_mut()
1912            .rev()
1913            .zip(Path::new(trimmed_path).ancestors())
1914        {
1915            range.end = ancestor.as_os_str().as_encoded_bytes().len();
1916        }
1917        Self {
1918            path: Cow::Borrowed(Path::new(path)),
1919            path_str: Cow::Borrowed(path),
1920            component_ranges,
1921        }
1922    }
1923
1924    fn elision_range(&self, budget: usize, matches: &[usize]) -> Option<Range<usize>> {
1925        let eligible_range = {
1926            assert!(matches.windows(2).all(|w| w[0] <= w[1]));
1927            let mut matches = matches.iter().copied().peekable();
1928            let mut longest: Option<Range<usize>> = None;
1929            let mut cur = 0..0;
1930            let mut seen_normal = false;
1931            for (i, (component, range)) in self.component_ranges.iter().enumerate() {
1932                let is_normal = matches!(component, Component::Normal(_));
1933                let is_first_normal = is_normal && !seen_normal;
1934                seen_normal |= is_normal;
1935                let is_last = i == self.component_ranges.len() - 1;
1936                let contains_match = matches.peek().is_some_and(|mat| range.contains(mat));
1937                if contains_match {
1938                    matches.next();
1939                }
1940                if is_first_normal || is_last || !is_normal || contains_match {
1941                    if longest
1942                        .as_ref()
1943                        .is_none_or(|old| old.end - old.start <= cur.end - cur.start)
1944                    {
1945                        longest = Some(cur);
1946                    }
1947                    cur = i + 1..i + 1;
1948                } else {
1949                    cur.end = i + 1;
1950                }
1951            }
1952            if longest
1953                .as_ref()
1954                .is_none_or(|old| old.end - old.start <= cur.end - cur.start)
1955            {
1956                longest = Some(cur);
1957            }
1958            longest
1959        };
1960
1961        let eligible_range = eligible_range?;
1962        assert!(eligible_range.start <= eligible_range.end);
1963        if eligible_range.is_empty() {
1964            return None;
1965        }
1966
1967        let elided_range: Range<usize> = {
1968            let byte_range = self.component_ranges[eligible_range.start].1.start
1969                ..self.component_ranges[eligible_range.end - 1].1.end;
1970            let midpoint = self.path_str.len() / 2;
1971            let distance_from_start = byte_range.start.abs_diff(midpoint);
1972            let distance_from_end = byte_range.end.abs_diff(midpoint);
1973            let pick_from_end = distance_from_start > distance_from_end;
1974            let mut len_with_elision = self.path_str.len();
1975            let mut i = eligible_range.start;
1976            while i < eligible_range.end {
1977                let x = if pick_from_end {
1978                    eligible_range.end - i + eligible_range.start - 1
1979                } else {
1980                    i
1981                };
1982                len_with_elision -= self.component_ranges[x]
1983                    .0
1984                    .as_os_str()
1985                    .as_encoded_bytes()
1986                    .len()
1987                    + 1;
1988                if len_with_elision <= budget {
1989                    break;
1990                }
1991                i += 1;
1992            }
1993            if len_with_elision > budget {
1994                return None;
1995            } else if pick_from_end {
1996                let x = eligible_range.end - i + eligible_range.start - 1;
1997                x..eligible_range.end
1998            } else {
1999                let x = i;
2000                eligible_range.start..x + 1
2001            }
2002        };
2003
2004        let byte_range = self.component_ranges[elided_range.start].1.start
2005            ..self.component_ranges[elided_range.end - 1].1.end;
2006        Some(byte_range)
2007    }
2008}