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