From 833a7b6e76ef2f87684624d34da53f56e02670f7 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Fri, 22 Apr 2022 09:43:15 -0700 Subject: [PATCH] WIP just missing insert line above and below --- assets/keymaps/vim.json | 63 ++-- crates/editor/src/editor.rs | 1 + crates/vim/src/motion.rs | 32 +- crates/vim/src/normal.rs | 587 +++++++++++++++++++++++++++++++++++- 4 files changed, 648 insertions(+), 35 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 0cf51568fe..789e05b912 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,39 @@ ] } }, - { - "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" + } + }, + { + "context": "Editor && vim_operator == g", + "bindings": { + "g": "vim::StartOfDocument", + "escape": [ + "vim::SwitchMode", + "Normal" ] } }, @@ -81,7 +88,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..ced21d8ee4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1456,6 +1456,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/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..a1740ea09e 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1,15 +1,60 @@ 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 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 +78,88 @@ 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| { + editor.move_cursors(cx, |map, cursor, goal| { + let (indent, _) = map.line_indent(cursor.row()); + let (cursor, _) = Motion::EndOfLine.move_point(map, cursor, goal); + (cursor, SelectionGoal::Column(indent)) + }); + editor.insert("\n", cx); + editor.move_cursors(cx, |_, mut cursor, goal| { + if let SelectionGoal::Column(column) = goal { + *cursor.column_mut() = column; + } + (cursor, SelectionGoal::None) + }); + }); + }); + }); +} + +fn insert_line_below(_: &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| { + editor.move_cursors(cx, |map, cursor, goal| { + let (indent, _) = map.line_indent(cursor.row()); + let (cursor, _) = Motion::StartOfLine.move_point(map, cursor, goal); + (cursor, SelectionGoal::Column(indent)) + }); + editor.insert("\n", cx); + editor.move_cursors(cx, |_, mut cursor, goal| { + *cursor.row_mut() -= 1; + if let SelectionGoal::Column(column) = goal { + *cursor.column_mut() = column; + } + (cursor, SelectionGoal::None) + }); + }); + }); + }); +} + #[cfg(test)] mod test { use indoc::indoc; @@ -63,18 +190,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 +273,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 +385,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 +432,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 +478,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 +540,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 +592,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"}, + ); + } }