Add c and d operators to vim normal mode

Extracted motions from normal mode
Changed vim_submode to be vim_operator to enable better composition of operators
This commit is contained in:
Keith Simmons 2022-04-15 16:00:44 -07:00
parent 670757e5c9
commit 63278041e1
10 changed files with 862 additions and 433 deletions

View file

@ -1,212 +1,77 @@
mod g_prefix;
use crate::VimState;
use editor::{char_kind, movement, Bias};
use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
use crate::{
motion::Motion,
state::{Mode, Operator},
Vim,
};
use editor::Bias;
use gpui::MutableAppContext;
use language::SelectionGoal;
use serde::Deserialize;
use workspace::Workspace;
#[derive(Clone, Deserialize)]
struct MoveToNextWordStart(pub bool);
#[derive(Clone, Deserialize)]
struct MoveToNextWordEnd(pub bool);
#[derive(Clone, Deserialize)]
struct MoveToPreviousWordStart(pub bool);
impl_actions!(
vim,
[
MoveToNextWordStart,
MoveToNextWordEnd,
MoveToPreviousWordStart,
]
);
actions!(
vim,
[
GPrefix,
MoveLeft,
MoveDown,
MoveUp,
MoveRight,
MoveToStartOfLine,
MoveToEndOfLine,
MoveToEnd,
]
);
pub fn init(cx: &mut MutableAppContext) {
g_prefix::init(cx);
cx.add_action(move_left);
cx.add_action(move_down);
cx.add_action(move_up);
cx.add_action(move_right);
cx.add_action(move_to_start_of_line);
cx.add_action(move_to_end_of_line);
cx.add_action(move_to_end);
cx.add_action(move_to_next_word_start);
cx.add_action(move_to_next_word_end);
cx.add_action(move_to_previous_word_start);
}
fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1);
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
});
})
}
fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, movement::down);
});
pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| {
match vim.state.operator_stack.pop() {
None => move_cursor(vim, motion, cx),
Some(Operator::Change) => change_over(vim, motion, cx),
Some(Operator::Delete) => delete_over(vim, motion, cx),
Some(Operator::Namespace(_)) => panic!(
"Normal mode recieved motion with namespaced operator. Likely this means an invalid keymap was used"),
}
vim.clear_operator(cx);
});
}
fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, movement::up);
});
fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, cursor, goal| motion.move_point(map, cursor, goal))
});
}
fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.column_mut() += 1;
(map.clip_point(cursor, Bias::Right), SelectionGoal::None)
});
});
});
}
fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
// Don't clip at line ends during change operation
editor.set_clip_at_line_ends(false, cx);
editor.move_selections(cx, |map, selection| motion.expand_selection(map, selection));
editor.set_clip_at_line_ends(true, cx);
match motion {
Motion::Up => editor.insert(&"\n", cx),
Motion::Down => editor.insert(&"\n", cx),
_ => editor.insert(&"", cx),
}
fn move_to_start_of_line(
_: &mut Workspace,
_: &MoveToStartOfLine,
cx: &mut ViewContext<Workspace>,
) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, cursor, _| {
(
movement::line_beginning(map, cursor, false),
SelectionGoal::None,
)
});
});
});
}
fn move_to_end_of_line(_: &mut Workspace, _: &MoveToEndOfLine, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, cursor, _| {
(
map.clip_point(movement::line_end(map, cursor, false), Bias::Left),
SelectionGoal::None,
)
});
});
});
}
fn move_to_end(_: &mut Workspace, _: &MoveToEnd, cx: &mut ViewContext<Workspace>) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.replace_selections_with(cx, |map| map.clip_point(map.max_point(), Bias::Left));
});
});
}
fn move_to_next_word_start(
_: &mut Workspace,
&MoveToNextWordStart(treat_punctuation_as_word): &MoveToNextWordStart,
cx: &mut ViewContext<Workspace>,
) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
let mut crossed_newline = false;
cursor = movement::find_boundary(map, cursor, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
let at_newline = right == '\n';
let found = (left_kind != right_kind && !right.is_whitespace())
|| (at_newline && crossed_newline)
|| (at_newline && left == '\n'); // Prevents skipping repeated empty lines
if at_newline {
crossed_newline = true;
}
found
if let Motion::Up = motion {
// Position cursor on previous line after change
editor.move_cursors(cx, |map, cursor, goal| {
Motion::Up.move_point(map, cursor, goal)
});
(cursor, SelectionGoal::None)
});
}
});
});
vim.switch_mode(Mode::Insert, cx)
}
fn move_to_next_word_end(
_: &mut Workspace,
&MoveToNextWordEnd(treat_punctuation_as_word): &MoveToNextWordEnd,
cx: &mut ViewContext<Workspace>,
) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.column_mut() += 1;
cursor = movement::find_boundary(map, cursor, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
// Don't clip at line ends during delete operation
editor.set_clip_at_line_ends(false, cx);
editor.move_selections(cx, |map, selection| motion.expand_selection(map, selection));
match motion {
Motion::Up => editor.insert(&"\n", cx),
Motion::Down => editor.insert(&"\n", cx),
_ => editor.insert(&"", cx),
}
left_kind != right_kind && !left.is_whitespace()
if let Motion::Up = motion {
// Position cursor on previous line after change
editor.move_cursors(cx, |map, cursor, goal| {
Motion::Up.move_point(map, cursor, goal)
});
// 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 !map
.chars_at(cursor)
.skip(1)
.next()
.map(|c| c == '\n')
.unwrap_or(true)
{
*cursor.column_mut() = cursor.column().saturating_sub(1);
}
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
});
});
}
fn move_to_previous_word_start(
_: &mut Workspace,
&MoveToPreviousWordStart(treat_punctuation_as_word): &MoveToPreviousWordStart,
cx: &mut ViewContext<Workspace>,
) {
VimState::update_global(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
// This works even though find_preceding_boundary is called for every character in the line containing
// cursor because the newline is checked only once.
cursor = movement::find_preceding_boundary(map, cursor, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(treat_punctuation_as_word);
let right_kind = char_kind(right).coerce_punctuation(treat_punctuation_as_word);
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
});
(cursor, SelectionGoal::None)
}
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.move_selection_heads(cx, |map, head, _| {
(map.clip_point(head, Bias::Left), SelectionGoal::None)
});
});
});
@ -217,7 +82,13 @@ mod test {
use indoc::indoc;
use util::test::marked_text;
use crate::vim_test_context::VimTestContext;
use crate::{
state::{
Mode::{self, *},
Namespace, Operator,
},
vim_test_context::VimTestContext,
};
#[gpui::test]
async fn test_hjkl(cx: &mut gpui::TestAppContext) {
@ -362,7 +233,7 @@ mod test {
}
// Reset and test ignoring punctuation
cx.simulate_keystrokes(&["g", "g"]);
cx.simulate_keystrokes(["g", "g"]);
let (_, cursor_offsets) = marked_text(indoc! {"
The |quick-brown
|
@ -392,7 +263,7 @@ mod test {
}
// Reset and test ignoring punctuation
cx.simulate_keystrokes(&["g", "g"]);
cx.simulate_keystrokes(["g", "g"]);
let (_, cursor_offsets) = marked_text(indoc! {"
Th|e quick-brow|n
@ -434,4 +305,232 @@ mod test {
cx.assert_newest_selection_head_offset(cursor_offset);
}
}
#[gpui::test]
async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true, "").await;
// Can abort with escape to get back to normal mode
cx.simulate_keystroke("g");
assert_eq!(cx.mode(), Normal);
assert_eq!(
cx.active_operator(),
Some(Operator::Namespace(Namespace::G))
);
cx.simulate_keystroke("escape");
assert_eq!(cx.mode(), Normal);
assert_eq!(cx.active_operator(), None);
}
#[gpui::test]
async fn test_move_to_start(cx: &mut gpui::TestAppContext) {
let initial_content = indoc! {"
The quick
brown fox jumps
over the lazy dog"};
let mut cx = VimTestContext::new(cx, true, initial_content).await;
// Jump to the end to
cx.simulate_keystroke("shift-G");
cx.assert_editor_state(indoc! {"
The quick
brown fox jumps
over the lazy do|g"});
// Jump to the start
cx.simulate_keystrokes(["g", "g"]);
cx.assert_editor_state(indoc! {"
|The quick
brown fox jumps
over the lazy dog"});
assert_eq!(cx.mode(), Normal);
assert_eq!(cx.active_operator(), None);
// Repeat action doesn't change
cx.simulate_keystrokes(["g", "g"]);
cx.assert_editor_state(indoc! {"
|The quick
brown fox jumps
over the lazy dog"});
assert_eq!(cx.mode(), Normal);
assert_eq!(cx.active_operator(), None);
}
#[gpui::test]
async fn test_change(cx: &mut gpui::TestAppContext) {
fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
cx.assert_binding(
["c", motion],
initial_state,
Mode::Normal,
state_after,
Mode::Insert,
);
}
let cx = &mut VimTestContext::new(cx, true, "").await;
assert("h", "Te|st", "T|st", cx);
assert("l", "Te|st", "Te|t", cx);
assert("w", "|Test", "|", cx);
assert("w", "Te|st", "Te|", cx);
assert("w", "Te|st Test", "Te| Test", cx);
assert("e", "Te|st Test", "Te| Test", cx);
assert("b", "Te|st", "|st", cx);
assert("b", "Test Te|st", "Test |st", cx);
assert(
"w",
indoc! {"
The quick
brown |fox
jumps over"},
indoc! {"
The quick
brown |
jumps over"},
cx,
);
assert(
"shift-W",
indoc! {"
The quick
brown |fox-fox
jumps over"},
indoc! {"
The quick
brown |
jumps over"},
cx,
);
assert(
"k",
indoc! {"
The quick
brown |fox"},
indoc! {"
|
"},
cx,
);
assert(
"j",
indoc! {"
The q|uick
brown fox"},
indoc! {"
|"},
cx,
);
assert(
"shift-$",
indoc! {"
The q|uick
brown fox"},
indoc! {"
The q|
brown fox"},
cx,
);
assert(
"0",
indoc! {"
The q|uick
brown fox"},
indoc! {"
|uick
brown fox"},
cx,
);
}
#[gpui::test]
async fn test_delete(cx: &mut gpui::TestAppContext) {
fn assert(motion: &str, initial_state: &str, state_after: &str, cx: &mut VimTestContext) {
cx.assert_binding(
["d", motion],
initial_state,
Mode::Normal,
state_after,
Mode::Normal,
);
}
let cx = &mut VimTestContext::new(cx, true, "").await;
assert("h", "Te|st", "T|st", cx);
assert("l", "Te|st", "Te|t", cx);
assert("w", "|Test", "|", cx);
assert("w", "Te|st", "T|e", cx);
assert("w", "Te|st Test", "Te|Test", cx);
assert("e", "Te|st Test", "Te| Test", cx);
assert("b", "Te|st", "|st", cx);
assert("b", "Test Te|st", "Test |st", cx);
assert(
"w",
indoc! {"
The quick
brown |fox
jumps over"},
// Trailing space after cursor
indoc! {"
The quick
brown|
jumps over"},
cx,
);
assert(
"shift-W",
indoc! {"
The quick
brown |fox-fox
jumps over"},
// Trailing space after cursor
indoc! {"
The quick
brown|
jumps over"},
cx,
);
assert(
"k",
indoc! {"
The quick
brown |fox"},
indoc! {"
|
"},
cx,
);
assert(
"j",
indoc! {"
The q|uick
brown fox"},
indoc! {"
|"},
cx,
);
assert(
"shift-$",
indoc! {"
The q|uick
brown fox"},
indoc! {"
The |q
brown fox"},
cx,
);
assert(
"0",
indoc! {"
The q|uick
brown fox"},
indoc! {"
|uick
brown fox"},
cx,
);
}
}