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