diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c0de3420f222566220d5f7e487554b3ee5bc33d5..c7e6199f444f6ce09cc1e1d6b39703b91cbd8fd2 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 611866bcadeaef851ba081434fadc04a2d3031ae..fae4109b94c7eee9e9883fdf055c168be8e7d035 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 c5ff1f027da7faac89bbcbf596d501fffbeae2cb..ac9a972f96e8c5f342ebcdb4635b345442215da1 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 a85c6fc0a3494ce15cf4c39b1d9a486afdb25566..2edbb8ff1c3264b50db9d295cb717671a9892281 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, 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, _: &FirstNonWhitespace, cx: _| { - motion(Motion::FirstNonWhitespace, 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), @@ -400,14 +497,38 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di fn down( map: &DisplaySnapshot, - mut point: DisplayPoint, + 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); + 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() + } + }; - while point.to_point(map).row < target { + 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) { + for _ in 0..times { (point, goal) = movement::down(map, point, goal, true); } @@ -416,16 +537,38 @@ fn down( fn up( map: &DisplaySnapshot, - mut point: DisplayPoint, + 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); + 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)); - while point.to_point(map).row > target { + (map.clip_point(point, Bias::Left), goal) +} + +fn up_display( + map: &DisplaySnapshot, + mut point: DisplayPoint, + mut goal: SelectionGoal, + times: usize, +) -> (DisplayPoint, SelectionGoal) { + 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 3a2d15a878865418f260c5a14dc9b09e5734944b..2b03632c42a2f1a3247c60409d1df2030913d282 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 50bc049a3aa96d37ae9acce6a1505369333bf534..5591de89c668be823b10f47bf41d2710619ae42c 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 1d53c6831cc0e92be9b021e1faa114928e03276b..b04596240a25d224e9785b4d169bee9743affe19 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 9cd927601f518315c133f3830afb712fca00233e..cfde221dc56f71aeb20e7830996dbd580dbf3d28 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 f4b0e961839d087b36acd52bd52ba28e54119af5..bc37f2fdd631d0dbf3405890d3f72f455c9aaafd 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 68f3374772bd34bea247a7e3e814c2b83b394c7f..3e59080b13040c81c362528afda42f5e3fa94ff6 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 b68da870f0d579bd555e6adba7b8e0890286c419..ee46a0d209f348f3fc25c2252ca9a8a06921fd68 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 0000000000000000000000000000000000000000..668df5ce269307a6c8d0b89a1ee67019b6746917 --- /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 0000000000000000000000000000000000000000..f9f54c5c43bf64397a958271cc4d44cbf84e4a9a --- /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"}}