diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 72aa719408..20b831b0e5 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -101,6 +101,8 @@ "vim::SwitchMode", "Normal" ], + "*": "vim::MoveToNext", + "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion "1": [ "vim::Number", @@ -240,7 +242,19 @@ "vim::SwitchMode", "Normal" ], - "d": "editor::GoToDefinition" + "d": "editor::GoToDefinition", + "*": [ + "vim::MoveToNext", + { + "partialWord": true + } + ], + "#": [ + "vim::MoveToPrev", + { + "partialWord": true + } + ] } }, { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 140f5accab..fb4d5da764 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -65,6 +65,7 @@ pub struct BufferSearchBar { pub query_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, + pending_match_direction: Option, active_searchable_item_subscription: Option, seachable_items_with_matches: HashMap, Vec>>, @@ -252,6 +253,7 @@ impl BufferSearchBar { default_options: SearchOptions::NONE, search_options: SearchOptions::NONE, pending_search: None, + pending_match_direction: None, query_contains_error: false, dismissed: true, } @@ -285,10 +287,10 @@ impl BufferSearchBar { &mut self, focus: bool, suggest_query: bool, - search_option: SearchOptions, + search_options: SearchOptions, cx: &mut ViewContext, ) -> bool { - self.search_options = search_option; + self.search_options = search_options; let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { SearchableItemHandle::boxed_clone(searchable_item.as_ref()) } else { @@ -486,6 +488,17 @@ impl BufferSearchBar { self.select_match(Direction::Prev, cx); } + pub fn select_word_under_cursor( + &mut self, + direction: Direction, + options: SearchOptions, + cx: &mut ViewContext, + ) { + self.active_match_index = None; + self.pending_match_direction = Some(direction); + self.show_with_options(false, true, options, 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() { @@ -567,6 +580,7 @@ impl BufferSearchBar { if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { self.active_match_index.take(); + self.pending_match_direction.take(); active_searchable_item.clear_matches(cx); } else { let query = if self.search_options.contains(SearchOptions::REGEX) { @@ -614,7 +628,15 @@ impl BufferSearchBar { .unwrap(); active_searchable_item.update_matches(matches, cx); if select_closest_match { - if let Some(match_ix) = this.active_match_index { + if let Some(mut match_ix) = this.active_match_index { + if let Some(direction) = this.pending_match_direction.take() + { + match_ix += match direction { + Direction::Next => 1, + Direction::Prev => matches.len() - 1, + }; + match_ix = match_ix % matches.len(); + } active_searchable_item .activate_match(match_ix, matches, cx); } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 1227afbb85..c382a08b5c 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -2,6 +2,7 @@ mod case; mod change; mod delete; mod scroll; +mod search; mod substitute; mod yank; @@ -27,6 +28,7 @@ use self::{ case::change_case, change::{change_motion, change_object}, delete::{delete_motion, delete_object}, + search::{move_to_next, move_to_prev}, substitute::substitute, yank::{yank_motion, yank_object}, }; @@ -57,6 +59,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(insert_line_above); cx.add_action(insert_line_below); cx.add_action(change_case); + cx.add_action(move_to_next); + cx.add_action(move_to_prev); cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs new file mode 100644 index 0000000000..49eb97c9ac --- /dev/null +++ b/crates/vim/src/normal/search.rs @@ -0,0 +1,108 @@ +use gpui::{impl_actions, ViewContext}; +use search::{BufferSearchBar, SearchOptions}; +use serde_derive::Deserialize; +use workspace::{searchable::Direction, Workspace}; + +use crate::Vim; + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MoveToNext { + #[serde(default)] + partial_word: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MoveToPrev { + #[serde(default)] + partial_word: bool, +} + +impl_actions!(vim, [MoveToNext, MoveToPrev]); + +pub(crate) fn move_to_next( + workspace: &mut Workspace, + action: &MoveToNext, + cx: &mut ViewContext, +) { + move_to_internal(workspace, Direction::Next, !action.partial_word, cx) +} + +pub(crate) fn move_to_prev( + workspace: &mut Workspace, + action: &MoveToPrev, + cx: &mut ViewContext, +) { + move_to_internal(workspace, Direction::Prev, !action.partial_word, cx) +} + +fn move_to_internal( + workspace: &mut Workspace, + direction: Direction, + whole_word: bool, + cx: &mut ViewContext, +) { + Vim::update(cx, |vim, cx| { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + let mut options = SearchOptions::CASE_SENSITIVE; + options.set(SearchOptions::WHOLE_WORD, whole_word); + search_bar.select_word_under_cursor(direction, options, cx); + }); + } + }); + vim.clear_operator(cx); + }); +} + +#[cfg(test)] +mod test { + use search::BufferSearchBar; + + use crate::{state::Mode, test::VimTestContext}; + + #[gpui::test] + async fn test_move_to_next(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + let search_bar = cx.workspace(|workspace, cx| { + workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .expect("Buffer search bar should be deployed") + }); + cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["*"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes(["*"]); + search_bar.next_notification(&cx).await; + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["#"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes(["#"]); + search_bar.next_notification(&cx).await; + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["g", "*"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["n"]); + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes(["g", "#"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); + } +}