vim: subword motions (#8725)

Add subword motions to vim, inspired by
[nvim-spider](https://github.com/chrisgrieser/nvim-spider),
[CamelCaseMotion](https://github.com/bkad/CamelCaseMotion).


Release Notes:

- Added subword motions to vim
This commit is contained in:
Rom Grk 2024-03-07 21:36:12 -05:00 committed by GitHub
parent 467a179837
commit d247086b21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 491 additions and 93 deletions

View file

@ -42,6 +42,18 @@ pub enum Motion {
PreviousWordEnd {
ignore_punctuation: bool,
},
NextSubwordStart {
ignore_punctuation: bool,
},
NextSubwordEnd {
ignore_punctuation: bool,
},
PreviousSubwordStart {
ignore_punctuation: bool,
},
PreviousSubwordEnd {
ignore_punctuation: bool,
},
FirstNonWhitespace {
display_lines: bool,
},
@ -110,6 +122,34 @@ struct PreviousWordEnd {
ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct NextSubwordStart {
#[serde(default)]
pub(crate) ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct NextSubwordEnd {
#[serde(default)]
pub(crate) ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PreviousSubwordStart {
#[serde(default)]
pub(crate) ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PreviousSubwordEnd {
#[serde(default)]
pub(crate) ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Up {
@ -153,10 +193,14 @@ impl_actions!(
FirstNonWhitespace,
Down,
Up,
NextWordStart,
NextWordEnd,
PreviousWordStart,
PreviousWordEnd,
NextWordEnd,
NextWordStart
NextSubwordStart,
NextSubwordEnd,
PreviousSubwordStart,
PreviousSubwordEnd,
]
);
@ -264,6 +308,31 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
);
workspace.register_action(
|_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
},
);
workspace.register_action(
|_: &mut Workspace, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx: _| {
motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
},
);
workspace.register_action(
|_: &mut Workspace, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx: _| {
motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
},
);
workspace.register_action(
|_: &mut Workspace,
&PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart,
cx: _| { motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx) },
);
workspace.register_action(
|_: &mut Workspace, &PreviousSubwordEnd { ignore_punctuation }, cx: _| {
motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
},
);
workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
motion(Motion::NextLineStart, cx)
});
@ -304,11 +373,6 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
motion(Motion::WindowBottom, cx)
});
workspace.register_action(
|_: &mut Workspace, &PreviousWordEnd { ignore_punctuation }, cx: _| {
motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
},
);
}
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
@ -349,7 +413,6 @@ impl Motion {
| WindowBottom
| EndOfParagraph => true,
EndOfLine { .. }
| NextWordEnd { .. }
| Matching
| FindForward { .. }
| Left
@ -360,8 +423,13 @@ impl Motion {
| EndOfLineDownward
| GoToColumn
| NextWordStart { .. }
| NextWordEnd { .. }
| PreviousWordStart { .. }
| PreviousWordEnd { .. }
| NextSubwordStart { .. }
| NextSubwordEnd { .. }
| PreviousSubwordStart { .. }
| PreviousSubwordEnd { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
| RepeatFind { .. }
@ -376,7 +444,6 @@ impl Motion {
Down { .. }
| Up { .. }
| EndOfLine { .. }
| NextWordEnd { .. }
| Matching
| FindForward { .. }
| RepeatFind { .. }
@ -391,14 +458,19 @@ impl Motion {
| EndOfLineDownward
| GoToColumn
| NextWordStart { .. }
| NextWordEnd { .. }
| PreviousWordStart { .. }
| PreviousWordEnd { .. }
| NextSubwordStart { .. }
| NextSubwordEnd { .. }
| PreviousSubwordStart { .. }
| PreviousSubwordEnd { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. }
| RepeatFindReversed { .. }
| WindowTop
| WindowMiddle
| WindowBottom
| PreviousWordEnd { .. }
| NextLineStart => false,
}
}
@ -413,13 +485,15 @@ impl Motion {
| CurrentLine
| EndOfLine { .. }
| EndOfLineDownward
| NextWordEnd { .. }
| Matching
| FindForward { .. }
| WindowTop
| WindowMiddle
| WindowBottom
| NextWordEnd { .. }
| PreviousWordEnd { .. }
| NextSubwordEnd { .. }
| PreviousSubwordEnd { .. }
| NextLineStart => true,
Left
| Backspace
@ -432,6 +506,8 @@ impl Motion {
| GoToColumn
| NextWordStart { .. }
| PreviousWordStart { .. }
| NextSubwordStart { .. }
| PreviousSubwordStart { .. }
| FirstNonWhitespace { .. }
| FindBackward { .. } => false,
RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
@ -473,7 +549,7 @@ impl Motion {
SelectionGoal::None,
),
NextWordEnd { ignore_punctuation } => (
next_word_end(map, point, *ignore_punctuation, times),
next_word_end(map, point, *ignore_punctuation, times, true),
SelectionGoal::None,
),
PreviousWordStart { ignore_punctuation } => (
@ -484,6 +560,22 @@ impl Motion {
previous_word_end(map, point, *ignore_punctuation, times),
SelectionGoal::None,
),
NextSubwordStart { ignore_punctuation } => (
next_subword_start(map, point, *ignore_punctuation, times),
SelectionGoal::None,
),
NextSubwordEnd { ignore_punctuation } => (
next_subword_end(map, point, *ignore_punctuation, times, true),
SelectionGoal::None,
),
PreviousSubwordStart { ignore_punctuation } => (
previous_subword_start(map, point, *ignore_punctuation, times),
SelectionGoal::None,
),
PreviousSubwordEnd { ignore_punctuation } => (
previous_subword_end(map, point, *ignore_punctuation, times),
SelectionGoal::None,
),
FirstNonWhitespace { display_lines } => (
first_non_whitespace(map, *display_lines, point),
SelectionGoal::None,
@ -819,6 +911,25 @@ pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize
point
}
pub(crate) fn next_char(
map: &DisplaySnapshot,
point: DisplayPoint,
allow_cross_newline: bool,
) -> DisplayPoint {
let mut new_point = point;
let mut max_column = map.line_len(new_point.row());
if !allow_cross_newline {
max_column -= 1;
}
if new_point.column() < max_column {
*new_point.column_mut() += 1;
} else if new_point < map.max_point() && allow_cross_newline {
*new_point.row_mut() += 1;
*new_point.column_mut() = 0;
}
new_point
}
pub(crate) fn next_word_start(
map: &DisplaySnapshot,
mut point: DisplayPoint,
@ -848,22 +959,17 @@ pub(crate) fn next_word_start(
point
}
fn next_word_end(
pub(crate) fn next_word_end(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
allow_cross_newline: bool,
) -> DisplayPoint {
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
for _ in 0..times {
let mut new_point = point;
if new_point.column() < map.line_len(new_point.row()) {
*new_point.column_mut() += 1;
} else if new_point < map.max_point() {
*new_point.row_mut() += 1;
*new_point.column_mut() = 0;
}
let new_point = next_char(map, point, allow_cross_newline);
let mut need_next_char = false;
let new_point = movement::find_boundary_exclusive(
map,
new_point,
@ -871,10 +977,21 @@ fn next_word_end(
|left, right| {
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
let at_newline = right == '\n';
if !allow_cross_newline && at_newline {
need_next_char = true;
return true;
}
left_kind != right_kind && left_kind != CharKind::Whitespace
},
);
let new_point = if need_next_char {
next_char(map, new_point, true)
} else {
new_point
};
let new_point = map.clip_point(new_point, Bias::Left);
if point == new_point {
break;
@ -913,6 +1030,210 @@ fn previous_word_start(
point
}
fn previous_word_end(
map: &DisplaySnapshot,
point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
let mut point = point.to_point(map);
if point.column < map.buffer_snapshot.line_len(point.row) {
point.column += 1;
}
for _ in 0..times {
let new_point = movement::find_preceding_boundary_point(
&map.buffer_snapshot,
point,
FindRange::MultiLine,
|left, right| {
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
match (left_kind, right_kind) {
(CharKind::Punctuation, CharKind::Whitespace)
| (CharKind::Punctuation, CharKind::Word)
| (CharKind::Word, CharKind::Whitespace)
| (CharKind::Word, CharKind::Punctuation) => true,
(CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
_ => false,
}
},
);
if new_point == point {
break;
}
point = new_point;
}
movement::saturating_left(map, point.to_display_point(map))
}
fn next_subword_start(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
for _ in 0..times {
let mut crossed_newline = false;
let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
let at_newline = right == '\n';
let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
let is_subword_start =
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
|| at_newline && crossed_newline
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
crossed_newline |= at_newline;
found
});
if point == new_point {
break;
}
point = new_point;
}
point
}
pub(crate) fn next_subword_end(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
allow_cross_newline: bool,
) -> DisplayPoint {
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
for _ in 0..times {
let new_point = next_char(map, point, allow_cross_newline);
let mut crossed_newline = false;
let mut need_backtrack = false;
let new_point =
movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
let at_newline = right == '\n';
if !allow_cross_newline && at_newline {
return true;
}
let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
let is_subword_end =
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
if found && (is_word_end || is_subword_end) {
need_backtrack = true;
}
crossed_newline |= at_newline;
found
});
let mut new_point = map.clip_point(new_point, Bias::Left);
if need_backtrack {
*new_point.column_mut() -= 1;
}
if point == new_point {
break;
}
point = new_point;
}
point
}
fn previous_subword_start(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
for _ in 0..times {
let mut crossed_newline = false;
// This works even though find_preceding_boundary is called for every character in the line containing
// cursor because the newline is checked only once.
let new_point = movement::find_preceding_boundary_display_point(
map,
point,
FindRange::MultiLine,
|left, right| {
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
let at_newline = right == '\n';
let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
let is_subword_start =
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
|| at_newline && crossed_newline
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
crossed_newline |= at_newline;
found
},
);
if point == new_point {
break;
}
point = new_point;
}
point
}
fn previous_subword_end(
map: &DisplaySnapshot,
point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
let mut point = point.to_point(map);
if point.column < map.buffer_snapshot.line_len(point.row) {
point.column += 1;
}
for _ in 0..times {
let new_point = movement::find_preceding_boundary_point(
&map.buffer_snapshot,
point,
FindRange::MultiLine,
|left, right| {
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
let is_subword_end =
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
if is_subword_end {
return true;
}
match (left_kind, right_kind) {
(CharKind::Word, CharKind::Whitespace)
| (CharKind::Word, CharKind::Punctuation) => true,
(CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
_ => false,
}
},
);
if new_point == point {
break;
}
point = new_point;
}
movement::saturating_left(map, point.to_display_point(map))
}
pub(crate) fn first_non_whitespace(
map: &DisplaySnapshot,
display_lines: bool,
@ -1217,44 +1538,6 @@ fn window_bottom(
}
}
fn previous_word_end(
map: &DisplaySnapshot,
point: DisplayPoint,
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
let mut point = point.to_point(map);
if point.column < map.buffer_snapshot.line_len(point.row) {
point.column += 1;
}
for _ in 0..times {
let new_point = movement::find_preceding_boundary_point(
&map.buffer_snapshot,
point,
FindRange::MultiLine,
|left, right| {
let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
match (left_kind, right_kind) {
(CharKind::Punctuation, CharKind::Whitespace)
| (CharKind::Punctuation, CharKind::Word)
| (CharKind::Word, CharKind::Whitespace)
| (CharKind::Word, CharKind::Punctuation) => true,
(CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
_ => false,
}
},
);
if new_point == point {
break;
}
point = new_point;
}
movement::saturating_left(map, point.to_display_point(map))
}
#[cfg(test)]
mod test {