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 gpui::{UpdateGlobal, VisualTestContext};
866 use indoc::indoc;
867 use project::FakeFs;
868 use search::{ProjectSearchView, project_search};
869 use serde_json::json;
870 use settings::SettingsStore;
871 use util::path;
872 use workspace::DeploySearch;
873
874 use crate::{VimAddon, state::Mode, test::VimTestContext};
875
876 #[gpui::test]
877 async fn test_word_motions(cx: &mut gpui::TestAppContext) {
878 let mut cx = VimTestContext::new(cx, true).await;
879 cx.enable_helix();
880 // «
881 // ˇ
882 // »
883 cx.set_state(
884 indoc! {"
885 Th«e quiˇ»ck brown
886 fox jumps over
887 the lazy dog."},
888 Mode::HelixNormal,
889 );
890
891 cx.simulate_keystrokes("w");
892
893 cx.assert_state(
894 indoc! {"
895 The qu«ick ˇ»brown
896 fox jumps over
897 the lazy dog."},
898 Mode::HelixNormal,
899 );
900
901 cx.simulate_keystrokes("w");
902
903 cx.assert_state(
904 indoc! {"
905 The quick «brownˇ»
906 fox jumps over
907 the lazy dog."},
908 Mode::HelixNormal,
909 );
910
911 cx.simulate_keystrokes("2 b");
912
913 cx.assert_state(
914 indoc! {"
915 The «ˇquick »brown
916 fox jumps over
917 the lazy dog."},
918 Mode::HelixNormal,
919 );
920
921 cx.simulate_keystrokes("down e up");
922
923 cx.assert_state(
924 indoc! {"
925 The quicˇk brown
926 fox jumps over
927 the lazy dog."},
928 Mode::HelixNormal,
929 );
930
931 cx.set_state("aa\n «ˇbb»", Mode::HelixNormal);
932
933 cx.simulate_keystroke("b");
934
935 cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal);
936 }
937
938 #[gpui::test]
939 async fn test_delete(cx: &mut gpui::TestAppContext) {
940 let mut cx = VimTestContext::new(cx, true).await;
941 cx.enable_helix();
942
943 // test delete a selection
944 cx.set_state(
945 indoc! {"
946 The qu«ick ˇ»brown
947 fox jumps over
948 the lazy dog."},
949 Mode::HelixNormal,
950 );
951
952 cx.simulate_keystrokes("d");
953
954 cx.assert_state(
955 indoc! {"
956 The quˇbrown
957 fox jumps over
958 the lazy dog."},
959 Mode::HelixNormal,
960 );
961
962 // test deleting a single character
963 cx.simulate_keystrokes("d");
964
965 cx.assert_state(
966 indoc! {"
967 The quˇrown
968 fox jumps over
969 the lazy dog."},
970 Mode::HelixNormal,
971 );
972 }
973
974 #[gpui::test]
975 async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
976 let mut cx = VimTestContext::new(cx, true).await;
977
978 cx.set_state(
979 indoc! {"
980 The quick brownˇ
981 fox jumps over
982 the lazy dog."},
983 Mode::HelixNormal,
984 );
985
986 cx.simulate_keystrokes("d");
987
988 cx.assert_state(
989 indoc! {"
990 The quick brownˇfox jumps over
991 the lazy dog."},
992 Mode::HelixNormal,
993 );
994 }
995
996 // #[gpui::test]
997 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
998 // let mut cx = VimTestContext::new(cx, true).await;
999
1000 // cx.set_state(
1001 // indoc! {"
1002 // The quick brown
1003 // fox jumps over
1004 // the lazy dog.ˇ"},
1005 // Mode::HelixNormal,
1006 // );
1007
1008 // cx.simulate_keystrokes("d");
1009
1010 // cx.assert_state(
1011 // indoc! {"
1012 // The quick brown
1013 // fox jumps over
1014 // the lazy dog.ˇ"},
1015 // Mode::HelixNormal,
1016 // );
1017 // }
1018
1019 #[gpui::test]
1020 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1021 let mut cx = VimTestContext::new(cx, true).await;
1022 cx.enable_helix();
1023
1024 cx.set_state(
1025 indoc! {"
1026 The quˇick brown
1027 fox jumps over
1028 the lazy dog."},
1029 Mode::HelixNormal,
1030 );
1031
1032 cx.simulate_keystrokes("f z");
1033
1034 cx.assert_state(
1035 indoc! {"
1036 The qu«ick brown
1037 fox jumps over
1038 the lazˇ»y dog."},
1039 Mode::HelixNormal,
1040 );
1041
1042 cx.simulate_keystrokes("F e F e");
1043
1044 cx.assert_state(
1045 indoc! {"
1046 The quick brown
1047 fox jumps ov«ˇer
1048 the» lazy dog."},
1049 Mode::HelixNormal,
1050 );
1051
1052 cx.simulate_keystrokes("e 2 F e");
1053
1054 cx.assert_state(
1055 indoc! {"
1056 Th«ˇe quick brown
1057 fox jumps over»
1058 the lazy dog."},
1059 Mode::HelixNormal,
1060 );
1061
1062 cx.simulate_keystrokes("t r t r");
1063
1064 cx.assert_state(
1065 indoc! {"
1066 The quick «brown
1067 fox jumps oveˇ»r
1068 the lazy dog."},
1069 Mode::HelixNormal,
1070 );
1071 }
1072
1073 #[gpui::test]
1074 async fn test_newline_char(cx: &mut gpui::TestAppContext) {
1075 let mut cx = VimTestContext::new(cx, true).await;
1076 cx.enable_helix();
1077
1078 cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
1079
1080 cx.simulate_keystroke("w");
1081
1082 cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
1083
1084 cx.set_state("aa«\nˇ»", Mode::HelixNormal);
1085
1086 cx.simulate_keystroke("b");
1087
1088 cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
1089 }
1090
1091 #[gpui::test]
1092 async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
1093 let mut cx = VimTestContext::new(cx, true).await;
1094 cx.enable_helix();
1095 cx.set_state(
1096 indoc! {"
1097 «The ˇ»quick brown
1098 fox jumps over
1099 the lazy dog."},
1100 Mode::HelixNormal,
1101 );
1102
1103 cx.simulate_keystrokes("i");
1104
1105 cx.assert_state(
1106 indoc! {"
1107 ˇThe quick brown
1108 fox jumps over
1109 the lazy dog."},
1110 Mode::Insert,
1111 );
1112 }
1113
1114 #[gpui::test]
1115 async fn test_append(cx: &mut gpui::TestAppContext) {
1116 let mut cx = VimTestContext::new(cx, true).await;
1117 cx.enable_helix();
1118 // test from the end of the selection
1119 cx.set_state(
1120 indoc! {"
1121 «Theˇ» quick brown
1122 fox jumps over
1123 the lazy dog."},
1124 Mode::HelixNormal,
1125 );
1126
1127 cx.simulate_keystrokes("a");
1128
1129 cx.assert_state(
1130 indoc! {"
1131 Theˇ quick brown
1132 fox jumps over
1133 the lazy dog."},
1134 Mode::Insert,
1135 );
1136
1137 // test from the beginning of the selection
1138 cx.set_state(
1139 indoc! {"
1140 «ˇThe» quick brown
1141 fox jumps over
1142 the lazy dog."},
1143 Mode::HelixNormal,
1144 );
1145
1146 cx.simulate_keystrokes("a");
1147
1148 cx.assert_state(
1149 indoc! {"
1150 Theˇ quick brown
1151 fox jumps over
1152 the lazy dog."},
1153 Mode::Insert,
1154 );
1155 }
1156
1157 #[gpui::test]
1158 async fn test_replace(cx: &mut gpui::TestAppContext) {
1159 let mut cx = VimTestContext::new(cx, true).await;
1160 cx.enable_helix();
1161
1162 // No selection (single character)
1163 cx.set_state("ˇaa", Mode::HelixNormal);
1164
1165 cx.simulate_keystrokes("r x");
1166
1167 cx.assert_state("ˇxa", Mode::HelixNormal);
1168
1169 // Cursor at the beginning
1170 cx.set_state("«ˇaa»", Mode::HelixNormal);
1171
1172 cx.simulate_keystrokes("r x");
1173
1174 cx.assert_state("«ˇxx»", Mode::HelixNormal);
1175
1176 // Cursor at the end
1177 cx.set_state("«aaˇ»", Mode::HelixNormal);
1178
1179 cx.simulate_keystrokes("r x");
1180
1181 cx.assert_state("«xxˇ»", Mode::HelixNormal);
1182 }
1183
1184 #[gpui::test]
1185 async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
1186 let mut cx = VimTestContext::new(cx, true).await;
1187 cx.enable_helix();
1188
1189 // Test yanking current character with no selection
1190 cx.set_state("hello ˇworld", Mode::HelixNormal);
1191 cx.simulate_keystrokes("y");
1192
1193 // Test cursor remains at the same position after yanking single character
1194 cx.assert_state("hello ˇworld", Mode::HelixNormal);
1195 cx.shared_clipboard().assert_eq("w");
1196
1197 // Move cursor and yank another character
1198 cx.simulate_keystrokes("l");
1199 cx.simulate_keystrokes("y");
1200 cx.shared_clipboard().assert_eq("o");
1201
1202 // Test yanking with existing selection
1203 cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
1204 cx.simulate_keystrokes("y");
1205 cx.shared_clipboard().assert_eq("worl");
1206 cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
1207
1208 // Test yanking in select mode character by character
1209 cx.set_state("hello ˇworld", Mode::HelixNormal);
1210 cx.simulate_keystroke("v");
1211 cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
1212 cx.simulate_keystroke("y");
1213 cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
1214 cx.shared_clipboard().assert_eq("w");
1215 }
1216
1217 #[gpui::test]
1218 async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
1219 let mut cx = VimTestContext::new(cx, true).await;
1220 cx.enable_helix();
1221
1222 // First copy some text to clipboard
1223 cx.set_state("«hello worldˇ»", Mode::HelixNormal);
1224 cx.simulate_keystrokes("y");
1225
1226 // Test paste with shift-r on single cursor
1227 cx.set_state("foo ˇbar", Mode::HelixNormal);
1228 cx.simulate_keystrokes("shift-r");
1229
1230 cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
1231
1232 // Test paste with shift-r on selection
1233 cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
1234 cx.simulate_keystrokes("shift-r");
1235
1236 cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
1237 }
1238
1239 #[gpui::test]
1240 async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
1241 let mut cx = VimTestContext::new(cx, true).await;
1242
1243 assert_eq!(cx.mode(), Mode::Normal);
1244 cx.enable_helix();
1245
1246 cx.simulate_keystrokes("v");
1247 assert_eq!(cx.mode(), Mode::HelixSelect);
1248 cx.simulate_keystrokes("escape");
1249 assert_eq!(cx.mode(), Mode::HelixNormal);
1250 }
1251
1252 #[gpui::test]
1253 async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
1254 let mut cx = VimTestContext::new(cx, true).await;
1255 cx.enable_helix();
1256
1257 // Make a modification at a specific location
1258 cx.set_state("ˇhello", Mode::HelixNormal);
1259 assert_eq!(cx.mode(), Mode::HelixNormal);
1260 cx.simulate_keystrokes("i");
1261 assert_eq!(cx.mode(), Mode::Insert);
1262 cx.simulate_keystrokes("escape");
1263 assert_eq!(cx.mode(), Mode::HelixNormal);
1264 }
1265
1266 #[gpui::test]
1267 async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
1268 let mut cx = VimTestContext::new(cx, true).await;
1269 cx.enable_helix();
1270
1271 // Make a modification at a specific location
1272 cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1273 cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1274 cx.simulate_keystrokes("i");
1275 cx.simulate_keystrokes("escape");
1276 cx.simulate_keystrokes("i");
1277 cx.simulate_keystrokes("m o d i f i e d space");
1278 cx.simulate_keystrokes("escape");
1279
1280 // TODO: this fails, because state is no longer helix
1281 cx.assert_state(
1282 "line one\nline modified ˇtwo\nline three",
1283 Mode::HelixNormal,
1284 );
1285
1286 // Move cursor away from the modification
1287 cx.simulate_keystrokes("up");
1288
1289 // Use "g ." to go back to last modification
1290 cx.simulate_keystrokes("g .");
1291
1292 // Verify we're back at the modification location and still in HelixNormal mode
1293 cx.assert_state(
1294 "line one\nline modifiedˇ two\nline three",
1295 Mode::HelixNormal,
1296 );
1297 }
1298
1299 #[gpui::test]
1300 async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
1301 let mut cx = VimTestContext::new(cx, true).await;
1302 cx.set_state(
1303 "line one\nline ˇtwo\nline three\nline four",
1304 Mode::HelixNormal,
1305 );
1306 cx.simulate_keystrokes("2 x");
1307 cx.assert_state(
1308 "line one\n«line two\nline three\nˇ»line four",
1309 Mode::HelixNormal,
1310 );
1311
1312 // Test extending existing line selection
1313 cx.set_state(
1314 indoc! {"
1315 li«ˇne one
1316 li»ne two
1317 line three
1318 line four"},
1319 Mode::HelixNormal,
1320 );
1321 cx.simulate_keystrokes("x");
1322 cx.assert_state(
1323 indoc! {"
1324 «line one
1325 line two
1326 ˇ»line three
1327 line four"},
1328 Mode::HelixNormal,
1329 );
1330
1331 // Pressing x in empty line, select next line (because helix considers cursor a selection)
1332 cx.set_state(
1333 indoc! {"
1334 line one
1335 ˇ
1336 line three
1337 line four"},
1338 Mode::HelixNormal,
1339 );
1340 cx.simulate_keystrokes("x");
1341 cx.assert_state(
1342 indoc! {"
1343 line one
1344 «
1345 line three
1346 ˇ»line four"},
1347 Mode::HelixNormal,
1348 );
1349
1350 // Empty line with count selects extra + count lines
1351 cx.set_state(
1352 indoc! {"
1353 line one
1354 ˇ
1355 line three
1356 line four
1357 line five"},
1358 Mode::HelixNormal,
1359 );
1360 cx.simulate_keystrokes("2 x");
1361 cx.assert_state(
1362 indoc! {"
1363 line one
1364 «
1365 line three
1366 line four
1367 ˇ»line five"},
1368 Mode::HelixNormal,
1369 );
1370
1371 // Compare empty vs non-empty line behavior
1372 cx.set_state(
1373 indoc! {"
1374 ˇnon-empty line
1375 line two
1376 line three"},
1377 Mode::HelixNormal,
1378 );
1379 cx.simulate_keystrokes("x");
1380 cx.assert_state(
1381 indoc! {"
1382 «non-empty line
1383 ˇ»line two
1384 line three"},
1385 Mode::HelixNormal,
1386 );
1387
1388 // Same test but with empty line - should select one extra
1389 cx.set_state(
1390 indoc! {"
1391 ˇ
1392 line two
1393 line three"},
1394 Mode::HelixNormal,
1395 );
1396 cx.simulate_keystrokes("x");
1397 cx.assert_state(
1398 indoc! {"
1399 «
1400 line two
1401 ˇ»line three"},
1402 Mode::HelixNormal,
1403 );
1404
1405 // Test selecting multiple lines with count
1406 cx.set_state(
1407 indoc! {"
1408 ˇline one
1409 line two
1410 line threeˇ
1411 line four
1412 line five"},
1413 Mode::HelixNormal,
1414 );
1415 cx.simulate_keystrokes("x");
1416 cx.assert_state(
1417 indoc! {"
1418 «line one
1419 ˇ»line two
1420 «line three
1421 ˇ»line four
1422 line five"},
1423 Mode::HelixNormal,
1424 );
1425 cx.simulate_keystrokes("x");
1426 // Adjacent line selections stay separate (not merged)
1427 cx.assert_state(
1428 indoc! {"
1429 «line one
1430 line two
1431 ˇ»«line three
1432 line four
1433 ˇ»line five"},
1434 Mode::HelixNormal,
1435 );
1436 }
1437
1438 #[gpui::test]
1439 async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
1440 let mut cx = VimTestContext::new(cx, true).await;
1441
1442 assert_eq!(cx.mode(), Mode::Normal);
1443 cx.enable_helix();
1444
1445 cx.set_state("ˇhello", Mode::HelixNormal);
1446 cx.simulate_keystrokes("l v l l");
1447 cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
1448 }
1449
1450 #[gpui::test]
1451 async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
1452 let mut cx = VimTestContext::new(cx, true).await;
1453
1454 assert_eq!(cx.mode(), Mode::Normal);
1455 cx.enable_helix();
1456
1457 // Start with multiple cursors (no selections)
1458 cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
1459
1460 // Enter select mode and move right twice
1461 cx.simulate_keystrokes("v l l");
1462
1463 // Each cursor should independently create and extend its own selection
1464 cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
1465 }
1466
1467 #[gpui::test]
1468 async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
1469 let mut cx = VimTestContext::new(cx, true).await;
1470
1471 cx.set_state("ˇone two", Mode::Normal);
1472 cx.simulate_keystrokes("v w");
1473 cx.assert_state("«one tˇ»wo", Mode::Visual);
1474
1475 // In Vim, this selects "t". In helix selections stops just before "t"
1476
1477 cx.enable_helix();
1478 cx.set_state("ˇone two", Mode::HelixNormal);
1479 cx.simulate_keystrokes("v w");
1480 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
1481 }
1482
1483 #[gpui::test]
1484 async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) {
1485 let mut cx = VimTestContext::new(cx, true).await;
1486
1487 cx.set_state("ˇone two", Mode::Normal);
1488 cx.simulate_keystrokes("v w");
1489 cx.assert_state("«one tˇ»wo", Mode::Visual);
1490 cx.simulate_keystrokes("escape");
1491 cx.assert_state("one ˇtwo", Mode::Normal);
1492
1493 cx.enable_helix();
1494 cx.set_state("ˇone two", Mode::HelixNormal);
1495 cx.simulate_keystrokes("v w");
1496 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
1497 cx.simulate_keystrokes("escape");
1498 cx.assert_state("«one ˇ»two", Mode::HelixNormal);
1499 }
1500
1501 #[gpui::test]
1502 async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) {
1503 let mut cx = VimTestContext::new(cx, true).await;
1504 cx.enable_helix();
1505
1506 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
1507 cx.simulate_keystrokes("w");
1508 cx.assert_state("«one ˇ»two three", Mode::HelixSelect);
1509
1510 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
1511 cx.simulate_keystrokes("e");
1512 cx.assert_state("«oneˇ» two three", Mode::HelixSelect);
1513 }
1514
1515 #[gpui::test]
1516 async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) {
1517 let mut cx = VimTestContext::new(cx, true).await;
1518 cx.enable_helix();
1519
1520 cx.set_state("ˇone two three", Mode::HelixNormal);
1521 cx.simulate_keystrokes("l l v h h h");
1522 cx.assert_state("«ˇone» two three", Mode::HelixSelect);
1523 }
1524
1525 #[gpui::test]
1526 async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
1527 let mut cx = VimTestContext::new(cx, true).await;
1528 cx.enable_helix();
1529
1530 cx.set_state("ˇone two one", Mode::HelixNormal);
1531 cx.simulate_keystrokes("x");
1532 cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
1533 cx.simulate_keystrokes("s o n e");
1534 cx.run_until_parked();
1535 cx.simulate_keystrokes("enter");
1536 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1537
1538 cx.simulate_keystrokes("x");
1539 cx.simulate_keystrokes("s");
1540 cx.run_until_parked();
1541 cx.simulate_keystrokes("enter");
1542 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1543
1544 // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
1545 // cx.set_state("ˇstuff one two one", Mode::HelixNormal);
1546 // cx.simulate_keystrokes("s o n e enter");
1547 // cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
1548 }
1549
1550 #[gpui::test]
1551 async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
1552 let mut cx = VimTestContext::new(cx, true).await;
1553
1554 cx.set_state("ˇhello two one two one two one", Mode::Visual);
1555 cx.simulate_keystrokes("/ o n e");
1556 cx.simulate_keystrokes("enter");
1557 cx.simulate_keystrokes("n n");
1558 cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
1559
1560 cx.set_state("ˇhello two one two one two one", Mode::Normal);
1561 cx.simulate_keystrokes("/ o n e");
1562 cx.simulate_keystrokes("enter");
1563 cx.simulate_keystrokes("n n");
1564 cx.assert_state("hello two one two one two ˇone", Mode::Normal);
1565
1566 cx.set_state("ˇhello two one two one two one", Mode::Normal);
1567 cx.simulate_keystrokes("/ o n e");
1568 cx.simulate_keystrokes("enter");
1569 cx.simulate_keystrokes("n g n g n");
1570 cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
1571
1572 cx.enable_helix();
1573
1574 cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
1575 cx.simulate_keystrokes("/ o n e");
1576 cx.simulate_keystrokes("enter");
1577 cx.simulate_keystrokes("n n");
1578 cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
1579
1580 cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
1581 cx.simulate_keystrokes("/ o n e");
1582 cx.simulate_keystrokes("enter");
1583 cx.simulate_keystrokes("n n");
1584 cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
1585 }
1586
1587 #[gpui::test]
1588 async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
1589 let mut cx = VimTestContext::new(cx, true).await;
1590
1591 cx.set_state("ˇone two", Mode::HelixNormal);
1592 cx.simulate_keystrokes("c");
1593 cx.assert_state("ˇne two", Mode::Insert);
1594
1595 cx.set_state("«oneˇ» two", Mode::HelixNormal);
1596 cx.simulate_keystrokes("c");
1597 cx.assert_state("ˇ two", Mode::Insert);
1598
1599 cx.set_state(
1600 indoc! {"
1601 oneˇ two
1602 three
1603 "},
1604 Mode::HelixNormal,
1605 );
1606 cx.simulate_keystrokes("x c");
1607 cx.assert_state(
1608 indoc! {"
1609 ˇ
1610 three
1611 "},
1612 Mode::Insert,
1613 );
1614
1615 cx.set_state(
1616 indoc! {"
1617 one twoˇ
1618 three
1619 "},
1620 Mode::HelixNormal,
1621 );
1622 cx.simulate_keystrokes("c");
1623 cx.assert_state(
1624 indoc! {"
1625 one twoˇthree
1626 "},
1627 Mode::Insert,
1628 );
1629
1630 // Helix doesn't set the cursor to the first non-blank one when
1631 // replacing lines: it uses language-dependent indent queries instead.
1632 cx.set_state(
1633 indoc! {"
1634 one two
1635 « indented
1636 three not indentedˇ»
1637 "},
1638 Mode::HelixNormal,
1639 );
1640 cx.simulate_keystrokes("c");
1641 cx.set_state(
1642 indoc! {"
1643 one two
1644 ˇ
1645 "},
1646 Mode::Insert,
1647 );
1648 }
1649
1650 #[gpui::test]
1651 async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
1652 let mut cx = VimTestContext::new(cx, true).await;
1653 cx.enable_helix();
1654
1655 // Test g l moves to last character, not after it
1656 cx.set_state("hello ˇworld!", Mode::HelixNormal);
1657 cx.simulate_keystrokes("g l");
1658 cx.assert_state("hello worldˇ!", Mode::HelixNormal);
1659
1660 // Test with Chinese characters, test if work with UTF-8?
1661 cx.set_state("ˇ你好世界", Mode::HelixNormal);
1662 cx.simulate_keystrokes("g l");
1663 cx.assert_state("你好世ˇ界", Mode::HelixNormal);
1664
1665 // Test with end of line
1666 cx.set_state("endˇ", Mode::HelixNormal);
1667 cx.simulate_keystrokes("g l");
1668 cx.assert_state("enˇd", Mode::HelixNormal);
1669
1670 // Test with empty line
1671 cx.set_state(
1672 indoc! {"
1673 hello
1674 ˇ
1675 world"},
1676 Mode::HelixNormal,
1677 );
1678 cx.simulate_keystrokes("g l");
1679 cx.assert_state(
1680 indoc! {"
1681 hello
1682 ˇ
1683 world"},
1684 Mode::HelixNormal,
1685 );
1686
1687 // Test with multiple lines
1688 cx.set_state(
1689 indoc! {"
1690 ˇfirst line
1691 second line
1692 third line"},
1693 Mode::HelixNormal,
1694 );
1695 cx.simulate_keystrokes("g l");
1696 cx.assert_state(
1697 indoc! {"
1698 first linˇe
1699 second line
1700 third line"},
1701 Mode::HelixNormal,
1702 );
1703 }
1704
1705 #[gpui::test]
1706 async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) {
1707 VimTestContext::init(cx);
1708
1709 let fs = FakeFs::new(cx.background_executor.clone());
1710 fs.insert_tree(
1711 path!("/dir"),
1712 json!({
1713 "file_a.rs": "// File A.",
1714 "file_b.rs": "// File B.",
1715 }),
1716 )
1717 .await;
1718
1719 let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1720 let workspace =
1721 cx.add_window(|window, cx| workspace::Workspace::test_new(project.clone(), window, cx));
1722
1723 cx.update(|cx| {
1724 VimTestContext::init_keybindings(true, cx);
1725 SettingsStore::update_global(cx, |store, cx| {
1726 store.update_user_settings(cx, |store| store.helix_mode = Some(true));
1727 })
1728 });
1729
1730 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1731
1732 workspace
1733 .update(cx, |workspace, window, cx| {
1734 ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx)
1735 })
1736 .unwrap();
1737
1738 let search_view = workspace
1739 .update(cx, |workspace, _, cx| {
1740 workspace
1741 .active_pane()
1742 .read(cx)
1743 .items()
1744 .find_map(|item| item.downcast::<ProjectSearchView>())
1745 .expect("Project search view should be active")
1746 })
1747 .unwrap();
1748
1749 project_search::perform_project_search(&search_view, "File A", cx);
1750
1751 search_view.update(cx, |search_view, cx| {
1752 let vim_mode = search_view
1753 .results_editor()
1754 .read(cx)
1755 .addon::<VimAddon>()
1756 .map(|addon| addon.entity.read(cx).mode);
1757
1758 assert_eq!(vim_mode, Some(Mode::HelixNormal));
1759 });
1760 }
1761}