When searching in visual mode switch to normal mode

Thorsten Ball and Conrad created

This matches Neovim behaviour by setting the mode to `Normal` when using
search while in visual mode.

Co-authored-by: Conrad <conrad@zed.dev>

Change summary

crates/gpui/src/keymap/matcher.rs                 |  4 --
crates/vim/src/normal/search.rs                   | 21 ++++++++++++++++
crates/vim/src/test/neovim_backed_test_context.rs | 18 ++++++++++++++
3 files changed, 39 insertions(+), 4 deletions(-)

Detailed changes

crates/gpui/src/keymap/matcher.rs 🔗

@@ -73,9 +73,7 @@ impl KeystrokeMatcher {
         if !found_actions.is_empty() {
             self.pending_keystrokes.clear();
             return KeyMatch::Some(found_actions);
-        }
-
-        if let Some(pending_key) = pending_key {
+        } else if let Some(pending_key) = pending_key {
             self.pending_keystrokes.push(pending_key);
             KeyMatch::Pending
         } else {

crates/vim/src/normal/search.rs 🔗

@@ -3,7 +3,12 @@ use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
 use serde_derive::Deserialize;
 use workspace::{searchable::Direction, Workspace};
 
-use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim};
+use crate::{
+    motion::Motion,
+    normal::move_cursor,
+    state::{Mode, SearchState},
+    Vim,
+};
 
 #[derive(Clone, Deserialize, PartialEq)]
 #[serde(rename_all = "camelCase")]
@@ -145,6 +150,7 @@ pub fn move_to_internal(
     Vim::update(cx, |vim, cx| {
         let pane = workspace.active_pane().clone();
         let count = vim.take_count(cx).unwrap_or(1);
+
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                 let search = search_bar.update(cx, |search_bar, cx| {
@@ -176,6 +182,11 @@ pub fn move_to_internal(
                 }
             }
         });
+
+        if vim.state().mode.is_visual() {
+            vim.switch_mode(Mode::Normal, false, cx)
+        }
+
         vim.clear_operator(cx);
     });
 }
@@ -465,6 +476,13 @@ mod test {
         cx.simulate_keystrokes(["/", "b"]);
         cx.simulate_keystrokes(["enter"]);
         cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
+
+        // check that searching switches to normal mode if in visual mode
+        cx.set_state("ˇone two one", Mode::Normal);
+        cx.simulate_keystrokes(["v", "l", "l"]);
+        cx.assert_editor_state("«oneˇ» two one");
+        cx.simulate_keystrokes(["*"]);
+        cx.assert_state("one two ˇone", Mode::Normal);
     }
 
     #[gpui::test]
@@ -488,5 +506,6 @@ mod test {
         cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
         cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await;
         cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await;
+        cx.assert_shared_mode(Mode::Normal).await;
     }
 }

crates/vim/src/test/neovim_backed_test_context.rs 🔗

@@ -277,6 +277,24 @@ impl NeovimBackedTestContext {
         self.neovim.mode().await.unwrap()
     }
 
+    pub async fn assert_shared_mode(&mut self, mode: Mode) {
+        let neovim = self.neovim_mode().await;
+        let editor = self.cx.mode();
+
+        if neovim != mode || editor != mode {
+            panic!(
+                indoc! {"Test failed (zed does not match nvim behaviour)
+                    # desired mode:
+                    {:?}
+                    # neovim mode:
+                    {:?}
+                    # zed mode:
+                    {:?}"},
+                mode, neovim, editor,
+            )
+        }
+    }
+
     pub async fn assert_state_matches(&mut self) {
         self.is_dirty = false;
         let neovim = self.neovim_state().await;