mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
vim: Change logic in subword motion subword_start calculation (#45341)
- Update subword boundary detection to stop at semantic punctuation (quotes, brackets, parens) - Treat `.`, `-`, and `_` as subword separators for consistent behavior - Extract `is_subword_start` and `is_subword_end` helpers for reuse in motion and object handling - Add comprehensive tests for all subword motions matching vim's CamelCaseMotion behavior Closes #23344 Release Notes: - Fixed bug in Vim mode's subword motion punctuation handling --------- Co-authored-by: dino <dinojoaocosta@gmail.com>
This commit is contained in:
parent
6b1b5fa6b2
commit
5a8a7d8666
2 changed files with 325 additions and 45 deletions
|
|
@ -1840,6 +1840,20 @@ fn previous_word_end(
|
|||
movement::saturating_left(map, point.to_display_point(map))
|
||||
}
|
||||
|
||||
/// Checks if there's a subword boundary start between `left` and `right` characters.
|
||||
/// This detects transitions like `_b` (separator to non-separator) or `aB` (lowercase to uppercase).
|
||||
pub(crate) fn is_subword_start(left: char, right: char, separators: &str) -> bool {
|
||||
let is_separator = |c: char| separators.contains(c);
|
||||
(is_separator(left) && !is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
|
||||
}
|
||||
|
||||
/// Checks if there's a subword boundary end between `left` and `right` characters.
|
||||
/// This detects transitions like `a_` (non-separator to separator) or `aB` (lowercase to uppercase).
|
||||
pub(crate) fn is_subword_end(left: char, right: char, separators: &str) -> bool {
|
||||
let is_separator = |c: char| separators.contains(c);
|
||||
(!is_separator(left) && is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
|
||||
}
|
||||
|
||||
fn next_subword_start(
|
||||
map: &DisplaySnapshot,
|
||||
mut point: DisplayPoint,
|
||||
|
|
@ -1857,11 +1871,11 @@ fn next_subword_start(
|
|||
let right_kind = classifier.kind(right);
|
||||
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))
|
||||
let is_stopping_punct = |c: char| "\"'{}[]()<>".contains(c);
|
||||
let is_word_start = (left_kind != right_kind)
|
||||
&& (!right.is_ascii_punctuation() || is_stopping_punct(right));
|
||||
let found_subword_start = is_subword_start(left, right, "._-");
|
||||
let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
|
||||
|| at_newline && crossed_newline
|
||||
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
||||
|
||||
|
|
@ -1902,13 +1916,15 @@ pub(crate) fn next_subword_end(
|
|||
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 is_stopping_punct = |c: char| ".\"'{}[]()<>".contains(c);
|
||||
let is_word_end = (left_kind != right_kind)
|
||||
&& (!left.is_ascii_punctuation() || is_stopping_punct(left));
|
||||
let found_subword_end = is_subword_end(left, right, "_-");
|
||||
|
||||
let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
|
||||
let found =
|
||||
!left.is_whitespace() && !at_newline && (is_word_end || found_subword_end);
|
||||
|
||||
if found && (is_word_end || is_subword_end) {
|
||||
if found {
|
||||
need_backtrack = true;
|
||||
}
|
||||
|
||||
|
|
@ -1951,11 +1967,12 @@ fn previous_subword_start(
|
|||
let right_kind = classifier.kind(right);
|
||||
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 is_stopping_punct = |c: char| ".\"'{}[]()<>".contains(c);
|
||||
let is_word_start = (left_kind != right_kind)
|
||||
&& (is_stopping_punct(right) || !right.is_ascii_punctuation());
|
||||
let found_subword_start = is_subword_start(left, right, "._-");
|
||||
|
||||
let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
|
||||
let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
|
||||
|| at_newline && crossed_newline
|
||||
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
||||
|
||||
|
|
@ -1998,16 +2015,17 @@ fn previous_subword_end(
|
|||
let left_kind = classifier.kind(left);
|
||||
let right_kind = classifier.kind(right);
|
||||
|
||||
let is_subword_end =
|
||||
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
|
||||
let is_stopping_punct = |c: char| ".;\"'{}[]()<>".contains(c);
|
||||
let found_subword_end = is_subword_end(left, right, "_-");
|
||||
|
||||
if is_subword_end {
|
||||
if found_subword_end {
|
||||
return true;
|
||||
}
|
||||
|
||||
match (left_kind, right_kind) {
|
||||
(CharKind::Word, CharKind::Whitespace)
|
||||
| (CharKind::Word, CharKind::Punctuation) => true,
|
||||
(CharKind::Punctuation, _) if is_stopping_punct(left) => true,
|
||||
(CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
|
||||
_ => false,
|
||||
}
|
||||
|
|
@ -4771,4 +4789,212 @@ mod test {
|
|||
the quick brown foˇd over the lazy dog"});
|
||||
assert!(!cx.cx.forced_motion());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// Setup custom keybindings for subword motions so we can use the bindings
|
||||
// in `simulate_keystrokes`.
|
||||
cx.update(|_window, cx| {
|
||||
cx.bind_keys([KeyBinding::new(
|
||||
"w",
|
||||
super::NextSubwordStart {
|
||||
ignore_punctuation: false,
|
||||
},
|
||||
None,
|
||||
)]);
|
||||
});
|
||||
|
||||
cx.set_state("ˇfoo.bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("w");
|
||||
cx.assert_state("foo.ˇbar", Mode::Normal);
|
||||
|
||||
cx.set_state("ˇfoo(bar)", Mode::Normal);
|
||||
cx.simulate_keystrokes("w");
|
||||
cx.assert_state("fooˇ(bar)", Mode::Normal);
|
||||
cx.simulate_keystrokes("w");
|
||||
cx.assert_state("foo(ˇbar)", Mode::Normal);
|
||||
cx.simulate_keystrokes("w");
|
||||
cx.assert_state("foo(barˇ)", Mode::Normal);
|
||||
|
||||
cx.set_state("ˇfoo_bar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("w");
|
||||
cx.assert_state("foo_ˇbar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("w");
|
||||
cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
|
||||
|
||||
cx.set_state("ˇfooBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("w");
|
||||
cx.assert_state("fooˇBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("w");
|
||||
cx.assert_state("fooBarˇBaz", Mode::Normal);
|
||||
|
||||
cx.set_state("ˇfoo;bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("w");
|
||||
cx.assert_state("foo;ˇbar", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// Setup custom keybindings for subword motions so we can use the bindings
|
||||
// in `simulate_keystrokes`.
|
||||
cx.update(|_window, cx| {
|
||||
cx.bind_keys([KeyBinding::new(
|
||||
"e",
|
||||
super::NextSubwordEnd {
|
||||
ignore_punctuation: false,
|
||||
},
|
||||
None,
|
||||
)]);
|
||||
});
|
||||
|
||||
cx.set_state("ˇfoo.bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("foˇo.bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("fooˇ.bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("foo.baˇr", Mode::Normal);
|
||||
|
||||
cx.set_state("ˇfoo(bar)", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("foˇo(bar)", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("fooˇ(bar)", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("foo(baˇr)", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("foo(barˇ)", Mode::Normal);
|
||||
|
||||
cx.set_state("ˇfoo_bar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("foˇo_bar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("foo_baˇr_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.assert_state("foo_bar_baˇz", Mode::Normal);
|
||||
|
||||
cx.set_state("ˇfooBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.set_state("foˇoBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.set_state("fooBaˇrBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.set_state("fooBarBaˇz", Mode::Normal);
|
||||
|
||||
cx.set_state("ˇfoo;bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.set_state("foˇo;bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.set_state("fooˇ;bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("e");
|
||||
cx.set_state("foo;baˇr", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// Setup custom keybindings for subword motions so we can use the bindings
|
||||
// in `simulate_keystrokes`.
|
||||
cx.update(|_window, cx| {
|
||||
cx.bind_keys([KeyBinding::new(
|
||||
"b",
|
||||
super::PreviousSubwordStart {
|
||||
ignore_punctuation: false,
|
||||
},
|
||||
None,
|
||||
)]);
|
||||
});
|
||||
|
||||
cx.set_state("foo.barˇ", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("foo.ˇbar", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("fooˇ.bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("ˇfoo.bar", Mode::Normal);
|
||||
|
||||
cx.set_state("foo(barˇ)", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("foo(ˇbar)", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("fooˇ(bar)", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("ˇfoo(bar)", Mode::Normal);
|
||||
|
||||
cx.set_state("foo_bar_bazˇ", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("foo_ˇbar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("ˇfoo_bar_baz", Mode::Normal);
|
||||
|
||||
cx.set_state("fooBarBazˇ", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("fooBarˇBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("fooˇBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("ˇfooBarBaz", Mode::Normal);
|
||||
|
||||
cx.set_state("foo;barˇ", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("foo;ˇbar", Mode::Normal);
|
||||
cx.simulate_keystrokes("b");
|
||||
cx.assert_state("ˇfoo;bar", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// Setup custom keybindings for subword motions so we can use the bindings
|
||||
// in `simulate_keystrokes`.
|
||||
cx.update(|_window, cx| {
|
||||
cx.bind_keys([KeyBinding::new(
|
||||
"g e",
|
||||
super::PreviousSubwordEnd {
|
||||
ignore_punctuation: false,
|
||||
},
|
||||
None,
|
||||
)]);
|
||||
});
|
||||
|
||||
cx.set_state("foo.baˇr", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("fooˇ.bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("foˇo.bar", Mode::Normal);
|
||||
|
||||
cx.set_state("foo(barˇ)", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("foo(baˇr)", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("fooˇ(bar)", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("foˇo(bar)", Mode::Normal);
|
||||
|
||||
cx.set_state("foo_bar_baˇz", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("foo_baˇr_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("foˇo_bar_baz", Mode::Normal);
|
||||
|
||||
cx.set_state("fooBarBaˇz", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("fooBaˇrBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("foˇoBarBaz", Mode::Normal);
|
||||
|
||||
cx.set_state("foo;baˇr", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("fooˇ;bar", Mode::Normal);
|
||||
cx.simulate_keystrokes("g e");
|
||||
cx.assert_state("foˇo;bar", Mode::Normal);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::ops::Range;
|
|||
|
||||
use crate::{
|
||||
Vim,
|
||||
motion::right,
|
||||
motion::{is_subword_end, is_subword_start, right},
|
||||
state::{Mode, Operator},
|
||||
};
|
||||
use editor::{
|
||||
|
|
@ -862,11 +862,8 @@ fn in_subword(
|
|||
.buffer_chars_at(offset)
|
||||
.next()
|
||||
.map(|(c, _)| {
|
||||
if classifier.is_word('-') {
|
||||
!classifier.is_whitespace(c) && c != '_' && c != '-'
|
||||
} else {
|
||||
!classifier.is_whitespace(c) && c != '_'
|
||||
}
|
||||
let is_separator = "._-".contains(c);
|
||||
!classifier.is_whitespace(c) && !is_separator
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
|
|
@ -877,28 +874,19 @@ fn in_subword(
|
|||
movement::FindRange::SingleLine,
|
||||
|left, right| {
|
||||
let is_word_start = classifier.kind(left) != classifier.kind(right);
|
||||
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|
||||
|| left == '_' && right != '_'
|
||||
|| left.is_lowercase() && right.is_uppercase();
|
||||
is_word_start || is_subword_start
|
||||
is_word_start || is_subword_start(left, right, "._-")
|
||||
},
|
||||
)
|
||||
} else {
|
||||
movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
|
||||
let is_word_start = classifier.kind(left) != classifier.kind(right);
|
||||
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|
||||
|| left == '_' && right != '_'
|
||||
|| left.is_lowercase() && right.is_uppercase();
|
||||
is_word_start || is_subword_start
|
||||
is_word_start || is_subword_start(left, right, "._-")
|
||||
})
|
||||
};
|
||||
|
||||
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
|
||||
let is_word_end = classifier.kind(left) != classifier.kind(right);
|
||||
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|
||||
|| left != '_' && right == '_'
|
||||
|| left.is_lowercase() && right.is_uppercase();
|
||||
is_word_end || is_subword_end
|
||||
is_word_end || is_subword_end(left, right, "._-")
|
||||
});
|
||||
|
||||
Some(start..end)
|
||||
|
|
@ -1039,20 +1027,17 @@ fn around_subword(
|
|||
right(map, relative_to, 1),
|
||||
movement::FindRange::SingleLine,
|
||||
|left, right| {
|
||||
let is_word_start = classifier.kind(left) != classifier.kind(right);
|
||||
let is_subword_start = classifier.is_word('-') && left != '-' && right == '-'
|
||||
|| left != '_' && right == '_'
|
||||
|| left.is_lowercase() && right.is_uppercase();
|
||||
is_word_start || is_subword_start
|
||||
let is_separator = |c: char| "._-".contains(c);
|
||||
let is_word_start =
|
||||
classifier.kind(left) != classifier.kind(right) && !is_separator(left);
|
||||
is_word_start || is_subword_start(left, right, "._-")
|
||||
},
|
||||
);
|
||||
|
||||
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
|
||||
let is_word_end = classifier.kind(left) != classifier.kind(right);
|
||||
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|
||||
|| left != '_' && right == '_'
|
||||
|| left.is_lowercase() && right.is_uppercase();
|
||||
is_word_end || is_subword_end
|
||||
let is_separator = |c: char| "._-".contains(c);
|
||||
let is_word_end = classifier.kind(left) != classifier.kind(right) && !is_separator(right);
|
||||
is_word_end || is_subword_end(left, right, "._-")
|
||||
});
|
||||
|
||||
Some(start..end).map(|range| expand_to_include_whitespace(map, range, true))
|
||||
|
|
@ -3909,4 +3894,73 @@ mod test {
|
|||
Mode::VisualLine,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_subword_object(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// Setup custom keybindings for subword object so we can use the
|
||||
// bindings in `simulate_keystrokes`.
|
||||
cx.update(|_window, cx| {
|
||||
cx.bind_keys([KeyBinding::new(
|
||||
"w",
|
||||
super::Subword {
|
||||
ignore_punctuation: false,
|
||||
},
|
||||
Some("vim_operator"),
|
||||
)]);
|
||||
});
|
||||
|
||||
cx.set_state("foo_ˇbar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("c i w");
|
||||
cx.assert_state("foo_ˇ_baz", Mode::Insert);
|
||||
|
||||
cx.set_state("ˇfoo_bar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("c i w");
|
||||
cx.assert_state("ˇ_bar_baz", Mode::Insert);
|
||||
|
||||
cx.set_state("foo_bar_baˇz", Mode::Normal);
|
||||
cx.simulate_keystrokes("c i w");
|
||||
cx.assert_state("foo_bar_ˇ", Mode::Insert);
|
||||
|
||||
cx.set_state("fooˇBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("c i w");
|
||||
cx.assert_state("fooˇBaz", Mode::Insert);
|
||||
|
||||
cx.set_state("ˇfooBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("c i w");
|
||||
cx.assert_state("ˇBarBaz", Mode::Insert);
|
||||
|
||||
cx.set_state("fooBarBaˇz", Mode::Normal);
|
||||
cx.simulate_keystrokes("c i w");
|
||||
cx.assert_state("fooBarˇ", Mode::Insert);
|
||||
|
||||
cx.set_state("foo.ˇbar.baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("c i w");
|
||||
cx.assert_state("foo.ˇ.baz", Mode::Insert);
|
||||
|
||||
cx.set_state("foo_ˇbar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("d i w");
|
||||
cx.assert_state("foo_ˇ_baz", Mode::Normal);
|
||||
|
||||
cx.set_state("fooˇBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("d i w");
|
||||
cx.assert_state("fooˇBaz", Mode::Normal);
|
||||
|
||||
cx.set_state("foo_ˇbar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("c a w");
|
||||
cx.assert_state("foo_ˇ_baz", Mode::Insert);
|
||||
|
||||
cx.set_state("fooˇBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("c a w");
|
||||
cx.assert_state("fooˇBaz", Mode::Insert);
|
||||
|
||||
cx.set_state("foo_ˇbar_baz", Mode::Normal);
|
||||
cx.simulate_keystrokes("d a w");
|
||||
cx.assert_state("foo_ˇ_baz", Mode::Normal);
|
||||
|
||||
cx.set_state("fooˇBarBaz", Mode::Normal);
|
||||
cx.simulate_keystrokes("d a w");
|
||||
cx.assert_state("fooˇBaz", Mode::Normal);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue