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