From bf9b443b4a9ed38f2b85aa89845c03f90e3af321 Mon Sep 17 00:00:00 2001 From: joaquin30 <46060104+joaquin30@users.noreply.github.com> Date: Fri, 5 Apr 2024 21:23:37 -0500 Subject: [PATCH] vim: Support gn command and remap gn to gl (#9982) Release Notes: - Resolves #4273 @algora-pbc /claim #4273 This is a work-in-progress. The process for `gn` command is: - maintain updated vim.workspace_state.search.initial_query - modify editor.select_next_state with vim.workspace_state.search.initial_query - use editor.select_next() - merge selections - set editor.select_next_state to previous state To make this possible, several private members and editor structures are made public. `gN` is not yet implemented and the cursor still does not jump to the next selection in the first use. Maybe there is an better way to do this? --------- Co-authored-by: Conrad Irwin --- assets/keymaps/vim.json | 6 +- crates/search/src/buffer_search.rs | 2 +- crates/vim/Cargo.toml | 1 + crates/vim/src/visual.rs | 315 ++++++++++++++++------ crates/vim/test_data/test_cgn_repeat.json | 14 + crates/vim/test_data/test_dgn_repeat.json | 14 + crates/vim/test_data/test_gn.json | 39 +++ crates/workspace/src/searchable.rs | 9 + 8 files changed, 311 insertions(+), 89 deletions(-) create mode 100644 crates/vim/test_data/test_cgn_repeat.json create mode 100644 crates/vim/test_data/test_dgn_repeat.json create mode 100644 crates/vim/test_data/test_gn.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 74aeca0cbd..2af5df13b8 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -137,8 +137,10 @@ "g d": "editor::GoToDefinition", "g shift-d": "editor::GoToTypeDefinition", "g x": "editor::OpenUrl", - "g n": "vim::SelectNext", - "g shift-n": "vim::SelectPrevious", + "g n": "vim::SelectNextMatch", + "g shift-n": "vim::SelectPreviousMatch", + "g l": "vim::SelectNext", + "g shift-l": "vim::SelectPrevious", "g >": [ "editor::SelectNext", { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index f0206aff9f..516422a04a 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -963,7 +963,7 @@ impl BufferSearchBar { done_rx } - fn update_match_index(&mut self, cx: &mut ViewContext) { + pub fn update_match_index(&mut self, cx: &mut ViewContext) { let new_index = self .active_searchable_item .as_ref() diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 4a78547d1e..5d6c1288b5 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -39,6 +39,7 @@ ui.workspace = true workspace.workspace = true zed_actions.workspace = true schemars.workspace = true +util.workspace = true [dev-dependencies] command_palette.workspace = true diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index b20b06bcb2..e70da37a0e 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -1,4 +1,3 @@ -use anyhow::Result; use std::sync::Arc; use collections::HashMap; @@ -10,10 +9,13 @@ use editor::{ }; use gpui::{actions, ViewContext, WindowContext}; use language::{Point, Selection, SelectionGoal}; -use workspace::Workspace; +use search::BufferSearchBar; +use util::ResultExt; +use workspace::{searchable::Direction, Workspace}; use crate::{ motion::{start_of_line, Motion}, + normal::substitute::substitute, object::Object, state::{Mode, Operator}, utils::{copy_selections_content, yank_selections_content}, @@ -31,6 +33,8 @@ actions!( OtherEnd, SelectNext, SelectPrevious, + SelectNextMatch, + SelectPreviousMatch, ] ); @@ -47,14 +51,29 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { }, ); workspace.register_action(other_end); - workspace.register_action(delete); - workspace.register_action(yank); - - workspace.register_action(|workspace, action, cx| { - select_next(workspace, action, cx).ok(); + workspace.register_action(|_, _: &VisualDelete, cx| { + Vim::update(cx, |vim, cx| { + vim.record_current_action(cx); + delete(vim, cx); + }); }); - workspace.register_action(|workspace, action, cx| { - select_previous(workspace, action, cx).ok(); + workspace.register_action(|_, _: &VisualYank, cx| { + Vim::update(cx, |vim, cx| { + yank(vim, cx); + }); + }); + + workspace.register_action(select_next); + workspace.register_action(select_previous); + workspace.register_action(|workspace, _: &SelectNextMatch, cx| { + Vim::update(cx, |vim, cx| { + select_match(workspace, vim, Direction::Next, cx); + }); + }); + workspace.register_action(|workspace, _: &SelectPreviousMatch, cx| { + Vim::update(cx, |vim, cx| { + select_match(workspace, vim, Direction::Prev, cx); + }); }); } @@ -333,70 +352,65 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.record_current_action(cx); - vim.update_active_editor(cx, |vim, editor, cx| { - let mut original_columns: HashMap<_, _> = Default::default(); - let line_mode = editor.selections.line_mode; +pub fn delete(vim: &mut Vim, cx: &mut WindowContext) { + vim.update_active_editor(cx, |vim, editor, cx| { + let mut original_columns: HashMap<_, _> = Default::default(); + let line_mode = editor.selections.line_mode; - editor.transact(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - if line_mode { - let mut position = selection.head(); - if !selection.reversed { - position = movement::left(map, position); - } - original_columns.insert(selection.id, position.to_point(map).column); - } - selection.goal = SelectionGoal::None; - }); - }); - copy_selections_content(vim, editor, line_mode, cx); - editor.insert("", cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head().to_point(map); - - if let Some(column) = original_columns.get(&selection.id) { - cursor.column = *column - } - let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); - selection.collapse_to(cursor, selection.goal) - }); - if vim.state().mode == Mode::VisualBlock { - s.select_anchors(vec![s.first_anchor()]) - } - }); - }) - }); - vim.switch_mode(Mode::Normal, true, cx); - }); -} - -pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |vim, editor, cx| { - let line_mode = editor.selections.line_mode; - yank_selections_content(vim, editor, line_mode, cx); - editor.change_selections(None, cx, |s| { + editor.transact(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { if line_mode { - selection.start = start_of_line(map, false, selection.start); - }; - selection.collapse_to(selection.start, SelectionGoal::None) + let mut position = selection.head(); + if !selection.reversed { + position = movement::left(map, position); + } + original_columns.insert(selection.id, position.to_point(map).column); + } + selection.goal = SelectionGoal::None; + }); + }); + copy_selections_content(vim, editor, line_mode, cx); + editor.insert("", cx); + + // Fixup cursor position after the deletion + editor.set_clip_at_line_ends(true, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.move_with(|map, selection| { + let mut cursor = selection.head().to_point(map); + + if let Some(column) = original_columns.get(&selection.id) { + cursor.column = *column + } + let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); + selection.collapse_to(cursor, selection.goal) }); if vim.state().mode == Mode::VisualBlock { s.select_anchors(vec![s.first_anchor()]) } }); - }); - vim.switch_mode(Mode::Normal, true, cx); + }) }); + vim.switch_mode(Mode::Normal, true, cx); +} + +pub fn yank(vim: &mut Vim, cx: &mut WindowContext) { + vim.update_active_editor(cx, |vim, editor, cx| { + let line_mode = editor.selections.line_mode; + yank_selections_content(vim, editor, line_mode, cx); + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + if line_mode { + selection.start = start_of_line(map, false, selection.start); + }; + selection.collapse_to(selection.start, SelectionGoal::None) + }); + if vim.state().mode == Mode::VisualBlock { + s.select_anchors(vec![s.first_anchor()]) + } + }); + }); + vim.switch_mode(Mode::Normal, true, cx); } pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { @@ -442,48 +456,112 @@ pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { }); } -pub fn select_next( - _: &mut Workspace, - _: &SelectNext, - cx: &mut ViewContext, -) -> Result<()> { +pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { let count = vim.take_count(cx) .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); vim.update_active_editor(cx, |_, editor, cx| { for _ in 0..count { - match editor.select_next(&Default::default(), cx) { - Err(a) => return Err(a), - _ => {} + if editor + .select_next(&Default::default(), cx) + .log_err() + .is_none() + { + break; } } - Ok(()) }) - }) - .unwrap_or(Ok(())) + }); } -pub fn select_previous( - _: &mut Workspace, - _: &SelectPrevious, - cx: &mut ViewContext, -) -> Result<()> { +pub fn select_previous(_: &mut Workspace, _: &SelectPrevious, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { let count = vim.take_count(cx) .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 }); vim.update_active_editor(cx, |_, editor, cx| { for _ in 0..count { - match editor.select_previous(&Default::default(), cx) { - Err(a) => return Err(a), - _ => {} + if editor + .select_previous(&Default::default(), cx) + .log_err() + .is_none() + { + break; } } - Ok(()) }) - }) - .unwrap_or(Ok(())) + }); +} + +pub fn select_match( + workspace: &mut Workspace, + vim: &mut Vim, + direction: Direction, + cx: &mut WindowContext, +) { + let count = vim.take_count(cx).unwrap_or(1); + let pane = workspace.active_pane().clone(); + let vim_is_normal = vim.state().mode == Mode::Normal; + let mut start_selection = 0usize; + let mut end_selection = 0usize; + + vim.update_active_editor(cx, |_, editor, _| { + editor.set_collapse_matches(false); + }); + + if vim_is_normal { + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + // without update_match_index there is a bug when the cursor is before the first match + search_bar.update_match_index(cx); + search_bar.select_match(direction.opposite(), 1, cx); + }); + } + }); + } + + vim.update_active_editor(cx, |_, editor, cx| { + let latest = editor.selections.newest::(cx); + start_selection = latest.start; + end_selection = latest.end; + }); + + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + search_bar.update_match_index(cx); + search_bar.select_match(direction, count, cx); + }); + } + }); + vim.update_active_editor(cx, |_, editor, cx| { + let latest = editor.selections.newest::(cx); + if vim_is_normal { + start_selection = latest.start; + end_selection = latest.end; + } else { + start_selection = start_selection.min(latest.start); + end_selection = end_selection.max(latest.end); + } + if direction == Direction::Prev { + std::mem::swap(&mut start_selection, &mut end_selection); + } + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([start_selection..end_selection]); + }); + editor.set_collapse_matches(true); + }); + match vim.maybe_pop_operator() { + Some(Operator::Change) => substitute(vim, None, false, cx), + Some(Operator::Delete) => { + vim.stop_recording(); + delete(vim, cx) + } + Some(Operator::Yank) => yank(vim, cx), + _ => {} // Ignoring other operators + }; } #[cfg(test)] @@ -1052,4 +1130,69 @@ mod test { cx.simulate_keystrokes(["cmd-shift-p", "escape"]); assert_eq!(cx.mode(), Mode::VisualBlock); } + + #[gpui::test] + async fn test_gn(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("aaˇ aa aa aa aa").await; + cx.simulate_shared_keystrokes(["/", "a", "a", "enter"]) + .await; + cx.assert_shared_state("aa ˇaa aa aa aa").await; + cx.simulate_shared_keystrokes(["g", "n"]).await; + cx.assert_shared_state("aa «aaˇ» aa aa aa").await; + cx.simulate_shared_keystrokes(["g", "n"]).await; + cx.assert_shared_state("aa «aa aaˇ» aa aa").await; + cx.simulate_shared_keystrokes(["escape", "d", "g", "n"]) + .await; + cx.assert_shared_state("aa aa ˇ aa aa").await; + + cx.set_shared_state("aaˇ aa aa aa aa").await; + cx.simulate_shared_keystrokes(["/", "a", "a", "enter"]) + .await; + cx.assert_shared_state("aa ˇaa aa aa aa").await; + cx.simulate_shared_keystrokes(["3", "g", "n"]).await; + cx.assert_shared_state("aa aa aa «aaˇ» aa").await; + + cx.set_shared_state("aaˇ aa aa aa aa").await; + cx.simulate_shared_keystrokes(["/", "a", "a", "enter"]) + .await; + cx.assert_shared_state("aa ˇaa aa aa aa").await; + cx.simulate_shared_keystrokes(["g", "shift-n"]).await; + cx.assert_shared_state("aa «ˇaa» aa aa aa").await; + cx.simulate_shared_keystrokes(["g", "shift-n"]).await; + cx.assert_shared_state("«ˇaa aa» aa aa aa").await; + } + + #[gpui::test] + async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("aaˇ aa aa aa aa").await; + cx.simulate_shared_keystrokes(["/", "a", "a", "enter"]) + .await; + cx.assert_shared_state("aa ˇaa aa aa aa").await; + cx.simulate_shared_keystrokes(["d", "g", "n"]).await; + + cx.assert_shared_state("aa ˇ aa aa aa").await; + cx.simulate_shared_keystrokes(["."]).await; + cx.assert_shared_state("aa ˇ aa aa").await; + cx.simulate_shared_keystrokes(["."]).await; + cx.assert_shared_state("aa ˇ aa").await; + } + + #[gpui::test] + async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("aaˇ aa aa aa aa").await; + cx.simulate_shared_keystrokes(["/", "a", "a", "enter"]) + .await; + cx.assert_shared_state("aa ˇaa aa aa aa").await; + cx.simulate_shared_keystrokes(["c", "g", "n", "x", "escape"]) + .await; + cx.assert_shared_state("aa ˇx aa aa aa").await; + cx.simulate_shared_keystrokes(["."]).await; + cx.assert_shared_state("aa x ˇx aa aa").await; + } } diff --git a/crates/vim/test_data/test_cgn_repeat.json b/crates/vim/test_data/test_cgn_repeat.json new file mode 100644 index 0000000000..4683a83d07 --- /dev/null +++ b/crates/vim/test_data/test_cgn_repeat.json @@ -0,0 +1,14 @@ +{"Put":{"state":"aaˇ aa aa aa aa"}} +{"Key":"/"} +{"Key":"a"} +{"Key":"a"} +{"Key":"enter"} +{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} +{"Key":"c"} +{"Key":"g"} +{"Key":"n"} +{"Key":"x"} +{"Key":"escape"} +{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"aa x ˇx aa aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dgn_repeat.json b/crates/vim/test_data/test_dgn_repeat.json new file mode 100644 index 0000000000..fc1db9e778 --- /dev/null +++ b/crates/vim/test_data/test_dgn_repeat.json @@ -0,0 +1,14 @@ +{"Put":{"state":"aaˇ aa aa aa aa"}} +{"Key":"/"} +{"Key":"a"} +{"Key":"a"} +{"Key":"enter"} +{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} +{"Key":"d"} +{"Key":"g"} +{"Key":"n"} +{"Get":{"state":"aa ˇ aa aa aa","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"aa ˇ aa aa","mode":"Normal"}} +{"Key":"."} +{"Get":{"state":"aa ˇ aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_gn.json b/crates/vim/test_data/test_gn.json new file mode 100644 index 0000000000..b9e0558fca --- /dev/null +++ b/crates/vim/test_data/test_gn.json @@ -0,0 +1,39 @@ +{"Put":{"state":"aaˇ aa aa aa aa"}} +{"Key":"/"} +{"Key":"a"} +{"Key":"a"} +{"Key":"enter"} +{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} +{"Key":"g"} +{"Key":"n"} +{"Get":{"state":"aa «aaˇ» aa aa aa","mode":"Visual"}} +{"Key":"g"} +{"Key":"n"} +{"Get":{"state":"aa «aa aaˇ» aa aa","mode":"Visual"}} +{"Key":"escape"} +{"Key":"d"} +{"Key":"g"} +{"Key":"n"} +{"Get":{"state":"aa aa ˇ aa aa","mode":"Normal"}} +{"Put":{"state":"aaˇ aa aa aa aa"}} +{"Key":"/"} +{"Key":"a"} +{"Key":"a"} +{"Key":"enter"} +{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} +{"Key":"3"} +{"Key":"g"} +{"Key":"n"} +{"Get":{"state":"aa aa aa «aaˇ» aa","mode":"Visual"}} +{"Put":{"state":"aaˇ aa aa aa aa"}} +{"Key":"/"} +{"Key":"a"} +{"Key":"a"} +{"Key":"enter"} +{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} +{"Key":"g"} +{"Key":"shift-n"} +{"Get":{"state":"aa «ˇaa» aa aa aa","mode":"Visual"}} +{"Key":"g"} +{"Key":"shift-n"} +{"Get":{"state":"«ˇaa aa» aa aa aa","mode":"Visual"}} diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 0d6b18ae2e..ad3190961c 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -24,6 +24,15 @@ pub enum Direction { Next, } +impl Direction { + pub fn opposite(&self) -> Self { + match self { + Direction::Prev => Direction::Next, + Direction::Next => Direction::Prev, + } + } +} + #[derive(Clone, Copy, Debug, Default)] pub struct SearchOptions { pub case: bool,