@@ -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;