diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 6228f6def6..06a55eac53 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -397,6 +397,7 @@ "'": "vim::Quotes", "`": "vim::BackQuotes", "\"": "vim::DoubleQuotes", + "q": "vim::AnyQuotes", "|": "vim::VerticalBars", "(": "vim::Parentheses", ")": "vim::Parentheses", diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 21f7749531..745d1adb78 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -25,6 +25,7 @@ pub enum Object { Paragraph, Quotes, BackQuotes, + AnyQuotes, DoubleQuotes, VerticalBars, Parentheses, @@ -61,6 +62,7 @@ actions!( Paragraph, Quotes, BackQuotes, + AnyQuotes, DoubleQuotes, VerticalBars, Parentheses, @@ -96,6 +98,9 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext) { Vim::action(editor, cx, |vim, _: &BackQuotes, cx| { vim.object(Object::BackQuotes, cx) }); + Vim::action(editor, cx, |vim, _: &AnyQuotes, cx| { + vim.object(Object::AnyQuotes, cx) + }); Vim::action(editor, cx, |vim, _: &DoubleQuotes, cx| { vim.object(Object::DoubleQuotes, cx) }); @@ -156,6 +161,7 @@ impl Object { Object::Word { .. } | Object::Quotes | Object::BackQuotes + | Object::AnyQuotes | Object::VerticalBars | Object::DoubleQuotes => false, Object::Sentence @@ -182,6 +188,7 @@ impl Object { | Object::IndentObj { .. } => false, Object::Quotes | Object::BackQuotes + | Object::AnyQuotes | Object::DoubleQuotes | Object::VerticalBars | Object::Parentheses @@ -200,6 +207,7 @@ impl Object { Object::Word { .. } | Object::Sentence | Object::Quotes + | Object::AnyQuotes | Object::BackQuotes | Object::DoubleQuotes => { if current_mode == Mode::VisualBlock { @@ -251,6 +259,35 @@ impl Object { Object::BackQuotes => { surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`') } + Object::AnyQuotes => { + let quote_types = ['\'', '"', '`']; // Types of quotes to handle + let relative_offset = relative_to.to_offset(map, Bias::Left) as isize; + + // Find the closest matching quote range + quote_types + .iter() + .flat_map(|"e| { + // Get ranges for each quote type + surrounding_markers( + map, + relative_to, + around, + self.is_multiline(), + quote, + quote, + ) + }) + .min_by_key(|range| { + // Calculate proximity of ranges to the cursor + let start_distance = (relative_offset + - range.start.to_offset(map, Bias::Left) as isize) + .abs(); + let end_distance = (relative_offset + - range.end.to_offset(map, Bias::Right) as isize) + .abs(); + start_distance + end_distance + }) + } Object::DoubleQuotes => { surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') } @@ -1751,6 +1788,120 @@ mod test { } } + #[gpui::test] + async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ + // Single quotes + ( + "c i q", + "This is a 'qˇuote' example.", + "This is a 'ˇ' example.", + Mode::Insert, + ), + ( + "c a q", + "This is a 'qˇuote' example.", + "This is a ˇexample.", + Mode::Insert, + ), + ( + "d i q", + "This is a 'qˇuote' example.", + "This is a 'ˇ' example.", + Mode::Normal, + ), + ( + "d a q", + "This is a 'qˇuote' example.", + "This is a ˇexample.", + Mode::Normal, + ), + // Double quotes + ( + "c i q", + "This is a \"qˇuote\" example.", + "This is a \"ˇ\" example.", + Mode::Insert, + ), + ( + "c a q", + "This is a \"qˇuote\" example.", + "This is a ˇexample.", + Mode::Insert, + ), + ( + "d i q", + "This is a \"qˇuote\" example.", + "This is a \"ˇ\" example.", + Mode::Normal, + ), + ( + "d a q", + "This is a \"qˇuote\" example.", + "This is a ˇexample.", + Mode::Normal, + ), + // Back quotes + ( + "c i q", + "This is a `qˇuote` example.", + "This is a `ˇ` example.", + Mode::Insert, + ), + ( + "c a q", + "This is a `qˇuote` example.", + "This is a ˇexample.", + Mode::Insert, + ), + ( + "d i q", + "This is a `qˇuote` example.", + "This is a `ˇ` example.", + Mode::Normal, + ), + ( + "d a q", + "This is a `qˇuote` example.", + "This is a ˇexample.", + Mode::Normal, + ), + ]; + + for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { + cx.set_state(initial_state, Mode::Normal); + + cx.simulate_keystrokes(keystrokes); + + cx.assert_state(expected_state, *expected_mode); + } + + const INVALID_CASES: &[(&str, &str, Mode)] = &[ + ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote + ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote + ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote + ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote + ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote + ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote + ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote + ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote + ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote + ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote + ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote + ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote + ]; + + for (keystrokes, initial_state, mode) in INVALID_CASES { + cx.set_state(initial_state, Mode::Normal); + + cx.simulate_keystrokes(keystrokes); + + cx.assert_state(initial_state, *mode); + } + } + #[gpui::test] async fn test_tags(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new_html(cx).await;