Add inclusive vs exclusive motions to vim mode

This commit is contained in:
Keith Simmons 2022-04-21 16:14:58 -07:00
parent 0c587ae73c
commit 5ea782de21
11 changed files with 1350 additions and 750 deletions

View file

@ -28,7 +28,7 @@ mod test {
#[gpui::test]
async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true, "").await;
let mut cx = VimTestContext::new(cx, true).await;
cx.simulate_keystroke("i");
assert_eq!(cx.mode(), Mode::Insert);
cx.simulate_keystrokes(["T", "e", "s", "t"]);

View file

@ -4,7 +4,7 @@ use editor::{
movement, Bias, DisplayPoint,
};
use gpui::{actions, impl_actions, MutableAppContext};
use language::SelectionGoal;
use language::{Selection, SelectionGoal};
use serde::Deserialize;
use workspace::Workspace;
@ -14,22 +14,15 @@ use crate::{
Vim,
};
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug)]
pub enum Motion {
Left,
Down,
Up,
Right,
NextWordStart {
ignore_punctuation: bool,
stop_at_newline: bool,
},
NextWordEnd {
ignore_punctuation: bool,
},
PreviousWordStart {
ignore_punctuation: bool,
},
NextWordStart { ignore_punctuation: bool },
NextWordEnd { ignore_punctuation: bool },
PreviousWordStart { ignore_punctuation: bool },
StartOfLine,
EndOfLine,
StartOfDocument,
@ -41,8 +34,6 @@ pub enum Motion {
struct NextWordStart {
#[serde(default)]
ignore_punctuation: bool,
#[serde(default)]
stop_at_newline: bool,
}
#[derive(Clone, Deserialize)]
@ -87,19 +78,8 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
cx.add_action(
|_: &mut Workspace,
&NextWordStart {
ignore_punctuation,
stop_at_newline,
}: &NextWordStart,
cx: _| {
motion(
Motion::NextWordStart {
ignore_punctuation,
stop_at_newline,
},
cx,
)
|_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
motion(Motion::NextWordStart { ignore_punctuation }, cx)
},
);
cx.add_action(
@ -128,29 +108,48 @@ fn motion(motion: Motion, cx: &mut MutableAppContext) {
}
}
// Motion handling is specified here:
// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
impl Motion {
pub fn linewise(self) -> bool {
use Motion::*;
match self {
Down | Up | StartOfDocument | EndOfDocument => true,
_ => false,
}
}
pub fn inclusive(self) -> bool {
use Motion::*;
if self.linewise() {
return true;
}
match self {
EndOfLine | NextWordEnd { .. } => true,
Left | Right | StartOfLine | NextWordStart { .. } | PreviousWordStart { .. } => false,
_ => panic!("Exclusivity not defined for {self:?}"),
}
}
pub fn move_point(
self,
map: &DisplaySnapshot,
point: DisplayPoint,
goal: SelectionGoal,
block_cursor_positioning: bool,
) -> (DisplayPoint, SelectionGoal) {
use Motion::*;
match self {
Left => (left(map, point), SelectionGoal::None),
Down => movement::down(map, point, goal),
Up => movement::up(map, point, goal),
Down => movement::down(map, point, goal, true),
Up => movement::up(map, point, goal, true),
Right => (right(map, point), SelectionGoal::None),
NextWordStart {
ignore_punctuation,
stop_at_newline,
} => (
next_word_start(map, point, ignore_punctuation, stop_at_newline),
NextWordStart { ignore_punctuation } => (
next_word_start(map, point, ignore_punctuation),
SelectionGoal::None,
),
NextWordEnd { ignore_punctuation } => (
next_word_end(map, point, ignore_punctuation, block_cursor_positioning),
next_word_end(map, point, ignore_punctuation),
SelectionGoal::None,
),
PreviousWordStart { ignore_punctuation } => (
@ -164,11 +163,55 @@ impl Motion {
}
}
pub fn line_wise(self) -> bool {
use Motion::*;
match self {
Down | Up | StartOfDocument | EndOfDocument => true,
_ => false,
// Expands a selection using self motion for an operator
pub fn expand_selection(
self,
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
expand_to_surrounding_newline: bool,
) {
let (head, goal) = self.move_point(map, selection.head(), selection.goal);
selection.set_head(head, goal);
if self.linewise() {
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
if expand_to_surrounding_newline {
if selection.end.row() < map.max_point().row() {
*selection.end.row_mut() += 1;
*selection.end.column_mut() = 0;
// Don't reset the end here
return;
} else if selection.start.row() > 0 {
*selection.start.row_mut() -= 1;
*selection.start.column_mut() = map.line_len(selection.start.row());
}
}
selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
} else {
// If the motion is exclusive and the end of the motion is in column 1, the
// end of the motion is moved to the end of the previous line and the motion
// becomes inclusive. Example: "}" moves to the first line after a paragraph,
// but "d}" will not include that line.
let mut inclusive = self.inclusive();
if !inclusive
&& selection.end.row() > selection.start.row()
&& selection.end.column() == 0
&& selection.end.row() > 0
{
inclusive = true;
*selection.end.row_mut() -= 1;
*selection.end.column_mut() = 0;
selection.end = map.clip_point(
map.next_line_boundary(selection.end.to_point(map)).1,
Bias::Left,
);
}
if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
*selection.end.column_mut() += 1;
}
}
}
}
@ -187,7 +230,6 @@ fn next_word_start(
map: &DisplaySnapshot,
point: DisplayPoint,
ignore_punctuation: bool,
stop_at_newline: bool,
) -> DisplayPoint {
let mut crossed_newline = false;
movement::find_boundary(map, point, |left, right| {
@ -196,8 +238,8 @@ fn next_word_start(
let at_newline = right == '\n';
let found = (left_kind != right_kind && !right.is_whitespace())
|| (at_newline && (crossed_newline || stop_at_newline))
|| (at_newline && left == '\n'); // Prevents skipping repeated empty lines
|| at_newline && crossed_newline
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
if at_newline {
crossed_newline = true;
@ -210,7 +252,6 @@ fn next_word_end(
map: &DisplaySnapshot,
mut point: DisplayPoint,
ignore_punctuation: bool,
before_end_character: bool,
) -> DisplayPoint {
*point.column_mut() += 1;
point = movement::find_boundary(map, point, |left, right| {
@ -221,13 +262,12 @@ fn next_word_end(
});
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
// we have backtraced already
if before_end_character
&& !map
.chars_at(point)
.skip(1)
.next()
.map(|c| c == '\n')
.unwrap_or(true)
if !map
.chars_at(point)
.skip(1)
.next()
.map(|c| c == '\n')
.unwrap_or(true)
{
*point.column_mut() = point.column().saturating_sub(1);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,436 @@
use crate::{motion::Motion, state::Mode, Vim};
use editor::{char_kind, movement};
use gpui::{impl_actions, MutableAppContext, ViewContext};
use serde::Deserialize;
use workspace::Workspace;
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ChangeWord {
#[serde(default)]
ignore_punctuation: bool,
}
impl_actions!(vim, [ChangeWord]);
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(change_word);
}
pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.move_selections(cx, |map, selection| {
motion.expand_selection(map, selection, false);
});
editor.insert(&"", cx);
});
});
vim.switch_mode(Mode::Insert, cx)
}
// From the docs https://vimhelp.org/change.txt.html#cw
// Special case: When the cursor is in a word, "cw" and "cW" do not include the
// white space after a word, they only change up to the end of the word. This is
// because Vim interprets "cw" as change-word, and a word does not include the
// following white space.
fn change_word(
_: &mut Workspace,
&ChangeWord { ignore_punctuation }: &ChangeWord,
cx: &mut ViewContext<Workspace>,
) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.move_selections(cx, |map, selection| {
if selection.end.column() == map.line_len(selection.end.row()) {
return;
}
selection.end = movement::find_boundary(map, selection.end, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind || left == '\n' || right == '\n'
});
});
editor.insert(&"", cx);
});
});
vim.switch_mode(Mode::Insert, cx);
});
}
#[cfg(test)]
mod test {
use indoc::indoc;
use crate::{state::Mode, vim_test_context::VimTestContext};
#[gpui::test]
async fn test_change_h(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "h"]).mode_after(Mode::Insert);
cx.assert("Te|st", "T|st");
cx.assert("T|est", "|est");
cx.assert("|Test", "|Test");
cx.assert(
indoc! {"
Test
|test"},
indoc! {"
Test
|test"},
);
}
#[gpui::test]
async fn test_change_l(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "l"]).mode_after(Mode::Insert);
cx.assert("Te|st", "Te|t");
cx.assert("Tes|t", "Tes|");
}
#[gpui::test]
async fn test_change_w(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "w"]).mode_after(Mode::Insert);
cx.assert("Te|st", "Te|");
cx.assert("T|est test", "T| test");
cx.assert("Test| test", "Test|test");
cx.assert(
indoc! {"
Test te|st
test"},
indoc! {"
Test te|
test"},
);
cx.assert(
indoc! {"
Test tes|t
test"},
indoc! {"
Test tes|
test"},
);
cx.assert(
indoc! {"
Test test
|
test"},
indoc! {"
Test test
|
test"},
);
let mut cx = cx.binding(["c", "shift-W"]);
cx.assert("Test te|st-test test", "Test te| test");
}
#[gpui::test]
async fn test_change_e(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "e"]).mode_after(Mode::Insert);
cx.assert("Te|st Test", "Te| Test");
cx.assert("T|est test", "T| test");
cx.assert(
indoc! {"
Test te|st
test"},
indoc! {"
Test te|
test"},
);
cx.assert(
indoc! {"
Test tes|t
test"},
"Test tes|",
);
cx.assert(
indoc! {"
Test test
|
test"},
indoc! {"
Test test
|
test"},
);
let mut cx = cx.binding(["c", "shift-E"]);
cx.assert("Test te|st-test test", "Test te| test");
}
#[gpui::test]
async fn test_change_b(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "b"]).mode_after(Mode::Insert);
cx.assert("Te|st Test", "|st Test");
cx.assert("Test |test", "|test");
cx.assert("Test1 test2 |test3", "Test1 |test3");
cx.assert(
indoc! {"
Test test
|test"},
indoc! {"
Test |
test"},
);
cx.assert(
indoc! {"
Test test
|
test"},
indoc! {"
Test |
test"},
);
let mut cx = cx.binding(["c", "shift-B"]);
cx.assert("Test test-test |test", "Test |test");
}
#[gpui::test]
async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "shift-$"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
The q|uick
brown fox"},
indoc! {"
The q|
brown fox"},
);
cx.assert(
indoc! {"
The quick
|
brown fox"},
indoc! {"
The quick
|
brown fox"},
);
}
#[gpui::test]
async fn test_change_0(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "0"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
The q|uick
brown fox"},
indoc! {"
|uick
brown fox"},
);
cx.assert(
indoc! {"
The quick
|
brown fox"},
indoc! {"
The quick
|
brown fox"},
);
}
#[gpui::test]
async fn test_change_k(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "k"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
The quick
brown |fox
jumps over"},
indoc! {"
|
jumps over"},
);
cx.assert(
indoc! {"
The quick
brown fox
jumps |over"},
indoc! {"
The quick
|"},
);
cx.assert(
indoc! {"
The q|uick
brown fox
jumps over"},
indoc! {"
|
brown fox
jumps over"},
);
cx.assert(
indoc! {"
|
brown fox
jumps over"},
indoc! {"
|
brown fox
jumps over"},
);
}
#[gpui::test]
async fn test_change_j(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "j"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
The quick
brown |fox
jumps over"},
indoc! {"
The quick
|"},
);
cx.assert(
indoc! {"
The quick
brown fox
jumps |over"},
indoc! {"
The quick
brown fox
|"},
);
cx.assert(
indoc! {"
The q|uick
brown fox
jumps over"},
indoc! {"
|
jumps over"},
);
cx.assert(
indoc! {"
The quick
brown fox
|"},
indoc! {"
The quick
brown fox
|"},
);
}
#[gpui::test]
async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "shift-G"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
The quick
brown| fox
jumps over
the lazy"},
indoc! {"
The quick
|"},
);
cx.assert(
indoc! {"
The quick
brown| fox
jumps over
the lazy"},
indoc! {"
The quick
|"},
);
cx.assert(
indoc! {"
The quick
brown fox
jumps over
the l|azy"},
indoc! {"
The quick
brown fox
jumps over
|"},
);
cx.assert(
indoc! {"
The quick
brown fox
jumps over
|"},
indoc! {"
The quick
brown fox
jumps over
|"},
);
}
#[gpui::test]
async fn test_change_gg(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["c", "g", "g"]).mode_after(Mode::Insert);
cx.assert(
indoc! {"
The quick
brown| fox
jumps over
the lazy"},
indoc! {"
|
jumps over
the lazy"},
);
cx.assert(
indoc! {"
The quick
brown fox
jumps over
the l|azy"},
"|",
);
cx.assert(
indoc! {"
The q|uick
brown fox
jumps over
the lazy"},
indoc! {"
|
brown fox
jumps over
the lazy"},
);
cx.assert(
indoc! {"
|
brown fox
jumps over
the lazy"},
indoc! {"
|
brown fox
jumps over
the lazy"},
);
}
}

View file

@ -0,0 +1,386 @@
use crate::{motion::Motion, Vim};
use editor::Bias;
use gpui::MutableAppContext;
use language::SelectionGoal;
pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.move_selections(cx, |map, selection| {
let original_head = selection.head();
motion.expand_selection(map, selection, true);
selection.goal = SelectionGoal::Column(original_head.column());
});
editor.insert(&"", cx);
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.move_cursors(cx, |map, mut cursor, goal| {
if motion.linewise() {
if let SelectionGoal::Column(column) = goal {
*cursor.column_mut() = column
}
}
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
});
});
}
#[cfg(test)]
mod test {
use indoc::indoc;
use crate::vim_test_context::VimTestContext;
#[gpui::test]
async fn test_delete_h(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "h"]);
cx.assert("Te|st", "T|st");
cx.assert("T|est", "|est");
cx.assert("|Test", "|Test");
cx.assert(
indoc! {"
Test
|test"},
indoc! {"
Test
|test"},
);
}
#[gpui::test]
async fn test_delete_l(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "l"]);
cx.assert("|Test", "|est");
cx.assert("Te|st", "Te|t");
cx.assert("Tes|t", "Te|s");
cx.assert(
indoc! {"
Tes|t
test"},
indoc! {"
Te|s
test"},
);
}
#[gpui::test]
async fn test_delete_w(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "w"]);
cx.assert("Te|st", "T|e");
cx.assert("T|est test", "T|test");
cx.assert(
indoc! {"
Test te|st
test"},
indoc! {"
Test t|e
test"},
);
cx.assert(
indoc! {"
Test tes|t
test"},
indoc! {"
Test te|s
test"},
);
cx.assert(
indoc! {"
Test test
|
test"},
indoc! {"
Test test
|
test"},
);
let mut cx = cx.binding(["d", "shift-W"]);
cx.assert("Test te|st-test test", "Test te|test");
}
#[gpui::test]
async fn test_delete_e(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "e"]);
cx.assert("Te|st Test", "Te| Test");
cx.assert("T|est test", "T| test");
cx.assert(
indoc! {"
Test te|st
test"},
indoc! {"
Test t|e
test"},
);
cx.assert(
indoc! {"
Test tes|t
test"},
"Test te|s",
);
cx.assert(
indoc! {"
Test test
|
test"},
indoc! {"
Test test
|
test"},
);
let mut cx = cx.binding(["d", "shift-E"]);
cx.assert("Test te|st-test test", "Test te| test");
}
#[gpui::test]
async fn test_delete_b(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "b"]);
cx.assert("Te|st Test", "|st Test");
cx.assert("Test |test", "|test");
cx.assert("Test1 test2 |test3", "Test1 |test3");
cx.assert(
indoc! {"
Test test
|test"},
// Trailing whitespace after cursor
indoc! {"
Test|
test"},
);
cx.assert(
indoc! {"
Test test
|
test"},
// Trailing whitespace after cursor
indoc! {"
Test|
test"},
);
let mut cx = cx.binding(["d", "shift-B"]);
cx.assert("Test test-test |test", "Test |test");
}
#[gpui::test]
async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "shift-$"]);
cx.assert(
indoc! {"
The q|uick
brown fox"},
indoc! {"
The |q
brown fox"},
);
cx.assert(
indoc! {"
The quick
|
brown fox"},
indoc! {"
The quick
|
brown fox"},
);
}
#[gpui::test]
async fn test_delete_0(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "0"]);
cx.assert(
indoc! {"
The q|uick
brown fox"},
indoc! {"
|uick
brown fox"},
);
cx.assert(
indoc! {"
The quick
|
brown fox"},
indoc! {"
The quick
|
brown fox"},
);
}
#[gpui::test]
async fn test_delete_k(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "k"]);
cx.assert(
indoc! {"
The quick
brown |fox
jumps over"},
"jumps |over",
);
cx.assert(
indoc! {"
The quick
brown fox
jumps |over"},
"The qu|ick",
);
cx.assert(
indoc! {"
The q|uick
brown fox
jumps over"},
indoc! {"
brown| fox
jumps over"},
);
cx.assert(
indoc! {"
|brown fox
jumps over"},
"|jumps over",
);
}
#[gpui::test]
async fn test_delete_j(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "j"]);
cx.assert(
indoc! {"
The quick
brown |fox
jumps over"},
"The qu|ick",
);
cx.assert(
indoc! {"
The quick
brown fox
jumps |over"},
indoc! {"
The quick
brown |fox"},
);
cx.assert(
indoc! {"
The q|uick
brown fox
jumps over"},
"jumps| over",
);
cx.assert(
indoc! {"
The quick
brown fox
|"},
indoc! {"
The quick
|brown fox"},
);
}
#[gpui::test]
async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "shift-G"]);
cx.assert(
indoc! {"
The quick
brown| fox
jumps over
the lazy"},
"The q|uick",
);
cx.assert(
indoc! {"
The quick
brown| fox
jumps over
the lazy"},
"The q|uick",
);
cx.assert(
indoc! {"
The quick
brown fox
jumps over
the l|azy"},
indoc! {"
The quick
brown fox
jumps| over"},
);
cx.assert(
indoc! {"
The quick
brown fox
jumps over
|"},
indoc! {"
The quick
brown fox
|jumps over"},
);
}
#[gpui::test]
async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
let cx = VimTestContext::new(cx, true).await;
let mut cx = cx.binding(["d", "g", "g"]);
cx.assert(
indoc! {"
The quick
brown| fox
jumps over
the lazy"},
indoc! {"
jumps| over
the lazy"},
);
cx.assert(
indoc! {"
The quick
brown fox
jumps over
the l|azy"},
"|",
);
cx.assert(
indoc! {"
The q|uick
brown fox
jumps over
the lazy"},
indoc! {"
brown| fox
jumps over
the lazy"},
);
cx.assert(
indoc! {"
|
brown fox
jumps over
the lazy"},
indoc! {"
|brown fox
jumps over
the lazy"},
);
}
}

View file

@ -1,10 +1,11 @@
#[cfg(test)]
mod vim_test_context;
mod editor_events;
mod insert;
mod motion;
mod normal;
mod state;
#[cfg(test)]
mod vim_test_context;
use collections::HashMap;
use editor::{CursorShape, Editor};
@ -25,6 +26,7 @@ impl_actions!(vim, [SwitchMode, PushOperator]);
pub fn init(cx: &mut MutableAppContext) {
editor_events::init(cx);
normal::init(cx);
insert::init(cx);
motion::init(cx);
@ -142,14 +144,14 @@ mod test {
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, false, "").await;
let mut cx = VimTestContext::new(cx, false).await;
cx.simulate_keystrokes(["h", "j", "k", "l"]);
cx.assert_editor_state("hjkl|");
}
#[gpui::test]
async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true, "").await;
let mut cx = VimTestContext::new(cx, true).await;
cx.simulate_keystroke("i");
assert_eq!(cx.mode(), Mode::Insert);

View file

@ -15,11 +15,7 @@ pub struct VimTestContext<'a> {
}
impl<'a> VimTestContext<'a> {
pub async fn new(
cx: &'a mut gpui::TestAppContext,
enabled: bool,
initial_editor_text: &str,
) -> VimTestContext<'a> {
pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
cx.update(|cx| {
editor::init(cx);
crate::init(cx);
@ -38,10 +34,7 @@ impl<'a> VimTestContext<'a> {
params
.fs
.as_fake()
.insert_tree(
"/root",
json!({ "dir": { "test.txt": initial_editor_text } }),
)
.insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
.await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
@ -202,6 +195,14 @@ impl<'a> VimTestContext<'a> {
assert_eq!(self.mode(), mode_after);
assert_eq!(self.active_operator(), None);
}
pub fn binding<const COUNT: usize>(
mut self,
keystrokes: [&'static str; COUNT],
) -> VimBindingTestContext<'a, COUNT> {
let mode = self.mode();
VimBindingTestContext::new(keystrokes, mode, mode, self)
}
}
impl<'a> Deref for VimTestContext<'a> {
@ -211,3 +212,61 @@ impl<'a> Deref for VimTestContext<'a> {
self.cx
}
}
pub struct VimBindingTestContext<'a, const COUNT: usize> {
cx: VimTestContext<'a>,
keystrokes_under_test: [&'static str; COUNT],
initial_mode: Mode,
mode_after: Mode,
}
impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> {
pub fn new(
keystrokes_under_test: [&'static str; COUNT],
initial_mode: Mode,
mode_after: Mode,
cx: VimTestContext<'a>,
) -> Self {
Self {
cx,
keystrokes_under_test,
initial_mode,
mode_after,
}
}
pub fn binding<const NEW_COUNT: usize>(
self,
keystrokes_under_test: [&'static str; NEW_COUNT],
) -> VimBindingTestContext<'a, NEW_COUNT> {
VimBindingTestContext {
keystrokes_under_test,
cx: self.cx,
initial_mode: self.initial_mode,
mode_after: self.mode_after,
}
}
pub fn mode_after(mut self, mode_after: Mode) -> Self {
self.mode_after = mode_after;
self
}
pub fn assert(&mut self, initial_state: &str, state_after: &str) {
self.cx.assert_binding(
self.keystrokes_under_test,
initial_state,
self.initial_mode,
state_after,
self.mode_after,
)
}
}
impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> {
type Target = VimTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}