vim: Create anyquotes, anybrackets, miniquotes, and minibrackets text objects (#26748)
## Why? Some users expressed a preference for the AnyQuotes and AnyBrackets text objects to align more closely with traditional Vim behavior, rather than the mini.ai plugin's approach. To address this, I’ve introduced two new text objects: MiniQuotes and MiniBrackets. These retain the mini.ai plugin behavior, while the updated AnyQuotes and AnyBrackets now follow the logic described in [this bug report](https://github.com/zed-industries/zed/issues/25563) and [this bug report](https://github.com/zed-industries/zed/issues/25562). ## Behavior Overview: ### AnyQuotes and AnyBrackets: These now prioritize the innermost range first (e.g., the closest quotes or brackets). If none are found, they fall back to searching the current line. This aligns with the behavior requested in the issue. ### MiniQuotes and MiniBrackets: These maintain the mini.ai plugin behavior, prioritizing the current line before expanding the search outward. ### Usage Examples: AnyQuotes: Works like ```ci', ci", ci` , ca', ca", ca` , etc.``` AnyBrackets: Works like ```ci(, ci[, ci{, ci<, ca(, ca[, ca{, ca<, etc.``` Please give these changes a try and let me know your thoughts! ### Release Notes: - vim: Add AnyQuotes, AnyBrackets, MiniQuotes and MiniBrackets text objects --------- Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
parent
33abf1ee7c
commit
a09e5d255b
4 changed files with 536 additions and 11 deletions
|
@ -516,12 +516,14 @@
|
|||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"q": "vim::AnyQuotes",
|
||||
// "q": "vim::AnyQuotes",
|
||||
"q": "vim::MiniQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
// "b": "vim::AnyBrackets",
|
||||
// "b": "vim::MiniBrackets",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"r": "vim::SquareBrackets",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -167,6 +167,79 @@ Zed's vim mode includes some features that are usually provided by very popular
|
|||
- You can use `gR` to do [ReplaceWithRegister](https://github.com/vim-scripts/ReplaceWithRegister).
|
||||
- You can use `cx` for [vim-exchange](https://github.com/tommcdo/vim-exchange) functionality. Note that it does not have a default binding in visual mode, but you can add one to your keymap (refer to the [optional key bindings](#optional-key-bindings) section).
|
||||
- You can navigate to indent depths relative to your cursor with the [indent wise](https://github.com/jeetsukumaran/vim-indentwise) plugin `[-`, `]-`, `[+`, `]+`, `[=`, `]=`.
|
||||
- You can select quoted text with AnyQuotes and bracketed text with AnyBrackets text objects. Zed also provides MiniQuotes and MiniBrackets which offer alternative selection behavior based on the [mini.ai](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-ai.md) Neovim plugin. See the [Quote and Bracket text objects](#quote-and-bracket-text-objects) section below for details.
|
||||
- You can configure AnyQuotes, AnyBrackets, MiniQuotes, and MiniBrackets text objects for selecting quoted and bracketed text using different selection strategies. See the [Any Bracket Functionality](#any-bracket-functionality) section below for details.
|
||||
|
||||
### Any Bracket Functionality
|
||||
|
||||
Zed offers two different strategies for selecting text surrounded by any quote, or any bracket. These text objects are **not enabled by default** and must be configured in your keymap to be used.
|
||||
|
||||
#### Included Characters
|
||||
|
||||
Each text object type works with specific characters:
|
||||
|
||||
| Text Object | Characters |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------- |
|
||||
| AnyQuotes/MiniQuotes | Single quote (`'`), Double quote (`"`), Backtick (`` ` ``) |
|
||||
| AnyBrackets/MiniBrackets | Parentheses (`()`), Square brackets (`[]`), Curly braces (`{}`), Angle brackets (`<>`) |
|
||||
|
||||
Both "Any" and "Mini" variants work with the same character sets, but differ in their selection strategy.
|
||||
|
||||
#### AnyQuotes and AnyBrackets (Traditional Vim behavior)
|
||||
|
||||
These text objects implement traditional Vim behavior:
|
||||
|
||||
- **Selection priority**: Finds the innermost (closest) quotes or brackets first
|
||||
- **Fallback mechanism**: If none are found, falls back to the current line
|
||||
- **Character-based matching**: Focuses solely on open and close characters without considering syntax
|
||||
- **Vanilla Vim similarity**: AnyBrackets matches the behavior of commands like `ci<`, `ci(`, etc., in vanilla Vim, including potential edge cases (like considering `>` in `=>` as a closing delimiter)
|
||||
|
||||
#### MiniQuotes and MiniBrackets (mini.ai behavior)
|
||||
|
||||
These text objects implement the behavior of the [mini.ai](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-ai.md) Neovim plugin:
|
||||
|
||||
- **Selection priority**: Searches the current line first before expanding outward
|
||||
- **Tree-sitter integration**: Uses Tree-sitter queries for more context-aware selections
|
||||
- **Syntax-aware matching**: Can distinguish between actual brackets and similar characters in other contexts (like `>` in `=>`)
|
||||
|
||||
#### Choosing Between Approaches
|
||||
|
||||
- Use **AnyQuotes/AnyBrackets** if you:
|
||||
|
||||
- Prefer traditional Vim behavior
|
||||
- Want consistent character-based selection prioritizing innermost delimiters
|
||||
- Need behavior that closely matches vanilla Vim's text objects
|
||||
|
||||
- Use **MiniQuotes/MiniBrackets** if you:
|
||||
- Prefer the mini.ai plugin behavior
|
||||
- Want more context-aware selections using Tree-sitter
|
||||
- Prefer current-line priority when searching
|
||||
|
||||
#### Example Configuration
|
||||
|
||||
To use these text objects, you need to add bindings to your keymap. Here's an example configuration that makes them available when using text object operators (`i` and `a`) or change-surrounds (`cs`):
|
||||
|
||||
```json
|
||||
{
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
|
||||
"bindings": {
|
||||
// Traditional Vim behavior
|
||||
"q": "vim::AnyQuotes",
|
||||
"b": "vim::AnyBrackets",
|
||||
|
||||
// mini.ai plugin behavior
|
||||
"Q": "vim::MiniQuotes",
|
||||
"B": "vim::MiniBrackets"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With this configuration, you can use commands like:
|
||||
|
||||
- `cib` - Change inside brackets using AnyBrackets behavior
|
||||
- `cim` - Change inside brackets using MiniBrackets behavior
|
||||
- `ciq` - Change inside quotes using AnyQuotes behavior
|
||||
- `ciM` - Change inside quotes using MiniQuotes behavior
|
||||
|
||||
## Command palette
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue