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