From 0dba953ee8b98f15557969f0c8b0d73266a54e40 Mon Sep 17 00:00:00 2001 From: Zachiah Sawyer Date: Wed, 8 May 2024 12:00:24 -0600 Subject: [PATCH] vim: Add support for <, >, and ^, marks --- crates/vim/src/motion.rs | 3 +- crates/vim/src/normal/mark.rs | 67 +++++++++- crates/vim/src/test.rs | 115 +++++++++++++++++- crates/vim/src/utils.rs | 18 +++ crates/vim/src/vim.rs | 25 +++- crates/vim/test_data/test_builtin_marks.json | 36 ++++++ crates/vim/test_data/test_caret_mark.json | 26 ++++ .../vim/test_data/test_lowercase_marks.json | 15 +++ crates/vim/test_data/test_lt_gt_marks.json | 18 +++ crates/vim/test_data/test_period_mark.json | 14 +++ 10 files changed, 325 insertions(+), 12 deletions(-) create mode 100644 crates/vim/test_data/test_builtin_marks.json create mode 100644 crates/vim/test_data/test_caret_mark.json create mode 100644 crates/vim/test_data/test_lowercase_marks.json create mode 100644 crates/vim/test_data/test_lt_gt_marks.json create mode 100644 crates/vim/test_data/test_period_mark.json diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index ee34ede20319..50f3e3f23118 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -473,6 +473,7 @@ impl Motion { | WindowTop | WindowMiddle | WindowBottom + | Jump { line: true, .. } | EndOfParagraph => true, EndOfLine { .. } | Matching @@ -496,7 +497,7 @@ impl Motion { | FindBackward { .. } | RepeatFind { .. } | RepeatFindReversed { .. } - | Jump { .. } + | Jump { line: false, .. } | ZedSearchResult { .. } => false, } } diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index 8359b7ca8d29..8824a1ad77d4 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -3,7 +3,7 @@ use std::{ops::Range, sync::Arc}; use collections::HashSet; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, - Anchor, DisplayPoint, + movement, Anchor, Bias, DisplayPoint, }; use gpui::WindowContext; use language::SelectionGoal; @@ -13,13 +13,13 @@ use crate::{ Vim, }; -pub fn create_mark(vim: &mut Vim, text: Arc, cx: &mut WindowContext) { +pub fn create_mark(vim: &mut Vim, text: Arc, tail: bool, cx: &mut WindowContext) { let Some(anchors) = vim.update_active_editor(cx, |_, editor, _| { editor .selections .disjoint_anchors() .iter() - .map(|s| s.head().clone()) + .map(|s| if tail { s.tail() } else { s.head() }) .collect::>() }) else { return; @@ -29,15 +29,72 @@ pub fn create_mark(vim: &mut Vim, text: Arc, cx: &mut WindowContext) { vim.clear_operator(cx); } -pub fn jump(text: Arc, line: bool, cx: &mut WindowContext) { - let Some(anchors) = Vim::read(cx).state().marks.get(&*text).cloned() else { +pub fn create_mark_after(vim: &mut Vim, text: Arc, cx: &mut WindowContext) { + let Some(anchors) = vim.update_active_editor(cx, |_, editor, cx| { + let (map, selections) = editor.selections.all_display(cx); + selections + .into_iter() + .map(|selection| { + let point = movement::saturating_right(&map, selection.tail()); + map.buffer_snapshot + .anchor_before(point.to_offset(&map, Bias::Left)) + }) + .collect::>() + }) else { return; }; + vim.update_state(|state| state.marks.insert(text.to_string(), anchors)); + vim.clear_operator(cx); +} + +pub fn create_mark_before(vim: &mut Vim, text: Arc, cx: &mut WindowContext) { + let Some(anchors) = vim.update_active_editor(cx, |_, editor, cx| { + let (map, selections) = editor.selections.all_display(cx); + selections + .into_iter() + .map(|selection| { + let point = movement::saturating_left(&map, selection.head()); + map.buffer_snapshot + .anchor_before(point.to_offset(&map, Bias::Left)) + }) + .collect::>() + }) else { + return; + }; + + vim.update_state(|state| state.marks.insert(text.to_string(), anchors)); + vim.clear_operator(cx); +} + +pub fn jump(text: Arc, line: bool, cx: &mut WindowContext) { + let anchors = match &*text { + "{" | "}" => Vim::update(cx, |vim, cx| { + vim.update_active_editor(cx, |_, editor, cx| { + let (map, selections) = editor.selections.all_display(cx); + selections + .into_iter() + .map(|selection| { + let point = if &*text == "{" { + movement::start_of_paragraph(&map, selection.head(), 1) + } else { + movement::end_of_paragraph(&map, selection.head(), 1) + }; + map.buffer_snapshot + .anchor_before(point.to_offset(&map, Bias::Left)) + }) + .collect::>() + }) + }), + _ => Vim::read(cx).state().marks.get(&*text).cloned(), + }; + Vim::update(cx, |vim, cx| { vim.pop_operator(cx); }); + let Some(anchors) = anchors else { return }; + let is_active_operator = Vim::read(cx).state().active_operator().is_some(); if is_active_operator { if let Some(anchor) = anchors.last() { diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 10bd876b7ba4..2d3946e9fbb4 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -1075,7 +1075,7 @@ async fn test_mouse_selection(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_marks(cx: &mut TestAppContext) { +async fn test_lowercase_marks(cx: &mut TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; cx.set_shared_state("line one\nline ˇtwo\nline three").await; @@ -1090,3 +1090,116 @@ async fn test_marks(cx: &mut TestAppContext) { cx.simulate_shared_keystrokes(["^", "d", "`", "a"]).await; cx.assert_shared_state("line one\nˇtwo\nline three").await; } + +#[gpui::test] +async fn test_lt_gt_marks(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc!( + " + Line one + Line two + Line ˇthree + Line four + Line five + " + )) + .await; + + cx.simulate_shared_keystrokes(["v", "j", "escape", "k", "k"]) + .await; + + cx.simulate_shared_keystrokes(["'", "<"]).await; + cx.assert_shared_state(indoc!( + " + Line one + Line two + ˇLine three + Line four + Line five + " + )) + .await; + + cx.simulate_shared_keystrokes(["`", "<"]).await; + cx.assert_shared_state(indoc!( + " + Line one + Line two + Line ˇthree + Line four + Line five + " + )) + .await; + + cx.simulate_shared_keystrokes(["'", ">"]).await; + cx.assert_shared_state(indoc!( + " + Line one + Line two + Line three + ˇLine four + Line five + " + )) + .await; + + cx.simulate_shared_keystrokes(["`", ">"]).await; + cx.assert_shared_state(indoc!( + " + Line one + Line two + Line three + Line ˇfour + Line five + " + )) + .await; +} + +#[gpui::test] +async fn test_caret_mark(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc!( + " + Line one + Line two + Line three + ˇLine four + Line five + " + )) + .await; + + cx.simulate_shared_keystrokes([ + "c", "w", "shift-s", "t", "r", "a", "i", "g", "h", "t", " ", "t", "h", "i", "n", "g", + "escape", "j", "j", + ]) + .await; + + cx.simulate_shared_keystrokes(["'", "^"]).await; + cx.assert_shared_state(indoc!( + " + Line one + Line two + Line three + ˇStraight thing four + Line five + " + )) + .await; + + cx.simulate_shared_keystrokes(["`", "^"]).await; + cx.assert_shared_state(indoc!( + " + Line one + Line two + Line three + Straight thingˇ four + Line five + " + )) + .await; +} diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index 3af455a3090e..1888b303eb85 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -39,6 +39,24 @@ fn copy_selections_content_internal( let mut text = String::new(); let mut clipboard_selections = Vec::with_capacity(selections.len()); let mut ranges_to_highlight = Vec::new(); + + vim.update_state(|state| { + state.marks.insert( + "[".to_string(), + selections + .iter() + .map(|s| buffer.anchor_before(s.start)) + .collect(), + ); + state.marks.insert( + "]".to_string(), + selections + .iter() + .map(|s| buffer.anchor_after(s.end)) + .collect(), + ) + }); + { let mut is_first = true; for selection in selections.iter() { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 5aebe43afcd5..20b07c8046c0 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -30,7 +30,10 @@ use gpui::{ use language::{CursorShape, Point, SelectionGoal, TransactionId}; pub use mode_indicator::ModeIndicator; use motion::Motion; -use normal::normal_replace; +use normal::{ + mark::{create_mark, create_mark_after, create_mark_before}, + normal_replace, +}; use replace::multi_replace; use schemars::JsonSchema; use serde::Deserialize; @@ -420,6 +423,10 @@ impl Vim { // Sync editor settings like clip mode self.sync_vim_settings(cx); + if mode != Mode::Insert && last_mode == Mode::Insert { + create_mark_after(self, "^".into(), cx) + } + if leave_selections { return; } @@ -616,6 +623,7 @@ impl Vim { let is_multicursor = editor.read(cx).selections.count() > 1; let state = self.state(); + let mut is_visual = state.mode.is_visual(); if state.mode == Mode::Insert && state.current_tx.is_some() { if state.current_anchor.is_none() { self.update_state(|state| state.current_anchor = Some(newest)); @@ -632,11 +640,18 @@ impl Vim { } else { self.switch_mode(Mode::Visual, false, cx) } + is_visual = true; } else if newest.start == newest.end && !is_multicursor && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&state.mode) { - self.switch_mode(Mode::Normal, true, cx) + self.switch_mode(Mode::Normal, true, cx); + is_visual = false; + } + + if is_visual { + create_mark_before(self, ">".into(), cx); + create_mark(self, "<".into(), true, cx) } } @@ -708,9 +723,9 @@ impl Vim { } _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), }, - Some(Operator::Mark) => { - Vim::update(cx, |vim, cx| normal::mark::create_mark(vim, text, cx)) - } + Some(Operator::Mark) => Vim::update(cx, |vim, cx| { + normal::mark::create_mark(vim, text, false, cx) + }), Some(Operator::Jump { line }) => normal::mark::jump(text, line, cx), _ => match Vim::read(cx).state().mode { Mode::Replace => multi_replace(text, cx), diff --git a/crates/vim/test_data/test_builtin_marks.json b/crates/vim/test_data/test_builtin_marks.json new file mode 100644 index 000000000000..0d05385960aa --- /dev/null +++ b/crates/vim/test_data/test_builtin_marks.json @@ -0,0 +1,36 @@ +{"Put":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n"}} +{"Key":"v"} +{"Key":"j"} +{"Key":"escape"} +{"Key":"k"} +{"Key":"k"} +{"Key":"'"} +{"Key":"<"} +{"Get":{"state":"Line one\nLine two\nˇLine three\nLine four\nLine five\n","mode":"Normal"}} +{"Key":"`"} +{"Key":"<"} +{"Get":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n","mode":"Normal"}} +{"Key":"'"} +{"Key":">"} +{"Get":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n","mode":"Normal"}} +{"Key":"`"} +{"Key":">"} +{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"g"} +{"Key":"^"} +{"Key":"j"} +{"Key":"j"} +{"Key":"l"} +{"Key":"l"} +{"Key":"c"} +{"Key":"e"} +{"Key":"k"} +{"Key":"e"} +{"Key":"escape"} +{"Key":"'"} +{"Key":"."} +{"Get":{"state":"Line one\nLine two\nˇLike three\nLine four\nLine five\n","mode":"Normal"}} +{"Key":"`"} +{"Key":"."} +{"Get":{"state":"Line one\nLine two\nLiˇke three\nLine four\nLine five\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_caret_mark.json b/crates/vim/test_data/test_caret_mark.json new file mode 100644 index 000000000000..01edb7e836db --- /dev/null +++ b/crates/vim/test_data/test_caret_mark.json @@ -0,0 +1,26 @@ +{"Put":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n"}} +{"Key":"c"} +{"Key":"w"} +{"Key":"shift-s"} +{"Key":"t"} +{"Key":"r"} +{"Key":"a"} +{"Key":"i"} +{"Key":"g"} +{"Key":"h"} +{"Key":"t"} +{"Key":" "} +{"Key":"t"} +{"Key":"h"} +{"Key":"i"} +{"Key":"n"} +{"Key":"g"} +{"Key":"escape"} +{"Key":"j"} +{"Key":"j"} +{"Key":"'"} +{"Key":"^"} +{"Get":{"state":"Line one\nLine two\nLine three\nˇStraight thing four\nLine five\n","mode":"Normal"}} +{"Key":"`"} +{"Key":"^"} +{"Get":{"state":"Line one\nLine two\nLine three\nStraight thingˇ four\nLine five\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_lowercase_marks.json b/crates/vim/test_data/test_lowercase_marks.json new file mode 100644 index 000000000000..ff02c58728a3 --- /dev/null +++ b/crates/vim/test_data/test_lowercase_marks.json @@ -0,0 +1,15 @@ +{"Put":{"state":"line one\nline ˇtwo\nline three"}} +{"Key":"m"} +{"Key":"a"} +{"Key":"l"} +{"Key":"'"} +{"Key":"a"} +{"Get":{"state":"line one\nˇline two\nline three","mode":"Normal"}} +{"Key":"`"} +{"Key":"a"} +{"Get":{"state":"line one\nline ˇtwo\nline three","mode":"Normal"}} +{"Key":"^"} +{"Key":"d"} +{"Key":"`"} +{"Key":"a"} +{"Get":{"state":"line one\nˇtwo\nline three","mode":"Normal"}} diff --git a/crates/vim/test_data/test_lt_gt_marks.json b/crates/vim/test_data/test_lt_gt_marks.json new file mode 100644 index 000000000000..acd750daddfd --- /dev/null +++ b/crates/vim/test_data/test_lt_gt_marks.json @@ -0,0 +1,18 @@ +{"Put":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n"}} +{"Key":"v"} +{"Key":"j"} +{"Key":"escape"} +{"Key":"k"} +{"Key":"k"} +{"Key":"'"} +{"Key":"<"} +{"Get":{"state":"Line one\nLine two\nˇLine three\nLine four\nLine five\n","mode":"Normal"}} +{"Key":"`"} +{"Key":"<"} +{"Get":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n","mode":"Normal"}} +{"Key":"'"} +{"Key":">"} +{"Get":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n","mode":"Normal"}} +{"Key":"`"} +{"Key":">"} +{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_period_mark.json b/crates/vim/test_data/test_period_mark.json new file mode 100644 index 000000000000..6d3acea83c90 --- /dev/null +++ b/crates/vim/test_data/test_period_mark.json @@ -0,0 +1,14 @@ +{"Put":{"state":"Line one\nLine two\nLiˇne three\nLine four\nLine five\n"}} +{"Key":"c"} +{"Key":"e"} +{"Key":"k"} +{"Key":"e"} +{"Key":"escape"} +{"Key":"j"} +{"Key":"j"} +{"Key":"'"} +{"Key":"."} +{"Get":{"state":"Line one\nLine two\nˇLike three\nLine four\nLine five\n","mode":"Normal"}} +{"Key":"`"} +{"Key":"."} +{"Get":{"state":"Line one\nLine two\nLiˇke three\nLine four\nLine five\n","mode":"Normal"}}