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:
parent
467a179837
commit
d247086b21
6 changed files with 491 additions and 93 deletions
|
@ -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 {
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue