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