Add inclusive vs exclusive motions to vim mode

This commit is contained in:
Keith Simmons 2022-04-21 16:14:58 -07:00
parent 0c587ae73c
commit 5ea782de21
11 changed files with 1350 additions and 750 deletions

View file

@ -4,7 +4,7 @@ use editor::{
movement, Bias, DisplayPoint,
};
use gpui::{actions, impl_actions, MutableAppContext};
use language::SelectionGoal;
use language::{Selection, SelectionGoal};
use serde::Deserialize;
use workspace::Workspace;
@ -14,22 +14,15 @@ use crate::{
Vim,
};
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
pub enum Motion {
Left,
Down,
Up,
Right,
NextWordStart {
ignore_punctuation: bool,
stop_at_newline: bool,
},
NextWordEnd {
ignore_punctuation: bool,
},
PreviousWordStart {
ignore_punctuation: bool,
},
NextWordStart { ignore_punctuation: bool },
NextWordEnd { ignore_punctuation: bool },
PreviousWordStart { ignore_punctuation: bool },
StartOfLine,
EndOfLine,
StartOfDocument,
@ -41,8 +34,6 @@ pub enum Motion {
struct NextWordStart {
#[serde(default)]
ignore_punctuation: bool,
#[serde(default)]
stop_at_newline: bool,
}
#[derive(Clone, Deserialize)]
@ -87,19 +78,8 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
cx.add_action(
|_: &mut Workspace,
&NextWordStart {
ignore_punctuation,
stop_at_newline,
}: &NextWordStart,
cx: _| {
motion(
Motion::NextWordStart {
ignore_punctuation,
stop_at_newline,
},
cx,
)
|_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
motion(Motion::NextWordStart { ignore_punctuation }, cx)
},
);
cx.add_action(
@ -128,29 +108,48 @@ fn motion(motion: Motion, cx: &mut MutableAppContext) {
}
}
// Motion handling is specified here:
// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
impl Motion {
pub fn linewise(self) -> bool {
use Motion::*;
match self {
Down | Up | StartOfDocument | EndOfDocument => true,
_ => false,
}
}
pub fn inclusive(self) -> bool {
use Motion::*;
if self.linewise() {
return true;
}
match self {
EndOfLine | NextWordEnd { .. } => true,
Left | Right | StartOfLine | NextWordStart { .. } | PreviousWordStart { .. } => false,
_ => panic!("Exclusivity not defined for {self:?}"),
}
}
pub fn move_point(
self,
map: &DisplaySnapshot,
point: DisplayPoint,
goal: SelectionGoal,
block_cursor_positioning: bool,
) -> (DisplayPoint, SelectionGoal) {
use Motion::*;
match self {
Left => (left(map, point), SelectionGoal::None),
Down => movement::down(map, point, goal),
Up => movement::up(map, point, goal),
Down => movement::down(map, point, goal, true),
Up => movement::up(map, point, goal, true),
Right => (right(map, point), SelectionGoal::None),
NextWordStart {
ignore_punctuation,
stop_at_newline,
} => (
next_word_start(map, point, ignore_punctuation, stop_at_newline),
NextWordStart { ignore_punctuation } => (
next_word_start(map, point, ignore_punctuation),
SelectionGoal::None,
),
NextWordEnd { ignore_punctuation } => (
next_word_end(map, point, ignore_punctuation, block_cursor_positioning),
next_word_end(map, point, ignore_punctuation),
SelectionGoal::None,
),
PreviousWordStart { ignore_punctuation } => (
@ -164,11 +163,55 @@ impl Motion {
}
}
pub fn line_wise(self) -> bool {
use Motion::*;
match self {
Down | Up | StartOfDocument | EndOfDocument => true,
_ => false,
// Expands a selection using self motion for an operator
pub fn expand_selection(
self,
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
expand_to_surrounding_newline: bool,
) {
let (head, goal) = self.move_point(map, selection.head(), selection.goal);
selection.set_head(head, goal);
if self.linewise() {
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
if expand_to_surrounding_newline {
if selection.end.row() < map.max_point().row() {
*selection.end.row_mut() += 1;
*selection.end.column_mut() = 0;
// Don't reset the end here
return;
} else if selection.start.row() > 0 {
*selection.start.row_mut() -= 1;
*selection.start.column_mut() = map.line_len(selection.start.row());
}
}
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
} else {
// If the motion is exclusive and the end of the motion is in column 1, the
// end of the motion is moved to the end of the previous line and the motion
// becomes inclusive. Example: "}" moves to the first line after a paragraph,
// but "d}" will not include that line.
let mut inclusive = self.inclusive();
if !inclusive
&& selection.end.row() > selection.start.row()
&& selection.end.column() == 0
&& selection.end.row() > 0
{
inclusive = true;
*selection.end.row_mut() -= 1;
*selection.end.column_mut() = 0;
selection.end = map.clip_point(
map.next_line_boundary(selection.end.to_point(map)).1,
Bias::Left,
);
}
if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
*selection.end.column_mut() += 1;
}
}
}
}
@ -187,7 +230,6 @@ fn next_word_start(
map: &DisplaySnapshot,
point: DisplayPoint,
ignore_punctuation: bool,
stop_at_newline: bool,
) -> DisplayPoint {
let mut crossed_newline = false;
movement::find_boundary(map, point, |left, right| {
@ -196,8 +238,8 @@ fn next_word_start(
let at_newline = right == '\n';
let found = (left_kind != right_kind && !right.is_whitespace())
|| (at_newline && (crossed_newline || stop_at_newline))
|| (at_newline && left == '\n'); // Prevents skipping repeated empty lines
|| at_newline && crossed_newline
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
if at_newline {
crossed_newline = true;
@ -210,7 +252,6 @@ fn next_word_end(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
before_end_character: bool,
) -> DisplayPoint {
*point.column_mut() += 1;
point = movement::find_boundary(map, point, |left, right| {
@ -221,13 +262,12 @@ fn next_word_end(
});
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
// we have backtraced already
if before_end_character
&& !map
.chars_at(point)
.skip(1)
.next()
.map(|c| c == '\n')
.unwrap_or(true)
if !map
.chars_at(point)
.skip(1)
.next()
.map(|c| c == '\n')
.unwrap_or(true)
{
*point.column_mut() = point.column().saturating_sub(1);
}