#[cfg(test)]mod file_finder_tests; pub mod file_finder_settings;mod new_path_prompt;mod open_path_prompt; use futures::future::join_all;pub use open_path_prompt::OpenPathDelegate; use collections::HashMap;use editor::Editor;use file_finder_settings::{FileFinderSettings, FileFinderWidth};use file_icons::FileIcons;use fuzzy::{CharBag, PathMatch, PathMatchCandidate};use gpui::{ actions, Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity, Window,};use new_path_prompt::NewPathPrompt;use open_path_prompt::OpenPathPrompt;use picker::{Picker, PickerDelegate};use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};use settings::Settings;use std::{ borrow::Cow, cmp, ops::Range, path::{Component, Path, PathBuf}, sync::{ atomic::{self, AtomicBool}, Arc, },};use text::Point;use ui::{ prelude::*, ContextMenu, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle,};use util::{maybe, paths::PathWithPosition, post_inc, ResultExt};use workspace::{ item::PreviewTabsSettings, notifications::NotifyResultExt, pane, ModalView, SplitDirection, Workspace,}; actions!(file_finder, [SelectPrev, ToggleMenu]); impl ModalView for FileFinder { fn on_before_dismiss( &mut self, window: &mut Window, cx: &mut Context<Self>, ) -> workspace::DismissDecision { let submenu_focused = self.picker.update(cx, |picker, cx| { picker.delegate.popover_menu_handle.is_focused(window, cx) }); workspace::DismissDecision::Dismiss(!submenu_focused) }} pub struct FileFinder { picker: Entity<Picker<FileFinderDelegate>>, picker_focus_handle: FocusHandle, init_modifiers: Option<Modifiers>,} pub fn init_settings(cx: &mut App) { FileFinderSettings::register(cx);} pub fn init(cx: &mut App) { init_settings(cx); cx.observe_new(FileFinder::register).detach(); cx.observe_new(NewPathPrompt::register).detach(); cx.observe_new(OpenPathPrompt::register).detach();} impl FileFinder { fn register( workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>, ) { workspace.register_action( |workspace, action: &workspace::ToggleFileFinder, window, cx| { let Some(file_finder) = workspace.active_modal::<Self>(cx) else { Self::open(workspace, action.separate_history, window, cx).detach(); return; }; file_finder.update(cx, |file_finder, cx| { file_finder.init_modifiers = Some(window.modifiers()); file_finder.picker.update(cx, |picker, cx| { picker.cycle_selection(window, cx); }); }); }, ); } fn open( workspace: &mut Workspace, separate_history: bool, window: &mut Window, cx: &mut Context<Workspace>, ) -> Task<()> { let project = workspace.project().read(cx); let fs = project.fs(); let currently_opened_path = workspace .active_item(cx) .and_then(|item| item.project_path(cx)) .map(|project_path| { let abs_path = project .worktree_for_id(project_path.worktree_id, cx) .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)); FoundPath::new(project_path, abs_path) }); let history_items = workspace .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx) .into_iter() .filter_map(|(project_path, abs_path)| { if project.entry_for_path(&project_path, cx).is_some() { return Some(Task::ready(Some(FoundPath::new(project_path, abs_path)))); } let abs_path = abs_path?; if project.is_local() { let fs = fs.clone(); Some(cx.background_spawn(async move { if fs.is_file(&abs_path).await { Some(FoundPath::new(project_path, Some(abs_path))) } else { None } })) } else { Some(Task::ready(Some(FoundPath::new( project_path, Some(abs_path), )))) } }) .collect::<Vec<_>>(); cx.spawn_in(window, move |workspace, mut cx| async move { let history_items = join_all(history_items).await.into_iter().flatten(); workspace .update_in(&mut cx, |workspace, window, cx| { let project = workspace.project().clone(); let weak_workspace = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| { let delegate = FileFinderDelegate::new( cx.entity().downgrade(), weak_workspace, project, currently_opened_path, history_items.collect(), separate_history, window, cx, ); FileFinder::new(delegate, window, cx) }); }) .ok(); }) } fn new(delegate: FileFinderDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self { let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); let picker_focus_handle = picker.focus_handle(cx); picker.update(cx, |picker, _| { picker.delegate.focus_handle = picker_focus_handle.clone(); }); Self { picker, picker_focus_handle, init_modifiers: window.modifiers().modified().then_some(window.modifiers()), } } fn handle_modifiers_changed( &mut self, event: &ModifiersChangedEvent, window: &mut Window, cx: &mut Context<Self>, ) { let Some(init_modifiers) = self.init_modifiers.take() else { return; }; if self.picker.read(cx).delegate.has_changed_selected_index { if !event.modified() || !init_modifiers.is_subset_of(&event) { self.init_modifiers = None; window.dispatch_action(menu::Confirm.boxed_clone(), cx); } } } fn handle_select_prev(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context<Self>) { self.init_modifiers = Some(window.modifiers()); window.dispatch_action(Box::new(menu::SelectPrev), cx); } fn handle_toggle_menu(&mut self, _: &ToggleMenu, window: &mut Window, cx: &mut Context<Self>) { self.picker.update(cx, |picker, cx| { let menu_handle = &picker.delegate.popover_menu_handle; if menu_handle.is_deployed() { menu_handle.hide(cx); } else { menu_handle.show(window, cx); } }); } fn go_to_file_split_left( &mut self, _: &pane::SplitLeft, window: &mut Window, cx: &mut Context<Self>, ) { self.go_to_file_split_inner(SplitDirection::Left, window, cx) } fn go_to_file_split_right( &mut self, _: &pane::SplitRight, window: &mut Window, cx: &mut Context<Self>, ) { self.go_to_file_split_inner(SplitDirection::Right, window, cx) } fn go_to_file_split_up( &mut self, _: &pane::SplitUp, window: &mut Window, cx: &mut Context<Self>, ) { self.go_to_file_split_inner(SplitDirection::Up, window, cx) } fn go_to_file_split_down( &mut self, _: &pane::SplitDown, window: &mut Window, cx: &mut Context<Self>, ) { self.go_to_file_split_inner(SplitDirection::Down, window, cx) } fn go_to_file_split_inner( &mut self, split_direction: SplitDirection, window: &mut Window, cx: &mut Context<Self>, ) { self.picker.update(cx, |picker, cx| { let delegate = &mut picker.delegate; if let Some(workspace) = delegate.workspace.upgrade() { if let Some(m) = delegate.matches.get(delegate.selected_index()) { let path = match &m { Match::History { path, .. } => { let worktree_id = path.project.worktree_id; ProjectPath { worktree_id, path: Arc::clone(&path.project.path), } } Match::Search(m) => ProjectPath { worktree_id: WorktreeId::from_usize(m.0.worktree_id), path: m.0.path.clone(), }, }; let open_task = workspace.update(cx, move |workspace, cx| { workspace.split_path_preview(path, false, Some(split_direction), window, cx) }); open_task.detach_and_log_err(cx); } } }) } pub fn modal_max_width(width_setting: Option<FileFinderWidth>, window: &mut Window) -> Pixels { let window_width = window.viewport_size().width; let small_width = Pixels(545.); match width_setting { None | Some(FileFinderWidth::Small) => small_width, Some(FileFinderWidth::Full) => window_width, Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width), Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width), Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width), } }} impl EventEmitter<DismissEvent> for FileFinder {} impl Focusable for FileFinder { fn focus_handle(&self, _: &App) -> FocusHandle { self.picker_focus_handle.clone() }} impl Render for FileFinder { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let key_context = self.picker.read(cx).delegate.key_context(window, cx); let file_finder_settings = FileFinderSettings::get_global(cx); let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, window); v_flex() .key_context(key_context) .w(modal_max_width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_select_prev)) .on_action(cx.listener(Self::handle_toggle_menu)) .on_action(cx.listener(Self::go_to_file_split_left)) .on_action(cx.listener(Self::go_to_file_split_right)) .on_action(cx.listener(Self::go_to_file_split_up)) .on_action(cx.listener(Self::go_to_file_split_down)) .child(self.picker.clone()) }} pub struct FileFinderDelegate { file_finder: WeakEntity<FileFinder>, workspace: WeakEntity<Workspace>, project: Entity<Project>, search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, latest_search_query: Option<FileSearchQuery>, currently_opened_path: Option<FoundPath>, matches: Matches, selected_index: usize, has_changed_selected_index: bool, cancel_flag: Arc<AtomicBool>, history_items: Vec<FoundPath>, separate_history: bool, first_update: bool, popover_menu_handle: PopoverMenuHandle<ContextMenu>, focus_handle: FocusHandle,} /// Use a custom ordering for file finder: the regular one/// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g:/// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]`////// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g:/// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]`/// as the files are shown in the project panel lists.#[derive(Debug, Clone, PartialEq, Eq)]struct ProjectPanelOrdMatch(PathMatch); impl Ord for ProjectPanelOrdMatch { fn cmp(&self, other: &Self) -> cmp::Ordering { self.0 .score .partial_cmp(&other.0.score) .unwrap_or(cmp::Ordering::Equal) .then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id)) .then_with(|| { other .0 .distance_to_relative_ancestor .cmp(&self.0.distance_to_relative_ancestor) }) .then_with(|| self.0.path.cmp(&other.0.path).reverse()) }} impl PartialOrd for ProjectPanelOrdMatch { fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { Some(self.cmp(other)) }} #[derive(Debug, Default)]struct Matches { separate_history: bool, matches: Vec<Match>,} #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]enum Match { History { path: FoundPath, panel_match: Option<ProjectPanelOrdMatch>, }, Search(ProjectPanelOrdMatch),} impl Match { fn path(&self) -> &Arc<Path> { match self { Match::History { path, .. } => &path.project.path, Match::Search(panel_match) => &panel_match.0.path, } } fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> { match self { Match::History { panel_match, .. } => panel_match.as_ref(), Match::Search(panel_match) => Some(&panel_match), } }} impl Matches { fn len(&self) -> usize { self.matches.len() } fn get(&self, index: usize) -> Option<&Match> { self.matches.get(index) } fn position( &self, entry: &Match, currently_opened: Option<&FoundPath>, ) -> Result<usize, usize> { if let Match::History { path, panel_match: None, } = entry { // Slow case: linear search by path. Should not happen actually, // since we call `position` only if matches set changed, but the query has not changed. // And History entries do not have panel_match if query is empty, so there's no // reason for the matches set to change. self.matches .iter() .position(|m| path.project.path == *m.path()) .ok_or(0) } else { self.matches.binary_search_by(|m| { // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b. // And we want the better entries go first. Self::cmp_matches(self.separate_history, currently_opened, &m, &entry).reverse() }) } } fn push_new_matches<'a>( &'a mut self, history_items: impl IntoIterator<Item = &'a FoundPath> + Clone, currently_opened: Option<&'a FoundPath>, query: Option<&FileSearchQuery>, new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>, extend_old_matches: bool, ) { let Some(query) = query else { // assuming that if there's no query, then there's no search matches. self.matches.clear(); let path_to_entry = |found_path: &FoundPath| Match::History { path: found_path.clone(), panel_match: None, }; self.matches .extend(currently_opened.into_iter().map(path_to_entry)); self.matches.extend( history_items .into_iter() .filter(|found_path| Some(*found_path) != currently_opened) .map(path_to_entry), ); return; }; let new_history_matches = matching_history_items(history_items, currently_opened, query); let new_search_matches: Vec<Match> = new_search_matches .filter(|path_match| !new_history_matches.contains_key(&path_match.0.path)) .map(Match::Search) .collect(); if extend_old_matches { // since we take history matches instead of new search matches // and history matches has not changed(since the query has not changed and we do not extend old matches otherwise), // old matches can't contain paths present in history_matches as well. self.matches.retain(|m| matches!(m, Match::Search(_))); } else { self.matches.clear(); } // At this point we have an unsorted set of new history matches, an unsorted set of new search matches // and a sorted set of old search matches. // It is possible that the new search matches' paths contain some of the old search matches' paths. // History matches' paths are unique, since store in a HashMap by path. // We build a sorted Vec<Match>, eliminating duplicate search matches. // Search matches with the same paths should have equal `ProjectPanelOrdMatch`, so we should // not have any duplicates after building the final list. for new_match in new_history_matches .into_values() .chain(new_search_matches.into_iter())Lines 1–500 of 1521. Load more
#[cfg(test)] mod file_finder_tests; pub mod file_finder_settings; mod new_path_prompt; mod open_path_prompt; use futures::future::join_all; pub use open_path_prompt::OpenPathDelegate; use collections::HashMap; use editor::Editor; use file_finder_settings::{FileFinderSettings, FileFinderWidth}; use file_icons::FileIcons; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ actions, Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity, Window, }; use new_path_prompt::NewPathPrompt; use open_path_prompt::OpenPathPrompt; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use settings::Settings; use std::{ borrow::Cow, cmp, ops::Range, path::{Component, Path, PathBuf}, sync::{ atomic::{self, AtomicBool}, Arc, }, }; use text::Point; use ui::{ prelude::*, ContextMenu, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, }; use util::{maybe, paths::PathWithPosition, post_inc, ResultExt}; use workspace::{ item::PreviewTabsSettings, notifications::NotifyResultExt, pane, ModalView, SplitDirection, Workspace, }; actions!(file_finder, [SelectPrev, ToggleMenu]); impl ModalView for FileFinder { fn on_before_dismiss( &mut self, window: &mut Window, cx: &mut Context<Self>, ) -> workspace::DismissDecision { let submenu_focused = self.picker.update(cx, |picker, cx| { picker.delegate.popover_menu_handle.is_focused(window, cx) }); workspace::DismissDecision::Dismiss(!submenu_focused) } } pub struct FileFinder { picker: Entity<Picker<FileFinderDelegate>>, picker_focus_handle: FocusHandle, init_modifiers: Option<Modifiers>, } pub fn init_settings(cx: &mut App) { FileFinderSettings::register(cx); } pub fn init(cx: &mut App) { init_settings(cx); cx.observe_new(FileFinder::register).detach(); cx.observe_new(NewPathPrompt::register).detach(); cx.observe_new(OpenPathPrompt::register).detach(); } impl FileFinder { fn register( workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context<Workspace>, ) { workspace.register_action( |workspace, action: &workspace::ToggleFileFinder, window, cx| { let Some(file_finder) = workspace.active_modal::<Self>(cx) else { Self::open(workspace, action.separate_history, window, cx).detach(); return; }; file_finder.update(cx, |file_finder, cx| { file_finder.init_modifiers = Some(window.modifiers()); file_finder.picker.update(cx, |picker, cx| { picker.cycle_selection(window, cx); }); }); }, ); } fn open( workspace: &mut Workspace, separate_history: bool, window: &mut Window, cx: &mut Context<Workspace>, ) -> Task<()> { let project = workspace.project().read(cx); let fs = project.fs(); let currently_opened_path = workspace .active_item(cx) .and_then(|item| item.project_path(cx)) .map(|project_path| { let abs_path = project .worktree_for_id(project_path.worktree_id, cx) .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)); FoundPath::new(project_path, abs_path) }); let history_items = workspace .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx) .into_iter() .filter_map(|(project_path, abs_path)| { if project.entry_for_path(&project_path, cx).is_some() { return Some(Task::ready(Some(FoundPath::new(project_path, abs_path)))); } let abs_path = abs_path?; if project.is_local() { let fs = fs.clone(); Some(cx.background_spawn(async move { if fs.is_file(&abs_path).await { Some(FoundPath::new(project_path, Some(abs_path))) } else { None } })) } else { Some(Task::ready(Some(FoundPath::new( project_path, Some(abs_path), )))) } }) .collect::<Vec<_>>(); cx.spawn_in(window, move |workspace, mut cx| async move { let history_items = join_all(history_items).await.into_iter().flatten(); workspace .update_in(&mut cx, |workspace, window, cx| { let project = workspace.project().clone(); let weak_workspace = cx.entity().downgrade(); workspace.toggle_modal(window, cx, |window, cx| { let delegate = FileFinderDelegate::new( cx.entity().downgrade(), weak_workspace, project, currently_opened_path, history_items.collect(), separate_history, window, cx, ); FileFinder::new(delegate, window, cx) }); }) .ok(); }) } fn new(delegate: FileFinderDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self { let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); let picker_focus_handle = picker.focus_handle(cx); picker.update(cx, |picker, _| { picker.delegate.focus_handle = picker_focus_handle.clone(); }); Self { picker, picker_focus_handle, init_modifiers: window.modifiers().modified().then_some(window.modifiers()), } } fn handle_modifiers_changed( &mut self, event: &ModifiersChangedEvent, window: &mut Window, cx: &mut Context<Self>, ) { let Some(init_modifiers) = self.init_modifiers.take() else { return; }; if self.picker.read(cx).delegate.has_changed_selected_index { if !event.modified() || !init_modifiers.is_subset_of(&event) { self.init_modifiers = None; window.dispatch_action(menu::Confirm.boxed_clone(), cx); } } } fn handle_select_prev(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context<Self>) { self.init_modifiers = Some(window.modifiers()); window.dispatch_action(Box::new(menu::SelectPrev), cx); } fn handle_toggle_menu(&mut self, _: &ToggleMenu, window: &mut Window, cx: &mut Context<Self>) { self.picker.update(cx, |picker, cx| { let menu_handle = &picker.delegate.popover_menu_handle; if menu_handle.is_deployed() { menu_handle.hide(cx); } else { menu_handle.show(window, cx); } }); } fn go_to_file_split_left( &mut self, _: &pane::SplitLeft, window: &mut Window, cx: &mut Context<Self>, ) { self.go_to_file_split_inner(SplitDirection::Left, window, cx) } fn go_to_file_split_right( &mut self, _: &pane::SplitRight, window: &mut Window, cx: &mut Context<Self>, ) { self.go_to_file_split_inner(SplitDirection::Right, window, cx) } fn go_to_file_split_up( &mut self, _: &pane::SplitUp, window: &mut Window, cx: &mut Context<Self>, ) { self.go_to_file_split_inner(SplitDirection::Up, window, cx) } fn go_to_file_split_down( &mut self, _: &pane::SplitDown, window: &mut Window, cx: &mut Context<Self>, ) { self.go_to_file_split_inner(SplitDirection::Down, window, cx) } fn go_to_file_split_inner( &mut self, split_direction: SplitDirection, window: &mut Window, cx: &mut Context<Self>, ) { self.picker.update(cx, |picker, cx| { let delegate = &mut picker.delegate; if let Some(workspace) = delegate.workspace.upgrade() { if let Some(m) = delegate.matches.get(delegate.selected_index()) { let path = match &m { Match::History { path, .. } => { let worktree_id = path.project.worktree_id; ProjectPath { worktree_id, path: Arc::clone(&path.project.path), } } Match::Search(m) => ProjectPath { worktree_id: WorktreeId::from_usize(m.0.worktree_id), path: m.0.path.clone(), }, }; let open_task = workspace.update(cx, move |workspace, cx| { workspace.split_path_preview(path, false, Some(split_direction), window, cx) }); open_task.detach_and_log_err(cx); } } }) } pub fn modal_max_width(width_setting: Option<FileFinderWidth>, window: &mut Window) -> Pixels { let window_width = window.viewport_size().width; let small_width = Pixels(545.); match width_setting { None | Some(FileFinderWidth::Small) => small_width, Some(FileFinderWidth::Full) => window_width, Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width), Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width), Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width), } } } impl EventEmitter<DismissEvent> for FileFinder {} impl Focusable for FileFinder { fn focus_handle(&self, _: &App) -> FocusHandle { self.picker_focus_handle.clone() } } impl Render for FileFinder { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let key_context = self.picker.read(cx).delegate.key_context(window, cx); let file_finder_settings = FileFinderSettings::get_global(cx); let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, window); v_flex() .key_context(key_context) .w(modal_max_width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_select_prev)) .on_action(cx.listener(Self::handle_toggle_menu)) .on_action(cx.listener(Self::go_to_file_split_left)) .on_action(cx.listener(Self::go_to_file_split_right)) .on_action(cx.listener(Self::go_to_file_split_up)) .on_action(cx.listener(Self::go_to_file_split_down)) .child(self.picker.clone()) } } pub struct FileFinderDelegate { file_finder: WeakEntity<FileFinder>, workspace: WeakEntity<Workspace>, project: Entity<Project>, search_count: usize, latest_search_id: usize, latest_search_did_cancel: bool, latest_search_query: Option<FileSearchQuery>, currently_opened_path: Option<FoundPath>, matches: Matches, selected_index: usize, has_changed_selected_index: bool, cancel_flag: Arc<AtomicBool>, history_items: Vec<FoundPath>, separate_history: bool, first_update: bool, popover_menu_handle: PopoverMenuHandle<ContextMenu>, focus_handle: FocusHandle, } /// Use a custom ordering for file finder: the regular one /// defines max element with the highest score and the latest alphanumerical path (in case of a tie on other params), e.g: /// `[{score: 0.5, path = "c/d" }, { score: 0.5, path = "/a/b" }]` /// /// In the file finder, we would prefer to have the max element with the highest score and the earliest alphanumerical path, e.g: /// `[{ score: 0.5, path = "/a/b" }, {score: 0.5, path = "c/d" }]` /// as the files are shown in the project panel lists. #[derive(Debug, Clone, PartialEq, Eq)] struct ProjectPanelOrdMatch(PathMatch); impl Ord for ProjectPanelOrdMatch { fn cmp(&self, other: &Self) -> cmp::Ordering { self.0 .score .partial_cmp(&other.0.score) .unwrap_or(cmp::Ordering::Equal) .then_with(|| self.0.worktree_id.cmp(&other.0.worktree_id)) .then_with(|| { other .0 .distance_to_relative_ancestor .cmp(&self.0.distance_to_relative_ancestor) }) .then_with(|| self.0.path.cmp(&other.0.path).reverse()) } } impl PartialOrd for ProjectPanelOrdMatch { fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { Some(self.cmp(other)) } } #[derive(Debug, Default)] struct Matches { separate_history: bool, matches: Vec<Match>, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] enum Match { History { path: FoundPath, panel_match: Option<ProjectPanelOrdMatch>, }, Search(ProjectPanelOrdMatch), } impl Match { fn path(&self) -> &Arc<Path> { match self { Match::History { path, .. } => &path.project.path, Match::Search(panel_match) => &panel_match.0.path, } } fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> { match self { Match::History { panel_match, .. } => panel_match.as_ref(), Match::Search(panel_match) => Some(&panel_match), } } } impl Matches { fn len(&self) -> usize { self.matches.len() } fn get(&self, index: usize) -> Option<&Match> { self.matches.get(index) } fn position( &self, entry: &Match, currently_opened: Option<&FoundPath>, ) -> Result<usize, usize> { if let Match::History { path, panel_match: None, } = entry { // Slow case: linear search by path. Should not happen actually, // since we call `position` only if matches set changed, but the query has not changed. // And History entries do not have panel_match if query is empty, so there's no // reason for the matches set to change. self.matches .iter() .position(|m| path.project.path == *m.path()) .ok_or(0) } else { self.matches.binary_search_by(|m| { // `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b. // And we want the better entries go first. Self::cmp_matches(self.separate_history, currently_opened, &m, &entry).reverse() }) } } fn push_new_matches<'a>( &'a mut self, history_items: impl IntoIterator<Item = &'a FoundPath> + Clone, currently_opened: Option<&'a FoundPath>, query: Option<&FileSearchQuery>, new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>, extend_old_matches: bool, ) { let Some(query) = query else { // assuming that if there's no query, then there's no search matches. self.matches.clear(); let path_to_entry = |found_path: &FoundPath| Match::History { path: found_path.clone(), panel_match: None, }; self.matches .extend(currently_opened.into_iter().map(path_to_entry)); self.matches.extend( history_items .into_iter() .filter(|found_path| Some(*found_path) != currently_opened) .map(path_to_entry), ); return; }; let new_history_matches = matching_history_items(history_items, currently_opened, query); let new_search_matches: Vec<Match> = new_search_matches .filter(|path_match| !new_history_matches.contains_key(&path_match.0.path)) .map(Match::Search) .collect(); if extend_old_matches { // since we take history matches instead of new search matches // and history matches has not changed(since the query has not changed and we do not extend old matches otherwise), // old matches can't contain paths present in history_matches as well. self.matches.retain(|m| matches!(m, Match::Search(_))); } else { self.matches.clear(); } // At this point we have an unsorted set of new history matches, an unsorted set of new search matches // and a sorted set of old search matches. // It is possible that the new search matches' paths contain some of the old search matches' paths. // History matches' paths are unique, since store in a HashMap by path. // We build a sorted Vec<Match>, eliminating duplicate search matches. // Search matches with the same paths should have equal `ProjectPanelOrdMatch`, so we should // not have any duplicates after building the final list. for new_match in new_history_matches .into_values() .chain(new_search_matches.into_iter())