diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 0cf51568fe..67775c6a67 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -2,10 +2,6 @@ { "context": "Editor && VimControl", "bindings": { - "i": [ - "vim::SwitchMode", - "Insert" - ], "g": [ "vim::PushOperator", { @@ -13,6 +9,7 @@ } ], "h": "vim::Left", + "backspace": "vim::Left", "j": "vim::Down", "k": "vim::Up", "l": "vim::Right", @@ -46,29 +43,41 @@ ] } }, - { - "context": "Editor && vim_operator == g", - "bindings": { - "g": "vim::StartOfDocument" - } - }, - { - "context": "Editor && vim_mode == insert", - "bindings": { - "escape": "vim::NormalBefore", - "ctrl-c": "vim::NormalBefore" - } - }, { "context": "Editor && vim_mode == normal", "bindings": { + "escape": "editor::Cancel", "c": [ "vim::PushOperator", "Change" ], + "shift-C": "vim::ChangeToEndOfLine", "d": [ "vim::PushOperator", "Delete" + ], + "shift-D": "vim::DeleteToEndOfLine", + "i": [ + "vim::SwitchMode", + "Insert" + ], + "shift-I": "vim::InsertFirstNonWhitespace", + "a": "vim::InsertAfter", + "shift-A": "vim::InsertEndOfLine", + "x": "vim::DeleteRight", + "shift-X": "vim::DeleteLeft", + "shift-^": "vim::FirstNonWhitespace", + "o": "vim::InsertLineBelow", + "shift-O": "vim::InsertLineAbove" + } + }, + { + "context": "Editor && vim_operator == g", + "bindings": { + "g": "vim::StartOfDocument", + "escape": [ + "vim::SwitchMode", + "Normal" ] } }, @@ -81,7 +90,27 @@ { "ignorePunctuation": true } - ] + ], + "c": "vim::CurrentLine" + } + }, + { + "context": "Editor && vim_operator == d", + "bindings": { + "d": "vim::CurrentLine" + } + }, + { + "context": "Editor && vim_mode == insert", + "bindings": { + "escape": "vim::NormalBefore", + "ctrl-c": "vim::NormalBefore" + } + }, + { + "context": "Editor && mode == singleline", + "bindings": { + "escape": "editor::Cancel" } } ] \ No newline at end of file diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d099cbef11..18cae80496 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1334,6 +1334,19 @@ impl Editor { self.update_selections(vec![selection], None, cx); } + pub fn display_selections( + &mut self, + cx: &mut ViewContext, + ) -> (DisplaySnapshot, Vec>) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self + .local_selections::(cx) + .into_iter() + .map(|selection| selection.map(|point| point.to_display_point(&display_map))) + .collect(); + (display_map, selections) + } + pub fn move_selections( &mut self, cx: &mut ViewContext, @@ -1382,6 +1395,25 @@ impl Editor { }); } + pub fn edit(&mut self, edits: I, cx: &mut ViewContext) + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + self.buffer.update(cx, |buffer, cx| buffer.edit(edits, cx)); + } + + pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut ViewContext) + where + I: IntoIterator, T)>, + S: ToOffset, + T: Into>, + { + self.buffer + .update(cx, |buffer, cx| buffer.edit_with_autoindent(edits, cx)); + } + fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { self.hide_context_menu(cx); @@ -1456,6 +1488,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; let newest_selection = self.newest_anchor_selection().clone(); + let position = display_map.clip_point(position, Bias::Left); let start; let end; diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 12b90ec8e6..f9dfc588e1 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -18,15 +18,11 @@ fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppCont } fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) { - let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) { - Mode::Insert - } else { - Mode::Normal - }; - Vim::update(cx, |state, cx| { state.active_editor = Some(editor.downgrade()); - state.switch_mode(mode, cx); + if editor.read(cx).mode() != EditorMode::Full { + state.switch_mode(Mode::Insert, cx); + } }); } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 95286516ba..ba4ccaf610 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1,7 +1,7 @@ use editor::{ char_kind, display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, Bias, DisplayPoint, + movement, Bias, CharKind, DisplayPoint, }; use gpui::{actions, impl_actions, MutableAppContext}; use language::{Selection, SelectionGoal}; @@ -23,6 +23,8 @@ pub enum Motion { NextWordStart { ignore_punctuation: bool }, NextWordEnd { ignore_punctuation: bool }, PreviousWordStart { ignore_punctuation: bool }, + FirstNonWhitespace, + CurrentLine, StartOfLine, EndOfLine, StartOfDocument, @@ -57,8 +59,10 @@ actions!( Down, Up, Right, + FirstNonWhitespace, StartOfLine, EndOfLine, + CurrentLine, StartOfDocument, EndOfDocument ] @@ -70,8 +74,12 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx)); cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx)); cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); + cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| { + motion(Motion::FirstNonWhitespace, cx) + }); cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx)); + cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx)); cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| { motion(Motion::StartOfDocument, cx) }); @@ -114,7 +122,7 @@ impl Motion { pub fn linewise(self) -> bool { use Motion::*; match self { - Down | Up | StartOfDocument | EndOfDocument => true, + Down | Up | StartOfDocument | EndOfDocument | CurrentLine => true, _ => false, } } @@ -156,8 +164,10 @@ impl Motion { previous_word_start(map, point, ignore_punctuation), SelectionGoal::None, ), + FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), StartOfLine => (start_of_line(map, point), SelectionGoal::None), EndOfLine => (end_of_line(map, point), SelectionGoal::None), + CurrentLine => (end_of_line(map, point), SelectionGoal::None), StartOfDocument => (start_of_document(map, point), SelectionGoal::None), EndOfDocument => (end_of_document(map, point), SelectionGoal::None), } @@ -290,6 +300,24 @@ fn previous_word_start( point } +fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + let mut column = 0; + for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) { + if ch == '\n' { + return point; + } + + if char_kind(ch) != CharKind::Whitespace { + break; + } + + column += ch.len_utf8() as u32; + } + + *point.column_mut() = column; + map.clip_point(point, Bias::Left) +} + fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { map.prev_line_boundary(point.to_point(map)).1 } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index dc919b651c..679f50bd26 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1,15 +1,62 @@ mod change; mod delete; -use crate::{motion::Motion, state::Operator, Vim}; +use crate::{ + motion::Motion, + state::{Mode, Operator}, + Vim, +}; use change::init as change_init; -use gpui::{actions, MutableAppContext}; +use collections::HashSet; +use editor::{Bias, DisplayPoint}; +use gpui::{actions, MutableAppContext, ViewContext}; +use language::SelectionGoal; +use workspace::Workspace; use self::{change::change_over, delete::delete_over}; -actions!(vim, [InsertLineAbove, InsertLineBelow, InsertAfter]); +actions!( + vim, + [ + InsertAfter, + InsertFirstNonWhitespace, + InsertEndOfLine, + InsertLineAbove, + InsertLineBelow, + DeleteLeft, + DeleteRight, + ChangeToEndOfLine, + DeleteToEndOfLine, + ] +); pub fn init(cx: &mut MutableAppContext) { + cx.add_action(insert_after); + cx.add_action(insert_first_non_whitespace); + cx.add_action(insert_end_of_line); + cx.add_action(insert_line_above); + cx.add_action(insert_line_below); + cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| { + Vim::update(cx, |vim, cx| { + delete_over(vim, Motion::Left, cx); + }) + }); + cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| { + Vim::update(cx, |vim, cx| { + delete_over(vim, Motion::Right, cx); + }) + }); + cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| { + Vim::update(cx, |vim, cx| { + change_over(vim, Motion::EndOfLine, cx); + }) + }); + cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| { + Vim::update(cx, |vim, cx| { + delete_over(vim, Motion::EndOfLine, cx); + }) + }); + change_init(cx); } @@ -33,6 +80,101 @@ fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) { }); } +fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.switch_mode(Mode::Insert, cx); + vim.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, cursor, goal| { + Motion::Right.move_point(map, cursor, goal) + }); + }); + }); +} + +fn insert_first_non_whitespace( + _: &mut Workspace, + _: &InsertFirstNonWhitespace, + cx: &mut ViewContext, +) { + Vim::update(cx, |vim, cx| { + vim.switch_mode(Mode::Insert, cx); + vim.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, cursor, goal| { + Motion::FirstNonWhitespace.move_point(map, cursor, goal) + }); + }); + }); +} + +fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.switch_mode(Mode::Insert, cx); + vim.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, cursor, goal| { + Motion::EndOfLine.move_point(map, cursor, goal) + }); + }); + }); +} + +fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.switch_mode(Mode::Insert, cx); + vim.update_active_editor(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + let (map, old_selections) = editor.display_selections(cx); + let selection_start_rows: HashSet = old_selections + .into_iter() + .map(|selection| selection.start.row()) + .collect(); + let edits = selection_start_rows.into_iter().map(|row| { + let (indent, _) = map.line_indent(row); + let start_of_line = map + .clip_point(DisplayPoint::new(row, 0), Bias::Left) + .to_point(&map); + let mut new_text = " ".repeat(indent as usize); + new_text.push('\n'); + (start_of_line..start_of_line, new_text) + }); + editor.edit(edits, cx); + editor.move_cursors(cx, |map, mut cursor, _| { + *cursor.row_mut() -= 1; + *cursor.column_mut() = map.line_len(cursor.row()); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); + }); + }); + }); +} + +fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + vim.switch_mode(Mode::Insert, cx); + vim.update_active_editor(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + let (map, old_selections) = editor.display_selections(cx); + let selection_end_rows: HashSet = old_selections + .into_iter() + .map(|selection| selection.end.row()) + .collect(); + let edits = selection_end_rows.into_iter().map(|row| { + let (indent, _) = map.line_indent(row); + let end_of_line = map + .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left) + .to_point(&map); + let mut new_text = "\n".to_string(); + new_text.push_str(&" ".repeat(indent as usize)); + (end_of_line..end_of_line, new_text) + }); + editor.move_cursors(cx, |map, cursor, goal| { + Motion::EndOfLine.move_point(map, cursor, goal) + }); + editor.edit(edits, cx); + }); + }); + }); +} + #[cfg(test)] mod test { use indoc::indoc; @@ -63,18 +205,18 @@ mod test { } #[gpui::test] - async fn test_l(cx: &mut gpui::TestAppContext) { + async fn test_backspace(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["l"]); - cx.assert("The q|uick", "The qu|ick"); - cx.assert("The quic|k", "The quic|k"); + let mut cx = cx.binding(["backspace"]); + cx.assert("The q|uick", "The |quick"); + cx.assert("|The quick", "|The quick"); cx.assert( indoc! {" - The quic|k - brown"}, + The quick + |brown"}, indoc! {" - The quic|k - brown"}, + The quick + |brown"}, ); } @@ -146,6 +288,22 @@ mod test { ); } + #[gpui::test] + async fn test_l(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["l"]); + cx.assert("The q|uick", "The qu|ick"); + cx.assert("The quic|k", "The quic|k"); + cx.assert( + indoc! {" + The quic|k + brown"}, + indoc! {" + The quic|k + brown"}, + ); + } + #[gpui::test] async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; @@ -242,7 +400,7 @@ mod test { } #[gpui::test] - async fn test_next_word_start(cx: &mut gpui::TestAppContext) { + async fn test_w(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; let (_, cursor_offsets) = marked_text(indoc! {" The |quick|-|brown @@ -289,7 +447,7 @@ mod test { } #[gpui::test] - async fn test_next_word_end(cx: &mut gpui::TestAppContext) { + async fn test_e(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; let (_, cursor_offsets) = marked_text(indoc! {" Th|e quic|k|-brow|n @@ -335,7 +493,7 @@ mod test { } #[gpui::test] - async fn test_previous_word_start(cx: &mut gpui::TestAppContext) { + async fn test_b(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; let (_, cursor_offsets) = marked_text(indoc! {" ||The |quick|-|brown @@ -397,7 +555,7 @@ mod test { } #[gpui::test] - async fn test_move_to_start(cx: &mut gpui::TestAppContext) { + async fn test_gg(cx: &mut gpui::TestAppContext) { let cx = VimTestContext::new(cx, true).await; let mut cx = cx.binding(["g", "g"]); cx.assert( @@ -449,4 +607,418 @@ mod test { over the lazy dog"}, ); } + + #[gpui::test] + async fn test_a(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["a"]).mode_after(Mode::Insert); + + cx.assert("The q|uick", "The qu|ick"); + cx.assert("The quic|k", "The quick|"); + } + + #[gpui::test] + async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["shift-A"]).mode_after(Mode::Insert); + cx.assert("The q|uick", "The quick|"); + cx.assert("The q|uick ", "The quick |"); + cx.assert("|", "|"); + cx.assert( + indoc! {" + The q|uick + brown fox"}, + indoc! {" + The quick| + brown fox"}, + ); + cx.assert( + indoc! {" + | + The quick"}, + indoc! {" + | + The quick"}, + ); + } + + #[gpui::test] + async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["shift-^"]); + cx.assert("The q|uick", "|The quick"); + cx.assert(" The q|uick", " |The quick"); + cx.assert("|", "|"); + cx.assert( + indoc! {" + The q|uick + brown fox"}, + indoc! {" + |The quick + brown fox"}, + ); + cx.assert( + indoc! {" + | + The quick"}, + indoc! {" + | + The quick"}, + ); + cx.assert( + indoc! {" + | + The quick"}, + indoc! {" + | + The quick"}, + ); + } + + #[gpui::test] + async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["shift-I"]).mode_after(Mode::Insert); + cx.assert("The q|uick", "|The quick"); + cx.assert(" The q|uick", " |The quick"); + cx.assert("|", "|"); + cx.assert( + indoc! {" + The q|uick + brown fox"}, + indoc! {" + |The quick + brown fox"}, + ); + cx.assert( + indoc! {" + | + The quick"}, + indoc! {" + | + The quick"}, + ); + } + + #[gpui::test] + async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["shift-D"]); + 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_x(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["x"]); + 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_left(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["shift-X"]); + 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_o(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["o"]).mode_after(Mode::Insert); + + cx.assert( + "|", + indoc! {" + + |"}, + ); + cx.assert( + "The |quick", + indoc! {" + The quick + |"}, + ); + cx.assert( + indoc! {" + The quick + brown |fox + jumps over"}, + indoc! {" + The quick + brown fox + | + jumps over"}, + ); + cx.assert( + indoc! {" + The quick + brown fox + jumps |over"}, + indoc! {" + The quick + brown fox + jumps over + |"}, + ); + cx.assert( + indoc! {" + The q|uick + brown fox + jumps over"}, + indoc! {" + The quick + | + brown fox + jumps over"}, + ); + cx.assert( + indoc! {" + The quick + | + brown fox"}, + indoc! {" + The quick + + | + brown fox"}, + ); + cx.assert( + indoc! {" + fn test() { + println!(|); + }"}, + indoc! {" + fn test() { + println!(); + | + }"}, + ); + cx.assert( + indoc! {" + fn test(|) { + println!(); + }"}, + indoc! {" + fn test() { + | + println!(); + }"}, + ); + } + + #[gpui::test] + async fn test_insert_line_above(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["shift-O"]).mode_after(Mode::Insert); + + cx.assert( + "|", + indoc! {" + | + "}, + ); + cx.assert( + "The |quick", + indoc! {" + | + The quick"}, + ); + cx.assert( + indoc! {" + The quick + brown |fox + jumps over"}, + indoc! {" + The quick + | + brown fox + jumps over"}, + ); + cx.assert( + indoc! {" + The quick + brown fox + jumps |over"}, + indoc! {" + The quick + brown fox + | + jumps over"}, + ); + cx.assert( + indoc! {" + The q|uick + brown fox + jumps over"}, + indoc! {" + | + The quick + brown fox + jumps over"}, + ); + cx.assert( + indoc! {" + The quick + | + brown fox"}, + indoc! {" + The quick + | + + brown fox"}, + ); + cx.assert( + indoc! {" + fn test() { + println!(|); + }"}, + indoc! {" + fn test() { + | + println!(); + }"}, + ); + cx.assert( + indoc! {" + fn test(|) { + println!(); + }"}, + indoc! {" + | + fn test() { + println!(); + }"}, + ); + } + + #[gpui::test] + async fn test_dd(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["d", "d"]); + + cx.assert("|", "|"); + cx.assert("The |quick", "|"); + cx.assert( + indoc! {" + The quick + brown |fox + jumps over"}, + indoc! {" + The quick + jumps |over"}, + ); + 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! {" + brown| fox + jumps over"}, + ); + cx.assert( + indoc! {" + The quick + | + brown fox"}, + indoc! {" + The quick + |brown fox"}, + ); + } + + #[gpui::test] + async fn test_cc(cx: &mut gpui::TestAppContext) { + let cx = VimTestContext::new(cx, true).await; + let mut cx = cx.binding(["c", "c"]).mode_after(Mode::Insert); + + cx.assert("|", "|"); + cx.assert("The |quick", "|"); + cx.assert( + indoc! {" + The quick + brown |fox + jumps over"}, + indoc! {" + The quick + | + jumps over"}, + ); + 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! {" + | + brown fox + jumps over"}, + ); + cx.assert( + indoc! {" + The quick + | + brown fox"}, + indoc! {" + The quick + | + brown fox"}, + ); + } }