Skip to content

Commit 3096eda

Browse files
committed
Add "shift-r" and "g ." support for helix mode
1 parent 6f2e7c3 commit 3096eda

File tree

10 files changed

+252
-16
lines changed

10 files changed

+252
-16
lines changed

assets/keymaps/vim.json

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,116 @@
323323
"] x": "editor::SelectSmallerSyntaxNode"
324324
}
325325
},
326+
{
327+
"context": "vim_mode == helix_select",
328+
"bindings": {
329+
"escape": "vim::NormalBefore",
330+
"ctrl-[": "editor::Cancel",
331+
";": "vim::HelixCollapseSelection",
332+
":": "command_palette::Toggle",
333+
"left": "vim::WrappingLeft",
334+
"right": "vim::WrappingRight",
335+
"h": "vim::WrappingLeft",
336+
"l": "vim::WrappingRight",
337+
"shift-d": "vim::DeleteToEndOfLine",
338+
"shift-j": "vim::JoinLines",
339+
"y": "editor::Copy",
340+
"shift-y": "vim::YankLine",
341+
"i": "vim::InsertBefore",
342+
"shift-i": "vim::InsertFirstNonWhitespace",
343+
"a": "vim::InsertAfter",
344+
"shift-a": "vim::InsertEndOfLine",
345+
"o": "vim::InsertLineBelow",
346+
"shift-o": "vim::InsertLineAbove",
347+
"~": "vim::ChangeCase",
348+
"ctrl-a": "vim::Increment",
349+
"ctrl-x": "vim::Decrement",
350+
"p": "vim::Paste",
351+
"shift-p": ["vim::Paste", { "before": true }],
352+
"u": "vim::Undo",
353+
"ctrl-r": "vim::Redo",
354+
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
355+
"t": ["vim::PushFindForward", { "before": true, "multiline": true }],
356+
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }],
357+
"shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }],
358+
"r": "vim::PushReplace",
359+
"s": "vim::Substitute",
360+
"shift-s": "vim::SubstituteLine",
361+
">": "vim::Indent",
362+
"<": "vim::Outdent",
363+
"=": "vim::AutoIndent",
364+
"g u": "vim::PushLowercase",
365+
"g shift-u": "vim::PushUppercase",
366+
"g ~": "vim::PushOppositeCase",
367+
"\"": "vim::PushRegister",
368+
"g q": "vim::PushRewrap",
369+
"g w": "vim::PushRewrap",
370+
"ctrl-pagedown": "pane::ActivateNextItem",
371+
"ctrl-pageup": "pane::ActivatePreviousItem",
372+
"insert": "vim::InsertBefore",
373+
".": "vim::Repeat",
374+
"alt-.": "vim::RepeatFind",
375+
"alt-;": "vim::OtherEnd",
376+
// tree-sitter related commands
377+
"[ x": "editor::SelectLargerSyntaxNode",
378+
"] x": "editor::SelectSmallerSyntaxNode",
379+
"] d": "editor::GoToDiagnostic",
380+
"[ d": "editor::GoToPreviousDiagnostic",
381+
"] c": "editor::GoToHunk",
382+
"[ c": "editor::GoToPreviousHunk",
383+
// Goto mode
384+
"g n": "pane::ActivateNextItem",
385+
"g p": "pane::ActivatePreviousItem",
386+
// "tab": "pane::ActivateNextItem",
387+
// "shift-tab": "pane::ActivatePrevItem",
388+
"shift-h": "pane::ActivatePreviousItem",
389+
"shift-l": "pane::ActivateNextItem",
390+
"g l": "vim::EndOfLine",
391+
"g h": "vim::StartOfLine",
392+
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
393+
"g e": "vim::EndOfDocument",
394+
"g y": "editor::GoToTypeDefinition",
395+
"g r": "editor::FindAllReferences", // zed specific
396+
"g t": "vim::WindowTop",
397+
"g c": "vim::WindowMiddle",
398+
"g b": "vim::WindowBottom",
399+
400+
"x": "editor::SelectLine",
401+
"shift-x": "editor::SelectLine",
402+
"%": "editor::SelectAll",
403+
// Window mode
404+
"space w h": "workspace::ActivatePaneLeft",
405+
"space w l": "workspace::ActivatePaneRight",
406+
"space w k": "workspace::ActivatePaneUp",
407+
"space w j": "workspace::ActivatePaneDown",
408+
"space w q": "pane::CloseActiveItem",
409+
"space w s": "pane::SplitRight",
410+
"space w r": "pane::SplitRight",
411+
"space w v": "pane::SplitDown",
412+
"space w d": "pane::SplitDown",
413+
// Space mode
414+
"space f": "file_finder::Toggle",
415+
"space k": "editor::Hover",
416+
"space s": "outline::Toggle",
417+
"space shift-s": "project_symbols::Toggle",
418+
"space d": "editor::GoToDiagnostic",
419+
"space r": "editor::Rename",
420+
"space a": "editor::ToggleCodeActions",
421+
"space h": "editor::SelectAllMatches",
422+
"space c": "editor::ToggleComments",
423+
"space y": "editor::Copy",
424+
"space p": "editor::Paste",
425+
// Match mode
426+
"m m": "vim::Matching",
427+
"m i w": ["workspace::SendKeystrokes", "v i w"],
428+
"shift-u": "editor::Redo",
429+
"ctrl-c": "editor::ToggleComments",
430+
"d": "vim::HelixDelete",
431+
"c": "vim::Substitute",
432+
"shift-c": "editor::AddSelectionBelow",
433+
"alt-shift-c": "editor::AddSelectionAbove"
434+
}
435+
},
326436
{
327437
"context": "vim_mode == insert",
328438
"bindings": {
@@ -428,11 +538,13 @@
428538
"g h": "vim::StartOfLine",
429539
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
430540
"g e": "vim::EndOfDocument",
541+
"g .": "vim::HelixGotoLastModification", // go to last modification
431542
"g r": "editor::FindAllReferences", // zed specific
432543
"g t": "vim::WindowTop",
433544
"g c": "vim::WindowMiddle",
434545
"g b": "vim::WindowBottom",
435546

547+
"shift-r": "editor::Paste",
436548
"x": "editor::SelectLine",
437549
"shift-x": "editor::SelectLine",
438550
"%": "editor::SelectAll",

crates/vim/src/helix.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ actions!(
2323
HelixInsert,
2424
/// Appends at the end of the selection.
2525
HelixAppend,
26+
/// Yanks the current selection or character if no selection.
27+
HelixYank,
28+
/// Goes to the location of the last modification.
29+
HelixGotoLastModification,
2630
]
2731
);
2832

@@ -31,6 +35,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
3135
Vim::action(editor, cx, Vim::helix_insert);
3236
Vim::action(editor, cx, Vim::helix_append);
3337
Vim::action(editor, cx, Vim::helix_yank);
38+
Vim::action(editor, cx, Vim::helix_goto_last_modification);
3439
}
3540

