diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 4b6aa72b68..22bc32cec8 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -102,6 +102,7 @@ "ctrl-e": "vim::LineDown", "ctrl-y": "vim::LineUp", // "g" commands + "g r": ["vim::PushOperator", "ReplaceWithRegister"], "g g": "vim::StartOfDocument", "g h": "editor::Hover", "g t": "pane::ActivateNextItem", diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 4d537f8fd7..d84285fad6 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -182,6 +182,9 @@ impl Vim { Some(Operator::ToggleComments) => { self.toggle_comments_motion(motion, times, window, cx) } + Some(Operator::ReplaceWithRegister) => { + self.replace_with_register_motion(motion, times, window, cx) + } Some(operator) => { // Can't do anything for text objects, Ignoring error!("Unexpected normal mode motion operator: {:?}", operator) @@ -228,6 +231,9 @@ impl Vim { Some(Operator::ToggleComments) => { self.toggle_comments_object(object, around, window, cx) } + Some(Operator::ReplaceWithRegister) => { + self.replace_with_register_object(object, around, window, cx) + } _ => { // Can't do anything for namespace operators. Ignoring } diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index e47e48a6b4..eefd92d0d1 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -6,6 +6,8 @@ use serde::Deserialize; use std::cmp; use crate::{ + motion::Motion, + object::Object, state::{Mode, Register}, Vim, }; @@ -192,12 +194,85 @@ impl Vim { }); self.switch_mode(Mode::Normal, true, window, cx); } + + pub fn replace_with_register_object( + &mut self, + object: Object, + around: bool, + window: &mut Window, + cx: &mut Context, + ) { + self.stop_recording(cx); + let selected_register = self.selected_register.take(); + self.update_editor(window, cx, |_, editor, window, cx| { + editor.transact(window, cx, |editor, window, cx| { + editor.set_clip_at_line_ends(false, cx); + editor.change_selections(None, window, cx, |s| { + s.move_with(|map, selection| { + object.expand_selection(map, selection, around); + }); + }); + + let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| { + globals.read_register(selected_register, Some(editor), cx) + }) + .filter(|reg| !reg.text.is_empty()) else { + return; + }; + editor.insert(&text, window, cx); + editor.set_clip_at_line_ends(true, cx); + editor.change_selections(None, window, cx, |s| { + s.move_with(|map, selection| { + selection.start = map.clip_point(selection.start, Bias::Left); + selection.end = selection.start + }) + }) + }); + }); + } + + pub fn replace_with_register_motion( + &mut self, + motion: Motion, + times: Option, + window: &mut Window, + cx: &mut Context, + ) { + self.stop_recording(cx); + let selected_register = self.selected_register.take(); + self.update_editor(window, cx, |_, editor, window, cx| { + let text_layout_details = editor.text_layout_details(window); + editor.transact(window, cx, |editor, window, cx| { + editor.set_clip_at_line_ends(false, cx); + editor.change_selections(None, window, cx, |s| { + s.move_with(|map, selection| { + motion.expand_selection(map, selection, times, false, &text_layout_details); + }); + }); + + let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| { + globals.read_register(selected_register, Some(editor), cx) + }) + .filter(|reg| !reg.text.is_empty()) else { + return; + }; + editor.insert(&text, window, cx); + editor.set_clip_at_line_ends(true, cx); + editor.change_selections(None, window, cx, |s| { + s.move_with(|map, selection| { + selection.start = map.clip_point(selection.start, Bias::Left); + selection.end = selection.start + }) + }) + }); + }); + } } #[cfg(test)] mod test { use crate::{ - state::Mode, + state::{Mode, Register}, test::{NeovimBackedTestContext, VimTestContext}, UseSystemClipboard, VimSettings, }; @@ -742,4 +817,37 @@ mod test { Mode::Normal, ); } + + #[gpui::test] + async fn test_replace_with_register(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {" + ˇfish one + two three + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("y i w"); + cx.simulate_keystrokes("w"); + cx.simulate_keystrokes("g r i w"); + cx.assert_state( + indoc! {" + fish fisˇh + two three + "}, + Mode::Normal, + ); + cx.simulate_keystrokes("j b g r e"); + cx.assert_state( + indoc! {" + fish fish + two fisˇh + "}, + Mode::Normal, + ); + let clipboard: Register = cx.read_from_clipboard().unwrap().into(); + assert_eq!(clipboard.text, "fish"); + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index ddd83f4666..a4994fb4d3 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -111,6 +111,7 @@ pub enum Operator { RecordRegister, ReplayRegister, ToggleComments, + ReplaceWithRegister, } #[derive(Default, Clone, Debug)] @@ -499,6 +500,7 @@ impl Operator { Operator::AutoIndent => "eq", Operator::ShellCommand => "sh", Operator::Rewrap => "gq", + Operator::ReplaceWithRegister => "gr", Operator::Outdent => "<", Operator::Uppercase => "gU", Operator::Lowercase => "gu", @@ -551,6 +553,7 @@ impl Operator { | Operator::ShellCommand | Operator::Lowercase | Operator::Uppercase + | Operator::ReplaceWithRegister | Operator::Object { .. } | Operator::ChangeSurrounds { target: None } | Operator::OppositeCase diff --git a/docs/src/vim.md b/docs/src/vim.md index 96f124897f..25e44dda3f 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -159,6 +159,7 @@ Zed's vim mode includes some features that are usually provided by very popular - You can comment and uncomment selections with `gc` in visual mode and `gcc` in normal mode. - The project panel supports many shortcuts modeled after the Vim plugin `netrw`: navigation with `hjkl`, open file with `o`, open file in a new tab with `t`, etc. - You can add key bindings to your keymap to navigate "camelCase" names. [Head down to the Optional key bindings](#optional-key-bindings) section to learn how. +- You can use `gr` to do [ReplaceWithRegister](https://github.com/vim-scripts/ReplaceWithRegister). ## Command palette