ZIm/crates/vim/src/normal.rs
Max Brunsfeld 5242a3a6dc Restructure action macro to assign a namespace to every action
Also, allow arbitrary types to be used as Actions via the impl_actions macro

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Keith Simmons <keith@zed.dev>
2022-04-07 16:20:49 -07:00

455 lines
14 KiB
Rust

mod g_prefix;
use crate::{mode::NormalState, Mode, SwitchMode, VimState};
use editor::{char_kind, movement, Bias};
use gpui::{actions, impl_actions, keymap::Binding, MutableAppContext, ViewContext};
use language::SelectionGoal;
use workspace::Workspace;
#[derive(Clone)]
struct MoveToNextWordStart(pub bool);
#[derive(Clone)]
struct MoveToNextWordEnd(pub bool);
#[derive(Clone)]
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) {
let context = Some("Editor && vim_mode == normal");
cx.add_bindings(vec![
Binding::new("i", SwitchMode(Mode::Insert), context),
Binding::new("g", SwitchMode(Mode::Normal(NormalState::GPrefix)), context),
Binding::new("h", MoveLeft, context),
Binding::new("j", MoveDown, context),
Binding::new("k", MoveUp, context),
Binding::new("l", MoveRight, context),
Binding::new("0", MoveToStartOfLine, context),
Binding::new("shift-$", MoveToEndOfLine, context),
Binding::new("shift-G", MoveToEnd, context),
Binding::new("w", MoveToNextWordStart(false), context),
Binding::new("shift-W", MoveToNextWordStart(true), context),
Binding::new("e", MoveToNextWordEnd(false), context),
Binding::new("shift-E", MoveToNextWordEnd(true), context),
Binding::new("b", MoveToPreviousWordStart(false), context),
Binding::new("shift-B", MoveToPreviousWordStart(true), context),
]);
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);
});
});
}
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_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 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
});
(cursor, SelectionGoal::None)
});
});
});
}
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);
left_kind != right_kind && !left.is_whitespace()
});
// 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)
});
});
});
}
#[cfg(test)]
mod test {
use indoc::indoc;
use util::test::marked_text;
use crate::vim_test_context::VimTestContext;
#[gpui::test]
async fn test_hjkl(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true, "Test\nTestTest\nTest").await;
cx.simulate_keystroke("l");
cx.assert_editor_state(indoc! {"
T|est
TestTest
Test"});
cx.simulate_keystroke("h");
cx.assert_editor_state(indoc! {"
|Test
TestTest
Test"});
cx.simulate_keystroke("j");
cx.assert_editor_state(indoc! {"
Test
|TestTest
Test"});
cx.simulate_keystroke("k");
cx.assert_editor_state(indoc! {"
|Test
TestTest
Test"});
cx.simulate_keystroke("j");
cx.assert_editor_state(indoc! {"
Test
|TestTest
Test"});
// When moving left, cursor does not wrap to the previous line
cx.simulate_keystroke("h");
cx.assert_editor_state(indoc! {"
Test
|TestTest
Test"});
// When moving right, cursor does not reach the line end or wrap to the next line
for _ in 0..9 {
cx.simulate_keystroke("l");
}
cx.assert_editor_state(indoc! {"
Test
TestTes|t
Test"});
// Goal column respects the inability to reach the end of the line
cx.simulate_keystroke("k");
cx.assert_editor_state(indoc! {"
Tes|t
TestTest
Test"});
cx.simulate_keystroke("j");
cx.assert_editor_state(indoc! {"
Test
TestTes|t
Test"});
}
#[gpui::test]
async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) {
let initial_content = indoc! {"
Test Test
T"};
let mut cx = VimTestContext::new(cx, true, initial_content).await;
cx.simulate_keystroke("shift-$");
cx.assert_editor_state(indoc! {"
Test Tes|t
T"});
cx.simulate_keystroke("0");
cx.assert_editor_state(indoc! {"
|Test Test
T"});
cx.simulate_keystroke("j");
cx.simulate_keystroke("shift-$");
cx.assert_editor_state(indoc! {"
Test Test
|
T"});
cx.simulate_keystroke("0");
cx.assert_editor_state(indoc! {"
Test Test
|
T"});
cx.simulate_keystroke("j");
cx.simulate_keystroke("shift-$");
cx.assert_editor_state(indoc! {"
Test Test
|T"});
cx.simulate_keystroke("0");
cx.assert_editor_state(indoc! {"
Test Test
|T"});
}
#[gpui::test]
async fn test_jump_to_end(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;
cx.simulate_keystroke("shift-G");
cx.assert_editor_state(indoc! {"
The quick
brown fox jumps
over the lazy do|g"});
// Repeat the action doesn't move
cx.simulate_keystroke("shift-G");
cx.assert_editor_state(indoc! {"
The quick
brown fox jumps
over the lazy do|g"});
}
#[gpui::test]
async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
let (initial_content, cursor_offsets) = marked_text(indoc! {"
The |quick|-|brown
|
|
|fox_jumps |over
|th||e"});
let mut cx = VimTestContext::new(cx, true, &initial_content).await;
for cursor_offset in cursor_offsets {
cx.simulate_keystroke("w");
cx.assert_newest_selection_head_offset(cursor_offset);
}
// Reset and test ignoring punctuation
cx.simulate_keystrokes(&["g", "g"]);
let (_, cursor_offsets) = marked_text(indoc! {"
The |quick-brown
|
|
|fox_jumps |over
|th||e"});
for cursor_offset in cursor_offsets {
cx.simulate_keystroke("shift-W");
cx.assert_newest_selection_head_offset(cursor_offset);
}
}
#[gpui::test]
async fn test_next_word_end(cx: &mut gpui::TestAppContext) {
let (initial_content, cursor_offsets) = marked_text(indoc! {"
Th|e quic|k|-brow|n
fox_jump|s ove|r
th|e"});
let mut cx = VimTestContext::new(cx, true, &initial_content).await;
for cursor_offset in cursor_offsets {
cx.simulate_keystroke("e");
cx.assert_newest_selection_head_offset(cursor_offset);
}
// Reset and test ignoring punctuation
cx.simulate_keystrokes(&["g", "g"]);
let (_, cursor_offsets) = marked_text(indoc! {"
Th|e quick-brow|n
fox_jump|s ove|r
th||e"});
for cursor_offset in cursor_offsets {
cx.simulate_keystroke("shift-E");
cx.assert_newest_selection_head_offset(cursor_offset);
}
}
#[gpui::test]
async fn test_previous_word_start(cx: &mut gpui::TestAppContext) {
let (initial_content, cursor_offsets) = marked_text(indoc! {"
||The |quick|-|brown
|
|
|fox_jumps |over
|the"});
let mut cx = VimTestContext::new(cx, true, &initial_content).await;
cx.simulate_keystroke("shift-G");
for cursor_offset in cursor_offsets.into_iter().rev() {
cx.simulate_keystroke("b");
cx.assert_newest_selection_head_offset(cursor_offset);
}
// Reset and test ignoring punctuation
cx.simulate_keystroke("shift-G");
let (_, cursor_offsets) = marked_text(indoc! {"
||The |quick-brown
|
|
|fox_jumps |over
|the"});
for cursor_offset in cursor_offsets.into_iter().rev() {
cx.simulate_keystroke("shift-B");
cx.assert_newest_selection_head_offset(cursor_offset);
}
}
}