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