From 0280d5d01092d03c854267c42c0c64d45ff1a586 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 12:53:20 -0600 Subject: [PATCH 1/3] vim change for wrapped lines --- crates/vim/src/motion.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 8cd29e5e9f..a85c6fc0a3 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{cmp, sync::Arc}; use editor::{ char_kind, @@ -404,9 +404,13 @@ fn down( mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { + let start_row = point.to_point(map).row; + let target = cmp::min(map.max_buffer_row(), start_row + times as u32); + + while point.to_point(map).row < target { (point, goal) = movement::down(map, point, goal, true); } + (point, goal) } @@ -416,7 +420,10 @@ fn up( mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { + let start_row = point.to_point(map).row; + let target = start_row.saturating_sub(times as u32); + + while point.to_point(map).row > target { (point, goal) = movement::up(map, point, goal, true); } (point, goal) From 20aa2a4c54df6268c2c18b36af4c36e599583138 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 22:11:51 -0600 Subject: [PATCH 2/3] vim: Fix relative line motion Before this change up and down were in display co-ordinates, after this change they are in fold coordinates (which matches the vim behaviour). To make this work without causing usabliity problems, a bunch of extra keyboard shortcuts now work: - vim: `z {o,c}` to open,close a fold - vim: `z f` to fold current visual selection - vim: `g {j,k,up,down}` to move up/down a display line - vim: `g {0,^,$,home,end}` to get to start/end of a display line Fixes: zed-industries/community#1562 --- assets/keymaps/vim.json | 57 ++++ crates/editor/src/display_map.rs | 17 +- crates/editor/src/editor.rs | 14 +- crates/vim/src/motion.rs | 267 ++++++++++++++---- crates/vim/src/normal.rs | 27 +- crates/vim/src/normal/change.rs | 6 +- crates/vim/src/normal/substitute.rs | 5 +- crates/vim/src/test.rs | 142 ++++++++++ .../src/test/neovim_backed_test_context.rs | 28 +- crates/vim/src/test/neovim_connection.rs | 24 ++ crates/vim/src/visual.rs | 11 +- crates/vim/test_data/test_folds.json | 23 ++ crates/vim/test_data/test_wrapped_lines.json | 26 ++ 13 files changed, 580 insertions(+), 67 deletions(-) create mode 100644 crates/vim/test_data/test_folds.json create mode 100644 crates/vim/test_data/test_wrapped_lines.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c0de3420f2..c7e6199f44 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -137,10 +137,67 @@ "partialWord": true } ], + "g j": [ + "vim::Down", + { + "displayLines": true + } + ], + "g down": [ + "vim::Down", + { + "displayLines": true + } + ], + "g k": [ + "vim::Up", + { + "displayLines": true + } + ], + "g up": [ + "vim::Up", + { + "displayLines": true + } + ], + "g $": [ + "vim::EndOfLine", + { + "displayLines": true + } + ], + "g end": [ + "vim::EndOfLine", + { + "displayLines": true + } + ], + "g 0": [ + "vim::StartOfLine", + { + "displayLines": true + } + ], + "g home": [ + "vim::StartOfLine", + { + "displayLines": true + } + ], + "g ^": [ + "vim::FirstNonWhitespace", + { + "displayLines": true + } + ], // z commands "z t": "editor::ScrollCursorTop", "z z": "editor::ScrollCursorCenter", "z b": "editor::ScrollCursorBottom", + "z c": "editor::Fold", + "z o": "editor::UnfoldLines", + "z f": "editor::FoldSelectedRanges", // Count support "1": [ "vim::Number", diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 611866bcad..fae4109b94 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -30,6 +30,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; +pub use self::fold_map::FoldPoint; pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -310,7 +311,7 @@ impl DisplayMap { pub struct DisplaySnapshot { pub buffer_snapshot: MultiBufferSnapshot, - fold_snapshot: fold_map::FoldSnapshot, + pub fold_snapshot: fold_map::FoldSnapshot, inlay_snapshot: inlay_map::InlaySnapshot, tab_snapshot: tab_map::TabSnapshot, wrap_snapshot: wrap_map::WrapSnapshot, @@ -438,6 +439,20 @@ impl DisplaySnapshot { fold_point.to_inlay_point(&self.fold_snapshot) } + pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint { + let block_point = point.0; + let wrap_point = self.block_snapshot.to_wrap_point(block_point); + let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); + self.tab_snapshot.to_fold_point(tab_point, bias).0 + } + + pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint { + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) + } + pub fn max_point(&self) -> DisplayPoint { DisplayPoint(self.block_snapshot.max_point()) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c5ff1f027d..ac9a972f96 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7198,7 +7198,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); + let selections = self.selections.all_adjusted(cx); for selection in selections { let range = selection.range().sorted(); let buffer_start_row = range.start.row; @@ -7274,7 +7274,17 @@ impl Editor { pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { let selections = self.selections.all::(cx); - let ranges = selections.into_iter().map(|s| s.start..s.end); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let line_mode = self.selections.line_mode; + let ranges = selections.into_iter().map(|s| { + if line_mode { + let start = Point::new(s.start.row, 0); + let end = Point::new(s.end.row, display_map.buffer_snapshot.line_len(s.end.row)); + start..end + } else { + s.start..s.end + } + }); self.fold_ranges(ranges, true, cx); } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a85c6fc0a3..2edbb8ff1c 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2,7 +2,7 @@ use std::{cmp, sync::Arc}; use editor::{ char_kind, - display_map::{DisplaySnapshot, ToDisplayPoint}, + display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, movement, Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; @@ -21,16 +21,16 @@ use crate::{ pub enum Motion { Left, Backspace, - Down, - Up, + Down { display_lines: bool }, + Up { display_lines: bool }, Right, NextWordStart { ignore_punctuation: bool }, NextWordEnd { ignore_punctuation: bool }, PreviousWordStart { ignore_punctuation: bool }, - FirstNonWhitespace, + FirstNonWhitespace { display_lines: bool }, CurrentLine, - StartOfLine, - EndOfLine, + StartOfLine { display_lines: bool }, + EndOfLine { display_lines: bool }, StartOfParagraph, EndOfParagraph, StartOfDocument, @@ -62,6 +62,41 @@ struct PreviousWordStart { ignore_punctuation: bool, } +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct Up { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct Down { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct FirstNonWhitespace { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct EndOfLine { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct StartOfLine { + #[serde(default)] + display_lines: bool, +} + #[derive(Clone, Deserialize, PartialEq)] struct RepeatFind { #[serde(default)] @@ -73,12 +108,7 @@ actions!( [ Left, Backspace, - Down, - Up, Right, - FirstNonWhitespace, - StartOfLine, - EndOfLine, CurrentLine, StartOfParagraph, EndOfParagraph, @@ -90,20 +120,63 @@ actions!( ); impl_actions!( vim, - [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind] + [ + NextWordStart, + NextWordEnd, + PreviousWordStart, + RepeatFind, + Up, + Down, + FirstNonWhitespace, + EndOfLine, + StartOfLine, + ] ); pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx)); cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx)); - cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx)); - cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx)); - cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); - cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| { - motion(Motion::FirstNonWhitespace, cx) + cx.add_action(|_: &mut Workspace, action: &Down, cx: _| { + motion( + Motion::Down { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &Up, cx: _| { + motion( + Motion::Up { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); + cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| { + motion( + Motion::FirstNonWhitespace { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| { + motion( + Motion::StartOfLine { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| { + motion( + Motion::EndOfLine { + display_lines: action.display_lines, + }, + cx, + ) }); - cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx)); - cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx)); cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| { motion(Motion::StartOfParagraph, cx) @@ -192,19 +265,25 @@ impl Motion { pub fn linewise(&self) -> bool { use Motion::*; match self { - Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart - | StartOfParagraph | EndOfParagraph => true, - EndOfLine + Down { .. } + | Up { .. } + | StartOfDocument + | EndOfDocument + | CurrentLine + | NextLineStart + | StartOfParagraph + | EndOfParagraph => true, + EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } | Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } => false, } } @@ -213,21 +292,21 @@ impl Motion { use Motion::*; match self { StartOfDocument | EndOfDocument | CurrentLine => true, - Down - | Up - | EndOfLine + Down { .. } + | Up { .. } + | EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } | Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | StartOfParagraph | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } | NextLineStart => false, } @@ -236,12 +315,12 @@ impl Motion { pub fn inclusive(&self) -> bool { use Motion::*; match self { - Down - | Up + Down { .. } + | Up { .. } | StartOfDocument | EndOfDocument | CurrentLine - | EndOfLine + | EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } @@ -249,12 +328,12 @@ impl Motion { Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | StartOfParagraph | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } => false, } } @@ -272,8 +351,18 @@ impl Motion { let (new_point, goal) = match self { Left => (left(map, point, times), SelectionGoal::None), Backspace => (backspace(map, point, times), SelectionGoal::None), - Down => down(map, point, goal, times), - Up => up(map, point, goal, times), + Down { + display_lines: false, + } => down(map, point, goal, times), + Down { + display_lines: true, + } => down_display(map, point, goal, times), + Up { + display_lines: false, + } => up(map, point, goal, times), + Up { + display_lines: true, + } => up_display(map, point, goal, times), Right => (right(map, point, times), SelectionGoal::None), NextWordStart { ignore_punctuation } => ( next_word_start(map, point, *ignore_punctuation, times), @@ -287,9 +376,17 @@ impl Motion { previous_word_start(map, point, *ignore_punctuation, times), SelectionGoal::None, ), - FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), - StartOfLine => (start_of_line(map, point), SelectionGoal::None), - EndOfLine => (end_of_line(map, point), SelectionGoal::None), + FirstNonWhitespace { display_lines } => ( + first_non_whitespace(map, *display_lines, point), + SelectionGoal::None, + ), + StartOfLine { display_lines } => ( + start_of_line(map, *display_lines, point), + SelectionGoal::None, + ), + EndOfLine { display_lines } => { + (end_of_line(map, *display_lines, point), SelectionGoal::None) + } StartOfParagraph => ( movement::start_of_paragraph(map, point, times), SelectionGoal::None, @@ -298,7 +395,7 @@ impl Motion { map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), SelectionGoal::None, ), - CurrentLine => (end_of_line(map, point), SelectionGoal::None), + CurrentLine => (end_of_line(map, false, point), SelectionGoal::None), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), EndOfDocument => ( end_of_document(map, point, maybe_times), @@ -399,15 +496,39 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di } fn down( + map: &DisplaySnapshot, + point: DisplayPoint, + mut goal: SelectionGoal, + times: usize, +) -> (DisplayPoint, SelectionGoal) { + let start = map.display_point_to_fold_point(point, Bias::Left); + + let goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => { + goal = SelectionGoal::Column(start.column()); + start.column() + } + }; + + let new_row = cmp::min( + start.row() + times as u32, + map.buffer_snapshot.max_point().row, + ); + let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); + let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col)); + + (map.clip_point(point, Bias::Left), goal) +} + +fn down_display( map: &DisplaySnapshot, mut point: DisplayPoint, mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - let start_row = point.to_point(map).row; - let target = cmp::min(map.max_buffer_row(), start_row + times as u32); - - while point.to_point(map).row < target { + for _ in 0..times { (point, goal) = movement::down(map, point, goal, true); } @@ -415,17 +536,39 @@ fn down( } fn up( + map: &DisplaySnapshot, + point: DisplayPoint, + mut goal: SelectionGoal, + times: usize, +) -> (DisplayPoint, SelectionGoal) { + let start = map.display_point_to_fold_point(point, Bias::Left); + + let goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => { + goal = SelectionGoal::Column(start.column()); + start.column() + } + }; + + let new_row = start.row().saturating_sub(times as u32); + let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); + let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col)); + + (map.clip_point(point, Bias::Left), goal) +} + +fn up_display( map: &DisplaySnapshot, mut point: DisplayPoint, mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - let start_row = point.to_point(map).row; - let target = start_row.saturating_sub(times as u32); - - while point.to_point(map).row > target { + for _ in 0..times { (point, goal) = movement::up(map, point, goal, true); } + (point, goal) } @@ -516,8 +659,12 @@ fn previous_word_start( point } -fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint { - let mut last_point = DisplayPoint::new(from.row(), 0); +fn first_non_whitespace( + map: &DisplaySnapshot, + display_lines: bool, + from: DisplayPoint, +) -> DisplayPoint { + let mut last_point = start_of_line(map, display_lines, from); let language = map.buffer_snapshot.language_at(from.to_point(map)); for (ch, point) in map.chars_at(last_point) { if ch == '\n' { @@ -534,12 +681,23 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi map.clip_point(last_point, Bias::Left) } -fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - map.prev_line_boundary(point.to_point(map)).1 +fn start_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { + if display_lines { + map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right) + } else { + map.prev_line_boundary(point.to_point(map)).1 + } } -fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) +fn end_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { + if display_lines { + map.clip_point( + DisplayPoint::new(point.row(), map.line_len(point.row())), + Bias::Left, + ) + } else { + map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) + } } fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint { @@ -664,6 +822,7 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> let new_row = (point.row() + times as u32).min(map.max_buffer_row()); first_non_whitespace( map, + false, map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left), ) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 3a2d15a878..2b03632c42 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -78,13 +78,27 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); - change_motion(vim, Motion::EndOfLine, times, cx); + change_motion( + vim, + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }) }); cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); - delete_motion(vim, Motion::EndOfLine, times, cx); + delete_motion( + vim, + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }) }); scroll::init(cx); @@ -165,7 +179,10 @@ fn insert_first_non_whitespace( vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::FirstNonWhitespace.move_point(map, cursor, goal, None) + Motion::FirstNonWhitespace { + display_lines: false, + } + .move_point(map, cursor, goal, None) }); }); }); @@ -178,7 +195,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::EndOfLine.move_point(map, cursor, goal, None) + Motion::CurrentLine.move_point(map, cursor, goal, None) }); }); }); @@ -238,7 +255,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex }); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::EndOfLine.move_point(map, cursor, goal, None) + Motion::CurrentLine.move_point(map, cursor, goal, None) }); }); editor.edit_with_autoindent(edits, cx); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 50bc049a3a..5591de89c6 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -10,7 +10,11 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m // Some motions ignore failure when switching to normal mode let mut motion_succeeded = matches!( motion, - Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine + Motion::Left + | Motion::Right + | Motion::EndOfLine { .. } + | Motion::Backspace + | Motion::StartOfLine { .. } ); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 1d53c6831c..b04596240a 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -15,7 +15,10 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { } if line_mode { Motion::CurrentLine.expand_selection(map, selection, None, false); - if let Some((point, _)) = Motion::FirstNonWhitespace.move_point( + if let Some((point, _)) = (Motion::FirstNonWhitespace { + display_lines: false, + }) + .move_point( map, selection.start, selection.goal, diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 9cd927601f..cfde221dc5 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -285,3 +285,145 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) { Mode::Visual, ) } + +#[gpui::test] +async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_wrap(12).await; + // tests line wrap as follows: + // 1: twelve char + // twelve char + // 2: twelve char + cx.set_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["j"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + tˇwelve char + "}) + .await; + cx.simulate_shared_keystrokes(["k"]).await; + cx.assert_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["g", "j"]).await; + cx.assert_shared_state(indoc! { " + twelve char tˇwelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["g", "j"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + tˇwelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "k"]).await; + cx.assert_shared_state(indoc! { " + twelve char tˇwelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "^"]).await; + cx.assert_shared_state(indoc! { " + twelve char ˇtwelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["^"]).await; + cx.assert_shared_state(indoc! { " + ˇtwelve char twelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "$"]).await; + cx.assert_shared_state(indoc! { " + twelve charˇ twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["$"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve chaˇr + twelve char + "}) + .await; +} + +#[gpui::test] +async fn test_folds(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_neovim_option("foldmethod=manual").await; + + cx.set_shared_state(indoc! { " + fn boop() { + ˇbarp() + bazp() + } + "}) + .await; + cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"]) + .await; + + // visual display is now: + // fn boop () { + // [FOLDED] + // } + + // TODO: this should not be needed but currently zf does not + // return to normal mode. + cx.simulate_shared_keystrokes(["escape"]).await; + + // skip over fold downward + cx.simulate_shared_keystrokes(["g", "g"]).await; + cx.assert_shared_state(indoc! { " + ˇfn boop() { + barp() + bazp() + } + "}) + .await; + + cx.simulate_shared_keystrokes(["j", "j"]).await; + cx.assert_shared_state(indoc! { " + fn boop() { + barp() + bazp() + ˇ} + "}) + .await; + + // skip over fold upward + cx.simulate_shared_keystrokes(["2", "k"]).await; + cx.assert_shared_state(indoc! { " + ˇfn boop() { + barp() + bazp() + } + "}) + .await; + + // yank the fold + cx.simulate_shared_keystrokes(["down", "y", "y"]).await; + cx.assert_shared_clipboard(" barp()\n bazp()\n").await; + + // re-open + cx.simulate_shared_keystrokes(["z", "o"]).await; + cx.assert_shared_state(indoc! { " + fn boop() { + ˇ barp() + bazp() + } + "}) + .await; +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index f4b0e96183..bc37f2fdd6 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,9 +1,14 @@ +use editor::EditorSettings; use indoc::indoc; +use settings::SettingsStore; use std::ops::{Deref, DerefMut, Range}; use collections::{HashMap, HashSet}; use gpui::ContextHandle; -use language::OffsetRangeExt; +use language::{ + language_settings::{AllLanguageSettings, LanguageSettings, SoftWrap}, + OffsetRangeExt, +}; use util::test::{generate_marked_text, marked_text_offsets}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; @@ -127,6 +132,27 @@ impl<'a> NeovimBackedTestContext<'a> { context_handle } + pub async fn set_shared_wrap(&mut self, columns: u32) { + if columns < 12 { + panic!("nvim doesn't support columns < 12") + } + self.neovim.set_option("wrap").await; + self.neovim.set_option("columns=12").await; + + self.update(|cx| { + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength); + settings.defaults.preferred_line_length = Some(columns); + }); + }) + }) + } + + pub async fn set_neovim_option(&mut self, option: &str) { + self.neovim.set_option(option).await; + } + pub async fn assert_shared_state(&mut self, marked_text: &str) { let neovim = self.neovim_state().await; let editor = self.editor_state(); diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 68f3374772..3e59080b13 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -41,6 +41,7 @@ pub enum NeovimData { Key(String), Get { state: String, mode: Option }, ReadRegister { name: char, value: String }, + SetOption { value: String }, } pub struct NeovimConnection { @@ -222,6 +223,29 @@ impl NeovimConnection { ); } + #[cfg(feature = "neovim")] + pub async fn set_option(&mut self, value: &str) { + self.nvim + .command_output(format!("set {}", value).as_str()) + .await + .unwrap(); + + self.data.push_back(NeovimData::SetOption { + value: value.to_string(), + }) + } + + #[cfg(not(feature = "neovim"))] + pub async fn set_option(&mut self, value: &str) { + assert_eq!( + self.data.pop_front(), + Some(NeovimData::SetOption { + value: value.to_string(), + }), + "operation does not match recorded script. re-record with --features=neovim" + ); + } + #[cfg(not(feature = "neovim"))] pub async fn read_register(&mut self, register: char) -> String { if let Some(NeovimData::Get { .. }) = self.data.front() { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index b68da870f0..ee46a0d209 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -51,8 +51,15 @@ pub fn init(cx: &mut AppContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - if vim.state().mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { - let is_up_or_down = matches!(motion, Motion::Up | Motion::Down); + if vim.state().mode == Mode::VisualBlock + && !matches!( + motion, + Motion::EndOfLine { + display_lines: false + } + ) + { + let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { motion.move_point(map, point, goal, times) }) diff --git a/crates/vim/test_data/test_folds.json b/crates/vim/test_data/test_folds.json new file mode 100644 index 0000000000..668df5ce26 --- /dev/null +++ b/crates/vim/test_data/test_folds.json @@ -0,0 +1,23 @@ +{"SetOption":{"value":"foldmethod=manual"}} +{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} +{"Key":"shift-v"} +{"Key":"j"} +{"Key":"z"} +{"Key":"f"} +{"Key":"escape"} +{"Key":"g"} +{"Key":"g"} +{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} +{"Key":"j"} +{"Key":"j"} +{"Get":{"state":"fn boop() {\n barp()\n bazp()\nˇ}\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"k"} +{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} +{"Key":"down"} +{"Key":"y"} +{"Key":"y"} +{"ReadRegister":{"name":"\"","value":" barp()\n bazp()\n"}} +{"Key":"z"} +{"Key":"o"} +{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json new file mode 100644 index 0000000000..f9f54c5c43 --- /dev/null +++ b/crates/vim/test_data/test_wrapped_lines.json @@ -0,0 +1,26 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=12"}} +{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} +{"Key":"j"} +{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} +{"Key":"k"} +{"Get":{"state":"tˇwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"j"} +{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"j"} +{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"k"} +{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"^"} +{"Get":{"state":"twelve char ˇtwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"^"} +{"Get":{"state":"ˇtwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"$"} +{"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"$"} +{"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}} From dee1a433dd7be8dbc0fc5590882ddc4017b7a281 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 22:48:58 -0600 Subject: [PATCH 3/3] A few more fixes for wrapped line motions --- crates/vim/src/motion.rs | 22 +++--- crates/vim/src/normal.rs | 23 +++--- crates/vim/src/test.rs | 73 +++++++++++++++++++ .../src/test/neovim_backed_test_context.rs | 3 +- crates/vim/test_data/test_wrapped_lines.json | 24 ++++++ 5 files changed, 123 insertions(+), 22 deletions(-) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 2edbb8ff1c..0d3fb700ef 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -535,7 +535,7 @@ fn down_display( (point, goal) } -fn up( +pub(crate) fn up( map: &DisplaySnapshot, point: DisplayPoint, mut goal: SelectionGoal, @@ -681,7 +681,11 @@ fn first_non_whitespace( map.clip_point(last_point, Bias::Left) } -fn start_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { +pub(crate) fn start_of_line( + map: &DisplaySnapshot, + display_lines: bool, + point: DisplayPoint, +) -> DisplayPoint { if display_lines { map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right) } else { @@ -689,7 +693,11 @@ fn start_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint } } -fn end_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { +pub(crate) fn end_of_line( + map: &DisplaySnapshot, + display_lines: bool, + point: DisplayPoint, +) -> DisplayPoint { if display_lines { map.clip_point( DisplayPoint::new(point.row(), map.line_len(point.row())), @@ -819,12 +827,8 @@ fn find_backward( } fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let new_row = (point.row() + times as u32).min(map.max_buffer_row()); - first_non_whitespace( - map, - false, - map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left), - ) + let correct_line = down(map, point, SelectionGoal::None, times).0; + first_non_whitespace(map, false, correct_line) } #[cfg(test)] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 2b03632c42..a73c518809 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -10,7 +10,7 @@ mod yank; use std::sync::Arc; use crate::{ - motion::Motion, + motion::{self, Motion}, object::Object, state::{Mode, Operator}, Vim, @@ -214,19 +214,19 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex .collect(); let edits = selection_start_rows.into_iter().map(|row| { let (indent, _) = map.line_indent(row); - let start_of_line = map - .clip_point(DisplayPoint::new(row, 0), Bias::Left) - .to_point(&map); + let start_of_line = + motion::start_of_line(&map, false, DisplayPoint::new(row, 0)) + .to_point(&map); let mut new_text = " ".repeat(indent as usize); new_text.push('\n'); (start_of_line..start_of_line, new_text) }); editor.edit_with_autoindent(edits, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_cursors_with(|map, mut cursor, _| { - *cursor.row_mut() -= 1; - *cursor.column_mut() = map.line_len(cursor.row()); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + s.move_cursors_with(|map, cursor, _| { + let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0; + let insert_point = motion::end_of_line(map, false, previous_line); + (insert_point, SelectionGoal::None) }); }); }); @@ -240,15 +240,16 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { let (map, old_selections) = editor.selections.all_display(cx); + let selection_end_rows: HashSet = old_selections .into_iter() .map(|selection| selection.end.row()) .collect(); let edits = selection_end_rows.into_iter().map(|row| { let (indent, _) = map.line_indent(row); - let end_of_line = map - .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left) - .to_point(&map); + let end_of_line = + motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map); + let mut new_text = "\n".to_string(); new_text.push_str(&" ".repeat(indent as usize)); (end_of_line..end_of_line, new_text) diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index cfde221dc5..88fa375851 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -358,6 +358,79 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { twelve char "}) .await; + + cx.set_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["enter"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + ˇtwelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["o", "o", "escape"]).await; + cx.assert_shared_state(indoc! { " + twelve char + twelve char twelve char + ˇo + twelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-a", "a", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + twelve char twelve charˇa + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-i", "i", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + ˇitwelve char twelve chara + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-d"]).await; + cx.assert_shared_state(indoc! { " + twelve char + ˇ + twelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + twelve char tˇwelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-o", "o", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + ˇo + twelve char twelve char + twelve char + "}) + .await; } #[gpui::test] diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index bc37f2fdd6..d04b1b7768 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,4 +1,3 @@ -use editor::EditorSettings; use indoc::indoc; use settings::SettingsStore; use std::ops::{Deref, DerefMut, Range}; @@ -6,7 +5,7 @@ use std::ops::{Deref, DerefMut, Range}; use collections::{HashMap, HashSet}; use gpui::ContextHandle; use language::{ - language_settings::{AllLanguageSettings, LanguageSettings, SoftWrap}, + language_settings::{AllLanguageSettings, SoftWrap}, OffsetRangeExt, }; use util::test::{generate_marked_text, marked_text_offsets}; diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json index f9f54c5c43..1ebbd4f205 100644 --- a/crates/vim/test_data/test_wrapped_lines.json +++ b/crates/vim/test_data/test_wrapped_lines.json @@ -24,3 +24,27 @@ {"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}} {"Key":"$"} {"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} +{"Key":"enter"} +{"Get":{"state":"twelve char twelve char\nˇtwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} +{"Key":"o"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"twelve char\ntwelve char twelve char\nˇo\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} +{"Key":"shift-a"} +{"Key":"a"} +{"Key":"escape"} +{"Get":{"state":"twelve char\ntwelve char twelve charˇa\ntwelve char\n","mode":"Normal"}} +{"Key":"shift-i"} +{"Key":"i"} +{"Key":"escape"} +{"Get":{"state":"twelve char\nˇitwelve char twelve chara\ntwelve char\n","mode":"Normal"}} +{"Key":"shift-d"} +{"Get":{"state":"twelve char\nˇ\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntwelve char tˇwelve char\ntwelve char\n"}} +{"Key":"shift-o"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}}