From 652fcb7f835786612127a7591bd759c29ee45a62 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Fri, 1 Aug 2025 11:25:41 +0100 Subject: [PATCH 1/5] Add "shift-r" and "g ." support for helix mode --- assets/keymaps/vim.json | 2 ++ crates/vim/src/helix.rs | 80 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index be6d34a1342b6f..64069318f19ae6 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -428,11 +428,13 @@ "g h": "vim::StartOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g e": "vim::EndOfDocument", + "g .": "vim::HelixGotoLastModification", // go to last modification "g r": "editor::FindAllReferences", // zed specific "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", + "shift-r": "editor::Paste", "x": "editor::SelectLine", "shift-x": "editor::SelectLine", "%": "editor::SelectAll", diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 0c8c06d8ab66f4..a1d07c0faaba38 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -23,6 +23,8 @@ actions!( HelixInsert, /// Appends at the end of the selection. HelixAppend, + /// Goes to the location of the last modification. + HelixGotoLastModification, ] ); @@ -31,6 +33,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); + Vim::action(editor, cx, Vim::helix_goto_last_modification); } impl Vim { @@ -443,6 +446,16 @@ impl Vim { }); self.switch_mode(Mode::HelixNormal, true, window, cx); } + + pub fn helix_goto_last_modification( + &mut self, + _: &HelixGotoLastModification, + window: &mut Window, + cx: &mut Context, + ) { + self.jump(".".into(), false, false, window, cx); + self.switch_mode(Mode::HelixNormal, true, window, cx); + } } #[cfg(test)] @@ -776,4 +789,71 @@ mod test { cx.shared_clipboard().assert_eq("worl"); cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); } + #[gpui::test] + async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // First copy some text to clipboard + cx.set_state("«hello worldˇ»", Mode::HelixNormal); + cx.simulate_keystrokes("y"); + + // Test paste with shift-r on single cursor + cx.set_state("foo ˇbar", Mode::HelixNormal); + cx.simulate_keystrokes("shift-r"); + + cx.assert_state("foo hello worldˇbar", Mode::HelixNormal); + + // Test paste with shift-r on selection + cx.set_state("foo «barˇ» baz", Mode::HelixNormal); + cx.simulate_keystrokes("shift-r"); + + cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal); + } + + #[gpui::test] + async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // Make a modification at a specific location + cx.set_state("ˇhello", Mode::HelixNormal); + assert_eq!(cx.mode(), Mode::HelixNormal); + cx.simulate_keystrokes("i"); + assert_eq!(cx.mode(), Mode::Insert); + cx.simulate_keystrokes("escape"); + assert_eq!(cx.mode(), Mode::HelixNormal); + } + + #[gpui::test] + async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); + + // Make a modification at a specific location + cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); + cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); + cx.simulate_keystrokes("i"); + cx.simulate_keystrokes("escape"); + cx.simulate_keystrokes("i"); + cx.simulate_keystrokes("m o d i f i e d space"); + cx.simulate_keystrokes("escape"); + + // TODO: this fails, because state is no longer helix + cx.assert_state( + "line one\nline modified ˇtwo\nline three", + Mode::HelixNormal, + ); + + // Move cursor away from the modification + cx.simulate_keystrokes("up"); + + // Use "g ." to go back to last modification + cx.simulate_keystrokes("g ."); + + // Verify we're back at the modification location and still in HelixNormal mode + cx.assert_state( + "line one\nline modifiedˇ two\nline three", + Mode::HelixNormal, + ); + } } From f20979a65cfcc0fde49ba5330b5cad97dbfda574 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Fri, 1 Aug 2025 11:33:27 +0100 Subject: [PATCH 2/5] No need to explicitly set mode --- crates/vim/src/helix.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index a1d07c0faaba38..0e7d68a83841b1 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -454,7 +454,6 @@ impl Vim { cx: &mut Context, ) { self.jump(".".into(), false, false, window, cx); - self.switch_mode(Mode::HelixNormal, true, window, cx); } } From ffbd317ff72d2bfa4ef80fe5e2c7e22a276055cf Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Fri, 1 Aug 2025 11:37:15 +0100 Subject: [PATCH 3/5] Run all helix tests with "helix_mode" enabled --- crates/vim/src/helix.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 0e7d68a83841b1..3cf8f5a23cb8f0 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -466,6 +466,7 @@ mod test { #[gpui::test] async fn test_word_motions(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // « // ˇ // » @@ -527,6 +528,7 @@ mod test { #[gpui::test] async fn test_delete(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // test delete a selection cx.set_state( @@ -607,6 +609,7 @@ mod test { #[gpui::test] async fn test_f_and_t(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); cx.set_state( indoc! {" @@ -660,6 +663,7 @@ mod test { #[gpui::test] async fn test_newline_char(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal); @@ -677,6 +681,7 @@ mod test { #[gpui::test] async fn test_insert_selected(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); cx.set_state( indoc! {" «The ˇ»quick brown @@ -699,6 +704,7 @@ mod test { #[gpui::test] async fn test_append(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // test from the end of the selection cx.set_state( indoc! {" @@ -741,6 +747,7 @@ mod test { #[gpui::test] async fn test_replace(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // No selection (single character) cx.set_state("ˇaa", Mode::HelixNormal); @@ -791,6 +798,7 @@ mod test { #[gpui::test] async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; + cx.enable_helix(); // First copy some text to clipboard cx.set_state("«hello worldˇ»", Mode::HelixNormal); From 20c45e383d0cf6b94f4be1b54ac127c098ae3da0 Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Fri, 1 Aug 2025 12:11:24 +0100 Subject: [PATCH 4/5] Add better yank --- crates/vim/src/helix.rs | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 3cf8f5a23cb8f0..c36a2c35eb81a9 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -23,6 +23,8 @@ actions!( HelixInsert, /// Appends at the end of the selection. HelixAppend, + /// Yanks the current selection or character if no selection. + HelixYank, /// Goes to the location of the last modification. HelixGotoLastModification, ] @@ -447,6 +449,47 @@ impl Vim { self.switch_mode(Mode::HelixNormal, true, window, cx); } + pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context) { + self.update_editor(window, cx, |vim, editor, window, cx| { + let has_selection = editor + .selections + .all_adjusted(cx) + .iter() + .any(|selection| !selection.is_empty()); + + if !has_selection { + // If no selection, expand to current character (like 'v' does) + editor.change_selections(Default::default(), window, cx, |s| { + s.move_with(|map, selection| { + let head = selection.head(); + let new_head = movement::saturating_right(map, head); + selection.set_tail(head, SelectionGoal::None); + selection.set_head(new_head, SelectionGoal::None); + }); + }); + vim.yank_selections_content( + editor, + crate::motion::MotionKind::Exclusive, + window, + cx, + ); + editor.change_selections(Default::default(), window, cx, |s| { + s.move_with(|_map, selection| { + selection.collapse_to(selection.start, SelectionGoal::None); + }); + }); + } else { + // Yank the selection(s) + vim.yank_selections_content( + editor, + crate::motion::MotionKind::Exclusive, + window, + cx, + ); + } + }); + } + pub fn helix_goto_last_modification( &mut self, _: &HelixGotoLastModification, From 559c39c16aeda8fa21c49c39e240b9d21ac4eb0a Mon Sep 17 00:00:00 2001 From: Romans Malinovskis Date: Sat, 16 Aug 2025 14:02:43 +0100 Subject: [PATCH 5/5] remove duplicates --- crates/vim/src/helix.rs | 43 ----------------------------------------- 1 file changed, 43 deletions(-) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index c36a2c35eb81a9..3cf8f5a23cb8f0 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -23,8 +23,6 @@ actions!( HelixInsert, /// Appends at the end of the selection. HelixAppend, - /// Yanks the current selection or character if no selection. - HelixYank, /// Goes to the location of the last modification. HelixGotoLastModification, ] @@ -449,47 +447,6 @@ impl Vim { self.switch_mode(Mode::HelixNormal, true, window, cx); } - pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context) { - self.update_editor(window, cx, |vim, editor, window, cx| { - let has_selection = editor - .selections - .all_adjusted(cx) - .iter() - .any(|selection| !selection.is_empty()); - - if !has_selection { - // If no selection, expand to current character (like 'v' does) - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let head = selection.head(); - let new_head = movement::saturating_right(map, head); - selection.set_tail(head, SelectionGoal::None); - selection.set_head(new_head, SelectionGoal::None); - }); - }); - vim.yank_selections_content( - editor, - crate::motion::MotionKind::Exclusive, - window, - cx, - ); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_map, selection| { - selection.collapse_to(selection.start, SelectionGoal::None); - }); - }); - } else { - // Yank the selection(s) - vim.yank_selections_content( - editor, - crate::motion::MotionKind::Exclusive, - window, - cx, - ); - } - }); - } - pub fn helix_goto_last_modification( &mut self, _: &HelixGotoLastModification,