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

@ -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",

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 {

View file

@ -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"},
);
}
}

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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 were waiting for a new key (and you probably dont 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 Zeds command palette with `:`. This means that you can use vim's command palette to run any action that Zed supports.