vim: Fix relative motions (#2888)

This changes vim motions to be relative to fold lines, not display
lines, to match the behaviour of vim.

This is necessary for relative line numbers to make sense (as the most
important thing is you can do `3j` to get th e line that is numbered 3).


Release Notes:

- vim: Fix handling of motions when `soft_wrap` is enabled in zed. Like
in vim `j,k,up,down,$,^,0,home,end` will all now navigate in file
coordinates not display coordinates.
- vim: Add `g {j,k,up,down,$,^,0,home,end}` to navigate in display
coordinates.
- vim: Add `z o` and `z c` to open and close folds.
- vim: Add `z f` in visual mode to fold selection.

Note: this may be a jarring change if you're grown used to the current
behaviour of `j` and `k`. You can make the issue less acute by setting
`"soft_wrap":"none"` in your settings; or you can manually copy the
bindings for `g j` to the binding for `j` (etc.) in your keymap.json to
preserve the existing behaviour.
This commit is contained in:
Conrad Irwin 2023-08-29 11:19:37 -07:00 committed by GitHub
commit dd577074f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 698 additions and 77 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

@ -7346,7 +7346,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;
@ -7422,7 +7422,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

@ -1,8 +1,8 @@
use std::sync::Arc; 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,6 +496,33 @@ 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,
@ -407,10 +531,35 @@ fn down(
for _ in 0..times { for _ in 0..times {
(point, goal) = movement::down(map, point, goal, true); (point, goal) = movement::down(map, point, goal, true);
} }
(point, goal) (point, goal)
} }
fn up( pub(crate) 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,
@ -419,6 +568,7 @@ fn up(
for _ in 0..times { for _ in 0..times {
(point, goal) = movement::up(map, point, goal, true); (point, goal) = movement::up(map, point, goal, true);
} }
(point, goal) (point, goal)
} }
@ -509,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' {
@ -527,12 +681,31 @@ 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 { pub(crate) fn start_of_line(
map.prev_line_boundary(point.to_point(map)).1 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 { pub(crate) fn end_of_line(
map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) 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 { fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
@ -654,11 +827,8 @@ fn find_backward(
} }
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
let new_row = (point.row() + times as u32).min(map.max_buffer_row()); let correct_line = down(map, point, SelectionGoal::None, times).0;
first_non_whitespace( first_non_whitespace(map, false, correct_line)
map,
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left),
)
} }
#[cfg(test)] #[cfg(test)]

View File

@ -10,7 +10,7 @@ mod yank;
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::{
motion::Motion, motion::{self, Motion},
object::Object, object::Object,
state::{Mode, Operator}, state::{Mode, Operator},
Vim, Vim,
@ -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)
}); });
}); });
}); });
@ -197,19 +214,19 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
.collect(); .collect();
let edits = selection_start_rows.into_iter().map(|row| { let edits = selection_start_rows.into_iter().map(|row| {
let (indent, _) = map.line_indent(row); let (indent, _) = map.line_indent(row);
let start_of_line = map let start_of_line =
.clip_point(DisplayPoint::new(row, 0), Bias::Left) motion::start_of_line(&map, false, DisplayPoint::new(row, 0))
.to_point(&map); .to_point(&map);
let mut new_text = " ".repeat(indent as usize); let mut new_text = " ".repeat(indent as usize);
new_text.push('\n'); new_text.push('\n');
(start_of_line..start_of_line, new_text) (start_of_line..start_of_line, new_text)
}); });
editor.edit_with_autoindent(edits, cx); editor.edit_with_autoindent(edits, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| { s.move_cursors_with(|map, cursor, _| {
*cursor.row_mut() -= 1; let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0;
*cursor.column_mut() = map.line_len(cursor.row()); let insert_point = motion::end_of_line(map, false, previous_line);
(map.clip_point(cursor, Bias::Left), SelectionGoal::None) (insert_point, SelectionGoal::None)
}); });
}); });
}); });
@ -223,22 +240,23 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
vim.update_active_editor(cx, |editor, cx| { vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| { editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.selections.all_display(cx); let (map, old_selections) = editor.selections.all_display(cx);
let selection_end_rows: HashSet<u32> = old_selections let selection_end_rows: HashSet<u32> = old_selections
.into_iter() .into_iter()
.map(|selection| selection.end.row()) .map(|selection| selection.end.row())
.collect(); .collect();
let edits = selection_end_rows.into_iter().map(|row| { let edits = selection_end_rows.into_iter().map(|row| {
let (indent, _) = map.line_indent(row); let (indent, _) = map.line_indent(row);
let end_of_line = map let end_of_line =
.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left) motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map);
.to_point(&map);
let mut new_text = "\n".to_string(); let mut new_text = "\n".to_string();
new_text.push_str(&" ".repeat(indent as usize)); new_text.push_str(&" ".repeat(indent as usize));
(end_of_line..end_of_line, new_text) (end_of_line..end_of_line, new_text)
}); });
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,218 @@ 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;
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]
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,13 @@
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, 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 +131,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,50 @@
{"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"}}
{"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"}}