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