From 1a27016123524dcd0e3d8c9f2c316f17223f4422 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 25 Apr 2024 11:19:15 +0800 Subject: [PATCH] Improve logic for obtaining surrounds range in Vim mode (#10938) 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) --- crates/vim/Cargo.toml | 1 + crates/vim/src/object.rs | 84 +++++++++++++++---- ...ounding_character_objects_with_escape.json | 10 +++ 3 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 5d6c1288b5..3efd05259e 100644 --- a/crates/vim/Cargo.toml +++ b/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 = [ diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index cd08c052ed..a3cd89fbe3 100644 --- a/crates/vim/src/object.rs +++ b/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; diff --git a/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json b/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json new file mode 100644 index 0000000000..0de952ac91 --- /dev/null +++ b/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"}}