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
|
@ -37,30 +37,42 @@
|
|||
"_": "vim::StartOfLineDownward",
|
||||
"g _": "vim::EndOfLineDownward",
|
||||
"shift-g": "vim::EndOfDocument",
|
||||
"w": "vim::NextWordStart",
|
||||
"{": "vim::StartOfParagraph",
|
||||
"}": "vim::EndOfParagraph",
|
||||
"|": "vim::GoToColumn",
|
||||
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
"e": "vim::NextWordEnd",
|
||||
"b": "vim::PreviousWordStart",
|
||||
"g e": "vim::PreviousWordEnd",
|
||||
|
||||
// Subword motions
|
||||
// "w": "vim::NextSubwordStart",
|
||||
// "b": "vim::PreviousSubwordStart",
|
||||
// "e": "vim::NextSubwordEnd",
|
||||
// "g e": "vim::PreviousSubwordEnd",
|
||||
|
||||
"shift-w": [
|
||||
"vim::NextWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"e": "vim::NextWordEnd",
|
||||
"shift-e": [
|
||||
"vim::NextWordEnd",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"b": "vim::PreviousWordStart",
|
||||
"shift-b": [
|
||||
"vim::PreviousWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
|
||||
"n": "search::SelectNextMatch",
|
||||
"shift-n": "search::SelectPrevMatch",
|
||||
"%": "vim::Matching",
|
||||
|
@ -117,8 +129,6 @@
|
|||
"ctrl-e": "vim::LineDown",
|
||||
"ctrl-y": "vim::LineUp",
|
||||
// "g" commands
|
||||
"g e": "vim::PreviousWordEnd",
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -407,11 +407,12 @@ pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{KeyBinding, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use crate::{
|
||||
motion,
|
||||
state::Mode::{self},
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
VimSettings,
|
||||
|
@ -1045,4 +1046,73 @@ mod test {
|
|||
cx.simulate_shared_keystrokes(["4", "$"]).await;
|
||||
cx.assert_shared_state("aa\nbb\ncˇc").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_subword_motions(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.update(|cx| {
|
||||
cx.bind_keys(vec![
|
||||
KeyBinding::new(
|
||||
"w",
|
||||
motion::NextSubwordStart {
|
||||
ignore_punctuation: false,
|
||||
},
|
||||
Some("Editor && VimControl && !VimWaiting && !menu"),
|
||||
),
|
||||
KeyBinding::new(
|
||||
"b",
|
||||
motion::PreviousSubwordStart {
|
||||
ignore_punctuation: false,
|
||||
},
|
||||
Some("Editor && VimControl && !VimWaiting && !menu"),
|
||||
),
|
||||
KeyBinding::new(
|
||||
"e",
|
||||
motion::NextSubwordEnd {
|
||||
ignore_punctuation: false,
|
||||
},
|
||||
Some("Editor && VimControl && !VimWaiting && !menu"),
|
||||
),
|
||||
KeyBinding::new(
|
||||
"g e",
|
||||
motion::PreviousSubwordEnd {
|
||||
ignore_punctuation: false,
|
||||
},
|
||||
Some("Editor && VimControl && !VimWaiting && !menu"),
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
cx.assert_binding_normal(
|
||||
["w"],
|
||||
indoc! {"ˇassert_binding"},
|
||||
indoc! {"assert_ˇbinding"},
|
||||
);
|
||||
// Special case: In 'cw', 'w' acts like 'e'
|
||||
cx.assert_binding(
|
||||
["c", "w"],
|
||||
indoc! {"ˇassert_binding"},
|
||||
Mode::Normal,
|
||||
indoc! {"ˇ_binding"},
|
||||
Mode::Insert,
|
||||
);
|
||||
|
||||
cx.assert_binding_normal(
|
||||
["e"],
|
||||
indoc! {"ˇassert_binding"},
|
||||
indoc! {"asserˇt_binding"},
|
||||
);
|
||||
|
||||
cx.assert_binding_normal(
|
||||
["b"],
|
||||
indoc! {"assert_ˇbinding"},
|
||||
indoc! {"ˇassert_binding"},
|
||||
);
|
||||
|
||||
cx.assert_binding_normal(
|
||||
["g", "e"],
|
||||
indoc! {"assert_bindinˇg"},
|
||||
indoc! {"asserˇt_binding"},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
use crate::{
|
||||
motion::Motion,
|
||||
motion::{self, Motion},
|
||||
object::Object,
|
||||
state::Mode,
|
||||
utils::{coerce_punctuation, copy_selections_content},
|
||||
utils::copy_selections_content,
|
||||
Vim,
|
||||
};
|
||||
use editor::{
|
||||
display_map::DisplaySnapshot,
|
||||
movement::{self, FindRange, TextLayoutDetails},
|
||||
scroll::Autoscroll,
|
||||
DisplayPoint,
|
||||
display_map::DisplaySnapshot, movement::TextLayoutDetails, scroll::Autoscroll, DisplayPoint,
|
||||
};
|
||||
use gpui::WindowContext;
|
||||
use language::{char_kind, CharKind, Selection};
|
||||
|
@ -39,6 +36,16 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
|||
times,
|
||||
ignore_punctuation,
|
||||
&text_layout_details,
|
||||
false,
|
||||
)
|
||||
} else if let Motion::NextSubwordStart { ignore_punctuation } = motion {
|
||||
expand_changed_word_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
ignore_punctuation,
|
||||
&text_layout_details,
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
motion.expand_selection(map, selection, times, false, &text_layout_details)
|
||||
|
@ -94,6 +101,7 @@ fn expand_changed_word_selection(
|
|||
times: Option<usize>,
|
||||
ignore_punctuation: bool,
|
||||
text_layout_details: &TextLayoutDetails,
|
||||
use_subword: bool,
|
||||
) -> bool {
|
||||
if times.is_none() || times.unwrap() == 1 {
|
||||
let scope = map
|
||||
|
@ -106,32 +114,30 @@ fn expand_changed_word_selection(
|
|||
.unwrap_or_default();
|
||||
|
||||
if in_word {
|
||||
selection.end =
|
||||
movement::find_boundary(map, selection.end, 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);
|
||||
|
||||
left_kind != right_kind && left_kind != CharKind::Whitespace
|
||||
});
|
||||
if !use_subword {
|
||||
selection.end =
|
||||
motion::next_word_end(map, selection.end, ignore_punctuation, 1, false);
|
||||
} else {
|
||||
selection.end =
|
||||
motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
|
||||
}
|
||||
selection.end = motion::next_char(map, selection.end, false);
|
||||
true
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
None,
|
||||
false,
|
||||
&text_layout_details,
|
||||
)
|
||||
let motion = if use_subword {
|
||||
Motion::NextSubwordStart { ignore_punctuation }
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }
|
||||
};
|
||||
motion.expand_selection(map, selection, None, false, &text_layout_details)
|
||||
}
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times,
|
||||
false,
|
||||
&text_layout_details,
|
||||
)
|
||||
let motion = if use_subword {
|
||||
Motion::NextSubwordStart { ignore_punctuation }
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }
|
||||
};
|
||||
motion.expand_selection(map, selection, times, false, &text_layout_details)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -159,6 +159,19 @@ impl VimTestContext {
|
|||
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
|
||||
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn assert_binding_normal<const COUNT: usize>(
|
||||
&mut self,
|
||||
keystrokes: [&str; COUNT],
|
||||
initial_state: &str,
|
||||
state_after: &str,
|
||||
) {
|
||||
self.set_state(initial_state, Mode::Normal);
|
||||
self.cx.simulate_keystrokes(keystrokes);
|
||||
self.cx.assert_editor_state(state_after);
|
||||
assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context());
|
||||
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for VimTestContext {
|
||||
|
|
|
@ -88,6 +88,22 @@ You can see the bindings that are enabled by default in vim mode [here](https://
|
|||
|
||||
The details of the context are a little out of scope for this doc, but suffice to say that `menu` is true when a menu is open (e.g. the completions menu), `VimWaiting` is true after you type `f` or `t` when we’re waiting for a new key (and you probably don’t want bindings to happen). Please reach out on [GitHub](https://github.com/zed-industries/zed) if you want help making a key bindings work.
|
||||
|
||||
## Subword motion
|
||||
|
||||
Subword motion is not enabled by default. To enable it, add these bindings to your keymap.
|
||||
|
||||
```json
|
||||
{
|
||||
"context": "Editor && VimControl && !VimWaiting && !menu",
|
||||
"bindings": {
|
||||
"w": "vim::NextSubwordStart",
|
||||
"b": "vim::PreviousSubwordStart",
|
||||
"e": "vim::NextSubwordEnd",
|
||||
"g e": "vim::PreviousSubwordEnd"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
## Command palette
|
||||
|
||||
Vim mode allows you to enable Zed’s command palette with `:`. This means that you can use vim's command palette to run any action that Zed supports.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue