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