diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 8f5f99e96f708dcc08cc1a9c1fcfc799d6ba43e7..5d98fd2a1c833f3003edf623b997cb321656fdbe 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -426,6 +426,7 @@ ";": "vim::HelixCollapseSelection", ":": "command_palette::Toggle", "m": "vim::PushHelixMatch", + "s": "vim::HelixSelectRegex", "]": ["vim::PushHelixNext", { "around": true }], "[": ["vim::PushHelixPrevious", { "around": true }], "left": "vim::WrappingLeft", diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index a1b311a3ac3b8ed330fee0f015c41d327efe342d..b5ae47bbdf0fc13a87b6bdac63f9f2a85594aca0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -44,7 +44,9 @@ use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, invalid_buffer_view::InvalidBufferView, item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, - searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, + searchable::{ + Direction, FilteredSearchRange, SearchEvent, SearchableItem, SearchableItemHandle, + }, }; use workspace::{ OpenOptions, @@ -1510,7 +1512,7 @@ impl SearchableItem for Editor { fn toggle_filtered_search_ranges( &mut self, - enabled: bool, + enabled: Option, _: &mut Window, cx: &mut Context, ) { @@ -1520,15 +1522,16 @@ impl SearchableItem for Editor { .map(|(_, ranges)| ranges) } - if !enabled { - return; - } + if let Some(range) = enabled { + let ranges = self.selections.disjoint_anchor_ranges().collect::>(); - let ranges = self.selections.disjoint_anchor_ranges().collect::>(); - if ranges.iter().any(|s| s.start != s.end) { - self.set_search_within_ranges(&ranges, cx); - } else if let Some(previous_search_ranges) = self.previous_search_ranges.take() { - self.set_search_within_ranges(&previous_search_ranges, cx) + if ranges.iter().any(|s| s.start != s.end) { + self.set_search_within_ranges(&ranges, cx); + } else if let Some(previous_search_ranges) = self.previous_search_ranges.take() + && range != FilteredSearchRange::Selection + { + self.set_search_within_ranges(&previous_search_ranges, cx); + } } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 126215a0a75ee5057c560462f40958ba71d8cf74..81dd81050e2504013a173b8165532c6177126845 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -38,7 +38,9 @@ use util::ResultExt; use workspace::{ ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::ItemHandle, - searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, + searchable::{ + Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle, + }, }; pub use registrar::DivRegistrar; @@ -117,7 +119,7 @@ pub struct BufferSearchBar { search_history: SearchHistory, search_history_cursor: SearchHistoryCursor, replace_enabled: bool, - selection_search_enabled: bool, + selection_search_enabled: Option, scroll_handle: ScrollHandle, editor_scroll_handle: ScrollHandle, editor_needed_width: Pixels, @@ -255,13 +257,13 @@ impl Render for BufferSearchBar { ) .style(ButtonStyle::Subtle) .shape(IconButtonShape::Square) - .when(self.selection_search_enabled, |button| { + .when(self.selection_search_enabled.is_some(), |button| { button.style(ButtonStyle::Filled) }) .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { this.toggle_selection(&ToggleSelection, window, cx); })) - .toggle_state(self.selection_search_enabled) + .toggle_state(self.selection_search_enabled.is_some()) .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { @@ -673,7 +675,7 @@ impl BufferSearchBar { search_history_cursor: Default::default(), active_search: None, replace_enabled: false, - selection_search_enabled: false, + selection_search_enabled: None, scroll_handle: ScrollHandle::new(), editor_scroll_handle: ScrollHandle::new(), editor_needed_width: px(0.), @@ -696,10 +698,10 @@ impl BufferSearchBar { } } if let Some(active_editor) = self.active_searchable_item.as_mut() { - self.selection_search_enabled = false; + self.selection_search_enabled = None; self.replace_enabled = false; active_editor.search_bar_visibility_changed(false, window, cx); - active_editor.toggle_filtered_search_ranges(false, window, cx); + active_editor.toggle_filtered_search_ranges(None, window, cx); let handle = active_editor.item_focus_handle(cx); self.focus(&handle, window); } @@ -711,18 +713,23 @@ impl BufferSearchBar { } pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context) -> bool { + let filtered_search_range = if deploy.selection_search_enabled { + Some(FilteredSearchRange::Default) + } else { + None + }; if self.show(window, cx) { if let Some(active_item) = self.active_searchable_item.as_mut() { - active_item.toggle_filtered_search_ranges( - deploy.selection_search_enabled, - window, - cx, - ); + active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx); } self.search_suggested(window, cx); self.smartcase(window, cx); self.replace_enabled = deploy.replace_enabled; - self.selection_search_enabled = deploy.selection_search_enabled; + self.selection_search_enabled = if deploy.selection_search_enabled { + Some(FilteredSearchRange::Default) + } else { + None + }; if deploy.focus { let mut handle = self.query_editor.focus_handle(cx); let mut select_query = true; @@ -923,6 +930,19 @@ impl BufferSearchBar { } } + pub fn set_search_within_selection( + &mut self, + search_within_selection: Option, + window: &mut Window, + cx: &mut Context, + ) -> Option> { + let active_item = self.active_searchable_item.as_mut()?; + self.selection_search_enabled = search_within_selection; + active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx); + cx.notify(); + Some(self.update_matches(false, false, window, cx)) + } + pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context) { self.search_options = search_options; self.adjust_query_regex_language(cx); @@ -957,7 +977,7 @@ impl BufferSearchBar { self.select_match(Direction::Prev, 1, window, cx); } - fn select_all_matches( + pub fn select_all_matches( &mut self, _: &SelectAllMatches, window: &mut Window, @@ -1125,12 +1145,15 @@ impl BufferSearchBar { window: &mut Window, cx: &mut Context, ) { - if let Some(active_item) = self.active_searchable_item.as_mut() { - self.selection_search_enabled = !self.selection_search_enabled; - active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx); - drop(self.update_matches(false, false, window, cx)); - cx.notify(); - } + self.set_search_within_selection( + if let Some(_) = self.selection_search_enabled { + None + } else { + Some(FilteredSearchRange::Default) + }, + window, + cx, + ); } fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context) { diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index ec1618311f8b8e16b71a39fc1d29b5c60eb49c96..e2174fe1a7adee3f5b6cdac5167f2c28fb3296c1 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -5,14 +5,20 @@ mod select; use editor::display_map::DisplaySnapshot; use editor::{ - DisplayPoint, Editor, HideMouseCursorOrigin, SelectionEffects, ToOffset, ToPoint, movement, + DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, SelectionEffects, ToOffset, + ToPoint, movement, }; use gpui::actions; use gpui::{Context, Window}; use language::{CharClassifier, CharKind, Point}; +use search::{BufferSearchBar, SearchOptions}; +use settings::Settings; use text::{Bias, SelectionGoal}; +use workspace::searchable; +use workspace::searchable::FilteredSearchRange; use crate::motion; +use crate::state::SearchState; use crate::{ Vim, motion::{Motion, right}, @@ -32,6 +38,8 @@ actions!( HelixGotoLastModification, /// Select entire line or multiple lines, extending downwards. HelixSelectLine, + /// Select all matches of a given pattern within the current selection. + HelixSelectRegex, ] ); @@ -42,6 +50,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_yank); Vim::action(editor, cx, Vim::helix_goto_last_modification); Vim::action(editor, cx, Vim::helix_paste); + Vim::action(editor, cx, Vim::helix_select_regex); } impl Vim { @@ -368,6 +377,64 @@ impl Vim { self.switch_mode(Mode::Insert, false, window, cx); } + fn helix_select_regex( + &mut self, + _: &HelixSelectRegex, + window: &mut Window, + cx: &mut Context, + ) { + Vim::take_forced_motion(cx); + let Some(pane) = self.pane(window, cx) else { + return; + }; + let prior_selections = self.editor_selections(window, cx); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + if !search_bar.show(window, cx) { + return; + } + + search_bar.select_query(window, cx); + cx.focus_self(window); + + search_bar.set_replacement(None, cx); + let mut options = SearchOptions::NONE; + options |= SearchOptions::REGEX; + if EditorSettings::get_global(cx).search.case_sensitive { + options |= SearchOptions::CASE_SENSITIVE; + } + search_bar.set_search_options(options, cx); + if let Some(search) = search_bar.set_search_within_selection( + Some(FilteredSearchRange::Selection), + window, + cx, + ) { + cx.spawn_in(window, async move |search_bar, cx| { + if search.await.is_ok() { + search_bar.update_in(cx, |search_bar, window, cx| { + search_bar.activate_current_match(window, cx) + }) + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); + } + self.search = SearchState { + direction: searchable::Direction::Next, + count: 1, + prior_selections, + prior_operator: self.operator_stack.last().cloned(), + prior_mode: self.mode, + helix_select: true, + } + }); + } + }); + self.start_recording(cx); + } + fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context) { self.start_recording(cx); self.switch_mode(Mode::Insert, false, window, cx); @@ -1121,4 +1188,28 @@ mod test { cx.simulate_keystrokes("v w"); cx.assert_state("«one ˇ»two", Mode::HelixSelect); } + + #[gpui::test] + async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + cx.set_state("ˇone two one", Mode::HelixNormal); + cx.simulate_keystrokes("x"); + cx.assert_state("«one two oneˇ»", Mode::HelixNormal); + cx.simulate_keystrokes("s o n e"); + cx.run_until_parked(); + cx.simulate_keystrokes("enter"); + cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal); + + cx.simulate_keystrokes("x"); + cx.simulate_keystrokes("s"); + cx.run_until_parked(); + cx.simulate_keystrokes("enter"); + cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal); + + cx.set_state("ˇone two one", Mode::HelixNormal); + cx.simulate_keystrokes("s o n e enter"); + cx.assert_state("ˇone two one", Mode::HelixNormal); + } } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 1023375cacc21152b27e8bf49177dc1d92f8ca91..0f7b4421ae7a19423be72be227667b2110dedf64 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -195,6 +195,7 @@ impl Vim { prior_selections, prior_operator: self.operator_stack.last().cloned(), prior_mode, + helix_select: false, } }); } @@ -218,6 +219,12 @@ impl Vim { let new_selections = self.editor_selections(window, cx); let result = pane.update(cx, |pane, cx| { let search_bar = pane.toolbar().read(cx).item_of_type::()?; + if self.search.helix_select { + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&Default::default(), window, cx) + }); + return None; + } search_bar.update(cx, |search_bar, cx| { let mut count = self.search.count; let direction = self.search.direction; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index f81299169058b430b6ea6557f3d66762a6705a82..ee1e0eedec799630662e9bc94721ceb588ad94a3 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -988,6 +988,7 @@ pub struct SearchState { pub prior_selections: Vec>, pub prior_operator: Option, pub prior_mode: Mode, + pub helix_select: bool, } impl Operator { diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index b21ba7a4b1a2ec7cc80521e91b4e5935333615f5..310fae908dbd6864c1636ebd393e4920d0f9ad02 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -45,6 +45,16 @@ pub struct SearchOptions { pub find_in_results: bool, } +// Whether to always select the current selection (even if empty) +// or to use the default (restoring the previous search ranges if some, +// otherwise using the whole file). +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub enum FilteredSearchRange { + Selection, + #[default] + Default, +} + pub trait SearchableItem: Item + EventEmitter { type Match: Any + Sync + Send + Clone; @@ -73,7 +83,7 @@ pub trait SearchableItem: Item + EventEmitter { fn toggle_filtered_search_ranges( &mut self, - _enabled: bool, + _enabled: Option, _window: &mut Window, _cx: &mut Context, ) { @@ -216,7 +226,12 @@ pub trait SearchableItemHandle: ItemHandle { ) -> Option; fn search_bar_visibility_changed(&self, visible: bool, window: &mut Window, cx: &mut App); - fn toggle_filtered_search_ranges(&mut self, enabled: bool, window: &mut Window, cx: &mut App); + fn toggle_filtered_search_ranges( + &mut self, + enabled: Option, + window: &mut Window, + cx: &mut App, + ); } impl SearchableItemHandle for Entity { @@ -362,7 +377,12 @@ impl SearchableItemHandle for Entity { }); } - fn toggle_filtered_search_ranges(&mut self, enabled: bool, window: &mut Window, cx: &mut App) { + fn toggle_filtered_search_ranges( + &mut self, + enabled: Option, + window: &mut Window, + cx: &mut App, + ) { self.update(cx, |this, cx| { this.toggle_filtered_search_ranges(enabled, window, cx) });