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