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