diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 74aeca0cbd3c9ea1c5886ded5968dc316b5d0b1a..2af5df13b82d68080c0799ab8d8362fc802d6721 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 f0206aff9fd2f975a975c29dc60340ed12f76d7c..516422a04acacec423d1a3526cd4117a9f80b996 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 4a78547d1e0d137ce857485a81d5e67c65df1803..5d6c1288b544a701df4a1f989d189068d4992a44 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 b20b06bcb2dffcbb0c20e719e8d81f15456f0525..e70da37a0e91ba75db4e6c50680cad6ece5d798a 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(|_, _: &VisualDelete, cx| { + Vim::update(cx, |vim, cx| { + vim.record_current_action(cx); + delete(vim, cx); + }); + }); + workspace.register_action(|_, _: &VisualYank, cx| { + Vim::update(cx, |vim, cx| { + yank(vim, cx); + }); + }); - workspace.register_action(|workspace, action, cx| { - select_next(workspace, action, cx).ok(); + 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, action, cx| { - select_previous(workspace, action, cx).ok(); + 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 + 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); } - 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()]) + original_columns.insert(selection.id, position.to_point(map).column); } + selection.goal = SelectionGoal::None; }); - }) - }); - vim.switch_mode(Mode::Normal, true, cx); - }); -} + }); + copy_selections_content(vim, editor, line_mode, cx); + editor.insert("", 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| { + // 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| { - if line_mode { - selection.start = start_of_line(map, false, selection.start); - }; - selection.collapse_to(selection.start, SelectionGoal::None) + 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(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); }); + 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 0000000000000000000000000000000000000000..4683a83d078284360df3e6e34c9b311b44e7b61d --- /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 0000000000000000000000000000000000000000..fc1db9e7789ae919c68390a77e3596b433e5ab45 --- /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 0000000000000000000000000000000000000000..b9e0558fcad2abf59afd644d55f4edc1de8ff17b --- /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 0d6b18ae2eec53af6d6f8f20f6a0d205d2b89573..ad3190961c34551d3fde1cfa9e114427c3b86416 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,