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
This commit is contained in:
Conrad Irwin 2023-08-24 22:11:51 -06:00
parent 0280d5d010
commit 20aa2a4c54
13 changed files with 580 additions and 67 deletions

View file

@ -137,10 +137,67 @@
"partialWord": true "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 commands
"z t": "editor::ScrollCursorTop", "z t": "editor::ScrollCursorTop",
"z z": "editor::ScrollCursorCenter", "z z": "editor::ScrollCursorCenter",
"z b": "editor::ScrollCursorBottom", "z b": "editor::ScrollCursorBottom",
"z c": "editor::Fold",
"z o": "editor::UnfoldLines",
"z f": "editor::FoldSelectedRanges",
// Count support // Count support
"1": [ "1": [
"vim::Number", "vim::Number",

View file

@ -30,6 +30,7 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
}; };
pub use self::fold_map::FoldPoint;
pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -310,7 +311,7 @@ impl DisplayMap {
pub struct DisplaySnapshot { pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot, pub buffer_snapshot: MultiBufferSnapshot,
fold_snapshot: fold_map::FoldSnapshot, pub fold_snapshot: fold_map::FoldSnapshot,
inlay_snapshot: inlay_map::InlaySnapshot, inlay_snapshot: inlay_map::InlaySnapshot,
tab_snapshot: tab_map::TabSnapshot, tab_snapshot: tab_map::TabSnapshot,
wrap_snapshot: wrap_map::WrapSnapshot, wrap_snapshot: wrap_map::WrapSnapshot,
@ -438,6 +439,20 @@ impl DisplaySnapshot {
fold_point.to_inlay_point(&self.fold_snapshot) 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 { pub fn max_point(&self) -> DisplayPoint {
DisplayPoint(self.block_snapshot.max_point()) DisplayPoint(self.block_snapshot.max_point())
} }

View file

@ -7198,7 +7198,7 @@ impl Editor {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx); let selections = self.selections.all_adjusted(cx);
for selection in selections { for selection in selections {
let range = selection.range().sorted(); let range = selection.range().sorted();
let buffer_start_row = range.start.row; let buffer_start_row = range.start.row;
@ -7274,7 +7274,17 @@ impl Editor {
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) { pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
let selections = self.selections.all::<Point>(cx); let selections = self.selections.all::<Point>(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); self.fold_ranges(ranges, true, cx);
} }

View file

@ -2,7 +2,7 @@ use std::{cmp, sync::Arc};
use editor::{ use editor::{
char_kind, char_kind,
display_map::{DisplaySnapshot, ToDisplayPoint}, display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
movement, Bias, CharKind, DisplayPoint, ToOffset, movement, Bias, CharKind, DisplayPoint, ToOffset,
}; };
use gpui::{actions, impl_actions, AppContext, WindowContext}; use gpui::{actions, impl_actions, AppContext, WindowContext};
@ -21,16 +21,16 @@ use crate::{
pub enum Motion { pub enum Motion {
Left, Left,
Backspace, Backspace,
Down, Down { display_lines: bool },
Up, Up { display_lines: bool },
Right, Right,
NextWordStart { ignore_punctuation: bool }, NextWordStart { ignore_punctuation: bool },
NextWordEnd { ignore_punctuation: bool }, NextWordEnd { ignore_punctuation: bool },
PreviousWordStart { ignore_punctuation: bool }, PreviousWordStart { ignore_punctuation: bool },
FirstNonWhitespace, FirstNonWhitespace { display_lines: bool },
CurrentLine, CurrentLine,
StartOfLine, StartOfLine { display_lines: bool },
EndOfLine, EndOfLine { display_lines: bool },
StartOfParagraph, StartOfParagraph,
EndOfParagraph, EndOfParagraph,
StartOfDocument, StartOfDocument,
@ -62,6 +62,41 @@ struct PreviousWordStart {
ignore_punctuation: bool, 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)] #[derive(Clone, Deserialize, PartialEq)]
struct RepeatFind { struct RepeatFind {
#[serde(default)] #[serde(default)]
@ -73,12 +108,7 @@ actions!(
[ [
Left, Left,
Backspace, Backspace,
Down,
Up,
Right, Right,
FirstNonWhitespace,
StartOfLine,
EndOfLine,
CurrentLine, CurrentLine,
StartOfParagraph, StartOfParagraph,
EndOfParagraph, EndOfParagraph,
@ -90,20 +120,63 @@ actions!(
); );
impl_actions!( impl_actions!(
vim, vim,
[NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind] [
NextWordStart,
NextWordEnd,
PreviousWordStart,
RepeatFind,
Up,
Down,
FirstNonWhitespace,
EndOfLine,
StartOfLine,
]
); );
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx)); 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, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx)); cx.add_action(|_: &mut Workspace, action: &Down, cx: _| {
cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx)); motion(
cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); Motion::Down {
cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| { display_lines: action.display_lines,
motion(Motion::FirstNonWhitespace, cx) },
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, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| { cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
motion(Motion::StartOfParagraph, cx) motion(Motion::StartOfParagraph, cx)
@ -192,19 +265,25 @@ impl Motion {
pub fn linewise(&self) -> bool { pub fn linewise(&self) -> bool {
use Motion::*; use Motion::*;
match self { match self {
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart Down { .. }
| StartOfParagraph | EndOfParagraph => true, | Up { .. }
EndOfLine | StartOfDocument
| EndOfDocument
| CurrentLine
| NextLineStart
| StartOfParagraph
| EndOfParagraph => true,
EndOfLine { .. }
| NextWordEnd { .. } | NextWordEnd { .. }
| Matching | Matching
| FindForward { .. } | FindForward { .. }
| Left | Left
| Backspace | Backspace
| Right | Right
| StartOfLine | StartOfLine { .. }
| NextWordStart { .. } | NextWordStart { .. }
| PreviousWordStart { .. } | PreviousWordStart { .. }
| FirstNonWhitespace | FirstNonWhitespace { .. }
| FindBackward { .. } => false, | FindBackward { .. } => false,
} }
} }
@ -213,21 +292,21 @@ impl Motion {
use Motion::*; use Motion::*;
match self { match self {
StartOfDocument | EndOfDocument | CurrentLine => true, StartOfDocument | EndOfDocument | CurrentLine => true,
Down Down { .. }
| Up | Up { .. }
| EndOfLine | EndOfLine { .. }
| NextWordEnd { .. } | NextWordEnd { .. }
| Matching | Matching
| FindForward { .. } | FindForward { .. }
| Left | Left
| Backspace | Backspace
| Right | Right
| StartOfLine | StartOfLine { .. }
| StartOfParagraph | StartOfParagraph
| EndOfParagraph | EndOfParagraph
| NextWordStart { .. } | NextWordStart { .. }
| PreviousWordStart { .. } | PreviousWordStart { .. }
| FirstNonWhitespace | FirstNonWhitespace { .. }
| FindBackward { .. } | FindBackward { .. }
| NextLineStart => false, | NextLineStart => false,
} }
@ -236,12 +315,12 @@ impl Motion {
pub fn inclusive(&self) -> bool { pub fn inclusive(&self) -> bool {
use Motion::*; use Motion::*;
match self { match self {
Down Down { .. }
| Up | Up { .. }
| StartOfDocument | StartOfDocument
| EndOfDocument | EndOfDocument
| CurrentLine | CurrentLine
| EndOfLine | EndOfLine { .. }
| NextWordEnd { .. } | NextWordEnd { .. }
| Matching | Matching
| FindForward { .. } | FindForward { .. }
@ -249,12 +328,12 @@ impl Motion {
Left Left
| Backspace | Backspace
| Right | Right
| StartOfLine | StartOfLine { .. }
| StartOfParagraph | StartOfParagraph
| EndOfParagraph | EndOfParagraph
| NextWordStart { .. } | NextWordStart { .. }
| PreviousWordStart { .. } | PreviousWordStart { .. }
| FirstNonWhitespace | FirstNonWhitespace { .. }
| FindBackward { .. } => false, | FindBackward { .. } => false,
} }
} }
@ -272,8 +351,18 @@ impl Motion {
let (new_point, goal) = match self { let (new_point, goal) = match self {
Left => (left(map, point, times), SelectionGoal::None), Left => (left(map, point, times), SelectionGoal::None),
Backspace => (backspace(map, point, times), SelectionGoal::None), Backspace => (backspace(map, point, times), SelectionGoal::None),
Down => down(map, point, goal, times), Down {
Up => up(map, point, goal, times), 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), Right => (right(map, point, times), SelectionGoal::None),
NextWordStart { ignore_punctuation } => ( NextWordStart { ignore_punctuation } => (
next_word_start(map, point, *ignore_punctuation, times), next_word_start(map, point, *ignore_punctuation, times),
@ -287,9 +376,17 @@ impl Motion {
previous_word_start(map, point, *ignore_punctuation, times), previous_word_start(map, point, *ignore_punctuation, times),
SelectionGoal::None, SelectionGoal::None,
), ),
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), FirstNonWhitespace { display_lines } => (
StartOfLine => (start_of_line(map, point), SelectionGoal::None), first_non_whitespace(map, *display_lines, point),
EndOfLine => (end_of_line(map, point), SelectionGoal::None), 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 => ( StartOfParagraph => (
movement::start_of_paragraph(map, point, times), movement::start_of_paragraph(map, point, times),
SelectionGoal::None, SelectionGoal::None,
@ -298,7 +395,7 @@ impl Motion {
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
SelectionGoal::None, 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), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => ( EndOfDocument => (
end_of_document(map, point, maybe_times), end_of_document(map, point, maybe_times),
@ -399,15 +496,39 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di
} }
fn down( 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, map: &DisplaySnapshot,
mut point: DisplayPoint, mut point: DisplayPoint,
mut goal: SelectionGoal, mut goal: SelectionGoal,
times: usize, times: usize,
) -> (DisplayPoint, SelectionGoal) { ) -> (DisplayPoint, SelectionGoal) {
let start_row = point.to_point(map).row; for _ in 0..times {
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) = movement::down(map, point, goal, true);
} }
@ -415,17 +536,39 @@ fn down(
} }
fn up( 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, map: &DisplaySnapshot,
mut point: DisplayPoint, mut point: DisplayPoint,
mut goal: SelectionGoal, mut goal: SelectionGoal,
times: usize, times: usize,
) -> (DisplayPoint, SelectionGoal) { ) -> (DisplayPoint, SelectionGoal) {
let start_row = point.to_point(map).row; for _ in 0..times {
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) = movement::up(map, point, goal, true);
} }
(point, goal) (point, goal)
} }
@ -516,8 +659,12 @@ fn previous_word_start(
point point
} }
fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint { fn first_non_whitespace(
let mut last_point = DisplayPoint::new(from.row(), 0); 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)); let language = map.buffer_snapshot.language_at(from.to_point(map));
for (ch, point) in map.chars_at(last_point) { for (ch, point) in map.chars_at(last_point) {
if ch == '\n' { if ch == '\n' {
@ -534,13 +681,24 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi
map.clip_point(last_point, Bias::Left) map.clip_point(last_point, Bias::Left)
} }
fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { 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 map.prev_line_boundary(point.to_point(map)).1
} }
}
fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { 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) 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 { fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map); let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
@ -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()); let new_row = (point.row() + times as u32).min(map.max_buffer_row());
first_non_whitespace( first_non_whitespace(
map, map,
false,
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left), map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left),
) )
} }

View file

@ -78,13 +78,27 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| { cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
let times = vim.pop_number_operator(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| { cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
let times = vim.pop_number_operator(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); scroll::init(cx);
@ -165,7 +179,10 @@ fn insert_first_non_whitespace(
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| { 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| { vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| { 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| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| { 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); editor.edit_with_autoindent(edits, cx);

View file

@ -10,7 +10,11 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
// Some motions ignore failure when switching to normal mode // Some motions ignore failure when switching to normal mode
let mut motion_succeeded = matches!( let mut motion_succeeded = matches!(
motion, 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| { vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {

View file

@ -15,7 +15,10 @@ pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
} }
if line_mode { if line_mode {
Motion::CurrentLine.expand_selection(map, selection, None, false); 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, map,
selection.start, selection.start,
selection.goal, selection.goal,

View file

@ -285,3 +285,145 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) {
Mode::Visual, 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;
}

View file

@ -1,9 +1,14 @@
use editor::EditorSettings;
use indoc::indoc; use indoc::indoc;
use settings::SettingsStore;
use std::ops::{Deref, DerefMut, Range}; use std::ops::{Deref, DerefMut, Range};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use gpui::ContextHandle; use gpui::ContextHandle;
use language::OffsetRangeExt; use language::{
language_settings::{AllLanguageSettings, LanguageSettings, SoftWrap},
OffsetRangeExt,
};
use util::test::{generate_marked_text, marked_text_offsets}; use util::test::{generate_marked_text, marked_text_offsets};
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
@ -127,6 +132,27 @@ impl<'a> NeovimBackedTestContext<'a> {
context_handle 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::<AllLanguageSettings>(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) { pub async fn assert_shared_state(&mut self, marked_text: &str) {
let neovim = self.neovim_state().await; let neovim = self.neovim_state().await;
let editor = self.editor_state(); let editor = self.editor_state();

View file

@ -41,6 +41,7 @@ pub enum NeovimData {
Key(String), Key(String),
Get { state: String, mode: Option<Mode> }, Get { state: String, mode: Option<Mode> },
ReadRegister { name: char, value: String }, ReadRegister { name: char, value: String },
SetOption { value: String },
} }
pub struct NeovimConnection { 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"))] #[cfg(not(feature = "neovim"))]
pub async fn read_register(&mut self, register: char) -> String { pub async fn read_register(&mut self, register: char) -> String {
if let Some(NeovimData::Get { .. }) = self.data.front() { if let Some(NeovimData::Get { .. }) = self.data.front() {

View file

@ -51,8 +51,15 @@ pub fn init(cx: &mut AppContext) {
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) { pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| { Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
if vim.state().mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { if vim.state().mode == Mode::VisualBlock
let is_up_or_down = matches!(motion, Motion::Up | Motion::Down); && !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| { visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
motion.move_point(map, point, goal, times) motion.move_point(map, point, goal, times)
}) })

View file

@ -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"}}

View file

@ -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"}}