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