Fix search skipping in vim mode (#25580)

Conrad Irwin , nilehmann , and Anthony Eid created

Closes #8049

Co-authored-by: nilehmann <nico.lehmannm@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Release Notes:

- vim: Fix skipping of search results occasionally

Co-authored-by: nilehmann <nico.lehmannm@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Change summary

crates/vim/src/normal/search.rs                | 52 +++++++++++++++----
crates/vim/src/state.rs                        |  1 
crates/vim/test_data/test_search_skipping.json | 12 ++++
crates/vim/test_data/test_v_search_aa.json     |  7 ++
4 files changed, 60 insertions(+), 12 deletions(-)

Detailed changes

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

@@ -140,7 +140,6 @@ impl Vim {
                     if !search_bar.show(window, cx) {
                         return;
                     }
-                    let query = search_bar.query(cx);
 
                     search_bar.select_query(window, cx);
                     cx.focus_self(window);
@@ -160,7 +159,6 @@ impl Vim {
                     self.search = SearchState {
                         direction,
                         count,
-                        initial_query: query,
                         prior_selections,
                         prior_operator: self.operator_stack.last().cloned(),
                         prior_mode,
@@ -181,16 +179,17 @@ impl Vim {
         let Some(pane) = self.pane(window, cx) else {
             return;
         };
+        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::<BufferSearchBar>()?;
             search_bar.update(cx, |search_bar, cx| {
                 let mut count = self.search.count;
                 let direction = self.search.direction;
-                // in the case that the query has changed, the search bar
-                // will have selected the next match already.
-                if (search_bar.query(cx) != self.search.initial_query)
-                    && self.search.direction == Direction::Next
-                {
+                search_bar.has_active_match();
+                let new_head = new_selections.last().unwrap().start;
+                let old_head = self.search.prior_selections.last().unwrap().start;
+
+                if new_head != old_head && self.search.direction == Direction::Next {
                     count = count.saturating_sub(1)
                 }
                 self.search.count = 1;
@@ -829,6 +828,16 @@ mod test {
         cx.shared_state().await.assert_eq("a a a« a aˇ» a");
     }
 
+    #[gpui::test]
+    async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("ˇaa aa").await;
+        cx.simulate_shared_keystrokes("v / a a").await;
+        cx.simulate_shared_keystrokes("enter").await;
+        cx.shared_state().await.assert_eq("«aa aˇ»a");
+    }
+
     #[gpui::test]
     async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -878,10 +887,9 @@ mod test {
             a
              "
         });
-        cx.executor().advance_clock(Duration::from_millis(250));
-        cx.run_until_parked();
 
-        cx.simulate_shared_keystrokes("/ a enter").await;
+        cx.simulate_shared_keystrokes("/ a").await;
+        cx.simulate_shared_keystrokes("enter").await;
         cx.shared_state().await.assert_eq(indoc! {
             "a
                 ba
@@ -894,7 +902,29 @@ mod test {
         });
     }
 
-    // cargo test -p vim --features neovim test_replace_with_range
+    #[gpui::test]
+    async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state(indoc! {
+            "ˇaa aa aa"
+        })
+        .await;
+
+        cx.simulate_shared_keystrokes("/ a a").await;
+        cx.simulate_shared_keystrokes("enter").await;
+
+        cx.shared_state().await.assert_eq(indoc! {
+            "aa ˇaa aa"
+        });
+
+        cx.simulate_shared_keystrokes("left / a a").await;
+        cx.simulate_shared_keystrokes("enter").await;
+
+        cx.shared_state().await.assert_eq(indoc! {
+            "aa ˇaa aa"
+        });
+    }
+
     #[gpui::test]
     async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
         let mut cx = NeovimBackedTestContext::new(cx).await;

crates/vim/src/state.rs 🔗

@@ -467,7 +467,6 @@ impl Clone for ReplayableAction {
 pub struct SearchState {
     pub direction: Direction,
     pub count: usize,
-    pub initial_query: String,
 
     pub prior_selections: Vec<Range<Anchor>>,
     pub prior_operator: Option<Operator>,

crates/vim/test_data/test_search_skipping.json 🔗

@@ -0,0 +1,12 @@
+{"Put":{"state":"ˇaa aa aa"}}
+{"Key":"/"}
+{"Key":"a"}
+{"Key":"a"}
+{"Key":"enter"}
+{"Get":{"state":"aa ˇaa aa","mode":"Normal"}}
+{"Key":"left"}
+{"Key":"/"}
+{"Key":"a"}
+{"Key":"a"}
+{"Key":"enter"}
+{"Get":{"state":"aa ˇaa aa","mode":"Normal"}}