From 29cbeb39bddd67b8846da75241595f809ff2c767 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 13 Jul 2023 14:29:02 +0300 Subject: [PATCH 1/6] Allow selecting all search matches in buffer --- assets/keymaps/default.json | 4 +- crates/editor/src/items.rs | 5 + crates/editor/src/selections_collection.rs | 4 +- crates/feedback/src/feedback_editor.rs | 5 + crates/language_tools/src/lsp_log.rs | 5 + crates/search/src/buffer_search.rs | 106 ++++++++++++++++++--- crates/search/src/search.rs | 3 +- crates/terminal/src/terminal.rs | 15 +++ crates/terminal_view/src/terminal_view.rs | 7 ++ crates/workspace/src/searchable.rs | 8 ++ 10 files changed, 146 insertions(+), 16 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 4726c67aea3f7836febece7546430660d57672ae..006719e5f5ac4b78df1e376e613d1f43e918d164 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -221,7 +221,8 @@ "escape": "buffer_search::Dismiss", "tab": "buffer_search::FocusEditor", "enter": "search::SelectNextMatch", - "shift-enter": "search::SelectPrevMatch" + "shift-enter": "search::SelectPrevMatch", + "cmd-shift-k": "search::CaretsToAllMatches" } }, { @@ -242,6 +243,7 @@ "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPrevMatch", + "cmd-shift-k": "search::CaretsToAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", "alt-cmd-r": "search::ToggleRegex" diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 431ccf0bfe44f2c1370fd07e8f88962fc82b44da..cc24cd35da2b3e5eb05f35e5cfec2de2866a6afb 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -941,6 +941,11 @@ impl SearchableItem for Editor { }); } + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.unfold_ranges(matches.clone(), false, false, cx); + self.change_selections(None, cx, |s| s.select_ranges(matches)); + } + fn match_index_for_direction( &mut self, matches: &Vec>, diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index d82ce5e21637fde58d1e19c866befe53270f2e9f..a22506f751b1ab36d518cdc13f60dc61dfecf6c7 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -16,13 +16,13 @@ use crate::{ Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset, }; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct PendingSelection { pub selection: Selection, pub mode: SelectMode, } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct SelectionsCollection { display_map: ModelHandle, buffer: ModelHandle, diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index 5a4f912e3a063068adba31d787a21d1df42368b2..663164dd0770ebf0253c99a86ac84b984a450ae4 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -391,6 +391,11 @@ impl SearchableItem for FeedbackEditor { .update(cx, |editor, cx| editor.activate_match(index, matches, cx)) } + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.select_matches(matches, cx)) + } + fn find_matches( &mut self, query: project::search::SearchQuery, diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 12d8c6b34d3abd93e25e1b18e03656de477d417a..b27349f412e59e4f36b7dd21071cfad2de7f42e4 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -494,6 +494,11 @@ impl SearchableItem for LspLogView { .update(cx, |e, cx| e.activate_match(index, matches, cx)) } + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.select_matches(matches, cx)) + } + fn find_matches( &mut self, query: project::search::SearchQuery, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 59d25c2659f5ebbcad2b0a7ca4825c8a5bbf0d37..22778f85e81bb4468fb3cfc772ed3c598221b9da 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,6 @@ use crate::{ - SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, - ToggleWholeWord, + CaretsToAllMatches, SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, + ToggleRegex, ToggleWholeWord, }; use collections::HashMap; use editor::Editor; @@ -39,8 +39,10 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::focus_editor); cx.add_action(BufferSearchBar::select_next_match); cx.add_action(BufferSearchBar::select_prev_match); + cx.add_action(BufferSearchBar::carets_to_all_matches); cx.add_action(BufferSearchBar::select_next_match_on_pane); cx.add_action(BufferSearchBar::select_prev_match_on_pane); + cx.add_action(BufferSearchBar::carets_to_all_matches_on_pane); cx.add_action(BufferSearchBar::handle_editor_cancel); add_toggle_option_action::(SearchOption::CaseSensitive, cx); add_toggle_option_action::(SearchOption::WholeWord, cx); @@ -66,7 +68,7 @@ pub struct BufferSearchBar { active_searchable_item: Option>, active_match_index: Option, active_searchable_item_subscription: Option, - seachable_items_with_matches: + searchable_items_with_matches: HashMap, Vec>>, pending_search: Option>, case_sensitive: bool, @@ -118,7 +120,7 @@ impl View for BufferSearchBar { .with_children(self.active_searchable_item.as_ref().and_then( |searchable_item| { let matches = self - .seachable_items_with_matches + .searchable_items_with_matches .get(&searchable_item.downgrade())?; let message = if let Some(match_ix) = self.active_match_index { format!("{}/{}", match_ix + 1, matches.len()) @@ -249,7 +251,7 @@ impl BufferSearchBar { active_searchable_item: None, active_searchable_item_subscription: None, active_match_index: None, - seachable_items_with_matches: Default::default(), + searchable_items_with_matches: Default::default(), case_sensitive: false, whole_word: false, regex: false, @@ -265,7 +267,7 @@ impl BufferSearchBar { pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { self.dismissed = true; - for searchable_item in self.seachable_items_with_matches.keys() { + for searchable_item in self.searchable_items_with_matches.keys() { if let Some(searchable_item) = WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) { @@ -488,11 +490,25 @@ impl BufferSearchBar { self.select_match(Direction::Prev, cx); } + fn carets_to_all_matches(&mut self, _: &CaretsToAllMatches, cx: &mut ViewContext) { + if !self.dismissed { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + searchable_item.select_matches(matches, cx); + self.focus_editor(&FocusEditor, cx); + } + } + } + } + pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(matches) = self - .seachable_items_with_matches + .searchable_items_with_matches .get(&searchable_item.downgrade()) { let new_match_index = @@ -524,6 +540,16 @@ impl BufferSearchBar { } } + fn carets_to_all_matches_on_pane( + pane: &mut Pane, + action: &CaretsToAllMatches, + cx: &mut ViewContext, + ) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.carets_to_all_matches(action, cx)); + } + } + fn on_query_editor_event( &mut self, _: ViewHandle, @@ -547,7 +573,7 @@ impl BufferSearchBar { fn clear_matches(&mut self, cx: &mut ViewContext) { let mut active_item_matches = None; - for (searchable_item, matches) in self.seachable_items_with_matches.drain() { + for (searchable_item, matches) in self.searchable_items_with_matches.drain() { if let Some(searchable_item) = WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) { @@ -559,7 +585,7 @@ impl BufferSearchBar { } } - self.seachable_items_with_matches + self.searchable_items_with_matches .extend(active_item_matches); } @@ -605,13 +631,13 @@ impl BufferSearchBar { if let Some(active_searchable_item) = WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) { - this.seachable_items_with_matches + this.searchable_items_with_matches .insert(active_searchable_item.downgrade(), matches); this.update_match_index(cx); if !this.dismissed { let matches = this - .seachable_items_with_matches + .searchable_items_with_matches .get(&active_searchable_item.downgrade()) .unwrap(); active_searchable_item.update_matches(matches, cx); @@ -637,7 +663,7 @@ impl BufferSearchBar { .as_ref() .and_then(|searchable_item| { let matches = self - .seachable_items_with_matches + .searchable_items_with_matches .get(&searchable_item.downgrade())?; searchable_item.active_match_index(matches, cx) }); @@ -966,4 +992,60 @@ mod tests { assert_eq!(search_bar.active_match_index, Some(2)); }); } + + #[gpui::test] + async fn test_search_carets_to_all_matches(cx: &mut TestAppContext) { + crate::project_search::tests::init_test(cx); + + let buffer_text = r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(); + let expected_query_matches_count = buffer_text + .chars() + .filter(|c| c.to_ascii_lowercase() == 'a') + .count(); + assert!( + expected_query_matches_count > 1, + "Should pick a query with multiple results" + ); + let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let (window_id, _root_view) = cx.add_window(|_| EmptyView); + + let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = cx.add_view(window_id, |cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(false, true, cx); + search_bar + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.set_query("a", cx); + }); + + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + let initial_selections = editor.selections.display_ranges(cx); + assert_eq!( + initial_selections.len(), 1, + "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", + ) + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.carets_to_all_matches(&CaretsToAllMatches, cx); + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + }); + } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 90ea508cc61ad59b64606bede16c35882452d2b7..da679d191ee06fc5a5691a5def621397507531f3 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -17,7 +17,8 @@ actions!( ToggleCaseSensitive, ToggleRegex, SelectNextMatch, - SelectPrevMatch + SelectPrevMatch, + CaretsToAllMatches, ] ); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index d14118bb186d9631958e01bfac74d56d52052f25..39e77b590b1077fd9cc917d2538ec0e563df453a 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -908,6 +908,21 @@ impl Terminal { } } + pub fn select_matches(&mut self, matches: Vec>) { + let matches_to_select = self + .matches + .iter() + .filter(|self_match| matches.contains(self_match)) + .cloned() + .collect::>(); + for match_to_select in matches_to_select { + self.set_selection(Some(( + make_selection(&match_to_select), + *match_to_select.end(), + ))); + } + } + fn set_selection(&mut self, selection: Option<(Selection, Point)>) { self.events .push_back(InternalEvent::SetSelection(selection)); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 36be6bee7f0460e0f64fca13784dc91db612c8b1..8e1e4ad62f0abe5b48d094b93612e4ed4f19c62c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -682,6 +682,13 @@ impl SearchableItem for TerminalView { cx.notify(); } + /// Add selections for all matches given. + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.terminal() + .update(cx, |term, _| term.select_matches(matches)); + cx.notify(); + } + /// Get all of the matches for this query, should be done on the background fn find_matches( &mut self, diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 7e3f7227b022f72bec64af3c765ab2b79cdafb49..4ebfe69c2181b74b555e6a799ad2c46effd8d7b9 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -47,6 +47,7 @@ pub trait SearchableItem: Item { matches: Vec, cx: &mut ViewContext, ); + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext); fn match_index_for_direction( &mut self, matches: &Vec, @@ -102,6 +103,7 @@ pub trait SearchableItemHandle: ItemHandle { matches: &Vec>, cx: &mut WindowContext, ); + fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext); fn match_index_for_direction( &self, matches: &Vec>, @@ -165,6 +167,12 @@ impl SearchableItemHandle for ViewHandle { let matches = downcast_matches(matches); self.update(cx, |this, cx| this.activate_match(index, matches, cx)); } + + fn select_matches(&self, matches: &Vec>, cx: &mut WindowContext) { + let matches = downcast_matches(matches); + self.update(cx, |this, cx| this.select_matches(matches, cx)); + } + fn match_index_for_direction( &self, matches: &Vec>, From 2053418f21b48c52f4b29be22a6c562398021ac3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 13 Jul 2023 14:45:42 +0300 Subject: [PATCH 2/6] Use VSCode-like shortcuts by default --- assets/keymaps/default.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 006719e5f5ac4b78df1e376e613d1f43e918d164..611d3633e39840e630ea3a916d036a294dc3968b 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -194,8 +194,8 @@ { "context": "Editor && mode == auto_height", "bindings": { - "alt-enter": "editor::Newline", - "cmd-alt-enter": "editor::NewlineBelow" + "shift-enter": "editor::Newline", + "cmd-shift-enter": "editor::NewlineBelow" } }, { @@ -222,7 +222,7 @@ "tab": "buffer_search::FocusEditor", "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPrevMatch", - "cmd-shift-k": "search::CaretsToAllMatches" + "alt-enter": "search::CaretsToAllMatches" } }, { @@ -243,7 +243,7 @@ "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPrevMatch", - "cmd-shift-k": "search::CaretsToAllMatches", + "alt-enter": "search::CaretsToAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", "alt-cmd-r": "search::ToggleRegex" From f710efca3bceef3b3373987274929b718d8c7749 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 13 Jul 2023 15:39:00 +0300 Subject: [PATCH 3/6] Use a better name --- assets/keymaps/default.json | 4 ++-- crates/search/src/buffer_search.rs | 18 +++++++++--------- crates/search/src/search.rs | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 611d3633e39840e630ea3a916d036a294dc3968b..c044f4b6004f85a43218f4944dff6d4959fb9f96 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -222,7 +222,7 @@ "tab": "buffer_search::FocusEditor", "enter": "search::SelectNextMatch", "shift-enter": "search::SelectPrevMatch", - "alt-enter": "search::CaretsToAllMatches" + "alt-enter": "search::SelectAllMatches" } }, { @@ -243,7 +243,7 @@ "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPrevMatch", - "alt-enter": "search::CaretsToAllMatches", + "alt-enter": "search::SelectAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", "alt-cmd-r": "search::ToggleRegex" diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 22778f85e81bb4468fb3cfc772ed3c598221b9da..a87587a92f2b3992cdf9a83ab23bffd2f1d6f6da 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,5 +1,5 @@ use crate::{ - CaretsToAllMatches, SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, + SearchOption, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; use collections::HashMap; @@ -39,10 +39,10 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::focus_editor); cx.add_action(BufferSearchBar::select_next_match); cx.add_action(BufferSearchBar::select_prev_match); - cx.add_action(BufferSearchBar::carets_to_all_matches); + cx.add_action(BufferSearchBar::select_all_matches); cx.add_action(BufferSearchBar::select_next_match_on_pane); cx.add_action(BufferSearchBar::select_prev_match_on_pane); - cx.add_action(BufferSearchBar::carets_to_all_matches_on_pane); + cx.add_action(BufferSearchBar::select_all_matches_on_pane); cx.add_action(BufferSearchBar::handle_editor_cancel); add_toggle_option_action::(SearchOption::CaseSensitive, cx); add_toggle_option_action::(SearchOption::WholeWord, cx); @@ -490,7 +490,7 @@ impl BufferSearchBar { self.select_match(Direction::Prev, cx); } - fn carets_to_all_matches(&mut self, _: &CaretsToAllMatches, cx: &mut ViewContext) { + fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext) { if !self.dismissed { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(matches) = self @@ -540,13 +540,13 @@ impl BufferSearchBar { } } - fn carets_to_all_matches_on_pane( + fn select_all_matches_on_pane( pane: &mut Pane, - action: &CaretsToAllMatches, + action: &SelectAllMatches, cx: &mut ViewContext, ) { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.carets_to_all_matches(action, cx)); + search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx)); } } @@ -994,7 +994,7 @@ mod tests { } #[gpui::test] - async fn test_search_carets_to_all_matches(cx: &mut TestAppContext) { + async fn test_search_select_all_matches(cx: &mut TestAppContext) { crate::project_search::tests::init_test(cx); let buffer_text = r#" @@ -1038,7 +1038,7 @@ mod tests { }); search_bar.update(cx, |search_bar, cx| { - search_bar.carets_to_all_matches(&CaretsToAllMatches, cx); + search_bar.select_all_matches(&SelectAllMatches, cx); let all_selections = editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); assert_eq!( diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index da679d191ee06fc5a5691a5def621397507531f3..7080b4c07e26496fb0c5f2059fd058a031163cb0 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -18,7 +18,7 @@ actions!( ToggleRegex, SelectNextMatch, SelectPrevMatch, - CaretsToAllMatches, + SelectAllMatches, ] ); From c130dd6b47c024ec9fa9b2b7a750010744d998ac Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 13 Jul 2023 09:48:27 -0400 Subject: [PATCH 4/6] Add styles for an `action_button` ahead of the "Select all matches" UI button --- styles/src/style_tree/search.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 5c16d03233ee7af3f22fb3c67bf9b5b4a69f524d..28940a836756a8e0e4492e8fac432e9ec493d5e3 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -83,6 +83,35 @@ export default function search(): any { }, }, }), + action_button: interactive({ + base: { + ...text(theme.highest, "mono", "on"), + background: background(theme.highest, "on"), + corner_radius: 6, + border: border(theme.highest, "on"), + margin: { + right: 4, + }, + padding: { + bottom: 2, + left: 10, + right: 10, + top: 2, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + }, + }), editor, invalid_editor: { ...editor, From ccc78000bd796105b2d9cd2d7fa69b7323fefcf4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 14 Jul 2023 14:33:26 +0300 Subject: [PATCH 5/6] Preserve serach index for multicaret selection editor events --- crates/editor/src/items.rs | 32 +++++++-- crates/feedback/src/feedback_editor.rs | 9 ++- crates/language_tools/src/lsp_log.rs | 9 ++- crates/search/src/buffer_search.rs | 79 ++++++++++++++++++++++- crates/terminal_view/src/terminal_view.rs | 6 +- crates/workspace/src/searchable.rs | 11 +++- 6 files changed, 130 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index cc24cd35da2b3e5eb05f35e5cfec2de2866a6afb..9498be184480f4b740ff15a1b6c2c08210d0aad1 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -887,10 +887,20 @@ pub(crate) enum BufferSearchHighlights {} impl SearchableItem for Editor { type Match = Range; - fn to_search_event(event: &Self::Event) -> Option { + fn to_search_event( + &mut self, + event: &Self::Event, + _: &mut ViewContext, + ) -> Option { match event { Event::BufferEdited => Some(SearchEvent::MatchesInvalidated), - Event::SelectionsChanged { .. } => Some(SearchEvent::ActiveMatchChanged), + Event::SelectionsChanged { .. } => { + if self.selections.disjoint_anchors().len() == 1 { + Some(SearchEvent::ActiveMatchChanged) + } else { + None + } + } _ => None, } } @@ -954,8 +964,16 @@ impl SearchableItem for Editor { cx: &mut ViewContext, ) -> usize { let buffer = self.buffer().read(cx).snapshot(cx); - let cursor = self.selections.newest_anchor().head(); - if matches[current_index].start.cmp(&cursor, &buffer).is_gt() { + let current_index_position = if self.selections.disjoint_anchors().len() == 1 { + self.selections.newest_anchor().head() + } else { + matches[current_index].start + }; + if matches[current_index] + .start + .cmp(¤t_index_position, &buffer) + .is_gt() + { if direction == Direction::Prev { if current_index == 0 { current_index = matches.len() - 1; @@ -963,7 +981,11 @@ impl SearchableItem for Editor { current_index -= 1; } } - } else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() { + } else if matches[current_index] + .end + .cmp(¤t_index_position, &buffer) + .is_lt() + { if direction == Direction::Next { current_index = 0; } diff --git a/crates/feedback/src/feedback_editor.rs b/crates/feedback/src/feedback_editor.rs index 663164dd0770ebf0253c99a86ac84b984a450ae4..bea398d3eb14a1630632f1d4726bd74fa3bdc39f 100644 --- a/crates/feedback/src/feedback_editor.rs +++ b/crates/feedback/src/feedback_editor.rs @@ -362,8 +362,13 @@ impl Item for FeedbackEditor { impl SearchableItem for FeedbackEditor { type Match = Range; - fn to_search_event(event: &Self::Event) -> Option { - Editor::to_search_event(event) + fn to_search_event( + &mut self, + event: &Self::Event, + cx: &mut ViewContext, + ) -> Option { + self.editor + .update(cx, |editor, cx| editor.to_search_event(event, cx)) } fn clear_matches(&mut self, cx: &mut ViewContext) { diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index b27349f412e59e4f36b7dd21071cfad2de7f42e4..0dc594a13f47898c4d5960941c594091e02bd5d0 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -467,8 +467,13 @@ impl Item for LspLogView { impl SearchableItem for LspLogView { type Match = ::Match; - fn to_search_event(event: &Self::Event) -> Option { - Editor::to_search_event(event) + fn to_search_event( + &mut self, + event: &Self::Event, + cx: &mut ViewContext, + ) -> Option { + self.editor + .update(cx, |editor, cx| editor.to_search_event(event, cx)) } fn clear_matches(&mut self, cx: &mut ViewContext) { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index a87587a92f2b3992cdf9a83ab23bffd2f1d6f6da..c3790116d348f7b6dca7b832ad5417304d0a81b3 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1029,12 +1029,16 @@ mod tests { }); editor.next_notification(cx).await; - editor.update(cx, |editor, cx| { - let initial_selections = editor.selections.display_ranges(cx); + let initial_selections = editor.update(cx, |editor, cx| { + let initial_selections = editor.selections.display_ranges(cx); assert_eq!( initial_selections.len(), 1, "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", - ) + ); + initial_selections + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); search_bar.update(cx, |search_bar, cx| { @@ -1047,5 +1051,74 @@ mod tests { "Should select all `a` characters in the buffer, but got: {all_selections:?}" ); }); + search_bar.update(cx, |search_bar, _| { + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should not change after selecting all matches" + ); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_next_match(&SelectNextMatch, cx); + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On next match, should deselect items and select the next match" + ); + assert_ne!( + all_selections, initial_selections, + "Next match should be different from the first selection" + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should be updated to the next one" + ); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should not change after selecting all matches" + ); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On previous match, should deselect items and select the previous item" + ); + assert_eq!( + all_selections, initial_selections, + "Previous match should be the same as the first selection" + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should be updated to the previous one" + ); + }); } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 8e1e4ad62f0abe5b48d094b93612e4ed4f19c62c..3dd401e392c32481f2f5da76cd7acebed1d79cd3 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -647,7 +647,11 @@ impl SearchableItem for TerminalView { } /// Convert events raised by this item into search-relevant events (if applicable) - fn to_search_event(event: &Self::Event) -> Option { + fn to_search_event( + &mut self, + event: &Self::Event, + _: &mut ViewContext, + ) -> Option { match event { Event::Wakeup => Some(SearchEvent::MatchesInvalidated), Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged), diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 4ebfe69c2181b74b555e6a799ad2c46effd8d7b9..3a3ba02e06653361e90f9136c70b4047ac82d031 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -37,7 +37,11 @@ pub trait SearchableItem: Item { regex: true, } } - fn to_search_event(event: &Self::Event) -> Option; + fn to_search_event( + &mut self, + event: &Self::Event, + cx: &mut ViewContext, + ) -> Option; fn clear_matches(&mut self, cx: &mut ViewContext); fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext); fn query_suggestion(&mut self, cx: &mut ViewContext) -> String; @@ -141,8 +145,9 @@ impl SearchableItemHandle for ViewHandle { cx: &mut WindowContext, handler: Box, ) -> Subscription { - cx.subscribe(self, move |_, event, cx| { - if let Some(search_event) = T::to_search_event(event) { + cx.subscribe(self, move |handle, event, cx| { + let search_event = handle.update(cx, |handle, cx| handle.to_search_event(event, cx)); + if let Some(search_event) = search_event { handler(search_event, cx) } }) From b14cd5f56d3029f986a848a971c96cbb1f5f00e9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 14 Jul 2023 15:50:45 +0300 Subject: [PATCH 6/6] Add a new button for the action --- crates/search/src/buffer_search.rs | 32 ++++++++++++++++++++++++++++++ crates/theme/src/theme.rs | 1 + 2 files changed, 33 insertions(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c3790116d348f7b6dca7b832ad5417304d0a81b3..f6466c85af3100b6160c6b978e269af59b85af85 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -148,6 +148,7 @@ impl View for BufferSearchBar { Flex::row() .with_child(self.render_nav_button("<", Direction::Prev, cx)) .with_child(self.render_nav_button(">", Direction::Next, cx)) + .with_child(self.render_action_button("Select All", cx)) .aligned(), ) .with_child( @@ -403,6 +404,37 @@ impl BufferSearchBar { .into_any() } + fn render_action_button( + &self, + icon: &'static str, + cx: &mut ViewContext, + ) -> AnyElement { + let tooltip = "Select All Matches"; + let tooltip_style = theme::current(cx).tooltip.clone(); + let action_type_id = 0_usize; + + enum ActionButton {} + MouseEventHandler::::new(action_type_id, cx, |state, cx| { + let theme = theme::current(cx); + let style = theme.search.action_button.style_for(state); + Label::new(icon, style.text.clone()) + .contained() + .with_style(style.container) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.select_all_matches(&SelectAllMatches, cx) + }) + .with_cursor_style(CursorStyle::PointingHand) + .with_tooltip::( + action_type_id, + tooltip.to_string(), + Some(Box::new(SelectAllMatches)), + tooltip_style, + cx, + ) + .into_any() + } + fn render_close_button( &self, theme: &theme::Search, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 4e8ece1c8fcf3e838431b1fc6cd6bbef2f798248..cdf3cadf594b68e29a6ce45451a1a4229d7eba7e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -379,6 +379,7 @@ pub struct Search { pub invalid_include_exclude_editor: ContainerStyle, pub include_exclude_inputs: ContainedText, pub option_button: Toggleable>, + pub action_button: Interactive, pub match_background: Color, pub match_index: ContainedText, pub results_status: TextStyle,