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