Skip to content

Commit

Permalink
vim: Add AnyQuotes support for unified quote handling similar to mini…
Browse files Browse the repository at this point in the history
….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>
  • Loading branch information
oca159 authored Jan 8, 2025
1 parent 811b872 commit 222b045
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 0 deletions.
1 change: 1 addition & 0 deletions assets/keymaps/vim.json
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"q": "vim::AnyQuotes",
"|": "vim::VerticalBars",
"(": "vim::Parentheses",
")": "vim::Parentheses",
Expand Down
151 changes: 151 additions & 0 deletions crates/vim/src/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub enum Object {
Paragraph,
Quotes,
BackQuotes,
AnyQuotes,
DoubleQuotes,
VerticalBars,
Parentheses,
Expand Down Expand Up @@ -61,6 +62,7 @@ actions!(
Paragraph,
Quotes,
BackQuotes,
AnyQuotes,
DoubleQuotes,
VerticalBars,
Parentheses,
Expand Down Expand Up @@ -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)
});
Expand Down Expand Up @@ -156,6 +161,7 @@ impl Object {
Object::Word { .. }
| Object::Quotes
| Object::BackQuotes
| Object::AnyQuotes
| Object::VerticalBars
| Object::DoubleQuotes => false,
Object::Sentence
Expand All @@ -182,6 +188,7 @@ impl Object {
| Object::IndentObj { .. } => false,
Object::Quotes
| Object::BackQuotes
| Object::AnyQuotes
| Object::DoubleQuotes
| Object::VerticalBars
| Object::Parentheses
Expand All @@ -200,6 +207,7 @@ impl Object {
Object::Word { .. }
| Object::Sentence
| Object::Quotes
| Object::AnyQuotes
| Object::BackQuotes
| Object::DoubleQuotes => {
if current_mode == Mode::VisualBlock {
Expand Down Expand Up @@ -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(), '"', '"')
}
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 222b045

Please sign in to comment.