1mod boundary;
2mod duplicate;
3mod object;
4mod paste;
5mod select;
6mod surround;
7
8use editor::display_map::DisplaySnapshot;
9use editor::{
10 DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset,
11 SelectionEffects, ToOffset, ToPoint, movement,
12};
13use gpui::actions;
14use gpui::{Context, Window};
15use language::{CharClassifier, CharKind, Point};
16use search::{BufferSearchBar, SearchOptions};
17use settings::Settings;
18use text::{Bias, SelectionGoal};
19use workspace::searchable::FilteredSearchRange;
20use workspace::searchable::{self, Direction};
21
22use crate::motion::{self, MotionKind};
23use crate::state::{Operator, SearchState};
24use crate::{
25 PushHelixSurroundAdd, PushHelixSurroundDelete, PushHelixSurroundReplace, Vim,
26 motion::{Motion, right},
27 state::Mode,
28};
29
30actions!(
31 vim,
32 [
33 /// Yanks the current selection or character if no selection.
34 HelixYank,
35 /// Inserts at the beginning of the selection.
36 HelixInsert,
37 /// Appends at the end of the selection.
38 HelixAppend,
39 /// Inserts at the end of the current Helix cursor line.
40 HelixInsertEndOfLine,
41 /// Goes to the location of the last modification.
42 HelixGotoLastModification,
43 /// Select entire line or multiple lines, extending downwards.
44 HelixSelectLine,
45 /// Select all matches of a given pattern within the current selection.
46 HelixSelectRegex,
47 /// Removes all but the one selection that was created last.
48 /// `Newest` can eventually be `Primary`.
49 HelixKeepNewestSelection,
50 /// Copies all selections below.
51 HelixDuplicateBelow,
52 /// Copies all selections above.
53 HelixDuplicateAbove,
54 /// Delete the selection and enter edit mode.
55 HelixSubstitute,
56 /// Delete the selection and enter edit mode, without yanking the selection.
57 HelixSubstituteNoYank,
58 /// Select the next match for the current search query.
59 HelixSelectNext,
60 /// Select the previous match for the current search query.
61 HelixSelectPrevious,
62 ]
63);
64
65pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
66 Vim::action(editor, cx, Vim::helix_select_lines);
67 Vim::action(editor, cx, Vim::helix_insert);
68 Vim::action(editor, cx, Vim::helix_append);
69 Vim::action(editor, cx, Vim::helix_insert_end_of_line);
70 Vim::action(editor, cx, Vim::helix_yank);
71 Vim::action(editor, cx, Vim::helix_goto_last_modification);
72 Vim::action(editor, cx, Vim::helix_paste);
73 Vim::action(editor, cx, Vim::helix_select_regex);
74 Vim::action(editor, cx, Vim::helix_keep_newest_selection);
75 Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| {
76 let times = Vim::take_count(cx);
77 vim.helix_duplicate_selections_below(times, window, cx);
78 });
79 Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| {
80 let times = Vim::take_count(cx);
81 vim.helix_duplicate_selections_above(times, window, cx);
82 });
83 Vim::action(editor, cx, Vim::helix_substitute);
84 Vim::action(editor, cx, Vim::helix_substitute_no_yank);
85 Vim::action(editor, cx, Vim::helix_select_next);
86 Vim::action(editor, cx, Vim::helix_select_previous);
87 Vim::action(editor, cx, |vim, _: &PushHelixSurroundAdd, window, cx| {
88 vim.clear_operator(window, cx);
89 vim.push_operator(Operator::HelixSurroundAdd, window, cx);
90 });
91 Vim::action(
92 editor,
93 cx,
94 |vim, _: &PushHelixSurroundReplace, window, cx| {
95 vim.clear_operator(window, cx);
96 vim.push_operator(
97 Operator::HelixSurroundReplace {
98 replaced_char: None,
99 },
100 window,
101 cx,
102 );
103 },
104 );
105 Vim::action(
106 editor,
107 cx,
108 |vim, _: &PushHelixSurroundDelete, window, cx| {
109 vim.clear_operator(window, cx);
110 vim.push_operator(Operator::HelixSurroundDelete, window, cx);
111 },
112 );
113}
114
115impl Vim {
116 pub fn helix_normal_motion(
117 &mut self,
118 motion: Motion,
119 times: Option<usize>,
120 window: &mut Window,
121 cx: &mut Context<Self>,
122 ) {
123 self.helix_move_cursor(motion, times, window, cx);
124 }
125
126 pub fn helix_select_motion(
127 &mut self,
128 motion: Motion,
129 times: Option<usize>,
130 window: &mut Window,
131 cx: &mut Context<Self>,
132 ) {
133 self.update_editor(cx, |_, editor, cx| {
134 let text_layout_details = editor.text_layout_details(window, cx);
135 editor.change_selections(Default::default(), window, cx, |s| {
136 if let Motion::ZedSearchResult { new_selections, .. } = &motion {
137 s.select_anchor_ranges(new_selections.clone());
138 return;
139 };
140
141 s.move_with(&mut |map, selection| {
142 let was_reversed = selection.reversed;
143 let mut current_head = selection.head();
144
145 // our motions assume the current character is after the cursor,
146 // but in (forward) visual mode the current character is just
147 // before the end of the selection.
148
149 // If the file ends with a newline (which is common) we don't do this.
150 // so that if you go to the end of such a file you can use "up" to go
151 // to the previous line and have it work somewhat as expected.
152 if !selection.reversed
153 && !selection.is_empty()
154 && !(selection.end.column() == 0 && selection.end == map.max_point())
155 {
156 current_head = movement::left(map, selection.end)
157 }
158
159 let (new_head, goal) = match motion {
160 // EndOfLine positions after the last character, but in
161 // helix visual mode we want the selection to end ON the
162 // last character. Adjust left here so the subsequent
163 // right-expansion (below) includes the last char without
164 // spilling into the newline.
165 Motion::EndOfLine { .. } => {
166 let (point, goal) = motion
167 .move_point(
168 map,
169 current_head,
170 selection.goal,
171 times,
172 &text_layout_details,
173 )
174 .unwrap_or((current_head, selection.goal));
175 (movement::saturating_left(map, point), goal)
176 }
177 // Going to next word start is special cased
178 // since Vim differs from Helix in that motion
179 // Vim: `w` goes to the first character of a word
180 // Helix: `w` goes to the character before a word
181 Motion::NextWordStart { ignore_punctuation } => {
182 let mut head = movement::right(map, current_head);
183 let classifier =
184 map.buffer_snapshot().char_classifier_at(head.to_point(map));
185 for _ in 0..times.unwrap_or(1) {
186 let (_, new_head) =
187 movement::find_boundary_trail(map, head, &mut |left, right| {
188 Self::is_boundary_right(ignore_punctuation)(
189 left,
190 right,
191 &classifier,
192 )
193 });
194 head = new_head;
195 }
196 head = movement::left(map, head);
197 (head, SelectionGoal::None)
198 }
199 _ => motion
200 .move_point(
201 map,
202 current_head,
203 selection.goal,
204 times,
205 &text_layout_details,
206 )
207 .unwrap_or((current_head, selection.goal)),
208 };
209
210 selection.set_head(new_head, goal);
211
212 // ensure the current character is included in the selection.
213 if !selection.reversed {
214 let next_point = movement::right(map, selection.end);
215
216 if !(next_point.column() == 0 && next_point == map.max_point()) {
217 selection.end = next_point;
218 }
219 }
220
221 // vim always ensures the anchor character stays selected.
222 // if our selection has reversed, we need to move the opposite end
223 // to ensure the anchor is still selected.
224 if was_reversed && !selection.reversed {
225 selection.start = movement::left(map, selection.start);
226 } else if !was_reversed && selection.reversed {
227 selection.end = movement::right(map, selection.end);
228 }
229 })
230 });
231 });
232 }
233
234 /// Updates all selections based on where the cursors are.
235 fn helix_new_selections(
236 &mut self,
237 window: &mut Window,
238 cx: &mut Context<Self>,
239 change: &mut dyn FnMut(
240 // the start of the cursor
241 DisplayPoint,
242 &DisplaySnapshot,
243 ) -> Option<(DisplayPoint, DisplayPoint)>,
244 ) {
245 self.update_editor(cx, |_, editor, cx| {
246 editor.change_selections(Default::default(), window, cx, |s| {
247 s.move_with(&mut |map, selection| {
248 let cursor_start = if selection.reversed || selection.is_empty() {
249 selection.head()
250 } else {
251 movement::left(map, selection.head())
252 };
253 let Some((head, tail)) = change(cursor_start, map) else {
254 return;
255 };
256
257 selection.set_head_tail(head, tail, SelectionGoal::None);
258 });
259 });
260 });
261 }
262
263 fn helix_find_range_forward(
264 &mut self,
265 times: Option<usize>,
266 window: &mut Window,
267 cx: &mut Context<Self>,
268 is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool,
269 ) {
270 let times = times.unwrap_or(1);
271 self.helix_new_selections(window, cx, &mut |cursor, map| {
272 let mut head = movement::right(map, cursor);
273 let mut tail = cursor;
274 let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map));
275 if head == map.max_point() {
276 return None;
277 }
278 for _ in 0..times {
279 let (maybe_next_tail, next_head) =
280 movement::find_boundary_trail(map, head, &mut |left, right| {
281 is_boundary(left, right, &classifier)
282 });
283
284 if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
285 break;
286 }
287
288 head = next_head;
289 if let Some(next_tail) = maybe_next_tail {
290 tail = next_tail;
291 }
292 }
293 Some((head, tail))
294 });
295 }
296
297 fn helix_find_range_backward(
298 &mut self,
299 times: Option<usize>,
300 window: &mut Window,
301 cx: &mut Context<Self>,
302 is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool,
303 ) {
304 let times = times.unwrap_or(1);
305 self.helix_new_selections(window, cx, &mut |cursor, map| {
306 let mut head = cursor;
307 // The original cursor was one character wide,
308 // but the search starts from the left side of it,
309 // so to include that space the selection must end one character to the right.
310 let mut tail = movement::right(map, cursor);
311 let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map));
312 if head == DisplayPoint::zero() {
313 return None;
314 }
315 for _ in 0..times {
316 let (maybe_next_tail, next_head) =
317 movement::find_preceding_boundary_trail(map, head, &mut |left, right| {
318 is_boundary(left, right, &classifier)
319 });
320
321 if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
322 break;
323 }
324
325 head = next_head;
326 if let Some(next_tail) = maybe_next_tail {
327 tail = next_tail;
328 }
329 }
330 Some((head, tail))
331 });
332 }
333
334 pub fn helix_move_and_collapse(
335 &mut self,
336 motion: Motion,
337 times: Option<usize>,
338 window: &mut Window,
339 cx: &mut Context<Self>,
340 ) {
341 self.update_editor(cx, |_, editor, cx| {
342 let text_layout_details = editor.text_layout_details(window, cx);
343 editor.change_selections(Default::default(), window, cx, |s| {
344 s.move_with(&mut |map, selection| {
345 let goal = selection.goal;
346 let cursor = if selection.is_empty() || selection.reversed {
347 selection.head()
348 } else {
349 movement::left(map, selection.head())
350 };
351
352 let (point, goal) = motion
353 .move_point(map, cursor, selection.goal, times, &text_layout_details)
354 .unwrap_or((cursor, goal));
355
356 selection.collapse_to(point, goal)
357 })
358 });
359 });
360 }
361
362 fn is_boundary_right(
363 ignore_punctuation: bool,
364 ) -> impl FnMut(char, char, &CharClassifier) -> bool {
365 move |left, right, classifier| {
366 let left_kind = classifier.kind_with(left, ignore_punctuation);
367 let right_kind = classifier.kind_with(right, ignore_punctuation);
368 let at_newline = (left == '\n') ^ (right == '\n');
369
370 (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline
371 }
372 }
373
374 fn is_boundary_left(
375 ignore_punctuation: bool,
376 ) -> impl FnMut(char, char, &CharClassifier) -> bool {
377 move |left, right, classifier| {
378 let left_kind = classifier.kind_with(left, ignore_punctuation);
379 let right_kind = classifier.kind_with(right, ignore_punctuation);
380 let at_newline = (left == '\n') ^ (right == '\n');
381
382 (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline
383 }
384 }
385
386 /// When `reversed` is true (used with `helix_find_range_backward`), the
387 /// `left` and `right` characters are yielded in reverse text order, so the
388 /// camelCase transition check must be flipped accordingly.
389 fn subword_boundary_start(
390 ignore_punctuation: bool,
391 reversed: bool,
392 ) -> impl FnMut(char, char, &CharClassifier) -> bool {
393 move |left, right, classifier| {
394 let left_kind = classifier.kind_with(left, ignore_punctuation);
395 let right_kind = classifier.kind_with(right, ignore_punctuation);
396 let at_newline = (left == '\n') ^ (right == '\n');
397 let is_separator = |c: char| "_$=".contains(c);
398
399 let is_word = left_kind != right_kind && right_kind != CharKind::Whitespace;
400 let is_subword = (is_separator(left) && !is_separator(right))
401 || if reversed {
402 right.is_lowercase() && left.is_uppercase()
403 } else {
404 left.is_lowercase() && right.is_uppercase()
405 };
406
407 is_word || (is_subword && !right.is_whitespace()) || at_newline
408 }
409 }
410
411 /// When `reversed` is true (used with `helix_find_range_backward`), the
412 /// `left` and `right` characters are yielded in reverse text order, so the
413 /// camelCase transition check must be flipped accordingly.
414 fn subword_boundary_end(
415 ignore_punctuation: bool,
416 reversed: bool,
417 ) -> impl FnMut(char, char, &CharClassifier) -> bool {
418 move |left, right, classifier| {
419 let left_kind = classifier.kind_with(left, ignore_punctuation);
420 let right_kind = classifier.kind_with(right, ignore_punctuation);
421 let at_newline = (left == '\n') ^ (right == '\n');
422 let is_separator = |c: char| "_$=".contains(c);
423
424 let is_word = left_kind != right_kind && left_kind != CharKind::Whitespace;
425 let is_subword = (!is_separator(left) && is_separator(right))
426 || if reversed {
427 right.is_lowercase() && left.is_uppercase()
428 } else {
429 left.is_lowercase() && right.is_uppercase()
430 };
431
432 is_word || (is_subword && !left.is_whitespace()) || at_newline
433 }
434 }
435
436 pub fn helix_move_cursor(
437 &mut self,
438 motion: Motion,
439 times: Option<usize>,
440 window: &mut Window,
441 cx: &mut Context<Self>,
442 ) {
443 match motion {
444 Motion::NextWordStart { ignore_punctuation } => {
445 let mut is_boundary = Self::is_boundary_right(ignore_punctuation);
446 self.helix_find_range_forward(times, window, cx, &mut is_boundary)
447 }
448 Motion::NextWordEnd { ignore_punctuation } => {
449 let mut is_boundary = Self::is_boundary_left(ignore_punctuation);
450 self.helix_find_range_forward(times, window, cx, &mut is_boundary)
451 }
452 Motion::PreviousWordStart { ignore_punctuation } => {
453 let mut is_boundary = Self::is_boundary_left(ignore_punctuation);
454 self.helix_find_range_backward(times, window, cx, &mut is_boundary)
455 }
456 Motion::PreviousWordEnd { ignore_punctuation } => {
457 let mut is_boundary = Self::is_boundary_right(ignore_punctuation);
458 self.helix_find_range_backward(times, window, cx, &mut is_boundary)
459 }
460 // The subword motions implementation is based off of the same
461 // commands present in Helix itself, namely:
462 //
463 // * `move_next_sub_word_start`
464 // * `move_next_sub_word_end`
465 // * `move_prev_sub_word_start`
466 // * `move_prev_sub_word_end`
467 Motion::NextSubwordStart { ignore_punctuation } => {
468 let mut is_boundary = Self::subword_boundary_start(ignore_punctuation, false);
469 self.helix_find_range_forward(times, window, cx, &mut is_boundary)
470 }
471 Motion::NextSubwordEnd { ignore_punctuation } => {
472 let mut is_boundary = Self::subword_boundary_end(ignore_punctuation, false);
473 self.helix_find_range_forward(times, window, cx, &mut is_boundary)
474 }
475 Motion::PreviousSubwordStart { ignore_punctuation } => {
476 let mut is_boundary = Self::subword_boundary_end(ignore_punctuation, true);
477 self.helix_find_range_backward(times, window, cx, &mut is_boundary)
478 }
479 Motion::PreviousSubwordEnd { ignore_punctuation } => {
480 let mut is_boundary = Self::subword_boundary_start(ignore_punctuation, true);
481 self.helix_find_range_backward(times, window, cx, &mut is_boundary)
482 }
483 Motion::EndOfLine { .. } => {
484 // In Helix mode, EndOfLine should position cursor ON the last character,
485 // not after it. We therefore need special handling for it.
486 self.update_editor(cx, |_, editor, cx| {
487 let text_layout_details = editor.text_layout_details(window, cx);
488 editor.change_selections(Default::default(), window, cx, |s| {
489 s.move_with(&mut |map, selection| {
490 let goal = selection.goal;
491 let cursor = if selection.is_empty() || selection.reversed {
492 selection.head()
493 } else {
494 movement::left(map, selection.head())
495 };
496
497 let (point, _goal) = motion
498 .move_point(map, cursor, goal, times, &text_layout_details)
499 .unwrap_or((cursor, goal));
500
501 // Move left by one character to position on the last character
502 let adjusted_point = movement::saturating_left(map, point);
503 selection.collapse_to(adjusted_point, SelectionGoal::None)
504 })
505 });
506 });
507 }
508 Motion::FindForward {
509 before,
510 char,
511 mode,
512 smartcase,
513 } => {
514 self.helix_new_selections(window, cx, &mut |cursor, map| {
515 let start = cursor;
516 let mut last_boundary = start;
517 for _ in 0..times.unwrap_or(1) {
518 last_boundary = movement::find_boundary(
519 map,
520 movement::right(map, last_boundary),
521 mode,
522 &mut |left, right| {
523 let current_char = if before { right } else { left };
524 motion::is_character_match(char, current_char, smartcase)
525 },
526 );
527 }
528 Some((last_boundary, start))
529 });
530 }
531 Motion::FindBackward {
532 after,
533 char,
534 mode,
535 smartcase,
536 } => {
537 self.helix_new_selections(window, cx, &mut |cursor, map| {
538 let start = cursor;
539 let mut last_boundary = start;
540 for _ in 0..times.unwrap_or(1) {
541 last_boundary = movement::find_preceding_boundary_display_point(
542 map,
543 last_boundary,
544 mode,
545 &mut |left, right| {
546 let current_char = if after { left } else { right };
547 motion::is_character_match(char, current_char, smartcase)
548 },
549 );
550 }
551 // The original cursor was one character wide,
552 // but the search started from the left side of it,
553 // so to include that space the selection must end one character to the right.
554 Some((last_boundary, movement::right(map, start)))
555 });
556 }
557 _ => self.helix_move_and_collapse(motion, times, window, cx),
558 }
559 }
560
561 pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
562 self.update_editor(cx, |vim, editor, cx| {
563 let has_selection = editor
564 .selections
565 .all_adjusted(&editor.display_snapshot(cx))
566 .iter()
567 .any(|selection| !selection.is_empty());
568
569 if !has_selection {
570 // If no selection, expand to current character (like 'v' does)
571 editor.change_selections(Default::default(), window, cx, |s| {
572 s.move_with(&mut |map, selection| {
573 let head = selection.head();
574 let new_head = movement::saturating_right(map, head);
575 selection.set_tail(head, SelectionGoal::None);
576 selection.set_head(new_head, SelectionGoal::None);
577 });
578 });
579 vim.yank_selections_content(
580 editor,
581 crate::motion::MotionKind::Exclusive,
582 window,
583 cx,
584 );
585 editor.change_selections(Default::default(), window, cx, |s| {
586 s.move_with(&mut |_map, selection| {
587 selection.collapse_to(selection.start, SelectionGoal::None);
588 });
589 });
590 } else {
591 // Yank the selection(s)
592 vim.yank_selections_content(
593 editor,
594 crate::motion::MotionKind::Exclusive,
595 window,
596 cx,
597 );
598 }
599 });
600
601 // Drop back to normal mode after yanking
602 self.switch_mode(Mode::HelixNormal, true, window, cx);
603 }
604
605 fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
606 self.start_recording(cx);
607 self.update_editor(cx, |_, editor, cx| {
608 editor.change_selections(Default::default(), window, cx, |s| {
609 s.move_with(&mut |_map, selection| {
610 // In helix normal mode, move cursor to start of selection and collapse
611 if !selection.is_empty() {
612 selection.collapse_to(selection.start, SelectionGoal::None);
613 }
614 });
615 });
616 });
617 self.switch_mode(Mode::Insert, false, window, cx);
618 }
619
620 fn helix_select_regex(
621 &mut self,
622 _: &HelixSelectRegex,
623 window: &mut Window,
624 cx: &mut Context<Self>,
625 ) {
626 Vim::take_forced_motion(cx);
627 let Some(pane) = self.pane(window, cx) else {
628 return;
629 };
630 let prior_selections = self.editor_selections(window, cx);
631 pane.update(cx, |pane, cx| {
632 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
633 search_bar.update(cx, |search_bar, cx| {
634 if !search_bar.show(window, cx) {
635 return;
636 }
637
638 search_bar.select_query(window, cx);
639 cx.focus_self(window);
640
641 search_bar.set_replacement(None, cx);
642 let mut options = SearchOptions::NONE;
643 options |= SearchOptions::REGEX;
644 if EditorSettings::get_global(cx).search.case_sensitive {
645 options |= SearchOptions::CASE_SENSITIVE;
646 }
647 search_bar.set_search_options(options, cx);
648 if let Some(search) = search_bar.set_search_within_selection(
649 Some(FilteredSearchRange::Selection),
650 window,
651 cx,
652 ) {
653 cx.spawn_in(window, async move |search_bar, cx| {
654 if search.await.is_ok() {
655 search_bar.update_in(cx, |search_bar, window, cx| {
656 search_bar.activate_current_match(window, cx)
657 })
658 } else {
659 Ok(())
660 }
661 })
662 .detach_and_log_err(cx);
663 }
664 self.search = SearchState {
665 direction: searchable::Direction::Next,
666 count: 1,
667 cmd_f_search: false,
668 prior_selections,
669 prior_operator: self.operator_stack.last().cloned(),
670 prior_mode: self.mode,
671 helix_select: true,
672 _dismiss_subscription: None,
673 }
674 });
675 }
676 });
677 self.start_recording(cx);
678 }
679
680 fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
681 self.start_recording(cx);
682 self.switch_mode(Mode::Insert, false, window, cx);
683 self.update_editor(cx, |_, editor, cx| {
684 editor.change_selections(Default::default(), window, cx, |s| {
685 s.move_with(&mut |map, selection| {
686 let point = if selection.is_empty() {
687 right(map, selection.head(), 1)
688 } else {
689 selection.end
690 };
691 selection.collapse_to(point, SelectionGoal::None);
692 });
693 });
694 });
695 }
696
697 /// Helix-specific implementation of `shift-a` that accounts for Helix's
698 /// selection model, where selecting a line with `x` creates a selection
699 /// from column 0 of the current row to column 0 of the next row, so the
700 /// default [`vim::normal::InsertEndOfLine`] would move the cursor to the
701 /// end of the wrong line.
702 fn helix_insert_end_of_line(
703 &mut self,
704 _: &HelixInsertEndOfLine,
705 window: &mut Window,
706 cx: &mut Context<Self>,
707 ) {
708 self.start_recording(cx);
709 self.switch_mode(Mode::Insert, false, window, cx);
710 self.update_editor(cx, |_, editor, cx| {
711 editor.change_selections(Default::default(), window, cx, |s| {
712 s.move_with(&mut |map, selection| {
713 let cursor = if !selection.is_empty() && !selection.reversed {
714 movement::left(map, selection.head())
715 } else {
716 selection.head()
717 };
718 selection
719 .collapse_to(motion::next_line_end(map, cursor, 1), SelectionGoal::None);
720 });
721 });
722 });
723 }
724
725 pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
726 self.update_editor(cx, |_, editor, cx| {
727 editor.transact(window, cx, |editor, window, cx| {
728 let display_map = editor.display_snapshot(cx);
729 let selections = editor.selections.all_display(&display_map);
730
731 let mut edits = Vec::new();
732 let mut selection_info = Vec::new();
733 for selection in &selections {
734 let mut range = selection.range();
735 let was_empty = range.is_empty();
736 let was_reversed = selection.reversed;
737
738 if was_empty {
739 range.end = movement::saturating_right(&display_map, range.start);
740 }
741
742 let byte_range = range.start.to_offset(&display_map, Bias::Left)
743 ..range.end.to_offset(&display_map, Bias::Left);
744
745 let snapshot = display_map.buffer_snapshot();
746 let grapheme_count = snapshot.grapheme_count_for_range(&byte_range);
747 let anchor = snapshot.anchor_before(byte_range.start);
748
749 selection_info.push((anchor, grapheme_count, was_empty, was_reversed));
750
751 if !byte_range.is_empty() {
752 let replacement_text = text.repeat(grapheme_count);
753 edits.push((byte_range, replacement_text));
754 }
755 }
756
757 editor.edit(edits, cx);
758
759 // Restore selections based on original info
760 let snapshot = editor.buffer().read(cx).snapshot(cx);
761 let ranges: Vec<_> = selection_info
762 .into_iter()
763 .map(|(start_anchor, grapheme_count, was_empty, was_reversed)| {
764 let start_point = start_anchor.to_point(&snapshot);
765 if was_empty {
766 start_point..start_point
767 } else {
768 let replacement_len = text.len() * grapheme_count;
769 let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
770 let end_point = snapshot.offset_to_point(end_offset);
771 if was_reversed {
772 end_point..start_point
773 } else {
774 start_point..end_point
775 }
776 }
777 })
778 .collect();
779
780 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
781 s.select_ranges(ranges);
782 });
783 });
784 });
785 self.switch_mode(Mode::HelixNormal, true, window, cx);
786 }
787
788 pub fn helix_goto_last_modification(
789 &mut self,
790 _: &HelixGotoLastModification,
791 window: &mut Window,
792 cx: &mut Context<Self>,
793 ) {
794 self.jump(".".into(), false, false, window, cx);
795 }
796
797 pub fn helix_select_lines(
798 &mut self,
799 _: &HelixSelectLine,
800 window: &mut Window,
801 cx: &mut Context<Self>,
802 ) {
803 let count = Vim::take_count(cx).unwrap_or(1);
804 self.update_editor(cx, |_, editor, cx| {
805 editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
806 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
807 let mut selections = editor.selections.all::<Point>(&display_map);
808 let max_point = display_map.buffer_snapshot().max_point();
809 let buffer_snapshot = &display_map.buffer_snapshot();
810
811 for selection in &mut selections {
812 // Start always goes to column 0 of the first selected line
813 let start_row = selection.start.row;
814 let current_end_row = selection.end.row;
815
816 // Check if cursor is on empty line by checking first character
817 let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0));
818 let first_char = buffer_snapshot.chars_at(line_start_offset).next();
819 let extra_line = if first_char == Some('\n') && selection.is_empty() {
820 1
821 } else {
822 0
823 };
824
825 let end_row = current_end_row + count as u32 + extra_line;
826
827 selection.start = Point::new(start_row, 0);
828 selection.end = if end_row > max_point.row {
829 max_point
830 } else {
831 Point::new(end_row, 0)
832 };
833 selection.reversed = false;
834 }
835
836 editor.change_selections(Default::default(), window, cx, |s| {
837 s.select(selections);
838 });
839 });
840 }
841
842 fn helix_keep_newest_selection(
843 &mut self,
844 _: &HelixKeepNewestSelection,
845 window: &mut Window,
846 cx: &mut Context<Self>,
847 ) {
848 self.update_editor(cx, |_, editor, cx| {
849 let newest = editor
850 .selections
851 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
852 editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
853 });
854 }
855
856 fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
857 self.update_editor(cx, |vim, editor, cx| {
858 editor.set_clip_at_line_ends(false, cx);
859 editor.transact(window, cx, |editor, window, cx| {
860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
861 s.move_with(&mut |map, selection| {
862 if selection.start == selection.end {
863 selection.end = movement::right(map, selection.end);
864 }
865
866 // If the selection starts and ends on a newline, we exclude the last one.
867 if !selection.is_empty()
868 && selection.start.column() == 0
869 && selection.end.column() == 0
870 {
871 selection.end = movement::left(map, selection.end);
872 }
873 })
874 });
875 if yank {
876 vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
877 }
878 let selections = editor
879 .selections
880 .all::<Point>(&editor.display_snapshot(cx))
881 .into_iter();
882 let edits = selections.map(|selection| (selection.start..selection.end, ""));
883 editor.edit(edits, cx);
884 });
885 });
886 self.switch_mode(Mode::Insert, true, window, cx);
887 }
888
889 fn helix_substitute(
890 &mut self,
891 _: &HelixSubstitute,
892 window: &mut Window,
893 cx: &mut Context<Self>,
894 ) {
895 self.do_helix_substitute(true, window, cx);
896 }
897
898 fn helix_substitute_no_yank(
899 &mut self,
900 _: &HelixSubstituteNoYank,
901 window: &mut Window,
902 cx: &mut Context<Self>,
903 ) {
904 self.do_helix_substitute(false, window, cx);
905 }
906
907 fn helix_select_next(
908 &mut self,
909 _: &HelixSelectNext,
910 window: &mut Window,
911 cx: &mut Context<Self>,
912 ) {
913 self.do_helix_select(Direction::Next, window, cx);
914 }
915
916 fn helix_select_previous(
917 &mut self,
918 _: &HelixSelectPrevious,
919 window: &mut Window,
920 cx: &mut Context<Self>,
921 ) {
922 self.do_helix_select(Direction::Prev, window, cx);
923 }
924
925 fn do_helix_select(
926 &mut self,
927 direction: searchable::Direction,
928 window: &mut Window,
929 cx: &mut Context<Self>,
930 ) {
931 let Some(pane) = self.pane(window, cx) else {
932 return;
933 };
934 let count = Vim::take_count(cx).unwrap_or(1);
935 Vim::take_forced_motion(cx);
936 let prior_selections = self.editor_selections(window, cx);
937
938 let success = pane.update(cx, |pane, cx| {
939 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
940 return false;
941 };
942 search_bar.update(cx, |search_bar, cx| {
943 if !search_bar.has_active_match() || !search_bar.show(window, cx) {
944 return false;
945 }
946 search_bar.select_match(direction, count, window, cx);
947 true
948 })
949 });
950
951 if !success {
952 return;
953 }
954 if self.mode == Mode::HelixSelect {
955 self.update_editor(cx, |_vim, editor, cx| {
956 let snapshot = editor.snapshot(window, cx);
957 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
958 let buffer = snapshot.buffer_snapshot();
959
960 s.select_ranges(
961 prior_selections
962 .iter()
963 .cloned()
964 .chain(s.all_anchors(&snapshot).iter().map(|s| s.range()))
965 .map(|range| {
966 let start = range.start.to_offset(buffer);
967 let end = range.end.to_offset(buffer);
968 start..end
969 }),
970 );
971 })
972 });
973 }
974 }
975}
976
977#[cfg(test)]
978mod test {
979 use gpui::{KeyBinding, UpdateGlobal, VisualTestContext};
980 use indoc::indoc;
981 use project::FakeFs;
982 use search::{ProjectSearchView, project_search};
983 use serde_json::json;
984 use settings::SettingsStore;
985 use util::path;
986 use workspace::{DeploySearch, MultiWorkspace};
987
988 use crate::{VimAddon, state::Mode, test::VimTestContext};
989
990 #[gpui::test]
991 async fn test_word_motions(cx: &mut gpui::TestAppContext) {
992 let mut cx = VimTestContext::new(cx, true).await;
993 cx.enable_helix();
994 // «
995 // ˇ
996 // »
997 cx.set_state(
998 indoc! {"
999 Th«e quiˇ»ck brown
1000 fox jumps over
1001 the lazy dog."},
1002 Mode::HelixNormal,
1003 );
1004
1005 cx.simulate_keystrokes("w");
1006
1007 cx.assert_state(
1008 indoc! {"
1009 The qu«ick ˇ»brown
1010 fox jumps over
1011 the lazy dog."},
1012 Mode::HelixNormal,
1013 );
1014
1015 cx.simulate_keystrokes("w");
1016
1017 cx.assert_state(
1018 indoc! {"
1019 The quick «brownˇ»
1020 fox jumps over
1021 the lazy dog."},
1022 Mode::HelixNormal,
1023 );
1024
1025 cx.simulate_keystrokes("2 b");
1026
1027 cx.assert_state(
1028 indoc! {"
1029 The «ˇquick »brown
1030 fox jumps over
1031 the lazy dog."},
1032 Mode::HelixNormal,
1033 );
1034
1035 cx.simulate_keystrokes("down e up");
1036
1037 cx.assert_state(
1038 indoc! {"
1039 The quicˇk brown
1040 fox jumps over
1041 the lazy dog."},
1042 Mode::HelixNormal,
1043 );
1044
1045 cx.set_state("aa\n «ˇbb»", Mode::HelixNormal);
1046
1047 cx.simulate_keystroke("b");
1048
1049 cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal);
1050 }
1051
1052 #[gpui::test]
1053 async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
1054 let mut cx = VimTestContext::new(cx, true).await;
1055 cx.enable_helix();
1056
1057 // Setup custom keybindings for subword motions so we can use the bindings
1058 // in `simulate_keystroke`.
1059 cx.update(|_window, cx| {
1060 cx.bind_keys([KeyBinding::new(
1061 "w",
1062 crate::motion::NextSubwordStart {
1063 ignore_punctuation: false,
1064 },
1065 None,
1066 )]);
1067 });
1068
1069 cx.set_state("ˇfoo.bar", Mode::HelixNormal);
1070 cx.simulate_keystroke("w");
1071 cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
1072 cx.simulate_keystroke("w");
1073 cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
1074 cx.simulate_keystroke("w");
1075 cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
1076
1077 cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
1078 cx.simulate_keystroke("w");
1079 cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
1080 cx.simulate_keystroke("w");
1081 cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
1082 cx.simulate_keystroke("w");
1083 cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
1084
1085 cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
1086 cx.simulate_keystroke("w");
1087 cx.assert_state("«foo_ˇ»bar_baz", Mode::HelixNormal);
1088 cx.simulate_keystroke("w");
1089 cx.assert_state("foo_«bar_ˇ»baz", Mode::HelixNormal);
1090
1091 cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
1092 cx.simulate_keystroke("w");
1093 cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
1094 cx.simulate_keystroke("w");
1095 cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
1096
1097 cx.set_state("ˇfoo;bar", Mode::HelixNormal);
1098 cx.simulate_keystroke("w");
1099 cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
1100 cx.simulate_keystroke("w");
1101 cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
1102 cx.simulate_keystroke("w");
1103 cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
1104
1105 cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
1106 cx.simulate_keystroke("w");
1107 cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
1108 cx.simulate_keystroke("w");
1109 cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
1110 cx.simulate_keystroke("w");
1111 cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
1112 cx.simulate_keystroke("w");
1113 cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
1114 cx.simulate_keystroke("w");
1115 cx.assert_state("<?php\n\n$some«Variable ˇ»= 2;", Mode::HelixNormal);
1116 cx.simulate_keystroke("w");
1117 cx.assert_state("<?php\n\n$someVariable «= ˇ»2;", Mode::HelixNormal);
1118 cx.simulate_keystroke("w");
1119 cx.assert_state("<?php\n\n$someVariable = «2ˇ»;", Mode::HelixNormal);
1120 cx.simulate_keystroke("w");
1121 cx.assert_state("<?php\n\n$someVariable = 2«;ˇ»", Mode::HelixNormal);
1122 }
1123
1124 #[gpui::test]
1125 async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
1126 let mut cx = VimTestContext::new(cx, true).await;
1127 cx.enable_helix();
1128
1129 // Setup custom keybindings for subword motions so we can use the bindings
1130 // in `simulate_keystroke`.
1131 cx.update(|_window, cx| {
1132 cx.bind_keys([KeyBinding::new(
1133 "e",
1134 crate::motion::NextSubwordEnd {
1135 ignore_punctuation: false,
1136 },
1137 None,
1138 )]);
1139 });
1140
1141 cx.set_state("ˇfoo.bar", Mode::HelixNormal);
1142 cx.simulate_keystroke("e");
1143 cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
1144 cx.simulate_keystroke("e");
1145 cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
1146 cx.simulate_keystroke("e");
1147 cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
1148
1149 cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
1150 cx.simulate_keystroke("e");
1151 cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
1152 cx.simulate_keystroke("e");
1153 cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
1154 cx.simulate_keystroke("e");
1155 cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
1156
1157 cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
1158 cx.simulate_keystroke("e");
1159 cx.assert_state("«fooˇ»_bar_baz", Mode::HelixNormal);
1160 cx.simulate_keystroke("e");
1161 cx.assert_state("foo«_barˇ»_baz", Mode::HelixNormal);
1162 cx.simulate_keystroke("e");
1163 cx.assert_state("foo_bar«_bazˇ»", Mode::HelixNormal);
1164
1165 cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
1166 cx.simulate_keystroke("e");
1167 cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
1168 cx.simulate_keystroke("e");
1169 cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
1170 cx.simulate_keystroke("e");
1171 cx.assert_state("fooBar«Bazˇ»", Mode::HelixNormal);
1172
1173 cx.set_state("ˇfoo;bar", Mode::HelixNormal);
1174 cx.simulate_keystroke("e");
1175 cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
1176 cx.simulate_keystroke("e");
1177 cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
1178 cx.simulate_keystroke("e");
1179 cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
1180
1181 cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
1182 cx.simulate_keystroke("e");
1183 cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
1184 cx.simulate_keystroke("e");
1185 cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
1186 cx.simulate_keystroke("e");
1187 cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
1188 cx.simulate_keystroke("e");
1189 cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
1190 cx.simulate_keystroke("e");
1191 cx.assert_state("<?php\n\n$some«Variableˇ» = 2;", Mode::HelixNormal);
1192 cx.simulate_keystroke("e");
1193 cx.assert_state("<?php\n\n$someVariable« =ˇ» 2;", Mode::HelixNormal);
1194 cx.simulate_keystroke("e");
1195 cx.assert_state("<?php\n\n$someVariable =« 2ˇ»;", Mode::HelixNormal);
1196 cx.simulate_keystroke("e");
1197 cx.assert_state("<?php\n\n$someVariable = 2«;ˇ»", Mode::HelixNormal);
1198 }
1199
1200 #[gpui::test]
1201 async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
1202 let mut cx = VimTestContext::new(cx, true).await;
1203 cx.enable_helix();
1204
1205 // Setup custom keybindings for subword motions so we can use the bindings
1206 // in `simulate_keystroke`.
1207 cx.update(|_window, cx| {
1208 cx.bind_keys([KeyBinding::new(
1209 "b",
1210 crate::motion::PreviousSubwordStart {
1211 ignore_punctuation: false,
1212 },
1213 None,
1214 )]);
1215 });
1216
1217 cx.set_state("foo.barˇ", Mode::HelixNormal);
1218 cx.simulate_keystroke("b");
1219 cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
1220 cx.simulate_keystroke("b");
1221 cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
1222 cx.simulate_keystroke("b");
1223 cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
1224
1225 cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
1226 cx.simulate_keystroke("b");
1227 cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
1228 cx.simulate_keystroke("b");
1229 cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
1230 cx.simulate_keystroke("b");
1231 cx.assert_state("foo«ˇ(»bar)", Mode::HelixNormal);
1232 cx.simulate_keystroke("b");
1233 cx.assert_state("«ˇfoo»(bar)", Mode::HelixNormal);
1234
1235 cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
1236 cx.simulate_keystroke("b");
1237 cx.assert_state("foo_bar_«ˇbaz»", Mode::HelixNormal);
1238 cx.simulate_keystroke("b");
1239 cx.assert_state("foo_«ˇbar_»baz", Mode::HelixNormal);
1240 cx.simulate_keystroke("b");
1241 cx.assert_state("«ˇfoo_»bar_baz", Mode::HelixNormal);
1242
1243 cx.set_state("foo;barˇ", Mode::HelixNormal);
1244 cx.simulate_keystroke("b");
1245 cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
1246 cx.simulate_keystroke("b");
1247 cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
1248 cx.simulate_keystroke("b");
1249 cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
1250
1251 cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
1252 cx.simulate_keystroke("b");
1253 cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
1254 cx.simulate_keystroke("b");
1255 cx.assert_state("<?php\n\n$someVariable = «ˇ2»;", Mode::HelixNormal);
1256 cx.simulate_keystroke("b");
1257 cx.assert_state("<?php\n\n$someVariable «ˇ= »2;", Mode::HelixNormal);
1258 cx.simulate_keystroke("b");
1259 cx.assert_state("<?php\n\n$some«ˇVariable »= 2;", Mode::HelixNormal);
1260 cx.simulate_keystroke("b");
1261 cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
1262 cx.simulate_keystroke("b");
1263 cx.assert_state("<?php\n\n«ˇ$»someVariable = 2;", Mode::HelixNormal);
1264 cx.simulate_keystroke("b");
1265 cx.assert_state("<?«ˇphp»\n\n$someVariable = 2;", Mode::HelixNormal);
1266 cx.simulate_keystroke("b");
1267 cx.assert_state("«ˇ<?»php\n\n$someVariable = 2;", Mode::HelixNormal);
1268
1269 cx.set_state("fooBarBazˇ", Mode::HelixNormal);
1270 cx.simulate_keystroke("b");
1271 cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
1272 cx.simulate_keystroke("b");
1273 cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
1274 cx.simulate_keystroke("b");
1275 cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
1276 }
1277
1278 #[gpui::test]
1279 async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
1280 let mut cx = VimTestContext::new(cx, true).await;
1281 cx.enable_helix();
1282
1283 // Setup custom keybindings for subword motions so we can use the bindings
1284 // in `simulate_keystrokes`.
1285 cx.update(|_window, cx| {
1286 cx.bind_keys([KeyBinding::new(
1287 "g e",
1288 crate::motion::PreviousSubwordEnd {
1289 ignore_punctuation: false,
1290 },
1291 None,
1292 )]);
1293 });
1294
1295 cx.set_state("foo.barˇ", Mode::HelixNormal);
1296 cx.simulate_keystrokes("g e");
1297 cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
1298 cx.simulate_keystrokes("g e");
1299 cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
1300 cx.simulate_keystrokes("g e");
1301 cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
1302
1303 cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
1304 cx.simulate_keystrokes("g e");
1305 cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
1306 cx.simulate_keystrokes("g e");
1307 cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
1308 cx.simulate_keystrokes("g e");
1309 cx.assert_state("foo«ˇ(»bar)", Mode::HelixNormal);
1310 cx.simulate_keystrokes("g e");
1311 cx.assert_state("«ˇfoo»(bar)", Mode::HelixNormal);
1312
1313 cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
1314 cx.simulate_keystrokes("g e");
1315 cx.assert_state("foo_bar«ˇ_baz»", Mode::HelixNormal);
1316 cx.simulate_keystrokes("g e");
1317 cx.assert_state("foo«ˇ_bar»_baz", Mode::HelixNormal);
1318 cx.simulate_keystrokes("g e");
1319 cx.assert_state("«ˇfoo»_bar_baz", Mode::HelixNormal);
1320
1321 cx.set_state("foo;barˇ", Mode::HelixNormal);
1322 cx.simulate_keystrokes("g e");
1323 cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
1324 cx.simulate_keystrokes("g e");
1325 cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
1326 cx.simulate_keystrokes("g e");
1327 cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
1328
1329 cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
1330 cx.simulate_keystrokes("g e");
1331 cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
1332 cx.simulate_keystrokes("g e");
1333 cx.assert_state("<?php\n\n$someVariable =«ˇ 2»;", Mode::HelixNormal);
1334 cx.simulate_keystrokes("g e");
1335 cx.assert_state("<?php\n\n$someVariable«ˇ =» 2;", Mode::HelixNormal);
1336 cx.simulate_keystrokes("g e");
1337 cx.assert_state("<?php\n\n$some«ˇVariable» = 2;", Mode::HelixNormal);
1338 cx.simulate_keystrokes("g e");
1339 cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
1340 cx.simulate_keystrokes("g e");
1341 cx.assert_state("<?php\n\n«ˇ$»someVariable = 2;", Mode::HelixNormal);
1342 cx.simulate_keystrokes("g e");
1343 cx.assert_state("<?«ˇphp»\n\n$someVariable = 2;", Mode::HelixNormal);
1344 cx.simulate_keystrokes("g e");
1345 cx.assert_state("«ˇ<?»php\n\n$someVariable = 2;", Mode::HelixNormal);
1346
1347 cx.set_state("fooBarBazˇ", Mode::HelixNormal);
1348 cx.simulate_keystrokes("g e");
1349 cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
1350 cx.simulate_keystrokes("g e");
1351 cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
1352 cx.simulate_keystrokes("g e");
1353 cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
1354 }
1355
1356 #[gpui::test]
1357 async fn test_delete(cx: &mut gpui::TestAppContext) {
1358 let mut cx = VimTestContext::new(cx, true).await;
1359 cx.enable_helix();
1360
1361 // test delete a selection
1362 cx.set_state(
1363 indoc! {"
1364 The qu«ick ˇ»brown
1365 fox jumps over
1366 the lazy dog."},
1367 Mode::HelixNormal,
1368 );
1369
1370 cx.simulate_keystrokes("d");
1371
1372 cx.assert_state(
1373 indoc! {"
1374 The quˇbrown
1375 fox jumps over
1376 the lazy dog."},
1377 Mode::HelixNormal,
1378 );
1379
1380 // test deleting a single character
1381 cx.simulate_keystrokes("d");
1382
1383 cx.assert_state(
1384 indoc! {"
1385 The quˇrown
1386 fox jumps over
1387 the lazy dog."},
1388 Mode::HelixNormal,
1389 );
1390 }
1391
1392 #[gpui::test]
1393 async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
1394 let mut cx = VimTestContext::new(cx, true).await;
1395
1396 cx.set_state(
1397 indoc! {"
1398 The quick brownˇ
1399 fox jumps over
1400 the lazy dog."},
1401 Mode::HelixNormal,
1402 );
1403
1404 cx.simulate_keystrokes("d");
1405
1406 cx.assert_state(
1407 indoc! {"
1408 The quick brownˇfox jumps over
1409 the lazy dog."},
1410 Mode::HelixNormal,
1411 );
1412 }
1413
1414 // #[gpui::test]
1415 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
1416 // let mut cx = VimTestContext::new(cx, true).await;
1417
1418 // cx.set_state(
1419 // indoc! {"
1420 // The quick brown
1421 // fox jumps over
1422 // the lazy dog.ˇ"},
1423 // Mode::HelixNormal,
1424 // );
1425
1426 // cx.simulate_keystrokes("d");
1427
1428 // cx.assert_state(
1429 // indoc! {"
1430 // The quick brown
1431 // fox jumps over
1432 // the lazy dog.ˇ"},
1433 // Mode::HelixNormal,
1434 // );
1435 // }
1436
1437 #[gpui::test]
1438 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1439 let mut cx = VimTestContext::new(cx, true).await;
1440 cx.enable_helix();
1441
1442 cx.set_state(
1443 indoc! {"
1444 The quˇick brown
1445 fox jumps over
1446 the lazy dog."},
1447 Mode::HelixNormal,
1448 );
1449
1450 cx.simulate_keystrokes("f z");
1451
1452 cx.assert_state(
1453 indoc! {"
1454 The qu«ick brown
1455 fox jumps over
1456 the lazˇ»y dog."},
1457 Mode::HelixNormal,
1458 );
1459
1460 cx.simulate_keystrokes("F e F e");
1461
1462 cx.assert_state(
1463 indoc! {"
1464 The quick brown
1465 fox jumps ov«ˇer
1466 the» lazy dog."},
1467 Mode::HelixNormal,
1468 );
1469
1470 cx.simulate_keystrokes("e 2 F e");
1471
1472 cx.assert_state(
1473 indoc! {"
1474 Th«ˇe quick brown
1475 fox jumps over»
1476 the lazy dog."},
1477 Mode::HelixNormal,
1478 );
1479
1480 cx.simulate_keystrokes("t r t r");
1481
1482 cx.assert_state(
1483 indoc! {"
1484 The quick «brown
1485 fox jumps oveˇ»r
1486 the lazy dog."},
1487 Mode::HelixNormal,
1488 );
1489 }
1490
1491 #[gpui::test]
1492 async fn test_newline_char(cx: &mut gpui::TestAppContext) {
1493 let mut cx = VimTestContext::new(cx, true).await;
1494 cx.enable_helix();
1495
1496 cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
1497
1498 cx.simulate_keystroke("w");
1499
1500 cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
1501
1502 cx.set_state("aa«\nˇ»", Mode::HelixNormal);
1503
1504 cx.simulate_keystroke("b");
1505
1506 cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
1507 }
1508
1509 #[gpui::test]
1510 async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
1511 let mut cx = VimTestContext::new(cx, true).await;
1512 cx.enable_helix();
1513 cx.set_state(
1514 indoc! {"
1515 «The ˇ»quick brown
1516 fox jumps over
1517 the lazy dog."},
1518 Mode::HelixNormal,
1519 );
1520
1521 cx.simulate_keystrokes("i");
1522
1523 cx.assert_state(
1524 indoc! {"
1525 ˇThe quick brown
1526 fox jumps over
1527 the lazy dog."},
1528 Mode::Insert,
1529 );
1530 }
1531
1532 #[gpui::test]
1533 async fn test_append(cx: &mut gpui::TestAppContext) {
1534 let mut cx = VimTestContext::new(cx, true).await;
1535 cx.enable_helix();
1536 // test from the end of the selection
1537 cx.set_state(
1538 indoc! {"
1539 «Theˇ» quick brown
1540 fox jumps over
1541 the lazy dog."},
1542 Mode::HelixNormal,
1543 );
1544
1545 cx.simulate_keystrokes("a");
1546
1547 cx.assert_state(
1548 indoc! {"
1549 Theˇ quick brown
1550 fox jumps over
1551 the lazy dog."},
1552 Mode::Insert,
1553 );
1554
1555 // test from the beginning of the selection
1556 cx.set_state(
1557 indoc! {"
1558 «ˇThe» quick brown
1559 fox jumps over
1560 the lazy dog."},
1561 Mode::HelixNormal,
1562 );
1563
1564 cx.simulate_keystrokes("a");
1565
1566 cx.assert_state(
1567 indoc! {"
1568 Theˇ quick brown
1569 fox jumps over
1570 the lazy dog."},
1571 Mode::Insert,
1572 );
1573 }
1574
1575 #[gpui::test]
1576 async fn test_replace(cx: &mut gpui::TestAppContext) {
1577 let mut cx = VimTestContext::new(cx, true).await;
1578 cx.enable_helix();
1579
1580 // No selection (single character)
1581 cx.set_state("ˇaa", Mode::HelixNormal);
1582
1583 cx.simulate_keystrokes("r x");
1584
1585 cx.assert_state("ˇxa", Mode::HelixNormal);
1586
1587 // Cursor at the beginning
1588 cx.set_state("«ˇaa»", Mode::HelixNormal);
1589
1590 cx.simulate_keystrokes("r x");
1591
1592 cx.assert_state("«ˇxx»", Mode::HelixNormal);
1593
1594 // Cursor at the end
1595 cx.set_state("«aaˇ»", Mode::HelixNormal);
1596
1597 cx.simulate_keystrokes("r x");
1598
1599 cx.assert_state("«xxˇ»", Mode::HelixNormal);
1600 }
1601
1602 #[gpui::test]
1603 async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
1604 let mut cx = VimTestContext::new(cx, true).await;
1605 cx.enable_helix();
1606
1607 // Test yanking current character with no selection
1608 cx.set_state("hello ˇworld", Mode::HelixNormal);
1609 cx.simulate_keystrokes("y");
1610
1611 // Test cursor remains at the same position after yanking single character
1612 cx.assert_state("hello ˇworld", Mode::HelixNormal);
1613 cx.shared_clipboard().assert_eq("w");
1614
1615 // Move cursor and yank another character
1616 cx.simulate_keystrokes("l");
1617 cx.simulate_keystrokes("y");
1618 cx.shared_clipboard().assert_eq("o");
1619
1620 // Test yanking with existing selection
1621 cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
1622 cx.simulate_keystrokes("y");
1623 cx.shared_clipboard().assert_eq("worl");
1624 cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
1625
1626 // Test yanking in select mode character by character
1627 cx.set_state("hello ˇworld", Mode::HelixNormal);
1628 cx.simulate_keystroke("v");
1629 cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
1630 cx.simulate_keystroke("y");
1631 cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
1632 cx.shared_clipboard().assert_eq("w");
1633 }
1634
1635 #[gpui::test]
1636 async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
1637 let mut cx = VimTestContext::new(cx, true).await;
1638 cx.enable_helix();
1639
1640 // First copy some text to clipboard
1641 cx.set_state("«hello worldˇ»", Mode::HelixNormal);
1642 cx.simulate_keystrokes("y");
1643
1644 // Test paste with shift-r on single cursor
1645 cx.set_state("foo ˇbar", Mode::HelixNormal);
1646 cx.simulate_keystrokes("shift-r");
1647
1648 cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
1649
1650 // Test paste with shift-r on selection
1651 cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
1652 cx.simulate_keystrokes("shift-r");
1653
1654 cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
1655 }
1656
1657 #[gpui::test]
1658 async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
1659 let mut cx = VimTestContext::new(cx, true).await;
1660
1661 assert_eq!(cx.mode(), Mode::Normal);
1662 cx.enable_helix();
1663
1664 cx.simulate_keystrokes("v");
1665 assert_eq!(cx.mode(), Mode::HelixSelect);
1666 cx.simulate_keystrokes("escape");
1667 assert_eq!(cx.mode(), Mode::HelixNormal);
1668 }
1669
1670 #[gpui::test]
1671 async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
1672 let mut cx = VimTestContext::new(cx, true).await;
1673 cx.enable_helix();
1674
1675 // Make a modification at a specific location
1676 cx.set_state("ˇhello", Mode::HelixNormal);
1677 assert_eq!(cx.mode(), Mode::HelixNormal);
1678 cx.simulate_keystrokes("i");
1679 assert_eq!(cx.mode(), Mode::Insert);
1680 cx.simulate_keystrokes("escape");
1681 assert_eq!(cx.mode(), Mode::HelixNormal);
1682 }
1683
1684 #[gpui::test]
1685 async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
1686 let mut cx = VimTestContext::new(cx, true).await;
1687 cx.enable_helix();
1688
1689 // Make a modification at a specific location
1690 cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1691 cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1692 cx.simulate_keystrokes("i");
1693 cx.simulate_keystrokes("escape");
1694 cx.simulate_keystrokes("i");
1695 cx.simulate_keystrokes("m o d i f i e d space");
1696 cx.simulate_keystrokes("escape");
1697
1698 // TODO: this fails, because state is no longer helix
1699 cx.assert_state(
1700 "line one\nline modified ˇtwo\nline three",
1701 Mode::HelixNormal,
1702 );
1703
1704 // Move cursor away from the modification
1705 cx.simulate_keystrokes("up");
1706
1707 // Use "g ." to go back to last modification
1708 cx.simulate_keystrokes("g .");
1709
1710 // Verify we're back at the modification location and still in HelixNormal mode
1711 cx.assert_state(
1712 "line one\nline modifiedˇ two\nline three",
1713 Mode::HelixNormal,
1714 );
1715 }
1716
1717 #[gpui::test]
1718 async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
1719 let mut cx = VimTestContext::new(cx, true).await;
1720 cx.set_state(
1721 "line one\nline ˇtwo\nline three\nline four",
1722 Mode::HelixNormal,
1723 );
1724 cx.simulate_keystrokes("2 x");
1725 cx.assert_state(
1726 "line one\n«line two\nline three\nˇ»line four",
1727 Mode::HelixNormal,
1728 );
1729
1730 // Test extending existing line selection
1731 cx.set_state(
1732 indoc! {"
1733 li«ˇne one
1734 li»ne two
1735 line three
1736 line four"},
1737 Mode::HelixNormal,
1738 );
1739 cx.simulate_keystrokes("x");
1740 cx.assert_state(
1741 indoc! {"
1742 «line one
1743 line two
1744 ˇ»line three
1745 line four"},
1746 Mode::HelixNormal,
1747 );
1748
1749 // Pressing x in empty line, select next line (because helix considers cursor a selection)
1750 cx.set_state(
1751 indoc! {"
1752 line one
1753 ˇ
1754 line three
1755 line four
1756 line five
1757 line six"},
1758 Mode::HelixNormal,
1759 );
1760 cx.simulate_keystrokes("x");
1761 cx.assert_state(
1762 indoc! {"
1763 line one
1764 «
1765 line three
1766 ˇ»line four
1767 line five
1768 line six"},
1769 Mode::HelixNormal,
1770 );
1771
1772 // Another x should only select the next line
1773 cx.simulate_keystrokes("x");
1774 cx.assert_state(
1775 indoc! {"
1776 line one
1777 «
1778 line three
1779 line four
1780 ˇ»line five
1781 line six"},
1782 Mode::HelixNormal,
1783 );
1784
1785 // Empty line with count selects extra + count lines
1786 cx.set_state(
1787 indoc! {"
1788 line one
1789 ˇ
1790 line three
1791 line four
1792 line five"},
1793 Mode::HelixNormal,
1794 );
1795 cx.simulate_keystrokes("2 x");
1796 cx.assert_state(
1797 indoc! {"
1798 line one
1799 «
1800 line three
1801 line four
1802 ˇ»line five"},
1803 Mode::HelixNormal,
1804 );
1805
1806 // Compare empty vs non-empty line behavior
1807 cx.set_state(
1808 indoc! {"
1809 ˇnon-empty line
1810 line two
1811 line three"},
1812 Mode::HelixNormal,
1813 );
1814 cx.simulate_keystrokes("x");
1815 cx.assert_state(
1816 indoc! {"
1817 «non-empty line
1818 ˇ»line two
1819 line three"},
1820 Mode::HelixNormal,
1821 );
1822
1823 // Same test but with empty line - should select one extra
1824 cx.set_state(
1825 indoc! {"
1826 ˇ
1827 line two
1828 line three"},
1829 Mode::HelixNormal,
1830 );
1831 cx.simulate_keystrokes("x");
1832 cx.assert_state(
1833 indoc! {"
1834 «
1835 line two
1836 ˇ»line three"},
1837 Mode::HelixNormal,
1838 );
1839
1840 // Test selecting multiple lines with count
1841 cx.set_state(
1842 indoc! {"
1843 ˇline one
1844 line two
1845 line threeˇ
1846 line four
1847 line five"},
1848 Mode::HelixNormal,
1849 );
1850 cx.simulate_keystrokes("x");
1851 cx.assert_state(
1852 indoc! {"
1853 «line one
1854 ˇ»line two
1855 «line three
1856 ˇ»line four
1857 line five"},
1858 Mode::HelixNormal,
1859 );
1860 cx.simulate_keystrokes("x");
1861 // Adjacent line selections stay separate (not merged)
1862 cx.assert_state(
1863 indoc! {"
1864 «line one
1865 line two
1866 ˇ»«line three
1867 line four
1868 ˇ»line five"},
1869 Mode::HelixNormal,
1870 );
1871
1872 // Test selecting with an empty line below the current line
1873 cx.set_state(
1874 indoc! {"
1875 line one
1876 line twoˇ
1877
1878 line four
1879 line five"},
1880 Mode::HelixNormal,
1881 );
1882 cx.simulate_keystrokes("x");
1883 cx.assert_state(
1884 indoc! {"
1885 line one
1886 «line two
1887 ˇ»
1888 line four
1889 line five"},
1890 Mode::HelixNormal,
1891 );
1892 cx.simulate_keystrokes("x");
1893 cx.assert_state(
1894 indoc! {"
1895 line one
1896 «line two
1897
1898 ˇ»line four
1899 line five"},
1900 Mode::HelixNormal,
1901 );
1902 cx.simulate_keystrokes("x");
1903 cx.assert_state(
1904 indoc! {"
1905 line one
1906 «line two
1907
1908 line four
1909 ˇ»line five"},
1910 Mode::HelixNormal,
1911 );
1912 }
1913
1914 #[gpui::test]
1915 async fn test_helix_insert_before_after_select_lines(cx: &mut gpui::TestAppContext) {
1916 let mut cx = VimTestContext::new(cx, true).await;
1917
1918 cx.set_state(
1919 "line one\nline ˇtwo\nline three\nline four",
1920 Mode::HelixNormal,
1921 );
1922 cx.simulate_keystrokes("2 x");
1923 cx.assert_state(
1924 "line one\n«line two\nline three\nˇ»line four",
1925 Mode::HelixNormal,
1926 );
1927 cx.simulate_keystrokes("o");
1928 cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1929
1930 cx.set_state(
1931 "line one\nline ˇtwo\nline three\nline four",
1932 Mode::HelixNormal,
1933 );
1934 cx.simulate_keystrokes("2 x");
1935 cx.assert_state(
1936 "line one\n«line two\nline three\nˇ»line four",
1937 Mode::HelixNormal,
1938 );
1939 cx.simulate_keystrokes("shift-o");
1940 cx.assert_state("line one\nˇ\nline two\nline three\nline four", Mode::Insert);
1941 }
1942
1943 #[gpui::test]
1944 async fn test_helix_insert_before_after_helix_select(cx: &mut gpui::TestAppContext) {
1945 let mut cx = VimTestContext::new(cx, true).await;
1946 cx.enable_helix();
1947
1948 // Test new line in selection direction
1949 cx.set_state(
1950 "ˇline one\nline two\nline three\nline four",
1951 Mode::HelixNormal,
1952 );
1953 cx.simulate_keystrokes("v j j");
1954 cx.assert_state(
1955 "«line one\nline two\nlˇ»ine three\nline four",
1956 Mode::HelixSelect,
1957 );
1958 cx.simulate_keystrokes("o");
1959 cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1960
1961 cx.set_state(
1962 "line one\nline two\nˇline three\nline four",
1963 Mode::HelixNormal,
1964 );
1965 cx.simulate_keystrokes("v k k");
1966 cx.assert_state(
1967 "«ˇline one\nline two\nl»ine three\nline four",
1968 Mode::HelixSelect,
1969 );
1970 cx.simulate_keystrokes("shift-o");
1971 cx.assert_state("ˇ\nline one\nline two\nline three\nline four", Mode::Insert);
1972
1973 // Test new line in opposite selection direction
1974 cx.set_state(
1975 "ˇline one\nline two\nline three\nline four",
1976 Mode::HelixNormal,
1977 );
1978 cx.simulate_keystrokes("v j j");
1979 cx.assert_state(
1980 "«line one\nline two\nlˇ»ine three\nline four",
1981 Mode::HelixSelect,
1982 );
1983 cx.simulate_keystrokes("shift-o");
1984 cx.assert_state("ˇ\nline one\nline two\nline three\nline four", Mode::Insert);
1985
1986 cx.set_state(
1987 "line one\nline two\nˇline three\nline four",
1988 Mode::HelixNormal,
1989 );
1990 cx.simulate_keystrokes("v k k");
1991 cx.assert_state(
1992 "«ˇline one\nline two\nl»ine three\nline four",
1993 Mode::HelixSelect,
1994 );
1995 cx.simulate_keystrokes("o");
1996 cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1997 }
1998
1999 #[gpui::test]
2000 async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
2001 let mut cx = VimTestContext::new(cx, true).await;
2002
2003 assert_eq!(cx.mode(), Mode::Normal);
2004 cx.enable_helix();
2005
2006 cx.set_state("ˇhello", Mode::HelixNormal);
2007 cx.simulate_keystrokes("l v l l");
2008 cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
2009 }
2010
2011 #[gpui::test]
2012 async fn test_helix_select_end_of_line(cx: &mut gpui::TestAppContext) {
2013 let mut cx = VimTestContext::new(cx, true).await;
2014 cx.enable_helix();
2015
2016 // v g l d should delete to end of line without consuming the newline
2017 cx.set_state("ˇThe quick brown\nfox jumps over", Mode::HelixNormal);
2018 cx.simulate_keystrokes("v g l d");
2019 cx.assert_state("ˇ\nfox jumps over", Mode::HelixNormal);
2020
2021 // same from the middle of a line — cursor lands on the last
2022 // remaining character (the space) after delete
2023 cx.set_state("The ˇquick brown\nfox jumps over", Mode::HelixNormal);
2024 cx.simulate_keystrokes("v g l d");
2025 cx.assert_state("Theˇ \nfox jumps over", Mode::HelixNormal);
2026 }
2027
2028 #[gpui::test]
2029 async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
2030 let mut cx = VimTestContext::new(cx, true).await;
2031
2032 assert_eq!(cx.mode(), Mode::Normal);
2033 cx.enable_helix();
2034
2035 // Start with multiple cursors (no selections)
2036 cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
2037
2038 // Enter select mode and move right twice
2039 cx.simulate_keystrokes("v l l");
2040
2041 // Each cursor should independently create and extend its own selection
2042 cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
2043 }
2044
2045 #[gpui::test]
2046 async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
2047 let mut cx = VimTestContext::new(cx, true).await;
2048
2049 cx.set_state("ˇone two", Mode::Normal);
2050 cx.simulate_keystrokes("v w");
2051 cx.assert_state("«one tˇ»wo", Mode::Visual);
2052
2053 // In Vim, this selects "t". In helix selections stops just before "t"
2054
2055 cx.enable_helix();
2056 cx.set_state("ˇone two", Mode::HelixNormal);
2057 cx.simulate_keystrokes("v w");
2058 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
2059 }
2060
2061 #[gpui::test]
2062 async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) {
2063 let mut cx = VimTestContext::new(cx, true).await;
2064
2065 cx.set_state("ˇone two", Mode::Normal);
2066 cx.simulate_keystrokes("v w");
2067 cx.assert_state("«one tˇ»wo", Mode::Visual);
2068 cx.simulate_keystrokes("escape");
2069 cx.assert_state("one ˇtwo", Mode::Normal);
2070
2071 cx.enable_helix();
2072 cx.set_state("ˇone two", Mode::HelixNormal);
2073 cx.simulate_keystrokes("v w");
2074 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
2075 cx.simulate_keystrokes("escape");
2076 cx.assert_state("«one ˇ»two", Mode::HelixNormal);
2077 }
2078
2079 #[gpui::test]
2080 async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) {
2081 let mut cx = VimTestContext::new(cx, true).await;
2082 cx.enable_helix();
2083
2084 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
2085 cx.simulate_keystrokes("w");
2086 cx.assert_state("«one ˇ»two three", Mode::HelixSelect);
2087
2088 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
2089 cx.simulate_keystrokes("e");
2090 cx.assert_state("«oneˇ» two three", Mode::HelixSelect);
2091 }
2092
2093 #[gpui::test]
2094 async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) {
2095 let mut cx = VimTestContext::new(cx, true).await;
2096 cx.enable_helix();
2097
2098 cx.set_state("ˇone two three", Mode::HelixNormal);
2099 cx.simulate_keystrokes("l l v h h h");
2100 cx.assert_state("«ˇone» two three", Mode::HelixSelect);
2101 }
2102
2103 #[gpui::test]
2104 async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
2105 let mut cx = VimTestContext::new(cx, true).await;
2106 cx.enable_helix();
2107
2108 cx.set_state("ˇone two one", Mode::HelixNormal);
2109 cx.simulate_keystrokes("x");
2110 cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
2111 cx.simulate_keystrokes("s o n e");
2112 cx.run_until_parked();
2113 cx.simulate_keystrokes("enter");
2114 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
2115
2116 cx.simulate_keystrokes("x");
2117 cx.simulate_keystrokes("s");
2118 cx.run_until_parked();
2119 cx.simulate_keystrokes("enter");
2120 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
2121
2122 // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
2123 // cx.set_state("ˇstuff one two one", Mode::HelixNormal);
2124 // cx.simulate_keystrokes("s o n e enter");
2125 // cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
2126 }
2127
2128 #[gpui::test]
2129 async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
2130 let mut cx = VimTestContext::new(cx, true).await;
2131
2132 cx.set_state("ˇhello two one two one two one", Mode::Visual);
2133 cx.simulate_keystrokes("/ o n e");
2134 cx.simulate_keystrokes("enter");
2135 cx.simulate_keystrokes("n n");
2136 cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
2137
2138 cx.set_state("ˇhello two one two one two one", Mode::Normal);
2139 cx.simulate_keystrokes("/ o n e");
2140 cx.simulate_keystrokes("enter");
2141 cx.simulate_keystrokes("n n");
2142 cx.assert_state("hello two one two one two ˇone", Mode::Normal);
2143
2144 cx.set_state("ˇhello two one two one two one", Mode::Normal);
2145 cx.simulate_keystrokes("/ o n e");
2146 cx.simulate_keystrokes("enter");
2147 cx.simulate_keystrokes("n g n g n");
2148 cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
2149
2150 cx.enable_helix();
2151
2152 cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
2153 cx.simulate_keystrokes("/ o n e");
2154 cx.simulate_keystrokes("enter");
2155 cx.simulate_keystrokes("n n");
2156 cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
2157
2158 cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
2159 cx.simulate_keystrokes("/ o n e");
2160 cx.simulate_keystrokes("enter");
2161 cx.simulate_keystrokes("n n");
2162 cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
2163 }
2164
2165 #[gpui::test]
2166 async fn test_helix_select_next_match_wrapping(cx: &mut gpui::TestAppContext) {
2167 let mut cx = VimTestContext::new(cx, true).await;
2168 cx.enable_helix();
2169
2170 // Three occurrences of "one". After selecting all three with `n n`,
2171 // pressing `n` again wraps the search to the first occurrence.
2172 // The prior selections (at higher offsets) are chained before the
2173 // wrapped selection (at a lower offset), producing unsorted anchors
2174 // that cause `rope::Cursor::summary` to panic with
2175 // "cannot summarize backward".
2176 cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
2177 cx.simulate_keystrokes("/ o n e");
2178 cx.simulate_keystrokes("enter");
2179 cx.simulate_keystrokes("n n n");
2180 // Should not panic; all three occurrences should remain selected.
2181 cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
2182 }
2183
2184 #[gpui::test]
2185 async fn test_helix_select_next_match_wrapping_from_normal(cx: &mut gpui::TestAppContext) {
2186 let mut cx = VimTestContext::new(cx, true).await;
2187 cx.enable_helix();
2188
2189 // Exact repro for #51573: start in HelixNormal, search, then `v` to
2190 // enter HelixSelect, then `n` past last match.
2191 //
2192 // In HelixNormal, search collapses the cursor to the match start.
2193 // Pressing `v` expands by only one character, creating a partial
2194 // selection that overlaps the full match range when the search wraps.
2195 // The overlapping ranges must be merged (not just deduped) to avoid
2196 // a backward-seeking rope cursor panic.
2197 cx.set_state(
2198 indoc! {"
2199 searˇch term
2200 stuff
2201 search term
2202 other stuff
2203 "},
2204 Mode::HelixNormal,
2205 );
2206 cx.simulate_keystrokes("/ t e r m");
2207 cx.simulate_keystrokes("enter");
2208 cx.simulate_keystrokes("v");
2209 cx.simulate_keystrokes("n");
2210 cx.simulate_keystrokes("n");
2211 // Should not panic when wrapping past last match.
2212 cx.assert_state(
2213 indoc! {"
2214 search «termˇ»
2215 stuff
2216 search «termˇ»
2217 other stuff
2218 "},
2219 Mode::HelixSelect,
2220 );
2221 }
2222
2223 #[gpui::test]
2224 async fn test_helix_select_star_then_match(cx: &mut gpui::TestAppContext) {
2225 let mut cx = VimTestContext::new(cx, true).await;
2226 cx.enable_helix();
2227
2228 // Repro attempts for #52852: `*` searches for word under cursor,
2229 // `v` enters select, `n` accumulates matches, `m` triggers match mode.
2230 // Try multiple cursor positions and match counts.
2231
2232 // Cursor on first occurrence, 3 more occurrences to select through
2233 cx.set_state(
2234 indoc! {"
2235 ˇone two one three one four one
2236 "},
2237 Mode::HelixNormal,
2238 );
2239 cx.simulate_keystrokes("*");
2240 cx.simulate_keystrokes("v");
2241 cx.simulate_keystrokes("n n n");
2242 // Should not panic on wrapping `n`.
2243
2244 // Cursor in the middle of text before matches
2245 cx.set_state(
2246 indoc! {"
2247 heˇllo one two one three one
2248 "},
2249 Mode::HelixNormal,
2250 );
2251 cx.simulate_keystrokes("*");
2252 cx.simulate_keystrokes("v");
2253 cx.simulate_keystrokes("n");
2254 // Should not panic.
2255
2256 // The original #52852 sequence: * v n n n then m m
2257 cx.set_state(
2258 indoc! {"
2259 fn ˇfoo() { bar(foo()) }
2260 fn baz() { foo() }
2261 "},
2262 Mode::HelixNormal,
2263 );
2264 cx.simulate_keystrokes("*");
2265 cx.simulate_keystrokes("v");
2266 cx.simulate_keystrokes("n n n");
2267 cx.simulate_keystrokes("m m");
2268 // Should not panic.
2269 }
2270
2271 #[gpui::test]
2272 async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
2273 let mut cx = VimTestContext::new(cx, true).await;
2274
2275 cx.set_state("ˇone two", Mode::HelixNormal);
2276 cx.simulate_keystrokes("c");
2277 cx.assert_state("ˇne two", Mode::Insert);
2278
2279 cx.set_state("«oneˇ» two", Mode::HelixNormal);
2280 cx.simulate_keystrokes("c");
2281 cx.assert_state("ˇ two", Mode::Insert);
2282
2283 cx.set_state(
2284 indoc! {"
2285 oneˇ two
2286 three
2287 "},
2288 Mode::HelixNormal,
2289 );
2290 cx.simulate_keystrokes("x c");
2291 cx.assert_state(
2292 indoc! {"
2293 ˇ
2294 three
2295 "},
2296 Mode::Insert,
2297 );
2298
2299 cx.set_state(
2300 indoc! {"
2301 one twoˇ
2302 three
2303 "},
2304 Mode::HelixNormal,
2305 );
2306 cx.simulate_keystrokes("c");
2307 cx.assert_state(
2308 indoc! {"
2309 one twoˇthree
2310 "},
2311 Mode::Insert,
2312 );
2313
2314 // Helix doesn't set the cursor to the first non-blank one when
2315 // replacing lines: it uses language-dependent indent queries instead.
2316 cx.set_state(
2317 indoc! {"
2318 one two
2319 « indented
2320 three not indentedˇ»
2321 "},
2322 Mode::HelixNormal,
2323 );
2324 cx.simulate_keystrokes("c");
2325 cx.set_state(
2326 indoc! {"
2327 one two
2328 ˇ
2329 "},
2330 Mode::Insert,
2331 );
2332 }
2333
2334 #[gpui::test]
2335 async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
2336 let mut cx = VimTestContext::new(cx, true).await;
2337 cx.enable_helix();
2338
2339 // Test g l moves to last character, not after it
2340 cx.set_state("hello ˇworld!", Mode::HelixNormal);
2341 cx.simulate_keystrokes("g l");
2342 cx.assert_state("hello worldˇ!", Mode::HelixNormal);
2343
2344 // Test with Chinese characters, test if work with UTF-8?
2345 cx.set_state("ˇ你好世界", Mode::HelixNormal);
2346 cx.simulate_keystrokes("g l");
2347 cx.assert_state("你好世ˇ界", Mode::HelixNormal);
2348
2349 // Test with end of line
2350 cx.set_state("endˇ", Mode::HelixNormal);
2351 cx.simulate_keystrokes("g l");
2352 cx.assert_state("enˇd", Mode::HelixNormal);
2353
2354 // Test with empty line
2355 cx.set_state(
2356 indoc! {"
2357 hello
2358 ˇ
2359 world"},
2360 Mode::HelixNormal,
2361 );
2362 cx.simulate_keystrokes("g l");
2363 cx.assert_state(
2364 indoc! {"
2365 hello
2366 ˇ
2367 world"},
2368 Mode::HelixNormal,
2369 );
2370
2371 // Test with multiple lines
2372 cx.set_state(
2373 indoc! {"
2374 ˇfirst line
2375 second line
2376 third line"},
2377 Mode::HelixNormal,
2378 );
2379 cx.simulate_keystrokes("g l");
2380 cx.assert_state(
2381 indoc! {"
2382 first linˇe
2383 second line
2384 third line"},
2385 Mode::HelixNormal,
2386 );
2387 }
2388
2389 #[gpui::test]
2390 async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) {
2391 VimTestContext::init(cx);
2392
2393 let fs = FakeFs::new(cx.background_executor.clone());
2394 fs.insert_tree(
2395 path!("/dir"),
2396 json!({
2397 "file_a.rs": "// File A.",
2398 "file_b.rs": "// File B.",
2399 }),
2400 )
2401 .await;
2402
2403 let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2404 let window_handle =
2405 cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2406 let workspace = window_handle
2407 .read_with(cx, |mw, _| mw.workspace().clone())
2408 .unwrap();
2409
2410 cx.update(|cx| {
2411 VimTestContext::init_keybindings(true, cx);
2412 SettingsStore::update_global(cx, |store, cx| {
2413 store.update_user_settings(cx, |store| store.helix_mode = Some(true));
2414 })
2415 });
2416
2417 let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
2418
2419 workspace.update_in(cx, |workspace, window, cx| {
2420 ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx)
2421 });
2422
2423 let search_view = workspace.update_in(cx, |workspace, _, cx| {
2424 workspace
2425 .active_pane()
2426 .read(cx)
2427 .items()
2428 .find_map(|item| item.downcast::<ProjectSearchView>())
2429 .expect("Project search view should be active")
2430 });
2431
2432 project_search::perform_project_search(&search_view, "File A", cx);
2433
2434 search_view.update(cx, |search_view, cx| {
2435 let vim_mode = search_view
2436 .results_editor()
2437 .read(cx)
2438 .addon::<VimAddon>()
2439 .map(|addon| addon.entity.read(cx).mode);
2440
2441 assert_eq!(vim_mode, Some(Mode::HelixNormal));
2442 });
2443 }
2444
2445 #[gpui::test]
2446 async fn test_scroll_with_selection(cx: &mut gpui::TestAppContext) {
2447 let mut cx = VimTestContext::new(cx, true).await;
2448 cx.enable_helix();
2449
2450 // Start with a selection
2451 cx.set_state(
2452 indoc! {"
2453 «lineˇ» one
2454 line two
2455 line three
2456 line four
2457 line five"},
2458 Mode::HelixNormal,
2459 );
2460
2461 // Scroll down, selection should collapse
2462 cx.simulate_keystrokes("ctrl-d");
2463 cx.assert_state(
2464 indoc! {"
2465 line one
2466 line two
2467 line three
2468 line four
2469 line fiveˇ"},
2470 Mode::HelixNormal,
2471 );
2472
2473 // Make a new selection
2474 cx.simulate_keystroke("b");
2475 cx.assert_state(
2476 indoc! {"
2477 line one
2478 line two
2479 line three
2480 line four
2481 line «ˇfive»"},
2482 Mode::HelixNormal,
2483 );
2484
2485 // And scroll up, once again collapsing the selection.
2486 cx.simulate_keystroke("ctrl-u");
2487 cx.assert_state(
2488 indoc! {"
2489 line one
2490 line two
2491 line three
2492 line ˇfour
2493 line five"},
2494 Mode::HelixNormal,
2495 );
2496
2497 // Enter select mode
2498 cx.simulate_keystroke("v");
2499 cx.assert_state(
2500 indoc! {"
2501 line one
2502 line two
2503 line three
2504 line «fˇ»our
2505 line five"},
2506 Mode::HelixSelect,
2507 );
2508
2509 // And now the selection should be kept/expanded.
2510 cx.simulate_keystroke("ctrl-d");
2511 cx.assert_state(
2512 indoc! {"
2513 line one
2514 line two
2515 line three
2516 line «four
2517 line fiveˇ»"},
2518 Mode::HelixSelect,
2519 );
2520 }
2521
2522 #[gpui::test]
2523 async fn test_helix_insert_end_of_line(cx: &mut gpui::TestAppContext) {
2524 let mut cx = VimTestContext::new(cx, true).await;
2525 cx.enable_helix();
2526
2527 // Ensure that, when lines are selected using `x`, pressing `shift-a`
2528 // actually puts the cursor at the end of the selected lines and not at
2529 // the end of the line below.
2530 cx.set_state(
2531 indoc! {"
2532 line oˇne
2533 line two"},
2534 Mode::HelixNormal,
2535 );
2536
2537 cx.simulate_keystrokes("x");
2538 cx.assert_state(
2539 indoc! {"
2540 «line one
2541 ˇ»line two"},
2542 Mode::HelixNormal,
2543 );
2544
2545 cx.simulate_keystrokes("shift-a");
2546 cx.assert_state(
2547 indoc! {"
2548 line oneˇ
2549 line two"},
2550 Mode::Insert,
2551 );
2552
2553 cx.set_state(
2554 indoc! {"
2555 line «one
2556 lineˇ» two"},
2557 Mode::HelixNormal,
2558 );
2559
2560 cx.simulate_keystrokes("shift-a");
2561 cx.assert_state(
2562 indoc! {"
2563 line one
2564 line twoˇ"},
2565 Mode::Insert,
2566 );
2567 }
2568
2569 #[gpui::test]
2570 async fn test_helix_replace_uses_graphemes(cx: &mut gpui::TestAppContext) {
2571 let mut cx = VimTestContext::new(cx, true).await;
2572 cx.enable_helix();
2573
2574 cx.set_state("«Hällöˇ» Wörld", Mode::HelixNormal);
2575 cx.simulate_keystrokes("r 1");
2576 cx.assert_state("«11111ˇ» Wörld", Mode::HelixNormal);
2577
2578 cx.set_state("«e\u{301}ˇ»", Mode::HelixNormal);
2579 cx.simulate_keystrokes("r 1");
2580 cx.assert_state("«1ˇ»", Mode::HelixNormal);
2581
2582 cx.set_state("«🙂ˇ»", Mode::HelixNormal);
2583 cx.simulate_keystrokes("r 1");
2584 cx.assert_state("«1ˇ»", Mode::HelixNormal);
2585 }
2586}