vim: Add indent text object (#21121)

Added support for the popular vim
[indent-text-object](https://github.com/michaeljsmith/vim-indent-object).
This is especially useful in indentation-sensitive languages like
python.

Release Notes:

- vim: Added `vii`, `vai` and `vaI` for selecting
[indent-text-object](https://github.com/michaeljsmith/vim-indent-object)s.
This commit is contained in:
vultix 2024-11-26 13:54:36 -07:00 committed by GitHub
parent 64708527e7
commit 597e5f8304
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 168 additions and 5 deletions

View file

@ -381,7 +381,9 @@
"shift-b": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets",
"a": "vim::Argument"
"a": "vim::Argument",
"i": "vim::IndentObj",
"shift-i": ["vim::IndentObj", { "includeBelow": true }]
}
},
{

View file

@ -28,6 +28,7 @@ pub enum Object {
CurlyBrackets,
AngleBrackets,
Argument,
IndentObj { include_below: bool },
Tag,
}
@ -37,8 +38,14 @@ struct Word {
#[serde(default)]
ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct IndentObj {
#[serde(default)]
include_below: bool,
}
impl_actions!(vim, [Word]);
impl_actions!(vim, [Word, IndentObj]);
actions!(
vim,
@ -100,6 +107,13 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &Argument, cx| {
vim.object(Object::Argument, cx)
});
Vim::action(
editor,
cx,
|vim, &IndentObj { include_below }: &IndentObj, cx| {
vim.object(Object::IndentObj { include_below }, cx)
},
);
}
impl Vim {
@ -129,13 +143,18 @@ impl Object {
| Object::AngleBrackets
| Object::CurlyBrackets
| Object::SquareBrackets
| Object::Argument => true,
| Object::Argument
| Object::IndentObj { .. } => true,
}
}
pub fn always_expands_both_ways(self) -> bool {
match self {
Object::Word { .. } | Object::Sentence | Object::Paragraph | Object::Argument => false,
Object::Word { .. }
| Object::Sentence
| Object::Paragraph
| Object::Argument
| Object::IndentObj { .. } => false,
Object::Quotes
| Object::BackQuotes
| Object::DoubleQuotes
@ -167,7 +186,8 @@ impl Object {
| Object::AngleBrackets
| Object::VerticalBars
| Object::Tag
| Object::Argument => Mode::Visual,
| Object::Argument
| Object::IndentObj { .. } => Mode::Visual,
Object::Paragraph => Mode::VisualLine,
}
}
@ -219,6 +239,7 @@ impl Object {
surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
}
Object::Argument => argument(map, relative_to, around),
Object::IndentObj { include_below } => indent(map, relative_to, around, include_below),
}
}
@ -569,6 +590,58 @@ fn argument(
}
}
fn indent(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
include_below: bool,
) -> Option<Range<DisplayPoint>> {
let point = relative_to.to_point(map);
let row = point.row;
let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
// Loop backwards until we find a non-blank line with less indent
let mut start_row = row;
for prev_row in (0..row).rev() {
let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row));
if indent.is_line_empty() {
continue;
}
if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
if around {
// When around is true, include the first line with less indent
start_row = prev_row;
}
break;
}
start_row = prev_row;
}
// Loop forwards until we find a non-blank line with less indent
let mut end_row = row;
let max_rows = map.max_buffer_row().0;
for next_row in (row + 1)..=max_rows {
let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row));
if indent.is_line_empty() {
continue;
}
if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs {
if around && include_below {
// When around is true and including below, include this line
end_row = next_row;
}
break;
}
end_row = next_row;
}
let end_len = map.buffer_snapshot.line_len(MultiBufferRow(end_row));
let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right);
let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left);
Some(start..end)
}
fn sentence(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
@ -1458,6 +1531,94 @@ mod test {
cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual);
}
#[gpui::test]
async fn test_indent_object(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// Base use case
cx.set_state(
indoc! {"
fn boop() {
// Comment
baz();ˇ
loop {
bar(1);
bar(2);
}
result
}
"},
Mode::Normal,
);
cx.simulate_keystrokes("v i i");
cx.assert_state(
indoc! {"
fn boop() {
« // Comment
baz();
loop {
bar(1);
bar(2);
}
resultˇ»
}
"},
Mode::Visual,
);
// Around indent (include line above)
cx.set_state(
indoc! {"
const ABOVE: str = true;
fn boop() {
hello();
worˇld()
}
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a i");
cx.assert_state(
indoc! {"
const ABOVE: str = true;
«fn boop() {
hello();
world()ˇ»
}
"},
Mode::Visual,
);
// Around indent (include line above & below)
cx.set_state(
indoc! {"
const ABOVE: str = true;
fn boop() {
hellˇo();
world()
}
const BELOW: str = true;
"},
Mode::Normal,
);
cx.simulate_keystrokes("c a shift-i");
cx.assert_state(
indoc! {"
const ABOVE: str = true;
ˇ
const BELOW: str = true;
"},
Mode::Insert,
);
}
#[gpui::test]
async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;