1use editor::display_map::DisplaySnapshot;
2use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement};
3use gpui::{Action, actions};
4use gpui::{Context, Window};
5use language::{CharClassifier, CharKind};
6use text::{Bias, SelectionGoal};
7
8use crate::motion;
9use crate::{
10 Vim,
11 motion::{Motion, right},
12 state::Mode,
13};
14
15actions!(
16 vim,
17 [
18 /// Switches to normal mode after the cursor (Helix-style).
19 HelixNormalAfter,
20 /// Yanks the current selection or character if no selection.
21 HelixYank,
22 /// Inserts at the beginning of the selection.
23 HelixInsert,
24 /// Appends at the end of the selection.
25 HelixAppend,
26 /// Goes to the location of the last modification.
27 HelixGotoLastModification,
28 ]
29);
30
31pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
32 Vim::action(editor, cx, Vim::helix_normal_after);
33 Vim::action(editor, cx, Vim::helix_insert);
34 Vim::action(editor, cx, Vim::helix_append);
35 Vim::action(editor, cx, Vim::helix_yank);
36 Vim::action(editor, cx, Vim::helix_goto_last_modification);
37}
38
39impl Vim {
40 pub fn helix_normal_after(
41 &mut self,
42 action: &HelixNormalAfter,
43 window: &mut Window,
44 cx: &mut Context<Self>,
45 ) {
46 if self.active_operator().is_some() {
47 self.operator_stack.clear();
48 self.sync_vim_settings(window, cx);
49 return;
50 }
51 self.stop_recording_immediately(action.boxed_clone(), cx);
52 self.switch_mode(Mode::HelixNormal, false, window, cx);
53 }
54
55 pub fn helix_normal_motion(
56 &mut self,
57 motion: Motion,
58 times: Option<usize>,
59 window: &mut Window,
60 cx: &mut Context<Self>,
61 ) {
62 self.helix_move_cursor(motion, times, window, cx);
63 }
64
65 /// Updates all selections based on where the cursors are.
66 fn helix_new_selections(
67 &mut self,
68 window: &mut Window,
69 cx: &mut Context<Self>,
70 mut change: impl FnMut(
71 // the start of the cursor
72 DisplayPoint,
73 &DisplaySnapshot,
74 ) -> Option<(DisplayPoint, DisplayPoint)>,
75 ) {
76 self.update_editor(cx, |_, editor, cx| {
77 editor.change_selections(Default::default(), window, cx, |s| {
78 s.move_with(|map, selection| {
79 let cursor_start = if selection.reversed || selection.is_empty() {
80 selection.head()
81 } else {
82 movement::left(map, selection.head())
83 };
84 let Some((head, tail)) = change(cursor_start, map) else {
85 return;
86 };
87
88 selection.set_head_tail(head, tail, SelectionGoal::None);
89 });
90 });
91 });
92 }
93
94 fn helix_find_range_forward(
95 &mut self,
96 times: Option<usize>,
97 window: &mut Window,
98 cx: &mut Context<Self>,
99 mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
100 ) {
101 let times = times.unwrap_or(1);
102 self.helix_new_selections(window, cx, |cursor, map| {
103 let mut head = movement::right(map, cursor);
104 let mut tail = cursor;
105 let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
106 if head == map.max_point() {
107 return None;
108 }
109 for _ in 0..times {
110 let (maybe_next_tail, next_head) =
111 movement::find_boundary_trail(map, head, |left, right| {
112 is_boundary(left, right, &classifier)
113 });
114
115 if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
116 break;
117 }
118
119 head = next_head;
120 if let Some(next_tail) = maybe_next_tail {
121 tail = next_tail;
122 }
123 }
124 Some((head, tail))
125 });
126 }
127
128 fn helix_find_range_backward(
129 &mut self,
130 times: Option<usize>,
131 window: &mut Window,
132 cx: &mut Context<Self>,
133 mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
134 ) {
135 let times = times.unwrap_or(1);
136 self.helix_new_selections(window, cx, |cursor, map| {
137 let mut head = cursor;
138 // The original cursor was one character wide,
139 // but the search starts from the left side of it,
140 // so to include that space the selection must end one character to the right.
141 let mut tail = movement::right(map, cursor);
142 let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
143 if head == DisplayPoint::zero() {
144 return None;
145 }
146 for _ in 0..times {
147 let (maybe_next_tail, next_head) =
148 movement::find_preceding_boundary_trail(map, head, |left, right| {
149 is_boundary(left, right, &classifier)
150 });
151
152 if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
153 break;
154 }
155
156 head = next_head;
157 if let Some(next_tail) = maybe_next_tail {
158 tail = next_tail;
159 }
160 }
161 Some((head, tail))
162 });
163 }
164
165 pub fn helix_move_and_collapse(
166 &mut self,
167 motion: Motion,
168 times: Option<usize>,
169 window: &mut Window,
170 cx: &mut Context<Self>,
171 ) {
172 self.update_editor(cx, |_, editor, cx| {
173 let text_layout_details = editor.text_layout_details(window);
174 editor.change_selections(Default::default(), window, cx, |s| {
175 s.move_with(|map, selection| {
176 let goal = selection.goal;
177 let cursor = if selection.is_empty() || selection.reversed {
178 selection.head()
179 } else {
180 movement::left(map, selection.head())
181 };
182
183 let (point, goal) = motion
184 .move_point(map, cursor, selection.goal, times, &text_layout_details)
185 .unwrap_or((cursor, goal));
186
187 selection.collapse_to(point, goal)
188 })
189 });
190 });
191 }
192
193 pub fn helix_move_cursor(
194 &mut self,
195 motion: Motion,
196 times: Option<usize>,
197 window: &mut Window,
198 cx: &mut Context<Self>,
199 ) {
200 match motion {
201 Motion::NextWordStart { ignore_punctuation } => {
202 self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
203 let left_kind = classifier.kind_with(left, ignore_punctuation);
204 let right_kind = classifier.kind_with(right, ignore_punctuation);
205 let at_newline = (left == '\n') ^ (right == '\n');
206
207 (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline
208 })
209 }
210 Motion::NextWordEnd { ignore_punctuation } => {
211 self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
212 let left_kind = classifier.kind_with(left, ignore_punctuation);
213 let right_kind = classifier.kind_with(right, ignore_punctuation);
214 let at_newline = (left == '\n') ^ (right == '\n');
215
216 (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline
217 })
218 }
219 Motion::PreviousWordStart { ignore_punctuation } => {
220 self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
221 let left_kind = classifier.kind_with(left, ignore_punctuation);
222 let right_kind = classifier.kind_with(right, ignore_punctuation);
223 let at_newline = (left == '\n') ^ (right == '\n');
224
225 (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline
226 })
227 }
228 Motion::PreviousWordEnd { ignore_punctuation } => {
229 self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
230 let left_kind = classifier.kind_with(left, ignore_punctuation);
231 let right_kind = classifier.kind_with(right, ignore_punctuation);
232 let at_newline = (left == '\n') ^ (right == '\n');
233
234 (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline
235 })
236 }
237 Motion::FindForward {
238 before,
239 char,
240 mode,
241 smartcase,
242 } => {
243 self.helix_new_selections(window, cx, |cursor, map| {
244 let start = cursor;
245 let mut last_boundary = start;
246 for _ in 0..times.unwrap_or(1) {
247 last_boundary = movement::find_boundary(
248 map,
249 movement::right(map, last_boundary),
250 mode,
251 |left, right| {
252 let current_char = if before { right } else { left };
253 motion::is_character_match(char, current_char, smartcase)
254 },
255 );
256 }
257 Some((last_boundary, start))
258 });
259 }
260 Motion::FindBackward {
261 after,
262 char,
263 mode,
264 smartcase,
265 } => {
266 self.helix_new_selections(window, cx, |cursor, map| {
267 let start = cursor;
268 let mut last_boundary = start;
269 for _ in 0..times.unwrap_or(1) {
270 last_boundary = movement::find_preceding_boundary_display_point(
271 map,
272 last_boundary,
273 mode,
274 |left, right| {
275 let current_char = if after { left } else { right };
276 motion::is_character_match(char, current_char, smartcase)
277 },
278 );
279 }
280 // The original cursor was one character wide,
281 // but the search started from the left side of it,
282 // so to include that space the selection must end one character to the right.
283 Some((last_boundary, movement::right(map, start)))
284 });
285 }
286 _ => self.helix_move_and_collapse(motion, times, window, cx),
287 }
288 }
289
290 pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
291 self.update_editor(cx, |vim, editor, cx| {
292 let has_selection = editor
293 .selections
294 .all_adjusted(cx)
295 .iter()
296 .any(|selection| !selection.is_empty());
297
298 if !has_selection {
299 // If no selection, expand to current character (like 'v' does)
300 editor.change_selections(Default::default(), window, cx, |s| {
301 s.move_with(|map, selection| {
302 let head = selection.head();
303 let new_head = movement::saturating_right(map, head);
304 selection.set_tail(head, SelectionGoal::None);
305 selection.set_head(new_head, SelectionGoal::None);
306 });
307 });
308 vim.yank_selections_content(
309 editor,
310 crate::motion::MotionKind::Exclusive,
311 window,
312 cx,
313 );
314 editor.change_selections(Default::default(), window, cx, |s| {
315 s.move_with(|_map, selection| {
316 selection.collapse_to(selection.start, SelectionGoal::None);
317 });
318 });
319 } else {
320 // Yank the selection(s)
321 vim.yank_selections_content(
322 editor,
323 crate::motion::MotionKind::Exclusive,
324 window,
325 cx,
326 );
327 }
328 });
329 }
330
331 fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
332 self.start_recording(cx);
333 self.update_editor(cx, |_, editor, cx| {
334 editor.change_selections(Default::default(), window, cx, |s| {
335 s.move_with(|_map, selection| {
336 // In helix normal mode, move cursor to start of selection and collapse
337 if !selection.is_empty() {
338 selection.collapse_to(selection.start, SelectionGoal::None);
339 }
340 });
341 });
342 });
343 self.switch_mode(Mode::Insert, false, window, cx);
344 }
345
346 fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
347 self.start_recording(cx);
348 self.switch_mode(Mode::Insert, false, window, cx);
349 self.update_editor(cx, |_, editor, cx| {
350 editor.change_selections(Default::default(), window, cx, |s| {
351 s.move_with(|map, selection| {
352 let point = if selection.is_empty() {
353 right(map, selection.head(), 1)
354 } else {
355 selection.end
356 };
357 selection.collapse_to(point, SelectionGoal::None);
358 });
359 });
360 });
361 }
362
363 pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
364 self.update_editor(cx, |_, editor, cx| {
365 editor.transact(window, cx, |editor, window, cx| {
366 let (map, selections) = editor.selections.all_display(cx);
367
368 // Store selection info for positioning after edit
369 let selection_info: Vec<_> = selections
370 .iter()
371 .map(|selection| {
372 let range = selection.range();
373 let start_offset = range.start.to_offset(&map, Bias::Left);
374 let end_offset = range.end.to_offset(&map, Bias::Left);
375 let was_empty = range.is_empty();
376 let was_reversed = selection.reversed;
377 (
378 map.buffer_snapshot.anchor_at(start_offset, Bias::Left),
379 end_offset - start_offset,
380 was_empty,
381 was_reversed,
382 )
383 })
384 .collect();
385
386 let mut edits = Vec::new();
387 for selection in &selections {
388 let mut range = selection.range();
389
390 // For empty selections, extend to replace one character
391 if range.is_empty() {
392 range.end = movement::saturating_right(&map, range.start);
393 }
394
395 let byte_range = range.start.to_offset(&map, Bias::Left)
396 ..range.end.to_offset(&map, Bias::Left);
397
398 if !byte_range.is_empty() {
399 let replacement_text = text.repeat(byte_range.len());
400 edits.push((byte_range, replacement_text));
401 }
402 }
403
404 editor.edit(edits, cx);
405
406 // Restore selections based on original info
407 let snapshot = editor.buffer().read(cx).snapshot(cx);
408 let ranges: Vec<_> = selection_info
409 .into_iter()
410 .map(|(start_anchor, original_len, was_empty, was_reversed)| {
411 let start_point = start_anchor.to_point(&snapshot);
412 if was_empty {
413 // For cursor-only, collapse to start
414 start_point..start_point
415 } else {
416 // For selections, span the replaced text
417 let replacement_len = text.len() * original_len;
418 let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
419 let end_point = snapshot.offset_to_point(end_offset);
420 if was_reversed {
421 end_point..start_point
422 } else {
423 start_point..end_point
424 }
425 }
426 })
427 .collect();
428
429 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
430 s.select_ranges(ranges);
431 });
432 });
433 });
434 self.switch_mode(Mode::HelixNormal, true, window, cx);
435 }
436
437 pub fn helix_goto_last_modification(
438 &mut self,
439 _: &HelixGotoLastModification,
440 window: &mut Window,
441 cx: &mut Context<Self>,
442 ) {
443 self.jump(".".into(), false, false, window, cx);
444 }
445}
446
447#[cfg(test)]
448mod test {
449 use indoc::indoc;
450
451 use crate::{state::Mode, test::VimTestContext};
452
453 #[gpui::test]
454 async fn test_word_motions(cx: &mut gpui::TestAppContext) {
455 let mut cx = VimTestContext::new(cx, true).await;
456 cx.enable_helix();
457 // «
458 // ˇ
459 // »
460 cx.set_state(
461 indoc! {"
462 Th«e quiˇ»ck brown
463 fox jumps over
464 the lazy dog."},
465 Mode::HelixNormal,
466 );
467
468 cx.simulate_keystrokes("w");
469
470 cx.assert_state(
471 indoc! {"
472 The qu«ick ˇ»brown
473 fox jumps over
474 the lazy dog."},
475 Mode::HelixNormal,
476 );
477
478 cx.simulate_keystrokes("w");
479
480 cx.assert_state(
481 indoc! {"
482 The quick «brownˇ»
483 fox jumps over
484 the lazy dog."},
485 Mode::HelixNormal,
486 );
487
488 cx.simulate_keystrokes("2 b");
489
490 cx.assert_state(
491 indoc! {"
492 The «ˇquick »brown
493 fox jumps over
494 the lazy dog."},
495 Mode::HelixNormal,
496 );
497
498 cx.simulate_keystrokes("down e up");
499
500 cx.assert_state(
501 indoc! {"
502 The quicˇk brown
503 fox jumps over
504 the lazy dog."},
505 Mode::HelixNormal,
506 );
507
508 cx.set_state("aa\n «ˇbb»", Mode::HelixNormal);
509
510 cx.simulate_keystroke("b");
511
512 cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal);
513 }
514
515 #[gpui::test]
516 async fn test_delete(cx: &mut gpui::TestAppContext) {
517 let mut cx = VimTestContext::new(cx, true).await;
518 cx.enable_helix();
519
520 // test delete a selection
521 cx.set_state(
522 indoc! {"
523 The qu«ick ˇ»brown
524 fox jumps over
525 the lazy dog."},
526 Mode::HelixNormal,
527 );
528
529 cx.simulate_keystrokes("d");
530
531 cx.assert_state(
532 indoc! {"
533 The quˇbrown
534 fox jumps over
535 the lazy dog."},
536 Mode::HelixNormal,
537 );
538
539 // test deleting a single character
540 cx.simulate_keystrokes("d");
541
542 cx.assert_state(
543 indoc! {"
544 The quˇrown
545 fox jumps over
546 the lazy dog."},
547 Mode::HelixNormal,
548 );
549 }
550
551 #[gpui::test]
552 async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
553 let mut cx = VimTestContext::new(cx, true).await;
554
555 cx.set_state(
556 indoc! {"
557 The quick brownˇ
558 fox jumps over
559 the lazy dog."},
560 Mode::HelixNormal,
561 );
562
563 cx.simulate_keystrokes("d");
564
565 cx.assert_state(
566 indoc! {"
567 The quick brownˇfox jumps over
568 the lazy dog."},
569 Mode::HelixNormal,
570 );
571 }
572
573 // #[gpui::test]
574 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
575 // let mut cx = VimTestContext::new(cx, true).await;
576
577 // cx.set_state(
578 // indoc! {"
579 // The quick brown
580 // fox jumps over
581 // the lazy dog.ˇ"},
582 // Mode::HelixNormal,
583 // );
584
585 // cx.simulate_keystrokes("d");
586
587 // cx.assert_state(
588 // indoc! {"
589 // The quick brown
590 // fox jumps over
591 // the lazy dog.ˇ"},
592 // Mode::HelixNormal,
593 // );
594 // }
595
596 #[gpui::test]
597 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
598 let mut cx = VimTestContext::new(cx, true).await;
599 cx.enable_helix();
600
601 cx.set_state(
602 indoc! {"
603 The quˇick brown
604 fox jumps over
605 the lazy dog."},
606 Mode::HelixNormal,
607 );
608
609 cx.simulate_keystrokes("f z");
610
611 cx.assert_state(
612 indoc! {"
613 The qu«ick brown
614 fox jumps over
615 the lazˇ»y dog."},
616 Mode::HelixNormal,
617 );
618
619 cx.simulate_keystrokes("F e F e");
620
621 cx.assert_state(
622 indoc! {"
623 The quick brown
624 fox jumps ov«ˇer
625 the» lazy dog."},
626 Mode::HelixNormal,
627 );
628
629 cx.simulate_keystrokes("e 2 F e");
630
631 cx.assert_state(
632 indoc! {"
633 Th«ˇe quick brown
634 fox jumps over»
635 the lazy dog."},
636 Mode::HelixNormal,
637 );
638
639 cx.simulate_keystrokes("t r t r");
640
641 cx.assert_state(
642 indoc! {"
643 The quick «brown
644 fox jumps oveˇ»r
645 the lazy dog."},
646 Mode::HelixNormal,
647 );
648 }
649
650 #[gpui::test]
651 async fn test_newline_char(cx: &mut gpui::TestAppContext) {
652 let mut cx = VimTestContext::new(cx, true).await;
653 cx.enable_helix();
654
655 cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
656
657 cx.simulate_keystroke("w");
658
659 cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
660
661 cx.set_state("aa«\nˇ»", Mode::HelixNormal);
662
663 cx.simulate_keystroke("b");
664
665 cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
666 }
667
668 #[gpui::test]
669 async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
670 let mut cx = VimTestContext::new(cx, true).await;
671 cx.enable_helix();
672 cx.set_state(
673 indoc! {"
674 «The ˇ»quick brown
675 fox jumps over
676 the lazy dog."},
677 Mode::HelixNormal,
678 );
679
680 cx.simulate_keystrokes("i");
681
682 cx.assert_state(
683 indoc! {"
684 ˇThe quick brown
685 fox jumps over
686 the lazy dog."},
687 Mode::Insert,
688 );
689 }
690
691 #[gpui::test]
692 async fn test_append(cx: &mut gpui::TestAppContext) {
693 let mut cx = VimTestContext::new(cx, true).await;
694 cx.enable_helix();
695 // test from the end of the selection
696 cx.set_state(
697 indoc! {"
698 «Theˇ» quick brown
699 fox jumps over
700 the lazy dog."},
701 Mode::HelixNormal,
702 );
703
704 cx.simulate_keystrokes("a");
705
706 cx.assert_state(
707 indoc! {"
708 Theˇ quick brown
709 fox jumps over
710 the lazy dog."},
711 Mode::Insert,
712 );
713
714 // test from the beginning of the selection
715 cx.set_state(
716 indoc! {"
717 «ˇThe» quick brown
718 fox jumps over
719 the lazy dog."},
720 Mode::HelixNormal,
721 );
722
723 cx.simulate_keystrokes("a");
724
725 cx.assert_state(
726 indoc! {"
727 Theˇ quick brown
728 fox jumps over
729 the lazy dog."},
730 Mode::Insert,
731 );
732 }
733
734 #[gpui::test]
735 async fn test_replace(cx: &mut gpui::TestAppContext) {
736 let mut cx = VimTestContext::new(cx, true).await;
737 cx.enable_helix();
738
739 // No selection (single character)
740 cx.set_state("ˇaa", Mode::HelixNormal);
741
742 cx.simulate_keystrokes("r x");
743
744 cx.assert_state("ˇxa", Mode::HelixNormal);
745
746 // Cursor at the beginning
747 cx.set_state("«ˇaa»", Mode::HelixNormal);
748
749 cx.simulate_keystrokes("r x");
750
751 cx.assert_state("«ˇxx»", Mode::HelixNormal);
752
753 // Cursor at the end
754 cx.set_state("«aaˇ»", Mode::HelixNormal);
755
756 cx.simulate_keystrokes("r x");
757
758 cx.assert_state("«xxˇ»", Mode::HelixNormal);
759 }
760
761 #[gpui::test]
762 async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
763 let mut cx = VimTestContext::new(cx, true).await;
764 cx.enable_helix();
765
766 // Test yanking current character with no selection
767 cx.set_state("hello ˇworld", Mode::HelixNormal);
768 cx.simulate_keystrokes("y");
769
770 // Test cursor remains at the same position after yanking single character
771 cx.assert_state("hello ˇworld", Mode::HelixNormal);
772 cx.shared_clipboard().assert_eq("w");
773
774 // Move cursor and yank another character
775 cx.simulate_keystrokes("l");
776 cx.simulate_keystrokes("y");
777 cx.shared_clipboard().assert_eq("o");
778
779 // Test yanking with existing selection
780 cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
781 cx.simulate_keystrokes("y");
782 cx.shared_clipboard().assert_eq("worl");
783 cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
784 }
785 #[gpui::test]
786 async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
787 let mut cx = VimTestContext::new(cx, true).await;
788 cx.enable_helix();
789
790 // First copy some text to clipboard
791 cx.set_state("«hello worldˇ»", Mode::HelixNormal);
792 cx.simulate_keystrokes("y");
793
794 // Test paste with shift-r on single cursor
795 cx.set_state("foo ˇbar", Mode::HelixNormal);
796 cx.simulate_keystrokes("shift-r");
797
798 cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
799
800 // Test paste with shift-r on selection
801 cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
802 cx.simulate_keystrokes("shift-r");
803
804 cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
805 }
806
807 #[gpui::test]
808 async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
809 let mut cx = VimTestContext::new(cx, true).await;
810 cx.enable_helix();
811
812 // Make a modification at a specific location
813 cx.set_state("ˇhello", Mode::HelixNormal);
814 assert_eq!(cx.mode(), Mode::HelixNormal);
815 cx.simulate_keystrokes("i");
816 assert_eq!(cx.mode(), Mode::Insert);
817 cx.simulate_keystrokes("escape");
818 assert_eq!(cx.mode(), Mode::HelixNormal);
819 }
820
821 #[gpui::test]
822 async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
823 let mut cx = VimTestContext::new(cx, true).await;
824 cx.enable_helix();
825
826 // Make a modification at a specific location
827 cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
828 cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
829 cx.simulate_keystrokes("i");
830 cx.simulate_keystrokes("escape");
831 cx.simulate_keystrokes("i");
832 cx.simulate_keystrokes("m o d i f i e d space");
833 cx.simulate_keystrokes("escape");
834
835 // TODO: this fails, because state is no longer helix
836 cx.assert_state(
837 "line one\nline modified ˇtwo\nline three",
838 Mode::HelixNormal,
839 );
840
841 // Move cursor away from the modification
842 cx.simulate_keystrokes("up");
843
844 // Use "g ." to go back to last modification
845 cx.simulate_keystrokes("g .");
846
847 // Verify we're back at the modification location and still in HelixNormal mode
848 cx.assert_state(
849 "line one\nline modifiedˇ two\nline three",
850 Mode::HelixNormal,
851 );
852 }
853}