From e36a2f27397ee822d6899ce63dd4a504d4afc620 Mon Sep 17 00:00:00 2001 From: 5brian Date: Tue, 8 Apr 2025 11:07:37 -0400 Subject: [PATCH] vim: Add indent-wise motions (#28044) Taken from: https://github.com/jeetsukumaran/vim-indentwise?tab=readme-ov-file#movements-by-relative-indent-depth > [- : Move to previous line of lesser indent than the current line. > [+ : Move to previous line of greater indent than the current line. > [= : Move to previous line of same indent as the current line that is separated from the current line by lines of different indents. > ]- : Move to next line of lesser indent than the current line. > ]+ : Move to next line of greater indent than the current line. > ]= : Move to next line of same indent as the current line that is separated from the current line by lines of different indents. Release Notes: - vim: Added indent-wise motions `] -/+/=` --- assets/keymaps/vim.json | 6 ++ crates/vim/src/motion.rs | 222 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 8197e40288..163020c5e8 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -44,6 +44,12 @@ "[ /": "vim::PreviousComment", "] *": "vim::NextComment", "] /": "vim::NextComment", + "[ -": "vim::PreviousLesserIndent", + "[ +": "vim::PreviousGreaterIndent", + "[ =": "vim::PreviousSameIndent", + "] -": "vim::NextLesserIndent", + "] +": "vim::NextGreaterIndent", + "] =": "vim::NextSameIndent", // Word motions "w": "vim::NextWordStart", "e": "vim::NextWordEnd", diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 7934739632..b80489d732 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -147,6 +147,12 @@ pub enum Motion { PreviousMethodEnd, NextComment, PreviousComment, + PreviousLesserIndent, + PreviousGreaterIndent, + PreviousSameIndent, + NextLesserIndent, + NextGreaterIndent, + NextSameIndent, // we don't have a good way to run a search synchronously, so // we handle search motions by running the search async and then @@ -161,6 +167,13 @@ pub enum Motion { }, } +#[derive(Clone, Copy)] +enum IndentType { + Lesser, + Greater, + Same, +} + #[derive(Clone, Deserialize, JsonSchema, PartialEq)] #[serde(deny_unknown_fields)] struct NextWordStart { @@ -323,6 +336,12 @@ actions!( PreviousMethodEnd, NextComment, PreviousComment, + PreviousLesserIndent, + PreviousGreaterIndent, + PreviousSameIndent, + NextLesserIndent, + NextGreaterIndent, + NextSameIndent, ] ); @@ -572,6 +591,24 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, &PreviousComment, window, cx| { vim.motion(Motion::PreviousComment, window, cx) }); + Vim::action(editor, cx, |vim, &PreviousLesserIndent, window, cx| { + vim.motion(Motion::PreviousLesserIndent, window, cx) + }); + Vim::action(editor, cx, |vim, &PreviousGreaterIndent, window, cx| { + vim.motion(Motion::PreviousGreaterIndent, window, cx) + }); + Vim::action(editor, cx, |vim, &PreviousSameIndent, window, cx| { + vim.motion(Motion::PreviousSameIndent, window, cx) + }); + Vim::action(editor, cx, |vim, &NextLesserIndent, window, cx| { + vim.motion(Motion::NextLesserIndent, window, cx) + }); + Vim::action(editor, cx, |vim, &NextGreaterIndent, window, cx| { + vim.motion(Motion::NextGreaterIndent, window, cx) + }); + Vim::action(editor, cx, |vim, &NextSameIndent, window, cx| { + vim.motion(Motion::NextSameIndent, window, cx) + }); } impl Vim { @@ -666,6 +703,12 @@ impl Motion { | PreviousMethodEnd | NextComment | PreviousComment + | PreviousLesserIndent + | PreviousGreaterIndent + | PreviousSameIndent + | NextLesserIndent + | NextGreaterIndent + | NextSameIndent | GoToPercentage | Jump { line: true, .. } => MotionKind::Linewise, EndOfLine { .. } @@ -765,6 +808,12 @@ impl Motion { | PreviousMethodEnd | NextComment | PreviousComment + | PreviousLesserIndent + | PreviousGreaterIndent + | PreviousSameIndent + | NextLesserIndent + | NextGreaterIndent + | NextSameIndent | Jump { .. } => false, } } @@ -1109,6 +1158,30 @@ impl Motion { comment_motion(map, point, times, Direction::Prev), SelectionGoal::None, ), + PreviousLesserIndent => ( + indent_motion(map, point, times, Direction::Prev, IndentType::Lesser), + SelectionGoal::None, + ), + PreviousGreaterIndent => ( + indent_motion(map, point, times, Direction::Prev, IndentType::Greater), + SelectionGoal::None, + ), + PreviousSameIndent => ( + indent_motion(map, point, times, Direction::Prev, IndentType::Same), + SelectionGoal::None, + ), + NextLesserIndent => ( + indent_motion(map, point, times, Direction::Next, IndentType::Lesser), + SelectionGoal::None, + ), + NextGreaterIndent => ( + indent_motion(map, point, times, Direction::Next, IndentType::Greater), + SelectionGoal::None, + ), + NextSameIndent => ( + indent_motion(map, point, times, Direction::Next, IndentType::Same), + SelectionGoal::None, + ), }; (new_point != point || infallible).then_some((new_point, goal)) @@ -2725,6 +2798,67 @@ fn section_motion( display_point } +fn matches_indent_type( + target_indent: &text::LineIndent, + current_indent: &text::LineIndent, + indent_type: IndentType, +) -> bool { + match indent_type { + IndentType::Lesser => { + target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs + } + IndentType::Greater => { + target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs + } + IndentType::Same => { + target_indent.spaces == current_indent.spaces + && target_indent.tabs == current_indent.tabs + } + } +} + +fn indent_motion( + map: &DisplaySnapshot, + mut display_point: DisplayPoint, + times: usize, + direction: Direction, + indent_type: IndentType, +) -> DisplayPoint { + let buffer_point = map.display_point_to_point(display_point, Bias::Left); + let current_row = MultiBufferRow(buffer_point.row); + let current_indent = map.line_indent_for_buffer_row(current_row); + if current_indent.is_line_empty() { + return display_point; + } + let max_row = map.max_point().to_point(map).row; + + for _ in 0..times { + let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row; + + let target_row = match direction { + Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| { + let indent = map.line_indent_for_buffer_row(MultiBufferRow(row)); + !indent.is_line_empty() + && matches_indent_type(&indent, ¤t_indent, indent_type) + }), + Direction::Prev => (0..current_buffer_row).rev().find(|&row| { + let indent = map.line_indent_for_buffer_row(MultiBufferRow(row)); + !indent.is_line_empty() + && matches_indent_type(&indent, ¤t_indent, indent_type) + }), + } + .unwrap_or(current_buffer_row); + + let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right); + let new_point = first_non_whitespace(map, false, new_point); + if new_point == display_point { + break; + } + display_point = new_point; + } + display_point +} + #[cfg(test)] mod test { @@ -3594,4 +3728,92 @@ mod test { πππˇπ πanotherline"}); } + + #[gpui::test] + async fn test_go_to_indent(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state( + indoc! { + "func empty(a string) bool { + ˇif a == \"\" { + return true + } + return false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("[ -"); + cx.assert_state( + indoc! { + "ˇfunc empty(a string) bool { + if a == \"\" { + return true + } + return false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("] ="); + cx.assert_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + return true + } + return false + ˇ}" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("[ +"); + cx.assert_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + return true + } + ˇreturn false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("2 [ ="); + cx.assert_state( + indoc! { + "func empty(a string) bool { + ˇif a == \"\" { + return true + } + return false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("] +"); + cx.assert_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + ˇreturn true + } + return false + }" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("] -"); + cx.assert_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + return true + ˇ} + return false + }" + }, + Mode::Normal, + ); + } }