@@ -28,9 +28,11 @@ pub enum Object {
Quotes,
BackQuotes,
AnyQuotes,
+ MiniQuotes,
DoubleQuotes,
VerticalBars,
AnyBrackets,
+ MiniBrackets,
Parentheses,
SquareBrackets,
CurlyBrackets,
@@ -158,7 +160,7 @@ impl DelimiterRange {
}
}
-fn find_any_delimiters(
+fn find_mini_delimiters(
map: &DisplaySnapshot,
display_point: DisplayPoint,
around: bool,
@@ -234,20 +236,20 @@ fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> b
)
}
-fn find_any_quotes(
+fn find_mini_quotes(
map: &DisplaySnapshot,
display_point: DisplayPoint,
around: bool,
) -> Option<Range<DisplayPoint>> {
- find_any_delimiters(map, display_point, around, &is_quote_delimiter)
+ find_mini_delimiters(map, display_point, around, &is_quote_delimiter)
}
-fn find_any_brackets(
+fn find_mini_brackets(
map: &DisplaySnapshot,
display_point: DisplayPoint,
around: bool,
) -> Option<Range<DisplayPoint>> {
- find_any_delimiters(map, display_point, around, &is_bracket_delimiter)
+ find_mini_delimiters(map, display_point, around, &is_bracket_delimiter)
}
impl_actions!(vim, [Word, Subword, IndentObj]);
@@ -259,10 +261,12 @@ actions!(
Paragraph,
Quotes,
BackQuotes,
+ MiniQuotes,
AnyQuotes,
DoubleQuotes,
VerticalBars,
Parentheses,
+ MiniBrackets,
AnyBrackets,
SquareBrackets,
CurlyBrackets,
@@ -306,6 +310,12 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| {
vim.object(Object::BackQuotes, window, cx)
});
+ Vim::action(editor, cx, |vim, _: &MiniQuotes, window, cx| {
+ vim.object(Object::MiniQuotes, window, cx)
+ });
+ Vim::action(editor, cx, |vim, _: &MiniBrackets, window, cx| {
+ vim.object(Object::MiniBrackets, window, cx)
+ });
Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
vim.object(Object::AnyQuotes, window, cx)
});
@@ -382,11 +392,13 @@ impl Object {
| Object::Quotes
| Object::BackQuotes
| Object::AnyQuotes
+ | Object::MiniQuotes
| Object::VerticalBars
| Object::DoubleQuotes => false,
Object::Sentence
| Object::Paragraph
| Object::AnyBrackets
+ | Object::MiniBrackets
| Object::Parentheses
| Object::Tag
| Object::AngleBrackets
@@ -412,9 +424,11 @@ impl Object {
Object::Quotes
| Object::BackQuotes
| Object::AnyQuotes
+ | Object::MiniQuotes
| Object::DoubleQuotes
| Object::VerticalBars
| Object::AnyBrackets
+ | Object::MiniBrackets
| Object::Parentheses
| Object::SquareBrackets
| Object::Tag
@@ -434,6 +448,7 @@ impl Object {
| Object::Sentence
| Object::Quotes
| Object::AnyQuotes
+ | Object::MiniQuotes
| Object::BackQuotes
| Object::DoubleQuotes => {
if current_mode == Mode::VisualBlock {
@@ -444,6 +459,7 @@ impl Object {
}
Object::Parentheses
| Object::AnyBrackets
+ | Object::MiniBrackets
| Object::SquareBrackets
| Object::CurlyBrackets
| Object::AngleBrackets
@@ -493,7 +509,67 @@ impl Object {
Object::BackQuotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
}
- Object::AnyQuotes => find_any_quotes(map, relative_to, around),
+ Object::AnyQuotes => {
+ let quote_types = ['\'', '"', '`'];
+ let cursor_offset = relative_to.to_offset(map, Bias::Left);
+
+ // Find innermost range directly without collecting all ranges
+ let mut innermost = None;
+ let mut min_size = usize::MAX;
+
+ // First pass: find innermost enclosing range
+ for quote in quote_types {
+ if let Some(range) = surrounding_markers(
+ map,
+ relative_to,
+ around,
+ self.is_multiline(),
+ quote,
+ quote,
+ ) {
+ let start_offset = range.start.to_offset(map, Bias::Left);
+ let end_offset = range.end.to_offset(map, Bias::Right);
+
+ if cursor_offset >= start_offset && cursor_offset <= end_offset {
+ let size = end_offset - start_offset;
+ if size < min_size {
+ min_size = size;
+ innermost = Some(range);
+ }
+ }
+ }
+ }
+
+ if let Some(range) = innermost {
+ return Some(range);
+ }
+
+ // Fallback: find nearest pair if not inside any quotes
+ quote_types
+ .iter()
+ .flat_map(|"e| {
+ surrounding_markers(
+ map,
+ relative_to,
+ around,
+ self.is_multiline(),
+ quote,
+ quote,
+ )
+ })
+ .min_by_key(|range| {
+ let start_offset = range.start.to_offset(map, Bias::Left);
+ let end_offset = range.end.to_offset(map, Bias::Right);
+ if cursor_offset < start_offset {
+ (start_offset - cursor_offset) as isize
+ } else if cursor_offset > end_offset {
+ (cursor_offset - end_offset) as isize
+ } else {
+ 0
+ }
+ })
+ }
+ Object::MiniQuotes => find_mini_quotes(map, relative_to, around),
Object::DoubleQuotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
}
@@ -508,7 +584,66 @@ impl Object {
let range = selection.range();
surrounding_html_tag(map, head, range, around)
}
- Object::AnyBrackets => find_any_brackets(map, relative_to, around),
+ Object::AnyBrackets => {
+ let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
+ let cursor_offset = relative_to.to_offset(map, Bias::Left);
+
+ // Find innermost enclosing bracket range
+ let mut innermost = None;
+ let mut min_size = usize::MAX;
+
+ for &(open, close) in bracket_pairs.iter() {
+ if let Some(range) = surrounding_markers(
+ map,
+ relative_to,
+ around,
+ self.is_multiline(),
+ open,
+ close,
+ ) {
+ let start_offset = range.start.to_offset(map, Bias::Left);
+ let end_offset = range.end.to_offset(map, Bias::Right);
+
+ if cursor_offset >= start_offset && cursor_offset <= end_offset {
+ let size = end_offset - start_offset;
+ if size < min_size {
+ min_size = size;
+ innermost = Some(range);
+ }
+ }
+ }
+ }
+
+ if let Some(range) = innermost {
+ return Some(range);
+ }
+
+ // Fallback: find nearest bracket pair if not inside any
+ bracket_pairs
+ .iter()
+ .flat_map(|&(open, close)| {
+ surrounding_markers(
+ map,
+ relative_to,
+ around,
+ self.is_multiline(),
+ open,
+ close,
+ )
+ })
+ .min_by_key(|range| {
+ let start_offset = range.start.to_offset(map, Bias::Left);
+ let end_offset = range.end.to_offset(map, Bias::Right);
+ if cursor_offset < start_offset {
+ (start_offset - cursor_offset) as isize
+ } else if cursor_offset > end_offset {
+ (cursor_offset - end_offset) as isize
+ } else {
+ 0
+ }
+ })
+ }
+ Object::MiniBrackets => find_mini_brackets(map, relative_to, around),
Object::SquareBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
}
@@ -1525,7 +1660,7 @@ mod test {
use indoc::indoc;
use crate::{
- object::AnyBrackets,
+ object::{AnyBrackets, AnyQuotes, MiniBrackets},
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
@@ -2193,6 +2328,158 @@ mod test {
#[gpui::test]
async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+ cx.update(|_, cx| {
+ cx.bind_keys([KeyBinding::new(
+ "q",
+ AnyQuotes,
+ Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
+ )]);
+ });
+
+ const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
+ // the false string in the middle should be considered
+ (
+ "c i q",
+ "'first' false ˇstring 'second'",
+ "'first'ˇ'second'",
+ Mode::Insert,
+ ),
+ // Single quotes
+ (
+ "c i q",
+ "Thisˇ is a 'quote' example.",
+ "This is a 'ˇ' example.",
+ Mode::Insert,
+ ),
+ (
+ "c a q",
+ "Thisˇ is a 'quote' example.",
+ "This is a ˇexample.",
+ Mode::Insert,
+ ),
+ (
+ "c i q",
+ "This is a \"simple 'qˇuote'\" example.",
+ "This is a \"simple 'ˇ'\" example.",
+ Mode::Insert,
+ ),
+ (
+ "c a q",
+ "This is a \"simple 'qˇuote'\" example.",
+ "This is a \"simpleˇ\" example.",
+ Mode::Insert,
+ ),
+ (
+ "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_miniquotes_object(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_typescript(cx).await;
const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
@@ -2220,7 +2507,7 @@ mod test {
Mode::Insert,
),
// If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote
- // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in AnyQuotes :)
+ // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :)
// Bug reference: https://github.com/zed-industries/zed/issues/23889
("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert),
// Single quotes
@@ -2367,6 +2654,169 @@ mod test {
)]);
});
+ const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
+ (
+ "c i b",
+ indoc! {"
+ {
+ {
+ ˇprint('hello')
+ }
+ }
+ "},
+ indoc! {"
+ {
+ {
+ ˇ
+ }
+ }
+ "},
+ Mode::Insert,
+ ),
+ // Bracket (Parentheses)
+ (
+ "c i b",
+ "Thisˇ is a (simple [quote]) example.",
+ "This is a (ˇ) example.",
+ Mode::Insert,
+ ),
+ (
+ "c i b",
+ "This is a [simple (qˇuote)] example.",
+ "This is a [simple (ˇ)] example.",
+ Mode::Insert,
+ ),
+ (
+ "c a b",
+ "This is a [simple (qˇuote)] example.",
+ "This is a [simple ˇ] example.",
+ Mode::Insert,
+ ),
+ (
+ "c a b",
+ "Thisˇ is a (simple [quote]) example.",
+ "This is a ˇ example.",
+ Mode::Insert,
+ ),
+ (
+ "c i b",
+ "This is a (qˇuote) example.",
+ "This is a (ˇ) example.",
+ Mode::Insert,
+ ),
+ (
+ "c a b",
+ "This is a (qˇuote) example.",
+ "This is a ˇ example.",
+ Mode::Insert,
+ ),
+ (
+ "d i b",
+ "This is a (qˇuote) example.",
+ "This is a (ˇ) example.",
+ Mode::Normal,
+ ),
+ (
+ "d a b",
+ "This is a (qˇuote) example.",
+ "This is a ˇ example.",
+ Mode::Normal,
+ ),
+ // Square brackets
+ (
+ "c i b",
+ "This is a [qˇuote] example.",
+ "This is a [ˇ] example.",
+ Mode::Insert,
+ ),
+ (
+ "c a b",
+ "This is a [qˇuote] example.",
+ "This is a ˇ example.",
+ Mode::Insert,
+ ),
+ (
+ "d i b",
+ "This is a [qˇuote] example.",
+ "This is a [ˇ] example.",
+ Mode::Normal,
+ ),
+ (
+ "d a b",
+ "This is a [qˇuote] example.",
+ "This is a ˇ example.",
+ Mode::Normal,
+ ),
+ // Curly brackets
+ (
+ "c i b",
+ "This is a {qˇuote} example.",
+ "This is a {ˇ} example.",
+ Mode::Insert,
+ ),
+ (
+ "c a b",
+ "This is a {qˇuote} example.",
+ "This is a ˇ example.",
+ Mode::Insert,
+ ),
+ (
+ "d i b",
+ "This is a {qˇuote} example.",
+ "This is a {ˇ} example.",
+ Mode::Normal,
+ ),
+ (
+ "d a b",
+ "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 b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
+ ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
+ ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
+ ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket
+ ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
+ ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
+ ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
+ ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket
+ ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
+ ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
+ ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
+ ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket
+ ];
+
+ 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_minibrackets_object(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+ cx.update(|_, cx| {
+ cx.bind_keys([KeyBinding::new(
+ "b",
+ MiniBrackets,
+ Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
+ )]);
+ });
+
const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
// Special cases from mini.ai plugin
// Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying
@@ -2572,7 +3022,7 @@ mod test {
}
#[gpui::test]
- async fn test_anybrackets_trailing_space(cx: &mut gpui::TestAppContext) {
+ async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("(trailingˇ whitespace )")
.await;