@@ -29,6 +29,7 @@ pub enum Object {
AnyQuotes,
DoubleQuotes,
VerticalBars,
+ AnyBrackets,
Parentheses,
SquareBrackets,
CurlyBrackets,
@@ -74,6 +75,7 @@ actions!(
DoubleQuotes,
VerticalBars,
Parentheses,
+ AnyBrackets,
SquareBrackets,
CurlyBrackets,
AngleBrackets,
@@ -115,6 +117,9 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| {
vim.object(Object::AnyQuotes, window, cx)
});
+ Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| {
+ vim.object(Object::AnyBrackets, window, cx)
+ });
Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| {
vim.object(Object::DoubleQuotes, window, cx)
});
@@ -186,6 +191,7 @@ impl Object {
| Object::DoubleQuotes => false,
Object::Sentence
| Object::Paragraph
+ | Object::AnyBrackets
| Object::Parentheses
| Object::Tag
| Object::AngleBrackets
@@ -212,6 +218,7 @@ impl Object {
| Object::AnyQuotes
| Object::DoubleQuotes
| Object::VerticalBars
+ | Object::AnyBrackets
| Object::Parentheses
| Object::SquareBrackets
| Object::Tag
@@ -239,6 +246,7 @@ impl Object {
}
}
Object::Parentheses
+ | Object::AnyBrackets
| Object::SquareBrackets
| Object::CurlyBrackets
| Object::AngleBrackets
@@ -306,16 +314,7 @@ impl Object {
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
- })
+ .min_by_key(|range| calculate_range_distance(range, relative_offset, map))
}
Object::DoubleQuotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
@@ -331,6 +330,24 @@ impl Object {
let range = selection.range();
surrounding_html_tag(map, head, range, around)
}
+ Object::AnyBrackets => {
+ let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
+ let relative_offset = relative_to.to_offset(map, Bias::Left) as isize;
+
+ bracket_pairs
+ .iter()
+ .flat_map(|&(open_bracket, close_bracket)| {
+ surrounding_markers(
+ map,
+ relative_to,
+ around,
+ self.is_multiline(),
+ open_bracket,
+ close_bracket,
+ )
+ })
+ .min_by_key(|range| calculate_range_distance(range, relative_offset, map))
+ }
Object::SquareBrackets => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
}
@@ -584,6 +601,37 @@ fn around_word(
}
}
+/// Calculate distance between a range and a cursor position
+///
+/// Returns a score where:
+/// - Lower values indicate better matches
+/// - Range containing cursor gets priority (returns range length)
+/// - For non-containing ranges, uses minimum distance to boundaries as primary factor
+/// - Range length is used as secondary factor for tiebreaking
+fn calculate_range_distance(
+ range: &Range<DisplayPoint>,
+ cursor_offset: isize,
+ map: &DisplaySnapshot,
+) -> isize {
+ let start_offset = range.start.to_offset(map, Bias::Left) as isize;
+ let end_offset = range.end.to_offset(map, Bias::Right) as isize;
+ let range_length = end_offset - start_offset;
+
+ // If cursor is inside the range, return range length
+ if cursor_offset >= start_offset && cursor_offset <= end_offset {
+ return range_length;
+ }
+
+ // Calculate minimum distance to range boundaries
+ let start_distance = (cursor_offset - start_offset).abs();
+ let end_distance = (cursor_offset - end_offset).abs();
+ let min_distance = start_distance.min(end_distance);
+
+ // Use min_distance as primary factor, range_length as secondary
+ // Multiply by large number to ensure distance is primary factor
+ min_distance * 10000 + range_length
+}
+
fn around_subword(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
@@ -1302,9 +1350,11 @@ fn surrounding_markers(
#[cfg(test)]
mod test {
+ use gpui::KeyBinding;
use indoc::indoc;
use crate::{
+ object::AnyBrackets,
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
@@ -1914,6 +1964,30 @@ mod test {
const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
// 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.",
@@ -2022,6 +2096,151 @@ mod test {
}
}
+ #[gpui::test]
+ async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+ cx.update(|_, cx| {
+ cx.bind_keys([KeyBinding::new(
+ "b",
+ AnyBrackets,
+ Some("vim_operator == a || vim_operator == i || vim_operator == cs"),
+ )]);
+ });
+
+ const TEST_CASES: &[(&str, &str, &str, Mode)] = &[
+ // 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_tags(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_html(cx).await;