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 /// Select the next match for the current search query.
57 HelixSelectNext,
58 /// Select the previous match for the current search query.
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, cx);
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, cx);
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, cx);
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 _dismiss_subscription: None,
587 }
588 });
589 }
590 });
591 self.start_recording(cx);
592 }
593
594 fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
595 self.start_recording(cx);
596 self.switch_mode(Mode::Insert, false, window, cx);
597 self.update_editor(cx, |_, editor, cx| {
598 editor.change_selections(Default::default(), window, cx, |s| {
599 s.move_with(|map, selection| {
600 let point = if selection.is_empty() {
601 right(map, selection.head(), 1)
602 } else {
603 selection.end
604 };
605 selection.collapse_to(point, SelectionGoal::None);
606 });
607 });
608 });
609 }
610
611 pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
612 self.update_editor(cx, |_, editor, cx| {
613 editor.transact(window, cx, |editor, window, cx| {
614 let display_map = editor.display_snapshot(cx);
615 let selections = editor.selections.all_display(&display_map);
616
617 // Store selection info for positioning after edit
618 let selection_info: Vec<_> = selections
619 .iter()
620 .map(|selection| {
621 let range = selection.range();
622 let start_offset = range.start.to_offset(&display_map, Bias::Left);
623 let end_offset = range.end.to_offset(&display_map, Bias::Left);
624 let was_empty = range.is_empty();
625 let was_reversed = selection.reversed;
626 (
627 display_map.buffer_snapshot().anchor_before(start_offset),
628 end_offset - start_offset,
629 was_empty,
630 was_reversed,
631 )
632 })
633 .collect();
634
635 let mut edits = Vec::new();
636 for selection in &selections {
637 let mut range = selection.range();
638
639 // For empty selections, extend to replace one character
640 if range.is_empty() {
641 range.end = movement::saturating_right(&display_map, range.start);
642 }
643
644 let byte_range = range.start.to_offset(&display_map, Bias::Left)
645 ..range.end.to_offset(&display_map, Bias::Left);
646
647 if !byte_range.is_empty() {
648 let replacement_text = text.repeat(byte_range.end - byte_range.start);
649 edits.push((byte_range, replacement_text));
650 }
651 }
652
653 editor.edit(edits, cx);
654
655 // Restore selections based on original info
656 let snapshot = editor.buffer().read(cx).snapshot(cx);
657 let ranges: Vec<_> = selection_info
658 .into_iter()
659 .map(|(start_anchor, original_len, was_empty, was_reversed)| {
660 let start_point = start_anchor.to_point(&snapshot);
661 if was_empty {
662 // For cursor-only, collapse to start
663 start_point..start_point
664 } else {
665 // For selections, span the replaced text
666 let replacement_len = text.len() * original_len;
667 let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
668 let end_point = snapshot.offset_to_point(end_offset);
669 if was_reversed {
670 end_point..start_point
671 } else {
672 start_point..end_point
673 }
674 }
675 })
676 .collect();
677
678 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
679 s.select_ranges(ranges);
680 });
681 });
682 });
683 self.switch_mode(Mode::HelixNormal, true, window, cx);
684 }
685
686 pub fn helix_goto_last_modification(
687 &mut self,
688 _: &HelixGotoLastModification,
689 window: &mut Window,
690 cx: &mut Context<Self>,
691 ) {
692 self.jump(".".into(), false, false, window, cx);
693 }
694
695 pub fn helix_select_lines(
696 &mut self,
697 _: &HelixSelectLine,
698 window: &mut Window,
699 cx: &mut Context<Self>,
700 ) {
701 let count = Vim::take_count(cx).unwrap_or(1);
702 self.update_editor(cx, |_, editor, cx| {
703 editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
704 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
705 let mut selections = editor.selections.all::<Point>(&display_map);
706 let max_point = display_map.buffer_snapshot().max_point();
707 let buffer_snapshot = &display_map.buffer_snapshot();
708
709 for selection in &mut selections {
710 // Start always goes to column 0 of the first selected line
711 let start_row = selection.start.row;
712 let current_end_row = selection.end.row;
713
714 // Check if cursor is on empty line by checking first character
715 let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0));
716 let first_char = buffer_snapshot.chars_at(line_start_offset).next();
717 let extra_line = if first_char == Some('\n') && selection.is_empty() {
718 1
719 } else {
720 0
721 };
722
723 let end_row = current_end_row + count as u32 + extra_line;
724
725 selection.start = Point::new(start_row, 0);
726 selection.end = if end_row > max_point.row {
727 max_point
728 } else {
729 Point::new(end_row, 0)
730 };
731 selection.reversed = false;
732 }
733
734 editor.change_selections(Default::default(), window, cx, |s| {
735 s.select(selections);
736 });
737 });
738 }
739
740 fn helix_keep_newest_selection(
741 &mut self,
742 _: &HelixKeepNewestSelection,
743 window: &mut Window,
744 cx: &mut Context<Self>,
745 ) {
746 self.update_editor(cx, |_, editor, cx| {
747 let newest = editor
748 .selections
749 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
750 editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
751 });
752 }
753
754 fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
755 self.update_editor(cx, |vim, editor, cx| {
756 editor.set_clip_at_line_ends(false, cx);
757 editor.transact(window, cx, |editor, window, cx| {
758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
759 s.move_with(|map, selection| {
760 if selection.start == selection.end {
761 selection.end = movement::right(map, selection.end);
762 }
763
764 // If the selection starts and ends on a newline, we exclude the last one.
765 if !selection.is_empty()
766 && selection.start.column() == 0
767 && selection.end.column() == 0
768 {
769 selection.end = movement::left(map, selection.end);
770 }
771 })
772 });
773 if yank {
774 vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
775 }
776 let selections = editor
777 .selections
778 .all::<Point>(&editor.display_snapshot(cx))
779 .into_iter();
780 let edits = selections.map(|selection| (selection.start..selection.end, ""));
781 editor.edit(edits, cx);
782 });
783 });
784 self.switch_mode(Mode::Insert, true, window, cx);
785 }
786
787 fn helix_substitute(
788 &mut self,
789 _: &HelixSubstitute,
790 window: &mut Window,
791 cx: &mut Context<Self>,
792 ) {
793 self.do_helix_substitute(true, window, cx);
794 }
795
796 fn helix_substitute_no_yank(
797 &mut self,
798 _: &HelixSubstituteNoYank,
799 window: &mut Window,
800 cx: &mut Context<Self>,
801 ) {
802 self.do_helix_substitute(false, window, cx);
803 }
804
805 fn helix_select_next(
806 &mut self,
807 _: &HelixSelectNext,
808 window: &mut Window,
809 cx: &mut Context<Self>,
810 ) {
811 self.do_helix_select(Direction::Next, window, cx);
812 }
813
814 fn helix_select_previous(
815 &mut self,
816 _: &HelixSelectPrevious,
817 window: &mut Window,
818 cx: &mut Context<Self>,
819 ) {
820 self.do_helix_select(Direction::Prev, window, cx);
821 }
822
823 fn do_helix_select(
824 &mut self,
825 direction: searchable::Direction,
826 window: &mut Window,
827 cx: &mut Context<Self>,
828 ) {
829 let Some(pane) = self.pane(window, cx) else {
830 return;
831 };
832 let count = Vim::take_count(cx).unwrap_or(1);
833 Vim::take_forced_motion(cx);
834 let prior_selections = self.editor_selections(window, cx);
835
836 let success = pane.update(cx, |pane, cx| {
837 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
838 return false;
839 };
840 search_bar.update(cx, |search_bar, cx| {
841 if !search_bar.has_active_match() || !search_bar.show(window, cx) {
842 return false;
843 }
844 search_bar.select_match(direction, count, window, cx);
845 true
846 })
847 });
848
849 if !success {
850 return;
851 }
852 if self.mode == Mode::HelixSelect {
853 self.update_editor(cx, |_vim, editor, cx| {
854 let snapshot = editor.snapshot(window, cx);
855 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
856 s.select_anchor_ranges(
857 prior_selections
858 .iter()
859 .cloned()
860 .chain(s.all_anchors(&snapshot).iter().map(|s| s.range())),
861 );
862 })
863 });
864 }
865 }
866}
867
868#[cfg(test)]
869mod test {
870 use gpui::{UpdateGlobal, VisualTestContext};
871 use indoc::indoc;
872 use project::FakeFs;
873 use search::{ProjectSearchView, project_search};
874 use serde_json::json;
875 use settings::SettingsStore;
876 use util::path;
877 use workspace::DeploySearch;
878
879 use crate::{VimAddon, state::Mode, test::VimTestContext};
880
881 #[gpui::test]
882 async fn test_word_motions(cx: &mut gpui::TestAppContext) {
883 let mut cx = VimTestContext::new(cx, true).await;
884 cx.enable_helix();
885 // «
886 // ˇ
887 // »
888 cx.set_state(
889 indoc! {"
890 Th«e quiˇ»ck brown
891 fox jumps over
892 the lazy dog."},
893 Mode::HelixNormal,
894 );
895
896 cx.simulate_keystrokes("w");
897
898 cx.assert_state(
899 indoc! {"
900 The qu«ick ˇ»brown
901 fox jumps over
902 the lazy dog."},
903 Mode::HelixNormal,
904 );
905
906 cx.simulate_keystrokes("w");
907
908 cx.assert_state(
909 indoc! {"
910 The quick «brownˇ»
911 fox jumps over
912 the lazy dog."},
913 Mode::HelixNormal,
914 );
915
916 cx.simulate_keystrokes("2 b");
917
918 cx.assert_state(
919 indoc! {"
920 The «ˇquick »brown
921 fox jumps over
922 the lazy dog."},
923 Mode::HelixNormal,
924 );
925
926 cx.simulate_keystrokes("down e up");
927
928 cx.assert_state(
929 indoc! {"
930 The quicˇk brown
931 fox jumps over
932 the lazy dog."},
933 Mode::HelixNormal,
934 );
935
936 cx.set_state("aa\n «ˇbb»", Mode::HelixNormal);
937
938 cx.simulate_keystroke("b");
939
940 cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal);
941 }
942
943 #[gpui::test]
944 async fn test_delete(cx: &mut gpui::TestAppContext) {
945 let mut cx = VimTestContext::new(cx, true).await;
946 cx.enable_helix();
947
948 // test delete a selection
949 cx.set_state(
950 indoc! {"
951 The qu«ick ˇ»brown
952 fox jumps over
953 the lazy dog."},
954 Mode::HelixNormal,
955 );
956
957 cx.simulate_keystrokes("d");
958
959 cx.assert_state(
960 indoc! {"
961 The quˇbrown
962 fox jumps over
963 the lazy dog."},
964 Mode::HelixNormal,
965 );
966
967 // test deleting a single character
968 cx.simulate_keystrokes("d");
969
970 cx.assert_state(
971 indoc! {"
972 The quˇrown
973 fox jumps over
974 the lazy dog."},
975 Mode::HelixNormal,
976 );
977 }
978
979 #[gpui::test]
980 async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
981 let mut cx = VimTestContext::new(cx, true).await;
982
983 cx.set_state(
984 indoc! {"
985 The quick brownˇ
986 fox jumps over
987 the lazy dog."},
988 Mode::HelixNormal,
989 );
990
991 cx.simulate_keystrokes("d");
992
993 cx.assert_state(
994 indoc! {"
995 The quick brownˇfox jumps over
996 the lazy dog."},
997 Mode::HelixNormal,
998 );
999 }
1000
1001 // #[gpui::test]
1002 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
1003 // let mut cx = VimTestContext::new(cx, true).await;
1004
1005 // cx.set_state(
1006 // indoc! {"
1007 // The quick brown
1008 // fox jumps over
1009 // the lazy dog.ˇ"},
1010 // Mode::HelixNormal,
1011 // );
1012
1013 // cx.simulate_keystrokes("d");
1014
1015 // cx.assert_state(
1016 // indoc! {"
1017 // The quick brown
1018 // fox jumps over
1019 // the lazy dog.ˇ"},
1020 // Mode::HelixNormal,
1021 // );
1022 // }
1023
1024 #[gpui::test]
1025 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1026 let mut cx = VimTestContext::new(cx, true).await;
1027 cx.enable_helix();
1028
1029 cx.set_state(
1030 indoc! {"
1031 The quˇick brown
1032 fox jumps over
1033 the lazy dog."},
1034 Mode::HelixNormal,
1035 );
1036
1037 cx.simulate_keystrokes("f z");
1038
1039 cx.assert_state(
1040 indoc! {"
1041 The qu«ick brown
1042 fox jumps over
1043 the lazˇ»y dog."},
1044 Mode::HelixNormal,
1045 );
1046
1047 cx.simulate_keystrokes("F e F e");
1048
1049 cx.assert_state(
1050 indoc! {"
1051 The quick brown
1052 fox jumps ov«ˇer
1053 the» lazy dog."},
1054 Mode::HelixNormal,
1055 );
1056
1057 cx.simulate_keystrokes("e 2 F e");
1058
1059 cx.assert_state(
1060 indoc! {"
1061 Th«ˇe quick brown
1062 fox jumps over»
1063 the lazy dog."},
1064 Mode::HelixNormal,
1065 );
1066
1067 cx.simulate_keystrokes("t r t r");
1068
1069 cx.assert_state(
1070 indoc! {"
1071 The quick «brown
1072 fox jumps oveˇ»r
1073 the lazy dog."},
1074 Mode::HelixNormal,
1075 );
1076 }
1077
1078 #[gpui::test]
1079 async fn test_newline_char(cx: &mut gpui::TestAppContext) {
1080 let mut cx = VimTestContext::new(cx, true).await;
1081 cx.enable_helix();
1082
1083 cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
1084
1085 cx.simulate_keystroke("w");
1086
1087 cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
1088
1089 cx.set_state("aa«\nˇ»", Mode::HelixNormal);
1090
1091 cx.simulate_keystroke("b");
1092
1093 cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
1094 }
1095
1096 #[gpui::test]
1097 async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
1098 let mut cx = VimTestContext::new(cx, true).await;
1099 cx.enable_helix();
1100 cx.set_state(
1101 indoc! {"
1102 «The ˇ»quick brown
1103 fox jumps over
1104 the lazy dog."},
1105 Mode::HelixNormal,
1106 );
1107
1108 cx.simulate_keystrokes("i");
1109
1110 cx.assert_state(
1111 indoc! {"
1112 ˇThe quick brown
1113 fox jumps over
1114 the lazy dog."},
1115 Mode::Insert,
1116 );
1117 }
1118
1119 #[gpui::test]
1120 async fn test_append(cx: &mut gpui::TestAppContext) {
1121 let mut cx = VimTestContext::new(cx, true).await;
1122 cx.enable_helix();
1123 // test from the end of the selection
1124 cx.set_state(
1125 indoc! {"
1126 «Theˇ» quick brown
1127 fox jumps over
1128 the lazy dog."},
1129 Mode::HelixNormal,
1130 );
1131
1132 cx.simulate_keystrokes("a");
1133
1134 cx.assert_state(
1135 indoc! {"
1136 Theˇ quick brown
1137 fox jumps over
1138 the lazy dog."},
1139 Mode::Insert,
1140 );
1141
1142 // test from the beginning of the selection
1143 cx.set_state(
1144 indoc! {"
1145 «ˇThe» quick brown
1146 fox jumps over
1147 the lazy dog."},
1148 Mode::HelixNormal,
1149 );
1150
1151 cx.simulate_keystrokes("a");
1152
1153 cx.assert_state(
1154 indoc! {"
1155 Theˇ quick brown
1156 fox jumps over
1157 the lazy dog."},
1158 Mode::Insert,
1159 );
1160 }
1161
1162 #[gpui::test]
1163 async fn test_replace(cx: &mut gpui::TestAppContext) {
1164 let mut cx = VimTestContext::new(cx, true).await;
1165 cx.enable_helix();
1166
1167 // No selection (single character)
1168 cx.set_state("ˇaa", Mode::HelixNormal);
1169
1170 cx.simulate_keystrokes("r x");
1171
1172 cx.assert_state("ˇxa", Mode::HelixNormal);
1173
1174 // Cursor at the beginning
1175 cx.set_state("«ˇaa»", Mode::HelixNormal);
1176
1177 cx.simulate_keystrokes("r x");
1178
1179 cx.assert_state("«ˇxx»", Mode::HelixNormal);
1180
1181 // Cursor at the end
1182 cx.set_state("«aaˇ»", Mode::HelixNormal);
1183
1184 cx.simulate_keystrokes("r x");
1185
1186 cx.assert_state("«xxˇ»", Mode::HelixNormal);
1187 }
1188
1189 #[gpui::test]
1190 async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
1191 let mut cx = VimTestContext::new(cx, true).await;
1192 cx.enable_helix();
1193
1194 // Test yanking current character with no selection
1195 cx.set_state("hello ˇworld", Mode::HelixNormal);
1196 cx.simulate_keystrokes("y");
1197
1198 // Test cursor remains at the same position after yanking single character
1199 cx.assert_state("hello ˇworld", Mode::HelixNormal);
1200 cx.shared_clipboard().assert_eq("w");
1201
1202 // Move cursor and yank another character
1203 cx.simulate_keystrokes("l");
1204 cx.simulate_keystrokes("y");
1205 cx.shared_clipboard().assert_eq("o");
1206
1207 // Test yanking with existing selection
1208 cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
1209 cx.simulate_keystrokes("y");
1210 cx.shared_clipboard().assert_eq("worl");
1211 cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
1212
1213 // Test yanking in select mode character by character
1214 cx.set_state("hello ˇworld", Mode::HelixNormal);
1215 cx.simulate_keystroke("v");
1216 cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
1217 cx.simulate_keystroke("y");
1218 cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
1219 cx.shared_clipboard().assert_eq("w");
1220 }
1221
1222 #[gpui::test]
1223 async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
1224 let mut cx = VimTestContext::new(cx, true).await;
1225 cx.enable_helix();
1226
1227 // First copy some text to clipboard
1228 cx.set_state("«hello worldˇ»", Mode::HelixNormal);
1229 cx.simulate_keystrokes("y");
1230
1231 // Test paste with shift-r on single cursor
1232 cx.set_state("foo ˇbar", Mode::HelixNormal);
1233 cx.simulate_keystrokes("shift-r");
1234
1235 cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
1236
1237 // Test paste with shift-r on selection
1238 cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
1239 cx.simulate_keystrokes("shift-r");
1240
1241 cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
1242 }
1243
1244 #[gpui::test]
1245 async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
1246 let mut cx = VimTestContext::new(cx, true).await;
1247
1248 assert_eq!(cx.mode(), Mode::Normal);
1249 cx.enable_helix();
1250
1251 cx.simulate_keystrokes("v");
1252 assert_eq!(cx.mode(), Mode::HelixSelect);
1253 cx.simulate_keystrokes("escape");
1254 assert_eq!(cx.mode(), Mode::HelixNormal);
1255 }
1256
1257 #[gpui::test]
1258 async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
1259 let mut cx = VimTestContext::new(cx, true).await;
1260 cx.enable_helix();
1261
1262 // Make a modification at a specific location
1263 cx.set_state("ˇhello", Mode::HelixNormal);
1264 assert_eq!(cx.mode(), Mode::HelixNormal);
1265 cx.simulate_keystrokes("i");
1266 assert_eq!(cx.mode(), Mode::Insert);
1267 cx.simulate_keystrokes("escape");
1268 assert_eq!(cx.mode(), Mode::HelixNormal);
1269 }
1270
1271 #[gpui::test]
1272 async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
1273 let mut cx = VimTestContext::new(cx, true).await;
1274 cx.enable_helix();
1275
1276 // Make a modification at a specific location
1277 cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1278 cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1279 cx.simulate_keystrokes("i");
1280 cx.simulate_keystrokes("escape");
1281 cx.simulate_keystrokes("i");
1282 cx.simulate_keystrokes("m o d i f i e d space");
1283 cx.simulate_keystrokes("escape");
1284
1285 // TODO: this fails, because state is no longer helix
1286 cx.assert_state(
1287 "line one\nline modified ˇtwo\nline three",
1288 Mode::HelixNormal,
1289 );
1290
1291 // Move cursor away from the modification
1292 cx.simulate_keystrokes("up");
1293
1294 // Use "g ." to go back to last modification
1295 cx.simulate_keystrokes("g .");
1296
1297 // Verify we're back at the modification location and still in HelixNormal mode
1298 cx.assert_state(
1299 "line one\nline modifiedˇ two\nline three",
1300 Mode::HelixNormal,
1301 );
1302 }
1303
1304 #[gpui::test]
1305 async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
1306 let mut cx = VimTestContext::new(cx, true).await;
1307 cx.set_state(
1308 "line one\nline ˇtwo\nline three\nline four",
1309 Mode::HelixNormal,
1310 );
1311 cx.simulate_keystrokes("2 x");
1312 cx.assert_state(
1313 "line one\n«line two\nline three\nˇ»line four",
1314 Mode::HelixNormal,
1315 );
1316
1317 // Test extending existing line selection
1318 cx.set_state(
1319 indoc! {"
1320 li«ˇne one
1321 li»ne two
1322 line three
1323 line four"},
1324 Mode::HelixNormal,
1325 );
1326 cx.simulate_keystrokes("x");
1327 cx.assert_state(
1328 indoc! {"
1329 «line one
1330 line two
1331 ˇ»line three
1332 line four"},
1333 Mode::HelixNormal,
1334 );
1335
1336 // Pressing x in empty line, select next line (because helix considers cursor a selection)
1337 cx.set_state(
1338 indoc! {"
1339 line one
1340 ˇ
1341 line three
1342 line four
1343 line five
1344 line six"},
1345 Mode::HelixNormal,
1346 );
1347 cx.simulate_keystrokes("x");
1348 cx.assert_state(
1349 indoc! {"
1350 line one
1351 «
1352 line three
1353 ˇ»line four
1354 line five
1355 line six"},
1356 Mode::HelixNormal,
1357 );
1358
1359 // Another x should only select the next line
1360 cx.simulate_keystrokes("x");
1361 cx.assert_state(
1362 indoc! {"
1363 line one
1364 «
1365 line three
1366 line four
1367 ˇ»line five
1368 line six"},
1369 Mode::HelixNormal,
1370 );
1371
1372 // Empty line with count selects extra + count lines
1373 cx.set_state(
1374 indoc! {"
1375 line one
1376 ˇ
1377 line three
1378 line four
1379 line five"},
1380 Mode::HelixNormal,
1381 );
1382 cx.simulate_keystrokes("2 x");
1383 cx.assert_state(
1384 indoc! {"
1385 line one
1386 «
1387 line three
1388 line four
1389 ˇ»line five"},
1390 Mode::HelixNormal,
1391 );
1392
1393 // Compare empty vs non-empty line behavior
1394 cx.set_state(
1395 indoc! {"
1396 ˇnon-empty line
1397 line two
1398 line three"},
1399 Mode::HelixNormal,
1400 );
1401 cx.simulate_keystrokes("x");
1402 cx.assert_state(
1403 indoc! {"
1404 «non-empty line
1405 ˇ»line two
1406 line three"},
1407 Mode::HelixNormal,
1408 );
1409
1410 // Same test but with empty line - should select one extra
1411 cx.set_state(
1412 indoc! {"
1413 ˇ
1414 line two
1415 line three"},
1416 Mode::HelixNormal,
1417 );
1418 cx.simulate_keystrokes("x");
1419 cx.assert_state(
1420 indoc! {"
1421 «
1422 line two
1423 ˇ»line three"},
1424 Mode::HelixNormal,
1425 );
1426
1427 // Test selecting multiple lines with count
1428 cx.set_state(
1429 indoc! {"
1430 ˇline one
1431 line two
1432 line threeˇ
1433 line four
1434 line five"},
1435 Mode::HelixNormal,
1436 );
1437 cx.simulate_keystrokes("x");
1438 cx.assert_state(
1439 indoc! {"
1440 «line one
1441 ˇ»line two
1442 «line three
1443 ˇ»line four
1444 line five"},
1445 Mode::HelixNormal,
1446 );
1447 cx.simulate_keystrokes("x");
1448 // Adjacent line selections stay separate (not merged)
1449 cx.assert_state(
1450 indoc! {"
1451 «line one
1452 line two
1453 ˇ»«line three
1454 line four
1455 ˇ»line five"},
1456 Mode::HelixNormal,
1457 );
1458 }
1459
1460 #[gpui::test]
1461 async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
1462 let mut cx = VimTestContext::new(cx, true).await;
1463
1464 assert_eq!(cx.mode(), Mode::Normal);
1465 cx.enable_helix();
1466
1467 cx.set_state("ˇhello", Mode::HelixNormal);
1468 cx.simulate_keystrokes("l v l l");
1469 cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
1470 }
1471
1472 #[gpui::test]
1473 async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
1474 let mut cx = VimTestContext::new(cx, true).await;
1475
1476 assert_eq!(cx.mode(), Mode::Normal);
1477 cx.enable_helix();
1478
1479 // Start with multiple cursors (no selections)
1480 cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
1481
1482 // Enter select mode and move right twice
1483 cx.simulate_keystrokes("v l l");
1484
1485 // Each cursor should independently create and extend its own selection
1486 cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
1487 }
1488
1489 #[gpui::test]
1490 async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
1491 let mut cx = VimTestContext::new(cx, true).await;
1492
1493 cx.set_state("ˇone two", Mode::Normal);
1494 cx.simulate_keystrokes("v w");
1495 cx.assert_state("«one tˇ»wo", Mode::Visual);
1496
1497 // In Vim, this selects "t". In helix selections stops just before "t"
1498
1499 cx.enable_helix();
1500 cx.set_state("ˇone two", Mode::HelixNormal);
1501 cx.simulate_keystrokes("v w");
1502 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
1503 }
1504
1505 #[gpui::test]
1506 async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) {
1507 let mut cx = VimTestContext::new(cx, true).await;
1508
1509 cx.set_state("ˇone two", Mode::Normal);
1510 cx.simulate_keystrokes("v w");
1511 cx.assert_state("«one tˇ»wo", Mode::Visual);
1512 cx.simulate_keystrokes("escape");
1513 cx.assert_state("one ˇtwo", Mode::Normal);
1514
1515 cx.enable_helix();
1516 cx.set_state("ˇone two", Mode::HelixNormal);
1517 cx.simulate_keystrokes("v w");
1518 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
1519 cx.simulate_keystrokes("escape");
1520 cx.assert_state("«one ˇ»two", Mode::HelixNormal);
1521 }
1522
1523 #[gpui::test]
1524 async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) {
1525 let mut cx = VimTestContext::new(cx, true).await;
1526 cx.enable_helix();
1527
1528 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
1529 cx.simulate_keystrokes("w");
1530 cx.assert_state("«one ˇ»two three", Mode::HelixSelect);
1531
1532 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
1533 cx.simulate_keystrokes("e");
1534 cx.assert_state("«oneˇ» two three", Mode::HelixSelect);
1535 }
1536
1537 #[gpui::test]
1538 async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) {
1539 let mut cx = VimTestContext::new(cx, true).await;
1540 cx.enable_helix();
1541
1542 cx.set_state("ˇone two three", Mode::HelixNormal);
1543 cx.simulate_keystrokes("l l v h h h");
1544 cx.assert_state("«ˇone» two three", Mode::HelixSelect);
1545 }
1546
1547 #[gpui::test]
1548 async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
1549 let mut cx = VimTestContext::new(cx, true).await;
1550 cx.enable_helix();
1551
1552 cx.set_state("ˇone two one", Mode::HelixNormal);
1553 cx.simulate_keystrokes("x");
1554 cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
1555 cx.simulate_keystrokes("s o n e");
1556 cx.run_until_parked();
1557 cx.simulate_keystrokes("enter");
1558 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1559
1560 cx.simulate_keystrokes("x");
1561 cx.simulate_keystrokes("s");
1562 cx.run_until_parked();
1563 cx.simulate_keystrokes("enter");
1564 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1565
1566 // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
1567 // cx.set_state("ˇstuff one two one", Mode::HelixNormal);
1568 // cx.simulate_keystrokes("s o n e enter");
1569 // cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
1570 }
1571
1572 #[gpui::test]
1573 async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
1574 let mut cx = VimTestContext::new(cx, true).await;
1575
1576 cx.set_state("ˇhello two one two one two one", Mode::Visual);
1577 cx.simulate_keystrokes("/ o n e");
1578 cx.simulate_keystrokes("enter");
1579 cx.simulate_keystrokes("n n");
1580 cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
1581
1582 cx.set_state("ˇhello two one two one two one", Mode::Normal);
1583 cx.simulate_keystrokes("/ o n e");
1584 cx.simulate_keystrokes("enter");
1585 cx.simulate_keystrokes("n n");
1586 cx.assert_state("hello two one two one two ˇone", Mode::Normal);
1587
1588 cx.set_state("ˇhello two one two one two one", Mode::Normal);
1589 cx.simulate_keystrokes("/ o n e");
1590 cx.simulate_keystrokes("enter");
1591 cx.simulate_keystrokes("n g n g n");
1592 cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
1593
1594 cx.enable_helix();
1595
1596 cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
1597 cx.simulate_keystrokes("/ o n e");
1598 cx.simulate_keystrokes("enter");
1599 cx.simulate_keystrokes("n n");
1600 cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
1601
1602 cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
1603 cx.simulate_keystrokes("/ o n e");
1604 cx.simulate_keystrokes("enter");
1605 cx.simulate_keystrokes("n n");
1606 cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
1607 }
1608
1609 #[gpui::test]
1610 async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
1611 let mut cx = VimTestContext::new(cx, true).await;
1612
1613 cx.set_state("ˇone two", Mode::HelixNormal);
1614 cx.simulate_keystrokes("c");
1615 cx.assert_state("ˇne two", Mode::Insert);
1616
1617 cx.set_state("«oneˇ» two", Mode::HelixNormal);
1618 cx.simulate_keystrokes("c");
1619 cx.assert_state("ˇ two", Mode::Insert);
1620
1621 cx.set_state(
1622 indoc! {"
1623 oneˇ two
1624 three
1625 "},
1626 Mode::HelixNormal,
1627 );
1628 cx.simulate_keystrokes("x c");
1629 cx.assert_state(
1630 indoc! {"
1631 ˇ
1632 three
1633 "},
1634 Mode::Insert,
1635 );
1636
1637 cx.set_state(
1638 indoc! {"
1639 one twoˇ
1640 three
1641 "},
1642 Mode::HelixNormal,
1643 );
1644 cx.simulate_keystrokes("c");
1645 cx.assert_state(
1646 indoc! {"
1647 one twoˇthree
1648 "},
1649 Mode::Insert,
1650 );
1651
1652 // Helix doesn't set the cursor to the first non-blank one when
1653 // replacing lines: it uses language-dependent indent queries instead.
1654 cx.set_state(
1655 indoc! {"
1656 one two
1657 « indented
1658 three not indentedˇ»
1659 "},
1660 Mode::HelixNormal,
1661 );
1662 cx.simulate_keystrokes("c");
1663 cx.set_state(
1664 indoc! {"
1665 one two
1666 ˇ
1667 "},
1668 Mode::Insert,
1669 );
1670 }
1671
1672 #[gpui::test]
1673 async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
1674 let mut cx = VimTestContext::new(cx, true).await;
1675 cx.enable_helix();
1676
1677 // Test g l moves to last character, not after it
1678 cx.set_state("hello ˇworld!", Mode::HelixNormal);
1679 cx.simulate_keystrokes("g l");
1680 cx.assert_state("hello worldˇ!", Mode::HelixNormal);
1681
1682 // Test with Chinese characters, test if work with UTF-8?
1683 cx.set_state("ˇ你好世界", Mode::HelixNormal);
1684 cx.simulate_keystrokes("g l");
1685 cx.assert_state("你好世ˇ界", Mode::HelixNormal);
1686
1687 // Test with end of line
1688 cx.set_state("endˇ", Mode::HelixNormal);
1689 cx.simulate_keystrokes("g l");
1690 cx.assert_state("enˇd", Mode::HelixNormal);
1691
1692 // Test with empty line
1693 cx.set_state(
1694 indoc! {"
1695 hello
1696 ˇ
1697 world"},
1698 Mode::HelixNormal,
1699 );
1700 cx.simulate_keystrokes("g l");
1701 cx.assert_state(
1702 indoc! {"
1703 hello
1704 ˇ
1705 world"},
1706 Mode::HelixNormal,
1707 );
1708
1709 // Test with multiple lines
1710 cx.set_state(
1711 indoc! {"
1712 ˇfirst line
1713 second line
1714 third line"},
1715 Mode::HelixNormal,
1716 );
1717 cx.simulate_keystrokes("g l");
1718 cx.assert_state(
1719 indoc! {"
1720 first linˇe
1721 second line
1722 third line"},
1723 Mode::HelixNormal,
1724 );
1725 }
1726
1727 #[gpui::test]
1728 async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) {
1729 VimTestContext::init(cx);
1730
1731 let fs = FakeFs::new(cx.background_executor.clone());
1732 fs.insert_tree(
1733 path!("/dir"),
1734 json!({
1735 "file_a.rs": "// File A.",
1736 "file_b.rs": "// File B.",
1737 }),
1738 )
1739 .await;
1740
1741 let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1742 let workspace =
1743 cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx));
1744
1745 cx.update(|cx| {
1746 VimTestContext::init_keybindings(true, cx);
1747 SettingsStore::update_global(cx, |store, cx| {
1748 store.update_user_settings(cx, |store| store.helix_mode = Some(true));
1749 })
1750 });
1751
1752 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1753
1754 workspace
1755 .update(cx, |workspace, window, cx| {
1756 ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx)
1757 })
1758 .unwrap();
1759
1760 let search_view = workspace
1761 .update(cx, |workspace, _, cx| {
1762 workspace
1763 .active_pane()
1764 .read(cx)
1765 .items()
1766 .find_map(|item| item.downcast::<ProjectSearchView>())
1767 .expect("Project search view should be active")
1768 })
1769 .unwrap();
1770
1771 project_search::perform_project_search(&search_view, "File A", cx);
1772
1773 search_view.update(cx, |search_view, cx| {
1774 let vim_mode = search_view
1775 .results_editor()
1776 .read(cx)
1777 .addon::<VimAddon>()
1778 .map(|addon| addon.entity.read(cx).mode);
1779
1780 assert_eq!(vim_mode, Some(Mode::HelixNormal));
1781 });
1782 }
1783
1784 #[gpui::test]
1785 async fn test_scroll_with_selection(cx: &mut gpui::TestAppContext) {
1786 let mut cx = VimTestContext::new(cx, true).await;
1787 cx.enable_helix();
1788
1789 // Start with a selection
1790 cx.set_state(
1791 indoc! {"
1792 «lineˇ» one
1793 line two
1794 line three
1795 line four
1796 line five"},
1797 Mode::HelixNormal,
1798 );
1799
1800 // Scroll down, selection should collapse
1801 cx.simulate_keystrokes("ctrl-d");
1802 cx.assert_state(
1803 indoc! {"
1804 line one
1805 line two
1806 line three
1807 line four
1808 line fiveˇ"},
1809 Mode::HelixNormal,
1810 );
1811
1812 // Make a new selection
1813 cx.simulate_keystroke("b");
1814 cx.assert_state(
1815 indoc! {"
1816 line one
1817 line two
1818 line three
1819 line four
1820 line «ˇfive»"},
1821 Mode::HelixNormal,
1822 );
1823
1824 // And scroll up, once again collapsing the selection.
1825 cx.simulate_keystroke("ctrl-u");
1826 cx.assert_state(
1827 indoc! {"
1828 line one
1829 line two
1830 line three
1831 line ˇfour
1832 line five"},
1833 Mode::HelixNormal,
1834 );
1835
1836 // Enter select mode
1837 cx.simulate_keystroke("v");
1838 cx.assert_state(
1839 indoc! {"
1840 line one
1841 line two
1842 line three
1843 line «fˇ»our
1844 line five"},
1845 Mode::HelixSelect,
1846 );
1847
1848 // And now the selection should be kept/expanded.
1849 cx.simulate_keystroke("ctrl-d");
1850 cx.assert_state(
1851 indoc! {"
1852 line one
1853 line two
1854 line three
1855 line «four
1856 line fiveˇ»"},
1857 Mode::HelixSelect,
1858 );
1859 }
1860}