3641
impl Vim {
@@ -46,7 +51,7 @@ impl Vim {
4651
return;
4752
}
4853
self.stop_recording_immediately(action.boxed_clone(), cx);
49-
self.switch_mode(Mode::HelixNormal, false, window, cx);
54+
self.switch_mode(Mode::HelixNormal, true, window, cx);
5055
return;
5156
}
5257

@@ -443,6 +448,15 @@ impl Vim {
443448
});
444449
self.switch_mode(Mode::HelixNormal, true, window, cx);
445450
}
451+
452+
pub fn helix_goto_last_modification(
453+
&mut self,
454+
_: &HelixGotoLastModification,
455+
window: &mut Window,
456+
cx: &mut Context<Self>,
457+
) {
458+
self.jump(".".into(), false, false, window, cx);
459+
}
446460
}
447461

448462
#[cfg(test)]
@@ -454,6 +468,7 @@ mod test {
454468
#[gpui::test]
455469
async fn test_word_motions(cx: &mut gpui::TestAppContext) {
456470
let mut cx = VimTestContext::new(cx, true).await;
471+
cx.enable_helix();
457472
// «
458473
// ˇ
459474
// »
@@ -515,6 +530,7 @@ mod test {
515530
#[gpui::test]
516531
async fn test_delete(cx: &mut gpui::TestAppContext) {
517532
let mut cx = VimTestContext::new(cx, true).await;
533+
cx.enable_helix();
518534

519535
// test delete a selection
520536
cx.set_state(
@@ -595,6 +611,7 @@ mod test {
595611
#[gpui::test]
596612
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
597613
let mut cx = VimTestContext::new(cx, true).await;
614+
cx.enable_helix();
598615

599616
cx.set_state(
600617
indoc! {"
@@ -648,6 +665,7 @@ mod test {
648665
#[gpui::test]
649666
async fn test_newline_char(cx: &mut gpui::TestAppContext) {
650667
let mut cx = VimTestContext::new(cx, true).await;
668+
cx.enable_helix();
651669

652670
cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
653671

@@ -665,6 +683,7 @@ mod test {
665683
#[gpui::test]
666684
async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
667685
let mut cx = VimTestContext::new(cx, true).await;
686+
cx.enable_helix();
668687
cx.set_state(
669688
indoc! {"
670689
«The ˇ»quick brown
@@ -687,6 +706,7 @@ mod test {
687706
#[gpui::test]
688707
async fn test_append(cx: &mut gpui::TestAppContext) {
689708
let mut cx = VimTestContext::new(cx, true).await;
709+
cx.enable_helix();
690710
// test from the end of the selection
691711
cx.set_state(
692712
indoc! {"
@@ -729,6 +749,7 @@ mod test {
729749
#[gpui::test]
730750
async fn test_replace(cx: &mut gpui::TestAppContext) {
731751
let mut cx = VimTestContext::new(cx, true).await;
752+
cx.enable_helix();
732753

733754
// No selection (single character)
734755
cx.set_state("ˇaa", Mode::HelixNormal);
@@ -776,4 +797,72 @@ mod test {
776797
cx.shared_clipboard().assert_eq("worl");
777798
cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
778799
}
800+
801+
async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
802+
let mut cx = VimTestContext::new(cx, true).await;
803+
cx.enable_helix();
804+
805+
// First copy some text to clipboard
806+
cx.set_state("«hello worldˇ»", Mode::HelixNormal);
807+
cx.simulate_keystrokes("y");
808+
809+
// Test paste with shift-r on single cursor
810+
cx.set_state("foo ˇbar", Mode::HelixNormal);
811+
cx.simulate_keystrokes("shift-r");
812+
813+
cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
814+
815+
// Test paste with shift-r on selection
816+
cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
817+
cx.simulate_keystrokes("shift-r");
818+
819+
cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
820+
}
821+
822+
#[gpui::test]
823+
async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
824+
let mut cx = VimTestContext::new(cx, true).await;
825+
cx.enable_helix();
826+
827+
// Make a modification at a specific location
828+
cx.set_state("ˇhello", Mode::HelixNormal);
829+
assert_eq!(cx.mode(), Mode::HelixNormal);
830+
cx.simulate_keystrokes("i");
831+
assert_eq!(cx.mode(), Mode::Insert);
832+
cx.simulate_keystrokes("escape");
833+
assert_eq!(cx.mode(), Mode::HelixNormal);
834+
}
835+
836+
#[gpui::test]
837+
async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
838+
let mut cx = VimTestContext::new(cx, true).await;
839+
cx.enable_helix();
840+
841+
// Make a modification at a specific location
842+
cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
843+
cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
844+
cx.simulate_keystrokes("i");
845+
cx.simulate_keystrokes("escape");
846+
cx.simulate_keystrokes("i");
847+
cx.simulate_keystrokes("m o d i f i e d space");
848+
cx.simulate_keystrokes("escape");
849+
850+
// TODO: this fails, because state is no longer helix
851+
cx.assert_state(
852+
"line one\nline modified ˇtwo\nline three",
853+
Mode::HelixNormal,
854+
);
855+
856+
// Move cursor away from the modification
857+
cx.simulate_keystrokes("up");
858+
859+
// Use "g ." to go back to last modification
860+
cx.simulate_keystrokes("g .");
861+
862+
// Verify we're back at the modification location and still in HelixNormal mode
863+
cx.assert_state(
864+
"line one\nline modifiedˇ two\nline three",
865+
Mode::HelixNormal,
866+
);
867+
}
779868
}

crates/vim/src/insert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ impl Vim {
6464
});
6565

6666
if HelixModeSetting::get_global(cx).0 {
67-
self.switch_mode(Mode::HelixNormal, false, window, cx);
67+
self.switch_mode(Mode::HelixNormal, true, window, cx);
6868
} else {
6969
self.switch_mode(Mode::Normal, false, window, cx);
7070
}

crates/vim/src/motion.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ impl Vim {
692692
}
693693
}
694694

695-
Mode::HelixNormal => {}
695+
Mode::HelixNormal | Mode::HelixSelect => {}
696696
}
697697
}
698698

@@ -729,7 +729,7 @@ impl Vim {
729729
)
730730
}
731731
}
732-
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
732+
Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => {
733733
self.visual_motion(motion.clone(), count, window, cx)
734734
}
735735

crates/vim/src/normal/convert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ impl Vim {
201201
let snapshot = editor.buffer().read(cx).snapshot(cx);
202202
for selection in editor.selections.all_adjusted(cx) {
203203
match vim.mode {
204-
Mode::Visual | Mode::VisualLine => {
204+
Mode::Visual | Mode::VisualLine | Mode::HelixSelect => {
205205
ranges.push(selection.start..selection.end);
206206
cursor_positions.push(selection.start..selection.start);
207207
}

crates/vim/src/object.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ impl Vim {
400400

401401
match self.mode {
402402
Mode::Normal => self.normal_object(object, count, window, cx),
403-
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
403+
Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => {
404404
self.visual_object(object, count, window, cx)
405405
}
406406
Mode::Insert | Mode::Replace | Mode::HelixNormal => {

crates/vim/src/state.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub enum Mode {
4444
VisualLine,
4545
VisualBlock,
4646
HelixNormal,
47+
HelixSelect,
4748
}
4849

4950
impl Display for Mode {
@@ -56,14 +57,15 @@ impl Display for Mode {
5657
Mode::VisualLine => write!(f, "VISUAL LINE"),
5758
Mode::VisualBlock => write!(f, "VISUAL BLOCK"),
5859
Mode::HelixNormal => write!(f, "HELIX NORMAL"),
60+
Mode::HelixSelect => write!(f, "HELIX SELECT"),
5961
}
6062
}
6163
}
6264

6365
impl Mode {
6466
pub fn is_visual(&self) -> bool {
6567
match self {
66-
Self::Visual | Self::VisualLine | Self::VisualBlock => true,
68+
Self::Visual | Self::VisualLine | Self::VisualBlock | Self::HelixSelect => true,
6769
Self::Normal | Self::Insert | Self::Replace | Self::HelixNormal => false,
6870
}
6971
}

crates/vim/src/test/neovim_connection.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ impl NeovimConnection {
442442
}
443443
Mode::Insert | Mode::Normal | Mode::Replace => selections
444444
.push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)),
445-
Mode::HelixNormal => unreachable!(),
445+
Mode::HelixNormal | Mode::HelixSelect => unreachable!(),
446446
}
447447

448448
let ranges = encode_ranges(&text, &selections);

0 commit comments

Comments
 (0)