vim: ? to search backwards, and /<enter> to repeat search

Conrad Irwin created

Change summary

assets/keymaps/vim.json                 |  10 +
crates/vim/src/normal.rs                |   4 
crates/vim/src/normal/search.rs         | 110 +++++++++++++++++++++++---
crates/vim/src/test.rs                  |   2 
crates/vim/src/test/vim_test_context.rs |   1 
5 files changed, 106 insertions(+), 21 deletions(-)

Detailed changes

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"
     }
   }
 ]

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);

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<Workspace>,
-) {
+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<Workspace>) {
     move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
 }
 
-pub(crate) fn move_to_prev(
-    workspace: &mut Workspace,
-    action: &MoveToPrev,
-    cx: &mut ViewContext<Workspace>,
-) {
+fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
     move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
 }
 
-fn move_to_internal(
+fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
+    let pane = workspace.active_pane().clone();
+    pane.update(cx, |pane, cx| {
+        if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
+            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::<BufferSearchBar>()
+                .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"]);
+
+        // ?<enter> 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"]);
+
+        // /<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}<enter> 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);
+    }
 }

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), "");
     })
 }
 

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());