vim: Add AnyQuotes support for unified quote handling similar to mini.ai nvim (#22263)

### Edit 1:
I tested it locally and it works!

### IMPORTANT: 
**Feedback and suggestions for improvement are greatly appreciated!**

This commit introduces a new AnyQuotes text object to handle text
surrounded by single quotes ('), double quotes ("), or back quotes (`)
seamlessly. The following changes are included:

- Added AnyQuotes to the Object enum to represent the new feature.
- Registered AnyQuotes as an action in the actions! macro and register
function to ensure proper integration with Vim actions like ci, ca, di,
and da.
- Extended Object::range to check for surrounding single, double, or
back quotes sequentially.
- Updated methods like is_multiline and always_expands_both_ways to
ensure consistent behavior with other text objects.
- Added support in surrounding_markers to evaluate any of the quote
types when AnyQuotes is invoked.
- This enhancement provides users with a flexible and unified way to
interact with text objects enclosed by different types of quotes.

Release Notes:

- vim: Add `aq`/`iq` "any quote" text objects that are the smallest of
`a"`, `a'` or <code>a`</code>
This commit is contained in:
Osvaldo 2025-01-08 03:00:20 +00:00 committed by GitHub
parent 811b872f4e
commit 222b04548d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 152 additions and 0 deletions

View file

@ -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>) {
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(|&quote| {
// 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;