diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index b9497394ab..4a215dcef3 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -197,10 +197,11 @@ "p": "vim::Paste", "u": "editor::Undo", "ctrl-r": "editor::Redo", - "/": [ - "buffer_search::Deploy", + "/": "vim::Search", + "?": [ + "vim::Search", { - "focus": true + "backwards": true, } ], "ctrl-f": "vim::PageDown", @@ -356,7 +357,8 @@ { "context": "BufferSearchBar", "bindings": { - "enter": "buffer_search::FocusEditor" + "enter": "buffer_search::FocusEditor", + "escape": "buffer_search::Dismiss" } } ] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index c382a08b5c..8dcaa5008e 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -28,7 +28,6 @@ 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}, }; @@ -59,8 +58,7 @@ 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); + search::init(cx); 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 index 49eb97c9ac..5f0ed9e7b9 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,4 +1,4 @@ -use gpui::{impl_actions, ViewContext}; +use gpui::{impl_actions, AppContext, ViewContext}; use search::{BufferSearchBar, SearchOptions}; use serde_derive::Deserialize; use workspace::{searchable::Direction, Workspace}; @@ -19,25 +19,47 @@ pub(crate) struct MoveToPrev { partial_word: bool, } -impl_actions!(vim, [MoveToNext, MoveToPrev]); +#[derive(Clone, Deserialize, PartialEq)] +pub(crate) struct Search { + #[serde(default)] + backwards: bool, +} -pub(crate) fn move_to_next( - workspace: &mut Workspace, - action: &MoveToNext, - cx: &mut ViewContext, -) { +impl_actions!(vim, [MoveToNext, MoveToPrev, Search]); + +pub(crate) fn init(cx: &mut AppContext) { + cx.add_action(move_to_next); + cx.add_action(move_to_prev); + cx.add_action(search); +} + +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, -) { +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( +fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext) { + 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 options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX; + let direction = if action.backwards { + Direction::Prev + } else { + Direction::Next + }; + search_bar.select_match(direction, cx); + search_bar.show_with_options(true, false, options, cx); + }) + } + }) +} + +pub fn move_to_internal( workspace: &mut Workspace, direction: Direction, whole_word: bool, @@ -60,6 +82,7 @@ fn move_to_internal( #[cfg(test)] mod test { + use editor::DisplayPoint; use search::BufferSearchBar; use crate::{state::Mode, test::VimTestContext}; @@ -105,4 +128,65 @@ mod test { search_bar.next_notification(&cx).await; cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); } + + #[gpui::test] + async fn test_search(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["/", "c", "c"]); + + 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") + }); + + search_bar.read_with(cx.cx, |bar, cx| { + assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); + }); + + // wait for the query editor change event to fire. + search_bar.next_notification(&cx).await; + + cx.update_editor(|editor, cx| { + let highlights = editor.all_background_highlights(cx); + assert_eq!(3, highlights.len()); + assert_eq!( + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2), + highlights[0].0 + ) + }); + + cx.simulate_keystrokes(["enter"]); + + // n to go to next/N to go to previous + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["n"]); + cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["shift-n"]); + + // ? to go to previous + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["?", "enter"]); + cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); + cx.simulate_keystrokes(["?", "enter"]); + + // / to go to next + cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["/", "enter"]); + cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); + + // ?{search} to search backwards + cx.simulate_keystrokes(["?", "b", "enter"]); + + // wait for the query editor change event to fire. + search_bar.next_notification(&cx).await; + + cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); + } } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index d9d24ec30e..8ed649e61b 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -97,7 +97,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) { }); search_bar.read_with(cx.cx, |bar, cx| { - assert_eq!(bar.query_editor.read(cx).text(cx), "jumps"); + assert_eq!(bar.query_editor.read(cx).text(cx), ""); }) } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index f9ba577231..56ca654644 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -90,6 +90,7 @@ impl<'a> VimTestContext<'a> { self.cx.set_state(text) } + #[track_caller] pub fn assert_state(&mut self, text: &str, mode: Mode) { self.assert_editor_state(text); assert_eq!(self.mode(), mode, "{}", self.assertion_context());