From c8054cacbd45065bbd648e12033689f18dd3d18b Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 10 Feb 2026 18:29:51 -0500 Subject: [PATCH] git: Fix searching in the split diff (#48894) - Fix panics caused by reusing cached matches for the wrong side - Highlight matches on the side that was searched only - Clear matches in non-searched editor when initiating a new search Release Notes: - N/A --------- Co-authored-by: Eric Co-authored-by: Jakub --- crates/agent_ui/src/text_thread_editor.rs | 20 +-- crates/debugger_tools/src/dap_log.rs | 18 ++- crates/editor/src/items.rs | 25 ++-- crates/editor/src/split.rs | 141 ++++++++++++++++++---- crates/git_ui/src/project_diff.rs | 2 +- crates/language_tools/src/lsp_log_view.rs | 18 ++- crates/outline_panel/src/outline_panel.rs | 2 +- crates/search/src/buffer_search.rs | 65 +++++----- crates/search/src/project_search.rs | 25 +++- crates/terminal_view/src/terminal_view.rs | 16 ++- crates/workspace/src/searchable.rs | 104 ++++++++++++++-- 11 files changed, 336 insertions(+), 100 deletions(-) diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 005032ddb40b9ffdba3a144aa8cfd895bfc07c1d..784f88d79d05ad46af90d8c8d8b22a81349774db 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -61,7 +61,7 @@ use ui::{ use util::{ResultExt, maybe}; use workspace::{ CollaboratorId, - searchable::{Direction, SearchableItemHandle}, + searchable::{Direction, SearchToken, SearchableItemHandle}, }; use workspace::{ @@ -2799,11 +2799,12 @@ impl SearchableItem for TextThreadEditor { &mut self, matches: &[Self::Match], active_match_index: Option, + token: SearchToken, window: &mut Window, cx: &mut Context, ) { self.editor.update(cx, |editor, cx| { - editor.update_matches(matches, active_match_index, window, cx) + editor.update_matches(matches, active_match_index, token, window, cx) }); } @@ -2816,33 +2817,37 @@ impl SearchableItem for TextThreadEditor { &mut self, index: usize, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) { self.editor.update(cx, |editor, cx| { - editor.activate_match(index, matches, window, cx); + editor.activate_match(index, matches, token, window, cx); }); } fn select_matches( &mut self, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) { - self.editor - .update(cx, |editor, cx| editor.select_matches(matches, window, cx)); + self.editor.update(cx, |editor, cx| { + editor.select_matches(matches, token, window, cx) + }); } fn replace( &mut self, identifier: &Self::Match, query: &project::search::SearchQuery, + token: SearchToken, window: &mut Window, cx: &mut Context, ) { self.editor.update(cx, |editor, cx| { - editor.replace(identifier, query, window, cx) + editor.replace(identifier, query, token, window, cx) }); } @@ -2860,11 +2865,12 @@ impl SearchableItem for TextThreadEditor { &mut self, direction: Direction, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) -> Option { self.editor.update(cx, |editor, cx| { - editor.active_match_index(direction, matches, window, cx) + editor.active_match_index(direction, matches, token, window, cx) }) } } diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index 8ff43f35ad9f5be2f4fbf975c229f3e9f8d96334..f952d4369fe28a52a002b64dbf7226cba22403d4 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -28,7 +28,7 @@ use util::maybe; use workspace::{ ToolbarItemEvent, ToolbarItemView, Workspace, item::Item, - searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, + searchable::{Direction, SearchEvent, SearchToken, SearchableItem, SearchableItemHandle}, ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex}, }; @@ -1018,11 +1018,12 @@ impl SearchableItem for DapLogView { &mut self, matches: &[Self::Match], active_match_index: Option, + token: SearchToken, window: &mut Window, cx: &mut Context, ) { self.editor.update(cx, |e, cx| { - e.update_matches(matches, active_match_index, window, cx) + e.update_matches(matches, active_match_index, token, window, cx) }) } @@ -1035,21 +1036,24 @@ impl SearchableItem for DapLogView { &mut self, index: usize, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) { - self.editor - .update(cx, |e, cx| e.activate_match(index, matches, window, cx)) + self.editor.update(cx, |e, cx| { + e.activate_match(index, matches, token, window, cx) + }) } fn select_matches( &mut self, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) { self.editor - .update(cx, |e, cx| e.select_matches(matches, window, cx)) + .update(cx, |e, cx| e.select_matches(matches, token, window, cx)) } fn find_matches( @@ -1066,6 +1070,7 @@ impl SearchableItem for DapLogView { &mut self, _: &Self::Match, _: &SearchQuery, + _token: SearchToken, _window: &mut Window, _: &mut Context, ) { @@ -1087,11 +1092,12 @@ impl SearchableItem for DapLogView { &mut self, direction: Direction, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) -> Option { self.editor.update(cx, |e, cx| { - e.active_match_index(direction, matches, window, cx) + e.active_match_index(direction, matches, token, window, cx) }) } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index ce7fe9cab923bdaa2bcb2706fce2d5c7eb9d4d60..73247cd3ce398d00cfb3519b9b22f26ac68ca67d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -46,7 +46,8 @@ use workspace::{ invalid_item_view::InvalidItemView, item::{FollowableItem, Item, ItemBufferKind, ItemEvent, ProjectItem, SaveOptions}, searchable::{ - Direction, FilteredSearchRange, SearchEvent, SearchableItem, SearchableItemHandle, + Direction, FilteredSearchRange, SearchEvent, SearchToken, SearchableItem, + SearchableItemHandle, }, }; use workspace::{ @@ -1496,12 +1497,15 @@ impl Editor { impl SearchableItem for Editor { type Match = Range; - fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec> { - self.background_highlights - .get(&HighlightKey::BufferSearchHighlights) - .map_or(Vec::new(), |(_color, ranges)| { - ranges.iter().cloned().collect() - }) + fn get_matches(&self, _window: &mut Window, _: &mut App) -> (Vec>, SearchToken) { + ( + self.background_highlights + .get(&HighlightKey::BufferSearchHighlights) + .map_or(Vec::new(), |(_color, ranges)| { + ranges.iter().cloned().collect() + }), + SearchToken::default(), + ) } fn clear_matches(&mut self, _: &mut Window, cx: &mut Context) { @@ -1517,6 +1521,7 @@ impl SearchableItem for Editor { &mut self, matches: &[Range], active_match_index: Option, + _token: SearchToken, _: &mut Window, cx: &mut Context, ) { @@ -1630,6 +1635,7 @@ impl SearchableItem for Editor { &mut self, index: usize, matches: &[Range], + _token: SearchToken, window: &mut Window, cx: &mut Context, ) { @@ -1648,6 +1654,7 @@ impl SearchableItem for Editor { fn select_matches( &mut self, matches: &[Self::Match], + _token: SearchToken, window: &mut Window, cx: &mut Context, ) { @@ -1660,6 +1667,7 @@ impl SearchableItem for Editor { &mut self, identifier: &Self::Match, query: &SearchQuery, + _token: SearchToken, window: &mut Window, cx: &mut Context, ) { @@ -1683,6 +1691,7 @@ impl SearchableItem for Editor { &mut self, matches: &mut dyn Iterator, query: &SearchQuery, + _token: SearchToken, window: &mut Window, cx: &mut Context, ) { @@ -1725,6 +1734,7 @@ impl SearchableItem for Editor { current_index: usize, direction: Direction, count: usize, + _token: SearchToken, _: &mut Window, cx: &mut Context, ) -> usize { @@ -1832,6 +1842,7 @@ impl SearchableItem for Editor { &mut self, direction: Direction, matches: &[Range], + _token: SearchToken, _: &mut Window, cx: &mut Context, ) -> Option { diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index 76ed8c536b18e6b8aad9b424643210cf422d2f10..9ebfdae5b0fec14f024acdbe0ddc8fb30299bbc2 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -24,12 +24,13 @@ use ui::{ use crate::{ display_map::CompanionExcerptPatch, + element::SplitSide, split_editor_view::{SplitEditorState, SplitEditorView}, }; use workspace::{ ActivatePaneLeft, ActivatePaneRight, Item, ToolbarItemLocation, Workspace, item::{BreadcrumbText, ItemBufferKind, ItemEvent, SaveOptions, TabContentParams}, - searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, + searchable::{SearchEvent, SearchToken, SearchableItem, SearchableItemHandle}, }; use crate::{ @@ -381,6 +382,7 @@ pub struct SplittableEditor { lhs: Option, workspace: WeakEntity, split_state: Entity, + searched_side: Option, _subscriptions: Vec, } @@ -420,7 +422,17 @@ impl SplittableEditor { } } - pub fn last_selected_editor(&self) -> &Entity { + fn focused_side(&self) -> SplitSide { + if let Some(lhs) = &self.lhs + && lhs.was_last_focused + { + SplitSide::Left + } else { + SplitSide::Right + } + } + + pub fn focused_editor(&self) -> &Entity { if let Some(lhs) = &self.lhs && lhs.was_last_focused { @@ -459,8 +471,10 @@ impl SplittableEditor { _ => cx.emit(event.clone()), }, ), - cx.subscribe(&rhs_editor, |_, _, event: &SearchEvent, cx| { - cx.emit(event.clone()); + cx.subscribe(&rhs_editor, |this, _, event: &SearchEvent, cx| { + if this.searched_side.is_none() || this.searched_side == Some(SplitSide::Right) { + cx.emit(event.clone()); + } }), ]; @@ -493,6 +507,7 @@ impl SplittableEditor { lhs: None, workspace: workspace.downgrade(), split_state, + searched_side: None, _subscriptions: subscriptions, } } @@ -596,13 +611,20 @@ impl SplittableEditor { }, )]; + subscriptions.push( + cx.subscribe(&lhs_editor, |this, _, event: &SearchEvent, cx| { + if this.searched_side == Some(SplitSide::Left) { + cx.emit(event.clone()); + } + }), + ); + let lhs_focus_handle = lhs_editor.read(cx).focus_handle(cx); subscriptions.push( cx.on_focus_in(&lhs_focus_handle, window, |this, _window, cx| { if let Some(lhs) = &mut this.lhs { if !lhs.was_last_focused { lhs.was_last_focused = true; - cx.emit(SearchEvent::MatchesInvalidated); cx.notify(); } } @@ -615,7 +637,6 @@ impl SplittableEditor { if let Some(lhs) = &mut this.lhs { if lhs.was_last_focused { lhs.was_last_focused = false; - cx.emit(SearchEvent::MatchesInvalidated); cx.notify(); } } @@ -1085,6 +1106,19 @@ impl SplittableEditor { }); } } + + fn search_token(&self) -> SearchToken { + SearchToken::new(self.focused_side() as u64) + } + + fn editor_for_token(&self, token: SearchToken) -> &Entity { + if token.value() == SplitSide::Left as u64 { + if let Some(lhs) = &self.lhs { + return &lhs.editor; + } + } + &self.rhs_editor + } } #[cfg(test)] @@ -1665,12 +1699,12 @@ impl Item for SplittableEditor { window: &mut Window, cx: &mut Context, ) -> bool { - self.last_selected_editor() + self.focused_editor() .update(cx, |editor, cx| editor.navigate(data, window, cx)) } fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { - self.last_selected_editor().update(cx, |editor, cx| { + self.focused_editor().update(cx, |editor, cx| { editor.deactivated(window, cx); }); } @@ -1709,9 +1743,7 @@ impl Item for SplittableEditor { } fn pixel_position_of_cursor(&self, cx: &App) -> Option> { - self.last_selected_editor() - .read(cx) - .pixel_position_of_cursor(cx) + self.focused_editor().read(cx).pixel_position_of_cursor(cx) } } @@ -1719,25 +1751,59 @@ impl SearchableItem for SplittableEditor { type Match = Range; fn clear_matches(&mut self, window: &mut Window, cx: &mut Context) { - self.last_selected_editor().update(cx, |editor, cx| { + self.rhs_editor.update(cx, |editor, cx| { editor.clear_matches(window, cx); }); + if let Some(lhs_editor) = self.lhs_editor() { + lhs_editor.update(cx, |editor, cx| { + editor.clear_matches(window, cx); + }) + } } fn update_matches( &mut self, matches: &[Self::Match], active_match_index: Option, + token: SearchToken, window: &mut Window, cx: &mut Context, ) { - self.last_selected_editor().update(cx, |editor, cx| { - editor.update_matches(matches, active_match_index, window, cx); + self.editor_for_token(token).update(cx, |editor, cx| { + editor.update_matches(matches, active_match_index, token, window, cx); }); } + fn search_bar_visibility_changed( + &mut self, + visible: bool, + window: &mut Window, + cx: &mut Context, + ) { + if visible { + let side = self.focused_side(); + self.searched_side = Some(side); + match side { + SplitSide::Left => { + self.rhs_editor.update(cx, |editor, cx| { + editor.clear_matches(window, cx); + }); + } + SplitSide::Right => { + if let Some(lhs) = &self.lhs { + lhs.editor.update(cx, |editor, cx| { + editor.clear_matches(window, cx); + }); + } + } + } + } else { + self.searched_side = None; + } + } + fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context) -> String { - self.last_selected_editor() + self.focused_editor() .update(cx, |editor, cx| editor.query_suggestion(window, cx)) } @@ -1745,22 +1811,24 @@ impl SearchableItem for SplittableEditor { &mut self, index: usize, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) { - self.last_selected_editor().update(cx, |editor, cx| { - editor.activate_match(index, matches, window, cx); + self.editor_for_token(token).update(cx, |editor, cx| { + editor.activate_match(index, matches, token, window, cx); }); } fn select_matches( &mut self, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) { - self.last_selected_editor().update(cx, |editor, cx| { - editor.select_matches(matches, window, cx); + self.editor_for_token(token).update(cx, |editor, cx| { + editor.select_matches(matches, token, window, cx); }); } @@ -1768,11 +1836,12 @@ impl SearchableItem for SplittableEditor { &mut self, identifier: &Self::Match, query: &project::search::SearchQuery, + token: SearchToken, window: &mut Window, cx: &mut Context, ) { - self.last_selected_editor().update(cx, |editor, cx| { - editor.replace(identifier, query, window, cx); + self.editor_for_token(token).update(cx, |editor, cx| { + editor.replace(identifier, query, token, window, cx); }); } @@ -1782,19 +1851,41 @@ impl SearchableItem for SplittableEditor { window: &mut Window, cx: &mut Context, ) -> gpui::Task> { - self.last_selected_editor() + self.focused_editor() .update(cx, |editor, cx| editor.find_matches(query, window, cx)) } + fn find_matches_with_token( + &mut self, + query: Arc, + window: &mut Window, + cx: &mut Context, + ) -> gpui::Task<(Vec, SearchToken)> { + let token = self.search_token(); + let editor = self.focused_editor().downgrade(); + cx.spawn_in(window, async move |_, cx| { + let Some(matches) = editor + .update_in(cx, |editor, window, cx| { + editor.find_matches(query, window, cx) + }) + .ok() + else { + return (Vec::new(), token); + }; + (matches.await, token) + }) + } + fn active_match_index( &mut self, direction: workspace::searchable::Direction, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) -> Option { - self.last_selected_editor().update(cx, |editor, cx| { - editor.active_match_index(direction, matches, window, cx) + self.editor_for_token(token).update(cx, |editor, cx| { + editor.active_match_index(direction, matches, token, window, cx) }) } } @@ -1803,7 +1894,7 @@ impl EventEmitter for SplittableEditor {} impl EventEmitter for SplittableEditor {} impl Focusable for SplittableEditor { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.last_selected_editor().read(cx).focus_handle(cx) + self.focused_editor().read(cx).focus_handle(cx) } } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index ccda40c98a50c05907534e2f853f7678cb52fb9e..f64931491d9ce5cbc4ed8261edbc12af4b628b69 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -450,7 +450,7 @@ impl ProjectDiff { } pub fn active_path(&self, cx: &App) -> Option { - let editor = self.editor.read(cx).last_selected_editor().read(cx); + let editor = self.editor.read(cx).focused_editor().read(cx); let position = editor.selections.newest_anchor().head(); let multi_buffer = editor.buffer().read(cx); let (_, buffer, _) = multi_buffer.excerpt_containing(position, cx)?; diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index e4ed650464ce5272356b031df362290289981f5b..23e8db974f4625ed3c8d4109c6f79731ab2989ff 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -23,7 +23,7 @@ use util::ResultExt as _; use workspace::{ SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, item::{Item, ItemHandle}, - searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, + searchable::{Direction, SearchEvent, SearchToken, SearchableItem, SearchableItemHandle}, }; use crate::get_or_create_tool; @@ -813,11 +813,12 @@ impl SearchableItem for LspLogView { &mut self, matches: &[Self::Match], active_match_index: Option, + token: SearchToken, window: &mut Window, cx: &mut Context, ) { self.editor.update(cx, |e, cx| { - e.update_matches(matches, active_match_index, window, cx) + e.update_matches(matches, active_match_index, token, window, cx) }) } @@ -830,21 +831,24 @@ impl SearchableItem for LspLogView { &mut self, index: usize, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) { - self.editor - .update(cx, |e, cx| e.activate_match(index, matches, window, cx)) + self.editor.update(cx, |e, cx| { + e.activate_match(index, matches, token, window, cx) + }) } fn select_matches( &mut self, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) { self.editor - .update(cx, |e, cx| e.select_matches(matches, window, cx)) + .update(cx, |e, cx| e.select_matches(matches, token, window, cx)) } fn find_matches( @@ -861,6 +865,7 @@ impl SearchableItem for LspLogView { &mut self, _: &Self::Match, _: &SearchQuery, + _token: SearchToken, _window: &mut Window, _: &mut Context, ) { @@ -881,11 +886,12 @@ impl SearchableItem for LspLogView { &mut self, direction: Direction, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) -> Option { self.editor.update(cx, |e, cx| { - e.active_match_index(direction, matches, window, cx) + e.active_match_index(direction, matches, token, window, cx) }) } } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index d497c16336d9453bdcfed796251147f878041469..638f2381836786474e691830aea1d003db2d49df 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4245,7 +4245,7 @@ impl OutlinePanel { let buffer_search_matches = self .active_editor() .map(|active_editor| { - active_editor.update(cx, |editor, cx| editor.get_matches(window, cx)) + active_editor.update(cx, |editor, cx| editor.get_matches(window, cx).0) }) .unwrap_or_default(); diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 02034a7bb7f9e28e6fb6cb0fee25893b9866bca7..def14d9660fb367440bc7e54e675cfc4bfff0fff 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -40,8 +40,8 @@ use workspace::{ ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::{ItemBufferKind, ItemHandle}, searchable::{ - CollapseDirection, Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle, - WeakSearchableItemHandle, + CollapseDirection, Direction, FilteredSearchRange, SearchEvent, SearchToken, + SearchableItemHandle, WeakSearchableItemHandle, }, }; @@ -76,7 +76,8 @@ pub struct BufferSearchBar { #[cfg(target_os = "macos")] pending_external_query: Option<(String, SearchOptions)>, active_search: Option>, - searchable_items_with_matches: HashMap, AnyVec>, + searchable_items_with_matches: + HashMap, (AnyVec, SearchToken)>, pending_search: Option>, search_options: SearchOptions, default_options: SearchOptions, @@ -233,7 +234,7 @@ impl Render for BufferSearchBar { let matches_count = self .searchable_items_with_matches .get(&searchable_item.downgrade()) - .map(AnyVec::len) + .map(|(matches, _)| matches.len()) .unwrap_or(0); if let Some(match_ix) = self.active_match_index { Some(format!("{}/{}", match_ix + 1, matches_count)) @@ -1041,11 +1042,11 @@ impl BufferSearchBar { pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context) { if let Some(match_ix) = self.active_match_index && let Some(active_searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self + && let Some((matches, token)) = self .searchable_items_with_matches .get(&active_searchable_item.downgrade()) { - active_searchable_item.activate_match(match_ix, matches, window, cx) + active_searchable_item.activate_match(match_ix, matches, *token, window, cx) } } @@ -1227,11 +1228,11 @@ impl BufferSearchBar { if !self.dismissed && self.active_match_index.is_some() && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self + && let Some((matches, token)) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) { - searchable_item.select_matches(matches, window, cx); + searchable_item.select_matches(matches, *token, window, cx); self.focus_editor(&FocusEditor, window, cx); } } @@ -1261,10 +1262,10 @@ impl BufferSearchBar { if let Some(index) = self.active_match_index && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self + && let Some((matches, token)) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) - .filter(|matches| !matches.is_empty()) + .filter(|(matches, _)| !matches.is_empty()) { // If 'wrapscan' is disabled, searches do not wrap around the end of the file. if !EditorSettings::get_global(cx).search_wrap @@ -1275,30 +1276,30 @@ impl BufferSearchBar { return; } let new_match_index = searchable_item - .match_index_for_direction(matches, index, direction, count, window, cx); + .match_index_for_direction(matches, index, direction, count, *token, window, cx); - searchable_item.update_matches(matches, Some(new_match_index), window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); + searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx); + searchable_item.activate_match(new_match_index, matches, *token, window, cx); } } pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context) { if let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self + && let Some((matches, token)) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) { if matches.is_empty() { return; } - searchable_item.update_matches(matches, Some(0), window, cx); - searchable_item.activate_match(0, matches, window, cx); + searchable_item.update_matches(matches, Some(0), *token, window, cx); + searchable_item.activate_match(0, matches, *token, window, cx); } } pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context) { if let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self + && let Some((matches, token)) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) { @@ -1306,8 +1307,8 @@ impl BufferSearchBar { return; } let new_match_index = matches.len() - 1; - searchable_item.update_matches(matches, Some(new_match_index), window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); + searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx); + searchable_item.activate_match(new_match_index, matches, *token, window, cx); } } @@ -1532,18 +1533,19 @@ impl BufferSearchBar { self.active_search = Some(query.clone()); let query_text = query.as_str().to_string(); - let matches = active_searchable_item.find_matches(query, window, cx); + let matches_with_token = + active_searchable_item.find_matches_with_token(query, window, cx); let active_searchable_item = active_searchable_item.downgrade(); self.pending_search = Some(cx.spawn_in(window, async move |this, cx| { - let matches = matches.await; + let (matches, token) = matches_with_token.await; this.update_in(cx, |this, window, cx| { if let Some(active_searchable_item) = WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) { this.searchable_items_with_matches - .insert(active_searchable_item.downgrade(), matches); + .insert(active_searchable_item.downgrade(), (matches, token)); this.update_match_index(window, cx); @@ -1552,7 +1554,7 @@ impl BufferSearchBar { .add(&mut this.search_history_cursor, query_text); } if !this.dismissed { - let matches = this + let (matches, token) = this .searchable_items_with_matches .get(&active_searchable_item.downgrade()) .unwrap(); @@ -1562,6 +1564,7 @@ impl BufferSearchBar { active_searchable_item.update_matches( matches, this.active_match_index, + *token, window, cx, ); @@ -1592,21 +1595,21 @@ impl BufferSearchBar { .active_searchable_item .as_ref() .and_then(|searchable_item| { - let matches = self + let (matches, token) = self .searchable_items_with_matches .get(&searchable_item.downgrade())?; - searchable_item.active_match_index(direction, matches, window, cx) + searchable_item.active_match_index(direction, matches, *token, window, cx) }); if new_index != self.active_match_index { self.active_match_index = new_index; if !self.dismissed { if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self + if let Some((matches, token)) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) { if !matches.is_empty() { - searchable_item.update_matches(matches, new_index, window, cx); + searchable_item.update_matches(matches, new_index, *token, window, cx); } } } @@ -1712,7 +1715,7 @@ impl BufferSearchBar { && self.active_search.is_some() && let Some(searchable_item) = self.active_searchable_item.as_ref() && let Some(query) = self.active_search.as_ref() - && let Some(matches) = self + && let Some((matches, token)) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) { @@ -1721,7 +1724,7 @@ impl BufferSearchBar { .as_ref() .clone() .with_replacement(self.replacement(cx)); - searchable_item.replace(matches.at(active_index), &query, window, cx); + searchable_item.replace(matches.at(active_index), &query, *token, window, cx); self.select_next_match(&SelectNextMatch, window, cx); } should_propagate = false; @@ -1736,7 +1739,7 @@ impl BufferSearchBar { && self.active_search.is_some() && let Some(searchable_item) = self.active_searchable_item.as_ref() && let Some(query) = self.active_search.as_ref() - && let Some(matches) = self + && let Some((matches, token)) = self .searchable_items_with_matches .get(&searchable_item.downgrade()) { @@ -1744,7 +1747,7 @@ impl BufferSearchBar { .as_ref() .clone() .with_replacement(self.replacement(cx)); - searchable_item.replace_all(&mut matches.iter(), &query, window, cx); + searchable_item.replace_all(&mut matches.iter(), &query, *token, window, cx); } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 45b72ed27f3efe6272987755df74d7e1aaadf911..5c8795a3c429a1fea0b862fe1e604e101d3918be 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -49,7 +49,10 @@ use workspace::{ DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, item::{Item, ItemEvent, ItemHandle, SaveOptions}, - searchable::{CollapseDirection, Direction, SearchEvent, SearchableItem, SearchableItemHandle}, + searchable::{ + CollapseDirection, Direction, SearchEvent, SearchToken, SearchableItem, + SearchableItemHandle, + }, }; actions!( @@ -731,7 +734,7 @@ impl ProjectSearchView { let mat = self.entity.read(cx).match_ranges.get(active_index).cloned(); self.results_editor.update(cx, |editor, cx| { if let Some(mat) = mat.as_ref() { - editor.replace(mat, &query, window, cx); + editor.replace(mat, &query, SearchToken::default(), window, cx); } }); self.select_match(Direction::Next, window, cx) @@ -761,7 +764,13 @@ impl ProjectSearchView { } self.results_editor.update(cx, |editor, cx| { - editor.replace_all(&mut match_ranges.iter(), &query, window, cx); + editor.replace_all( + &mut match_ranges.iter(), + &query, + SearchToken::default(), + window, + cx, + ); }); self.entity.update(cx, |model, _cx| { @@ -1394,7 +1403,15 @@ impl ProjectSearchView { } let new_index = self.results_editor.update(cx, |editor, cx| { - editor.match_index_for_direction(&match_ranges, index, direction, 1, window, cx) + editor.match_index_for_direction( + &match_ranges, + index, + direction, + 1, + SearchToken::default(), + window, + cx, + ) }); let range_to_select = match_ranges[new_index].clone(); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index b3598197eeff1ad17cf15450379eef5651e4952b..50f9ac03ddde19566c60cb10b4624af88f6e422b 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -56,7 +56,9 @@ use workspace::{ BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent, }, register_serializable_item, - searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, + searchable::{ + Direction, SearchEvent, SearchOptions, SearchToken, SearchableItem, SearchableItemHandle, + }, }; use zed_actions::{agent::AddSelectionToThread, assistant::InlineAssist}; @@ -1664,6 +1666,7 @@ impl SearchableItem for TerminalView { &mut self, matches: &[Self::Match], _active_match_index: Option, + _token: SearchToken, _window: &mut Window, cx: &mut Context, ) { @@ -1686,6 +1689,7 @@ impl SearchableItem for TerminalView { &mut self, index: usize, _: &[Self::Match], + _token: SearchToken, _window: &mut Window, cx: &mut Context, ) { @@ -1695,7 +1699,13 @@ impl SearchableItem for TerminalView { } /// Add selections for all matches given. - fn select_matches(&mut self, matches: &[Self::Match], _: &mut Window, cx: &mut Context) { + fn select_matches( + &mut self, + matches: &[Self::Match], + _token: SearchToken, + _: &mut Window, + cx: &mut Context, + ) { self.terminal() .update(cx, |term, _| term.select_matches(matches)); cx.notify(); @@ -1721,6 +1731,7 @@ impl SearchableItem for TerminalView { &mut self, direction: Direction, matches: &[Self::Match], + _token: SearchToken, _: &mut Window, cx: &mut Context, ) -> Option { @@ -1774,6 +1785,7 @@ impl SearchableItem for TerminalView { &mut self, _: &Self::Match, _: &SearchQuery, + _token: SearchToken, _window: &mut Window, _: &mut Context, ) { diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 06fc02d6d5368651d8bfa533a7f901e3cc977631..6c9d8edeb487da17314ef033da37fd7caa51d650 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -12,6 +12,19 @@ use crate::{ item::{Item, WeakItemHandle}, }; +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct SearchToken(u64); + +impl SearchToken { + pub fn new(value: u64) -> Self { + Self(value) + } + + pub fn value(&self) -> u64 { + self.0 + } +} + #[derive(Clone, Debug)] pub enum CollapseDirection { Collapsed, @@ -96,14 +109,15 @@ pub trait SearchableItem: Item + EventEmitter { ) { } - fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec { - Vec::new() + fn get_matches(&self, _window: &mut Window, _: &mut App) -> (Vec, SearchToken) { + (Vec::new(), SearchToken::default()) } fn clear_matches(&mut self, window: &mut Window, cx: &mut Context); fn update_matches( &mut self, matches: &[Self::Match], active_match_index: Option, + token: SearchToken, window: &mut Window, cx: &mut Context, ); @@ -112,12 +126,14 @@ pub trait SearchableItem: Item + EventEmitter { &mut self, index: usize, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ); fn select_matches( &mut self, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ); @@ -125,6 +141,7 @@ pub trait SearchableItem: Item + EventEmitter { &mut self, _: &Self::Match, _: &SearchQuery, + _token: SearchToken, _window: &mut Window, _: &mut Context, ); @@ -132,11 +149,12 @@ pub trait SearchableItem: Item + EventEmitter { &mut self, matches: &mut dyn Iterator, query: &SearchQuery, + token: SearchToken, window: &mut Window, cx: &mut Context, ) { for item in matches { - self.replace(item, query, window, cx); + self.replace(item, query, token, window, cx); } } fn match_index_for_direction( @@ -145,6 +163,7 @@ pub trait SearchableItem: Item + EventEmitter { current_index: usize, direction: Direction, count: usize, + _token: SearchToken, _window: &mut Window, _: &mut Context, ) -> usize { @@ -166,10 +185,22 @@ pub trait SearchableItem: Item + EventEmitter { window: &mut Window, cx: &mut Context, ) -> Task>; + + fn find_matches_with_token( + &mut self, + query: Arc, + window: &mut Window, + cx: &mut Context, + ) -> Task<(Vec, SearchToken)> { + let matches = self.find_matches(query, window, cx); + cx.spawn(async move |_, _| (matches.await, SearchToken::default())) + } + fn active_match_index( &mut self, direction: Direction, matches: &[Self::Match], + token: SearchToken, window: &mut Window, cx: &mut Context, ) -> Option; @@ -191,6 +222,7 @@ pub trait SearchableItemHandle: ItemHandle { &self, matches: &AnyVec, active_match_index: Option, + token: SearchToken, window: &mut Window, cx: &mut App, ); @@ -199,14 +231,22 @@ pub trait SearchableItemHandle: ItemHandle { &self, index: usize, matches: &AnyVec, + token: SearchToken, + window: &mut Window, + cx: &mut App, + ); + fn select_matches( + &self, + matches: &AnyVec, + token: SearchToken, window: &mut Window, cx: &mut App, ); - fn select_matches(&self, matches: &AnyVec, window: &mut Window, cx: &mut App); fn replace( &self, _: any_vec::element::ElementRef<'_, dyn Send>, _: &SearchQuery, + token: SearchToken, _window: &mut Window, _: &mut App, ); @@ -214,6 +254,7 @@ pub trait SearchableItemHandle: ItemHandle { &self, matches: &mut dyn Iterator>, query: &SearchQuery, + token: SearchToken, window: &mut Window, cx: &mut App, ); @@ -223,6 +264,7 @@ pub trait SearchableItemHandle: ItemHandle { current_index: usize, direction: Direction, count: usize, + token: SearchToken, window: &mut Window, cx: &mut App, ) -> usize; @@ -232,10 +274,17 @@ pub trait SearchableItemHandle: ItemHandle { window: &mut Window, cx: &mut App, ) -> Task>; + fn find_matches_with_token( + &self, + query: Arc, + window: &mut Window, + cx: &mut App, + ) -> Task<(AnyVec, SearchToken)>; fn active_match_index( &self, direction: Direction, matches: &AnyVec, + token: SearchToken, window: &mut Window, cx: &mut App, ) -> Option; @@ -282,12 +331,13 @@ impl SearchableItemHandle for Entity { &self, matches: &AnyVec, active_match_index: Option, + token: SearchToken, window: &mut Window, cx: &mut App, ) { let matches = matches.downcast_ref().unwrap(); self.update(cx, |this, cx| { - this.update_matches(matches.as_slice(), active_match_index, window, cx) + this.update_matches(matches.as_slice(), active_match_index, token, window, cx) }); } fn query_suggestion(&self, window: &mut Window, cx: &mut App) -> String { @@ -297,19 +347,26 @@ impl SearchableItemHandle for Entity { &self, index: usize, matches: &AnyVec, + token: SearchToken, window: &mut Window, cx: &mut App, ) { let matches = matches.downcast_ref().unwrap(); self.update(cx, |this, cx| { - this.activate_match(index, matches.as_slice(), window, cx) + this.activate_match(index, matches.as_slice(), token, window, cx) }); } - fn select_matches(&self, matches: &AnyVec, window: &mut Window, cx: &mut App) { + fn select_matches( + &self, + matches: &AnyVec, + token: SearchToken, + window: &mut Window, + cx: &mut App, + ) { let matches = matches.downcast_ref().unwrap(); self.update(cx, |this, cx| { - this.select_matches(matches.as_slice(), window, cx) + this.select_matches(matches.as_slice(), token, window, cx) }); } @@ -319,6 +376,7 @@ impl SearchableItemHandle for Entity { current_index: usize, direction: Direction, count: usize, + token: SearchToken, window: &mut Window, cx: &mut App, ) -> usize { @@ -329,6 +387,7 @@ impl SearchableItemHandle for Entity { current_index, direction, count, + token, window, cx, ) @@ -353,16 +412,38 @@ impl SearchableItemHandle for Entity { any_matches }) } + fn find_matches_with_token( + &self, + query: Arc, + window: &mut Window, + cx: &mut App, + ) -> Task<(AnyVec, SearchToken)> { + let matches_with_token = self.update(cx, |this, cx| { + this.find_matches_with_token(query, window, cx) + }); + window.spawn(cx, async |_| { + let (matches, token) = matches_with_token.await; + let mut any_matches = AnyVec::with_capacity::(matches.len()); + { + let mut any_matches = any_matches.downcast_mut::().unwrap(); + for mat in matches { + any_matches.push(mat); + } + } + (any_matches, token) + }) + } fn active_match_index( &self, direction: Direction, matches: &AnyVec, + token: SearchToken, window: &mut Window, cx: &mut App, ) -> Option { let matches = matches.downcast_ref()?; self.update(cx, |this, cx| { - this.active_match_index(direction, matches.as_slice(), window, cx) + this.active_match_index(direction, matches.as_slice(), token, window, cx) }) } @@ -370,17 +451,19 @@ impl SearchableItemHandle for Entity { &self, mat: any_vec::element::ElementRef<'_, dyn Send>, query: &SearchQuery, + token: SearchToken, window: &mut Window, cx: &mut App, ) { let mat = mat.downcast_ref().unwrap(); - self.update(cx, |this, cx| this.replace(mat, query, window, cx)) + self.update(cx, |this, cx| this.replace(mat, query, token, window, cx)) } fn replace_all( &self, matches: &mut dyn Iterator>, query: &SearchQuery, + token: SearchToken, window: &mut Window, cx: &mut App, ) { @@ -388,6 +471,7 @@ impl SearchableItemHandle for Entity { this.replace_all( &mut matches.map(|m| m.downcast_ref().unwrap()), query, + token, window, cx, );