From 8566b64668a871bd44c66f06e62b47151033e11d Mon Sep 17 00:00:00 2001 From: Zachiah Sawyer Date: Tue, 7 May 2024 11:49:15 -0600 Subject: [PATCH] vim: Add basic mark support --- assets/keymaps/vim.json | 3 + crates/vim/src/motion.rs | 12 +++- crates/vim/src/normal.rs | 1 + crates/vim/src/normal/mark.rs | 86 ++++++++++++++++++++++++++++ crates/vim/src/state.rs | 14 ++++- crates/vim/src/test.rs | 17 ++++++ crates/vim/src/vim.rs | 8 ++- crates/vim/test_data/test_marks.json | 15 +++++ 8 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 crates/vim/src/normal/mark.rs create mode 100644 crates/vim/test_data/test_marks.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1bb362a324075..552624c82f186 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -117,6 +117,9 @@ } } ], + "m": ["vim::PushOperator", "Mark"], + "'": ["vim::PushOperator", { "Jump": { "line": true } }], + "`": ["vim::PushOperator", { "Jump": { "line": false } }], ";": "vim::RepeatFind", ",": "vim::RepeatFindReversed", "ctrl-o": "pane::GoBack", diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 2c50f52f5a353..ee34ede203197 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -13,7 +13,7 @@ use std::ops::Range; use workspace::Workspace; use crate::{ - normal::normal_motion, + normal::{mark, normal_motion}, state::{Mode, Operator}, surrounds::SurroundsType, utils::coerce_punctuation, @@ -105,6 +105,10 @@ pub enum Motion { prior_selections: Vec>, new_selections: Vec>, }, + Jump { + anchor: Anchor, + line: bool, + }, } #[derive(Clone, Deserialize, PartialEq)] @@ -492,6 +496,7 @@ impl Motion { | FindBackward { .. } | RepeatFind { .. } | RepeatFindReversed { .. } + | Jump { .. } | ZedSearchResult { .. } => false, } } @@ -531,7 +536,8 @@ impl Motion { | WindowMiddle | WindowBottom | NextLineStart - | ZedSearchResult { .. } => false, + | ZedSearchResult { .. } + | Jump { .. } => false, } } @@ -570,6 +576,7 @@ impl Motion { | PreviousSubwordStart { .. } | FirstNonWhitespace { .. } | FindBackward { .. } + | Jump { .. } | ZedSearchResult { .. } => false, RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => { motion.inclusive() @@ -761,6 +768,7 @@ impl Motion { WindowTop => window_top(map, point, &text_layout_details, times - 1), WindowMiddle => window_middle(map, point, &text_layout_details), WindowBottom => window_bottom(map, point, &text_layout_details, times - 1), + Jump { line, anchor } => mark::jump_motion(map, *anchor, *line), ZedSearchResult { new_selections, .. } => { // There will be only one selection, as // Search::SelectNextMatch selects a single match. diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index e0eb1e46d1123..9510e4f05078c 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -2,6 +2,7 @@ mod case; mod change; mod delete; mod increment; +pub(crate) mod mark; mod paste; pub(crate) mod repeat; mod scroll; diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs new file mode 100644 index 0000000000000..8359b7ca8d29a --- /dev/null +++ b/crates/vim/src/normal/mark.rs @@ -0,0 +1,86 @@ +use std::{ops::Range, sync::Arc}; + +use collections::HashSet; +use editor::{ + display_map::{DisplaySnapshot, ToDisplayPoint}, + Anchor, DisplayPoint, +}; +use gpui::WindowContext; +use language::SelectionGoal; + +use crate::{ + motion::{self, Motion}, + Vim, +}; + +pub fn create_mark(vim: &mut Vim, text: Arc, cx: &mut WindowContext) { + let Some(anchors) = vim.update_active_editor(cx, |_, editor, _| { + editor + .selections + .disjoint_anchors() + .iter() + .map(|s| s.head().clone()) + .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 Some(anchors) = Vim::read(cx).state().marks.get(&*text).cloned() else { + return; + }; + + Vim::update(cx, |vim, cx| { + vim.pop_operator(cx); + }); + + let is_active_operator = Vim::read(cx).state().active_operator().is_some(); + if is_active_operator { + if let Some(anchor) = anchors.last() { + motion::motion( + Motion::Jump { + anchor: *anchor, + line, + }, + cx, + ) + } + return; + } else { + Vim::update(cx, |vim, cx| { + vim.update_active_editor(cx, |_, editor, cx| { + let map = editor.snapshot(cx); + let mut ranges: HashSet> = HashSet::default(); + for mut anchor in anchors { + if line { + let mut point = anchor.to_display_point(&map.display_snapshot); + point = motion::first_non_whitespace(&map.display_snapshot, false, point); + anchor = map + .display_snapshot + .buffer_snapshot + .anchor_before(point.to_point(&map.display_snapshot)); + } + ranges.insert(anchor..anchor); + } + editor.change_selections(None, cx, |s| s.select_anchor_ranges(ranges)) + }); + }) + } +} + +pub fn jump_motion( + map: &DisplaySnapshot, + anchor: Anchor, + line: bool, +) -> (DisplayPoint, SelectionGoal) { + let mut point = anchor.to_display_point(map); + if line { + point = motion::first_non_whitespace(map, false, point) + } + + (point, SelectionGoal::None) +} diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 9ece818b16caf..ed9861a97ae4a 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -59,6 +59,8 @@ pub enum Operator { AddSurrounds { target: Option }, ChangeSurrounds { target: Option }, DeleteSurrounds, + Mark, + Jump { line: bool }, } #[derive(Default, Clone)] @@ -74,6 +76,8 @@ pub struct EditorState { pub operator_stack: Vec, pub replacements: Vec<(Range, String)>, + pub marks: HashMap>, + pub current_tx: Option, pub current_anchor: Option>, pub undo_modes: HashMap, @@ -172,7 +176,10 @@ impl EditorState { } matches!( self.operator_stack.last(), - Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) + Some(Operator::FindForward { .. }) + | Some(Operator::FindBackward { .. }) + | Some(Operator::Mark) + | Some(Operator::Jump { .. }) ) } @@ -254,6 +261,9 @@ impl Operator { Operator::AddSurrounds { .. } => "ys", Operator::ChangeSurrounds { .. } => "cs", Operator::DeleteSurrounds => "ds", + Operator::Mark => "m", + Operator::Jump { line: true } => "'", + Operator::Jump { line: false } => "`", } } @@ -261,6 +271,8 @@ impl Operator { match self { Operator::Object { .. } | Operator::ChangeSurrounds { target: None } => &["VimObject"], Operator::FindForward { .. } + | Operator::Mark + | Operator::Jump { .. } | Operator::FindBackward { .. } | Operator::Replace | Operator::AddSurrounds { target: Some(_) } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 29aefbae76f08..10bd876b7ba4a 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -1073,3 +1073,20 @@ async fn test_mouse_selection(cx: &mut TestAppContext) { cx.assert_state("one «ˇtwo» three", Mode::Visual) } + +#[gpui::test] +async fn test_marks(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("line one\nline ˇtwo\nline three").await; + cx.simulate_shared_keystrokes(["m", "a", "l", "'", "a"]) + .await; + cx.assert_shared_state("line one\nˇline two\nline three") + .await; + cx.simulate_shared_keystrokes(["`", "a"]).await; + cx.assert_shared_state("line one\nline ˇtwo\nline three") + .await; + + cx.simulate_shared_keystrokes(["^", "d", "`", "a"]).await; + cx.assert_shared_state("line one\nˇtwo\nline three").await; +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 95645f83de577..5aebe43afcd57 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -194,7 +194,9 @@ fn observe_keystrokes(keystroke_event: &KeystrokeEvent, cx: &mut WindowContext) | Operator::Replace | Operator::AddSurrounds { .. } | Operator::ChangeSurrounds { .. } - | Operator::DeleteSurrounds, + | Operator::DeleteSurrounds + | Operator::Mark + | Operator::Jump { .. }, ) => {} Some(_) => { vim.clear_operator(cx); @@ -706,6 +708,10 @@ 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::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_marks.json b/crates/vim/test_data/test_marks.json new file mode 100644 index 0000000000000..ff02c58728a34 --- /dev/null +++ b/crates/vim/test_data/test_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"}}