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