Improve logic for obtaining surrounds range in Vim mode (#10938)

Hans created

now correctly retrieves range in cases where escape characters are
present. Fixed #10827


Release Notes:

- vim: Fix logic for finding surrounding quotes to ignore escaped
characters (#10827)

Change summary

crates/vim/Cargo.toml                                                               |  1 
crates/vim/src/object.rs                                                            | 84 
crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json | 10 
3 files changed, 77 insertions(+), 18 deletions(-)

Detailed changes

crates/vim/Cargo.toml 🔗

@@ -23,6 +23,7 @@ collections.workspace = true
 command_palette_hooks.workspace = true
 editor.workspace = true
 gpui.workspace = true
+itertools.workspace = true
 language.workspace = true
 log.workspace = true
 nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = [

crates/vim/src/object.rs 🔗

@@ -9,6 +9,9 @@ use editor::{
     movement::{self, FindRange},
     Bias, DisplayPoint,
 };
+
+use itertools::Itertools;
+
 use gpui::{actions, impl_actions, ViewContext, WindowContext};
 use language::{char_kind, BufferSnapshot, CharKind, Point, Selection};
 use serde::Deserialize;
@@ -801,15 +804,20 @@ fn surrounding_markers(
     let mut matched_closes = 0;
     let mut opening = None;
 
+    let mut before_ch = match movement::chars_before(map, point).next() {
+        Some((ch, _)) => ch,
+        _ => '\0',
+    };
     if let Some((ch, range)) = movement::chars_after(map, point).next() {
-        if ch == open_marker {
+        if ch == open_marker && before_ch != '\\' {
             if open_marker == close_marker {
                 let mut total = 0;
-                for (ch, _) in movement::chars_before(map, point) {
+                for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows()
+                {
                     if ch == '\n' {
                         break;
                     }
-                    if ch == open_marker {
+                    if ch == open_marker && before_ch != '\\' {
                         total += 1;
                     }
                 }
@@ -823,11 +831,15 @@ fn surrounding_markers(
     }
 
     if opening.is_none() {
-        for (ch, range) in movement::chars_before(map, point) {
+        for ((ch, range), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() {
             if ch == '\n' && !search_across_lines {
                 break;
             }
 
+            if before_ch == '\\' {
+                continue;
+            }
+
             if ch == open_marker {
                 if matched_closes == 0 {
                     opening = Some(range);
@@ -839,15 +851,18 @@ fn surrounding_markers(
             }
         }
     }
-
     if opening.is_none() {
         for (ch, range) in movement::chars_after(map, point) {
-            if ch == open_marker {
-                opening = Some(range);
-                break;
-            } else if ch == close_marker {
-                break;
+            if before_ch != '\\' {
+                if ch == open_marker {
+                    opening = Some(range);
+                    break;
+                } else if ch == close_marker {
+                    break;
+                }
             }
+
+            before_ch = ch;
         }
     }
 
@@ -857,21 +872,28 @@ fn surrounding_markers(
 
     let mut matched_opens = 0;
     let mut closing = None;
-
+    before_ch = match movement::chars_before(map, opening.end).next() {
+        Some((ch, _)) => ch,
+        _ => '\0',
+    };
     for (ch, range) in movement::chars_after(map, opening.end) {
         if ch == '\n' && !search_across_lines {
             break;
         }
 
-        if ch == close_marker {
-            if matched_opens == 0 {
-                closing = Some(range);
-                break;
+        if before_ch != '\\' {
+            if ch == close_marker {
+                if matched_opens == 0 {
+                    closing = Some(range);
+                    break;
+                }
+                matched_opens -= 1;
+            } else if ch == open_marker {
+                matched_opens += 1;
             }
-            matched_opens -= 1;
-        } else if ch == open_marker {
-            matched_opens += 1;
         }
+
+        before_ch = ch;
     }
 
     let Some(mut closing) = closing else {
@@ -1467,6 +1489,32 @@ mod test {
             .await;
     }
 
+    #[gpui::test]
+    async fn test_singleline_surrounding_character_objects_with_escape(
+        cx: &mut gpui::TestAppContext,
+    ) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+        cx.set_shared_state(indoc! {
+            "h\"e\\\"lˇlo \\\"world\"!"
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
+        cx.assert_shared_state(indoc! {
+            "h\"«e\\\"llo \\\"worldˇ»\"!"
+        })
+        .await;
+
+        cx.set_shared_state(indoc! {
+            "hello \"teˇst \\\"inside\\\" world\""
+        })
+        .await;
+        cx.simulate_shared_keystrokes(["v", "i", "\""]).await;
+        cx.assert_shared_state(indoc! {
+            "hello \"«test \\\"inside\\\" worldˇ»\""
+        })
+        .await;
+    }
+
     #[gpui::test]
     async fn test_vertical_bars(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;

crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json 🔗

@@ -0,0 +1,10 @@
+{"Put":{"state":"h\"e\\\"lˇlo \\\"world\"!"}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"h\"«e\\\"llo \\\"worldˇ»\"!","mode":"Visual"}}
+{"Put":{"state":"hello \"teˇst \\\"inside\\\" world\""}}
+{"Key":"v"}
+{"Key":"i"}
+{"Key":"\""}
+{"Get":{"state":"hello \"«test \\\"inside\\\" worldˇ»\"","mode":"Visual"}}