diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1be3e8c9c1..d0c7ae192b 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -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 }] } }, { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index f97312e7f8..7ed97358ff 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -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::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> { + 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;