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 cmd_f_search: false,
652 prior_selections,
653 prior_operator: self.operator_stack.last().cloned(),
654 prior_mode: self.mode,
655 helix_select: true,
656 _dismiss_subscription: None,
657 }
658 });
659 }
660 });
661 self.start_recording(cx);
662 }
663
664 fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
665 self.start_recording(cx);
666 self.switch_mode(Mode::Insert, false, window, cx);
667 self.update_editor(cx, |_, editor, cx| {
668 editor.change_selections(Default::default(), window, cx, |s| {
669 s.move_with(&mut |map, selection| {
670 let point = if selection.is_empty() {
671 right(map, selection.head(), 1)
672 } else {
673 selection.end
674 };
675 selection.collapse_to(point, SelectionGoal::None);
676 });
677 });
678 });
679 }
680
681 /// Helix-specific implementation of `shift-a` that accounts for Helix's
682 /// selection model, where selecting a line with `x` creates a selection
683 /// from column 0 of the current row to column 0 of the next row, so the
684 /// default [`vim::normal::InsertEndOfLine`] would move the cursor to the
685 /// end of the wrong line.
686 fn helix_insert_end_of_line(
687 &mut self,
688 _: &HelixInsertEndOfLine,
689 window: &mut Window,
690 cx: &mut Context<Self>,
691 ) {
692 self.start_recording(cx);
693 self.switch_mode(Mode::Insert, false, window, cx);
694 self.update_editor(cx, |_, editor, cx| {
695 editor.change_selections(Default::default(), window, cx, |s| {
696 s.move_with(&mut |map, selection| {
697 let cursor = if !selection.is_empty() && !selection.reversed {
698 movement::left(map, selection.head())
699 } else {
700 selection.head()
701 };
702 selection
703 .collapse_to(motion::next_line_end(map, cursor, 1), SelectionGoal::None);
704 });
705 });
706 });
707 }
708
709 pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
710 self.update_editor(cx, |_, editor, cx| {
711 editor.transact(window, cx, |editor, window, cx| {
712 let display_map = editor.display_snapshot(cx);
713 let selections = editor.selections.all_display(&display_map);
714
715 let mut edits = Vec::new();
716 let mut selection_info = Vec::new();
717 for selection in &selections {
718 let mut range = selection.range();
719 let was_empty = range.is_empty();
720 let was_reversed = selection.reversed;
721
722 if was_empty {
723 range.end = movement::saturating_right(&display_map, range.start);
724 }
725
726 let byte_range = range.start.to_offset(&display_map, Bias::Left)
727 ..range.end.to_offset(&display_map, Bias::Left);
728
729 let snapshot = display_map.buffer_snapshot();
730 let grapheme_count = snapshot.grapheme_count_for_range(&byte_range);
731 let anchor = snapshot.anchor_before(byte_range.start);
732
733 selection_info.push((anchor, grapheme_count, was_empty, was_reversed));
734
735 if !byte_range.is_empty() {
736 let replacement_text = text.repeat(grapheme_count);
737 edits.push((byte_range, replacement_text));
738 }
739 }
740
741 editor.edit(edits, cx);
742
743 // Restore selections based on original info
744 let snapshot = editor.buffer().read(cx).snapshot(cx);
745 let ranges: Vec<_> = selection_info
746 .into_iter()
747 .map(|(start_anchor, grapheme_count, was_empty, was_reversed)| {
748 let start_point = start_anchor.to_point(&snapshot);
749 if was_empty {
750 start_point..start_point
751 } else {
752 let replacement_len = text.len() * grapheme_count;
753 let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
754 let end_point = snapshot.offset_to_point(end_offset);
755 if was_reversed {
756 end_point..start_point
757 } else {
758 start_point..end_point
759 }
760 }
761 })
762 .collect();
763
764 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
765 s.select_ranges(ranges);
766 });
767 });
768 });
769 self.switch_mode(Mode::HelixNormal, true, window, cx);
770 }
771
772 pub fn helix_goto_last_modification(
773 &mut self,
774 _: &HelixGotoLastModification,
775 window: &mut Window,
776 cx: &mut Context<Self>,
777 ) {
778 self.jump(".".into(), false, false, window, cx);
779 }
780
781 pub fn helix_select_lines(
782 &mut self,
783 _: &HelixSelectLine,
784 window: &mut Window,
785 cx: &mut Context<Self>,
786 ) {
787 let count = Vim::take_count(cx).unwrap_or(1);
788 self.update_editor(cx, |_, editor, cx| {
789 editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
790 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
791 let mut selections = editor.selections.all::<Point>(&display_map);
792 let max_point = display_map.buffer_snapshot().max_point();
793 let buffer_snapshot = &display_map.buffer_snapshot();
794
795 for selection in &mut selections {
796 // Start always goes to column 0 of the first selected line
797 let start_row = selection.start.row;
798 let current_end_row = selection.end.row;
799
800 // Check if cursor is on empty line by checking first character
801 let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0));
802 let first_char = buffer_snapshot.chars_at(line_start_offset).next();
803 let extra_line = if first_char == Some('\n') && selection.is_empty() {
804 1
805 } else {
806 0
807 };
808
809 let end_row = current_end_row + count as u32 + extra_line;
810
811 selection.start = Point::new(start_row, 0);
812 selection.end = if end_row > max_point.row {
813 max_point
814 } else {
815 Point::new(end_row, 0)
816 };
817 selection.reversed = false;
818 }
819
820 editor.change_selections(Default::default(), window, cx, |s| {
821 s.select(selections);
822 });
823 });
824 }
825
826 fn helix_keep_newest_selection(
827 &mut self,
828 _: &HelixKeepNewestSelection,
829 window: &mut Window,
830 cx: &mut Context<Self>,
831 ) {
832 self.update_editor(cx, |_, editor, cx| {
833 let newest = editor
834 .selections
835 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
836 editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
837 });
838 }
839
840 fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
841 self.update_editor(cx, |vim, editor, cx| {
842 editor.set_clip_at_line_ends(false, cx);
843 editor.transact(window, cx, |editor, window, cx| {
844 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
845 s.move_with(&mut |map, selection| {
846 if selection.start == selection.end {
847 selection.end = movement::right(map, selection.end);
848 }
849
850 // If the selection starts and ends on a newline, we exclude the last one.
851 if !selection.is_empty()
852 && selection.start.column() == 0
853 && selection.end.column() == 0
854 {
855 selection.end = movement::left(map, selection.end);
856 }
857 })
858 });
859 if yank {
860 vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
861 }
862 let selections = editor
863 .selections
864 .all::<Point>(&editor.display_snapshot(cx))
865 .into_iter();
866 let edits = selections.map(|selection| (selection.start..selection.end, ""));
867 editor.edit(edits, cx);
868 });
869 });
870 self.switch_mode(Mode::Insert, true, window, cx);
871 }
872
873 fn helix_substitute(
874 &mut self,
875 _: &HelixSubstitute,
876 window: &mut Window,
877 cx: &mut Context<Self>,
878 ) {
879 self.do_helix_substitute(true, window, cx);
880 }
881
882 fn helix_substitute_no_yank(
883 &mut self,
884 _: &HelixSubstituteNoYank,
885 window: &mut Window,
886 cx: &mut Context<Self>,
887 ) {
888 self.do_helix_substitute(false, window, cx);
889 }
890
891 fn helix_select_next(
892 &mut self,
893 _: &HelixSelectNext,
894 window: &mut Window,
895 cx: &mut Context<Self>,
896 ) {
897 self.do_helix_select(Direction::Next, window, cx);
898 }
899
900 fn helix_select_previous(
901 &mut self,
902 _: &HelixSelectPrevious,
903 window: &mut Window,
904 cx: &mut Context<Self>,
905 ) {
906 self.do_helix_select(Direction::Prev, window, cx);
907 }
908
909 fn do_helix_select(
910 &mut self,
911 direction: searchable::Direction,
912 window: &mut Window,
913 cx: &mut Context<Self>,
914 ) {
915 let Some(pane) = self.pane(window, cx) else {
916 return;
917 };
918 let count = Vim::take_count(cx).unwrap_or(1);
919 Vim::take_forced_motion(cx);
920 let prior_selections = self.editor_selections(window, cx);
921
922 let success = pane.update(cx, |pane, cx| {
923 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
924 return false;
925 };
926 search_bar.update(cx, |search_bar, cx| {
927 if !search_bar.has_active_match() || !search_bar.show(window, cx) {
928 return false;
929 }
930 search_bar.select_match(direction, count, window, cx);
931 true
932 })
933 });
934
935 if !success {
936 return;
937 }
938 if self.mode == Mode::HelixSelect {
939 self.update_editor(cx, |_vim, editor, cx| {
940 let snapshot = editor.snapshot(window, cx);
941 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
942 let buffer = snapshot.buffer_snapshot();
943
944 s.select_anchor_ranges(
945 prior_selections
946 .iter()
947 .cloned()
948 .chain(s.all_anchors(&snapshot).iter().map(|s| s.range()))
949 .sorted_by(|a, b| {
950 a.start
951 .cmp(&b.start, buffer)
952 .then_with(|| a.end.cmp(&b.end, buffer))
953 })
954 .dedup_by(|a, b| {
955 a.start.cmp(&b.start, buffer).is_eq()
956 && a.end.cmp(&b.end, buffer).is_eq()
957 }),
958 );
959 })
960 });
961 }
962 }
963}
964
965#[cfg(test)]
966mod test {
967 use gpui::{KeyBinding, UpdateGlobal, VisualTestContext};
968 use indoc::indoc;
969 use project::FakeFs;
970 use search::{ProjectSearchView, project_search};
971 use serde_json::json;
972 use settings::SettingsStore;
973 use util::path;
974 use workspace::{DeploySearch, MultiWorkspace};
975
976 use crate::{VimAddon, state::Mode, test::VimTestContext};
977
978 #[gpui::test]
979 async fn test_word_motions(cx: &mut gpui::TestAppContext) {
980 let mut cx = VimTestContext::new(cx, true).await;
981 cx.enable_helix();
982 // «
983 // ˇ
984 // »
985 cx.set_state(
986 indoc! {"
987 Th«e quiˇ»ck brown
988 fox jumps over
989 the lazy dog."},
990 Mode::HelixNormal,
991 );
992
993 cx.simulate_keystrokes("w");
994
995 cx.assert_state(
996 indoc! {"
997 The qu«ick ˇ»brown
998 fox jumps over
999 the lazy dog."},
1000 Mode::HelixNormal,
1001 );
1002
1003 cx.simulate_keystrokes("w");
1004
1005 cx.assert_state(
1006 indoc! {"
1007 The quick «brownˇ»
1008 fox jumps over
1009 the lazy dog."},
1010 Mode::HelixNormal,
1011 );
1012
1013 cx.simulate_keystrokes("2 b");
1014
1015 cx.assert_state(
1016 indoc! {"
1017 The «ˇquick »brown
1018 fox jumps over
1019 the lazy dog."},
1020 Mode::HelixNormal,
1021 );
1022
1023 cx.simulate_keystrokes("down e up");
1024
1025 cx.assert_state(
1026 indoc! {"
1027 The quicˇk brown
1028 fox jumps over
1029 the lazy dog."},
1030 Mode::HelixNormal,
1031 );
1032
1033 cx.set_state("aa\n «ˇbb»", Mode::HelixNormal);
1034
1035 cx.simulate_keystroke("b");
1036
1037 cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal);
1038 }
1039
1040 #[gpui::test]
1041 async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
1042 let mut cx = VimTestContext::new(cx, true).await;
1043 cx.enable_helix();
1044
1045 // Setup custom keybindings for subword motions so we can use the bindings
1046 // in `simulate_keystroke`.
1047 cx.update(|_window, cx| {
1048 cx.bind_keys([KeyBinding::new(
1049 "w",
1050 crate::motion::NextSubwordStart {
1051 ignore_punctuation: false,
1052 },
1053 None,
1054 )]);
1055 });
1056
1057 cx.set_state("ˇfoo.bar", Mode::HelixNormal);
1058 cx.simulate_keystroke("w");
1059 cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
1060 cx.simulate_keystroke("w");
1061 cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
1062 cx.simulate_keystroke("w");
1063 cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
1064
1065 cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
1066 cx.simulate_keystroke("w");
1067 cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
1068 cx.simulate_keystroke("w");
1069 cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
1070 cx.simulate_keystroke("w");
1071 cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
1072
1073 cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
1074 cx.simulate_keystroke("w");
1075 cx.assert_state("«foo_ˇ»bar_baz", Mode::HelixNormal);
1076 cx.simulate_keystroke("w");
1077 cx.assert_state("foo_«bar_ˇ»baz", Mode::HelixNormal);
1078
1079 cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
1080 cx.simulate_keystroke("w");
1081 cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
1082 cx.simulate_keystroke("w");
1083 cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
1084
1085 cx.set_state("ˇfoo;bar", Mode::HelixNormal);
1086 cx.simulate_keystroke("w");
1087 cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
1088 cx.simulate_keystroke("w");
1089 cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
1090 cx.simulate_keystroke("w");
1091 cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
1092
1093 cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
1094 cx.simulate_keystroke("w");
1095 cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
1096 cx.simulate_keystroke("w");
1097 cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
1098 cx.simulate_keystroke("w");
1099 cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
1100 cx.simulate_keystroke("w");
1101 cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
1102 cx.simulate_keystroke("w");
1103 cx.assert_state("<?php\n\n$some«Variable ˇ»= 2;", Mode::HelixNormal);
1104 cx.simulate_keystroke("w");
1105 cx.assert_state("<?php\n\n$someVariable «= ˇ»2;", Mode::HelixNormal);
1106 cx.simulate_keystroke("w");
1107 cx.assert_state("<?php\n\n$someVariable = «2ˇ»;", Mode::HelixNormal);
1108 cx.simulate_keystroke("w");
1109 cx.assert_state("<?php\n\n$someVariable = 2«;ˇ»", Mode::HelixNormal);
1110 }
1111
1112 #[gpui::test]
1113 async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
1114 let mut cx = VimTestContext::new(cx, true).await;
1115 cx.enable_helix();
1116
1117 // Setup custom keybindings for subword motions so we can use the bindings
1118 // in `simulate_keystroke`.
1119 cx.update(|_window, cx| {
1120 cx.bind_keys([KeyBinding::new(
1121 "e",
1122 crate::motion::NextSubwordEnd {
1123 ignore_punctuation: false,
1124 },
1125 None,
1126 )]);
1127 });
1128
1129 cx.set_state("ˇfoo.bar", Mode::HelixNormal);
1130 cx.simulate_keystroke("e");
1131 cx.assert_state("«fooˇ».bar", Mode::HelixNormal);
1132 cx.simulate_keystroke("e");
1133 cx.assert_state("foo«.ˇ»bar", Mode::HelixNormal);
1134 cx.simulate_keystroke("e");
1135 cx.assert_state("foo.«barˇ»", Mode::HelixNormal);
1136
1137 cx.set_state("ˇfoo(bar)", Mode::HelixNormal);
1138 cx.simulate_keystroke("e");
1139 cx.assert_state("«fooˇ»(bar)", Mode::HelixNormal);
1140 cx.simulate_keystroke("e");
1141 cx.assert_state("foo«(ˇ»bar)", Mode::HelixNormal);
1142 cx.simulate_keystroke("e");
1143 cx.assert_state("foo(«barˇ»)", Mode::HelixNormal);
1144
1145 cx.set_state("ˇfoo_bar_baz", Mode::HelixNormal);
1146 cx.simulate_keystroke("e");
1147 cx.assert_state("«fooˇ»_bar_baz", Mode::HelixNormal);
1148 cx.simulate_keystroke("e");
1149 cx.assert_state("foo«_barˇ»_baz", Mode::HelixNormal);
1150 cx.simulate_keystroke("e");
1151 cx.assert_state("foo_bar«_bazˇ»", Mode::HelixNormal);
1152
1153 cx.set_state("ˇfooBarBaz", Mode::HelixNormal);
1154 cx.simulate_keystroke("e");
1155 cx.assert_state("«fooˇ»BarBaz", Mode::HelixNormal);
1156 cx.simulate_keystroke("e");
1157 cx.assert_state("foo«Barˇ»Baz", Mode::HelixNormal);
1158 cx.simulate_keystroke("e");
1159 cx.assert_state("fooBar«Bazˇ»", Mode::HelixNormal);
1160
1161 cx.set_state("ˇfoo;bar", Mode::HelixNormal);
1162 cx.simulate_keystroke("e");
1163 cx.assert_state("«fooˇ»;bar", Mode::HelixNormal);
1164 cx.simulate_keystroke("e");
1165 cx.assert_state("foo«;ˇ»bar", Mode::HelixNormal);
1166 cx.simulate_keystroke("e");
1167 cx.assert_state("foo;«barˇ»", Mode::HelixNormal);
1168
1169 cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::HelixNormal);
1170 cx.simulate_keystroke("e");
1171 cx.assert_state("«<?ˇ»php\n\n$someVariable = 2;", Mode::HelixNormal);
1172 cx.simulate_keystroke("e");
1173 cx.assert_state("<?«phpˇ»\n\n$someVariable = 2;", Mode::HelixNormal);
1174 cx.simulate_keystroke("e");
1175 cx.assert_state("<?php\n\n«$ˇ»someVariable = 2;", Mode::HelixNormal);
1176 cx.simulate_keystroke("e");
1177 cx.assert_state("<?php\n\n$«someˇ»Variable = 2;", Mode::HelixNormal);
1178 cx.simulate_keystroke("e");
1179 cx.assert_state("<?php\n\n$some«Variableˇ» = 2;", Mode::HelixNormal);
1180 cx.simulate_keystroke("e");
1181 cx.assert_state("<?php\n\n$someVariable« =ˇ» 2;", Mode::HelixNormal);
1182 cx.simulate_keystroke("e");
1183 cx.assert_state("<?php\n\n$someVariable =« 2ˇ»;", Mode::HelixNormal);
1184 cx.simulate_keystroke("e");
1185 cx.assert_state("<?php\n\n$someVariable = 2«;ˇ»", Mode::HelixNormal);
1186 }
1187
1188 #[gpui::test]
1189 async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
1190 let mut cx = VimTestContext::new(cx, true).await;
1191 cx.enable_helix();
1192
1193 // Setup custom keybindings for subword motions so we can use the bindings
1194 // in `simulate_keystroke`.
1195 cx.update(|_window, cx| {
1196 cx.bind_keys([KeyBinding::new(
1197 "b",
1198 crate::motion::PreviousSubwordStart {
1199 ignore_punctuation: false,
1200 },
1201 None,
1202 )]);
1203 });
1204
1205 cx.set_state("foo.barˇ", Mode::HelixNormal);
1206 cx.simulate_keystroke("b");
1207 cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
1208 cx.simulate_keystroke("b");
1209 cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
1210 cx.simulate_keystroke("b");
1211 cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
1212
1213 cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
1214 cx.simulate_keystroke("b");
1215 cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
1216 cx.simulate_keystroke("b");
1217 cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
1218 cx.simulate_keystroke("b");
1219 cx.assert_state("foo«ˇ(»bar)", Mode::HelixNormal);
1220 cx.simulate_keystroke("b");
1221 cx.assert_state("«ˇfoo»(bar)", Mode::HelixNormal);
1222
1223 cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
1224 cx.simulate_keystroke("b");
1225 cx.assert_state("foo_bar_«ˇbaz»", Mode::HelixNormal);
1226 cx.simulate_keystroke("b");
1227 cx.assert_state("foo_«ˇbar_»baz", Mode::HelixNormal);
1228 cx.simulate_keystroke("b");
1229 cx.assert_state("«ˇfoo_»bar_baz", Mode::HelixNormal);
1230
1231 cx.set_state("foo;barˇ", Mode::HelixNormal);
1232 cx.simulate_keystroke("b");
1233 cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
1234 cx.simulate_keystroke("b");
1235 cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
1236 cx.simulate_keystroke("b");
1237 cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
1238
1239 cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
1240 cx.simulate_keystroke("b");
1241 cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
1242 cx.simulate_keystroke("b");
1243 cx.assert_state("<?php\n\n$someVariable = «ˇ2»;", Mode::HelixNormal);
1244 cx.simulate_keystroke("b");
1245 cx.assert_state("<?php\n\n$someVariable «ˇ= »2;", Mode::HelixNormal);
1246 cx.simulate_keystroke("b");
1247 cx.assert_state("<?php\n\n$some«ˇVariable »= 2;", Mode::HelixNormal);
1248 cx.simulate_keystroke("b");
1249 cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
1250 cx.simulate_keystroke("b");
1251 cx.assert_state("<?php\n\n«ˇ$»someVariable = 2;", Mode::HelixNormal);
1252 cx.simulate_keystroke("b");
1253 cx.assert_state("<?«ˇphp»\n\n$someVariable = 2;", Mode::HelixNormal);
1254 cx.simulate_keystroke("b");
1255 cx.assert_state("«ˇ<?»php\n\n$someVariable = 2;", Mode::HelixNormal);
1256
1257 cx.set_state("fooBarBazˇ", Mode::HelixNormal);
1258 cx.simulate_keystroke("b");
1259 cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
1260 cx.simulate_keystroke("b");
1261 cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
1262 cx.simulate_keystroke("b");
1263 cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
1264 }
1265
1266 #[gpui::test]
1267 async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
1268 let mut cx = VimTestContext::new(cx, true).await;
1269 cx.enable_helix();
1270
1271 // Setup custom keybindings for subword motions so we can use the bindings
1272 // in `simulate_keystrokes`.
1273 cx.update(|_window, cx| {
1274 cx.bind_keys([KeyBinding::new(
1275 "g e",
1276 crate::motion::PreviousSubwordEnd {
1277 ignore_punctuation: false,
1278 },
1279 None,
1280 )]);
1281 });
1282
1283 cx.set_state("foo.barˇ", Mode::HelixNormal);
1284 cx.simulate_keystrokes("g e");
1285 cx.assert_state("foo.«ˇbar»", Mode::HelixNormal);
1286 cx.simulate_keystrokes("g e");
1287 cx.assert_state("foo«ˇ.»bar", Mode::HelixNormal);
1288 cx.simulate_keystrokes("g e");
1289 cx.assert_state("«ˇfoo».bar", Mode::HelixNormal);
1290
1291 cx.set_state("foo(bar)ˇ", Mode::HelixNormal);
1292 cx.simulate_keystrokes("g e");
1293 cx.assert_state("foo(bar«ˇ)»", Mode::HelixNormal);
1294 cx.simulate_keystrokes("g e");
1295 cx.assert_state("foo(«ˇbar»)", Mode::HelixNormal);
1296 cx.simulate_keystrokes("g e");
1297 cx.assert_state("foo«ˇ(»bar)", Mode::HelixNormal);
1298 cx.simulate_keystrokes("g e");
1299 cx.assert_state("«ˇfoo»(bar)", Mode::HelixNormal);
1300
1301 cx.set_state("foo_bar_bazˇ", Mode::HelixNormal);
1302 cx.simulate_keystrokes("g e");
1303 cx.assert_state("foo_bar«ˇ_baz»", Mode::HelixNormal);
1304 cx.simulate_keystrokes("g e");
1305 cx.assert_state("foo«ˇ_bar»_baz", Mode::HelixNormal);
1306 cx.simulate_keystrokes("g e");
1307 cx.assert_state("«ˇfoo»_bar_baz", Mode::HelixNormal);
1308
1309 cx.set_state("foo;barˇ", Mode::HelixNormal);
1310 cx.simulate_keystrokes("g e");
1311 cx.assert_state("foo;«ˇbar»", Mode::HelixNormal);
1312 cx.simulate_keystrokes("g e");
1313 cx.assert_state("foo«ˇ;»bar", Mode::HelixNormal);
1314 cx.simulate_keystrokes("g e");
1315 cx.assert_state("«ˇfoo»;bar", Mode::HelixNormal);
1316
1317 cx.set_state("<?php\n\n$someVariable = 2;ˇ", Mode::HelixNormal);
1318 cx.simulate_keystrokes("g e");
1319 cx.assert_state("<?php\n\n$someVariable = 2«ˇ;»", Mode::HelixNormal);
1320 cx.simulate_keystrokes("g e");
1321 cx.assert_state("<?php\n\n$someVariable =«ˇ 2»;", Mode::HelixNormal);
1322 cx.simulate_keystrokes("g e");
1323 cx.assert_state("<?php\n\n$someVariable«ˇ =» 2;", Mode::HelixNormal);
1324 cx.simulate_keystrokes("g e");
1325 cx.assert_state("<?php\n\n$some«ˇVariable» = 2;", Mode::HelixNormal);
1326 cx.simulate_keystrokes("g e");
1327 cx.assert_state("<?php\n\n$«ˇsome»Variable = 2;", Mode::HelixNormal);
1328 cx.simulate_keystrokes("g e");
1329 cx.assert_state("<?php\n\n«ˇ$»someVariable = 2;", Mode::HelixNormal);
1330 cx.simulate_keystrokes("g e");
1331 cx.assert_state("<?«ˇphp»\n\n$someVariable = 2;", Mode::HelixNormal);
1332 cx.simulate_keystrokes("g e");
1333 cx.assert_state("«ˇ<?»php\n\n$someVariable = 2;", Mode::HelixNormal);
1334
1335 cx.set_state("fooBarBazˇ", Mode::HelixNormal);
1336 cx.simulate_keystrokes("g e");
1337 cx.assert_state("fooBar«ˇBaz»", Mode::HelixNormal);
1338 cx.simulate_keystrokes("g e");
1339 cx.assert_state("foo«ˇBar»Baz", Mode::HelixNormal);
1340 cx.simulate_keystrokes("g e");
1341 cx.assert_state("«ˇfoo»BarBaz", Mode::HelixNormal);
1342 }
1343
1344 #[gpui::test]
1345 async fn test_delete(cx: &mut gpui::TestAppContext) {
1346 let mut cx = VimTestContext::new(cx, true).await;
1347 cx.enable_helix();
1348
1349 // test delete a selection
1350 cx.set_state(
1351 indoc! {"
1352 The qu«ick ˇ»brown
1353 fox jumps over
1354 the lazy dog."},
1355 Mode::HelixNormal,
1356 );
1357
1358 cx.simulate_keystrokes("d");
1359
1360 cx.assert_state(
1361 indoc! {"
1362 The quˇbrown
1363 fox jumps over
1364 the lazy dog."},
1365 Mode::HelixNormal,
1366 );
1367
1368 // test deleting a single character
1369 cx.simulate_keystrokes("d");
1370
1371 cx.assert_state(
1372 indoc! {"
1373 The quˇrown
1374 fox jumps over
1375 the lazy dog."},
1376 Mode::HelixNormal,
1377 );
1378 }
1379
1380 #[gpui::test]
1381 async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
1382 let mut cx = VimTestContext::new(cx, true).await;
1383
1384 cx.set_state(
1385 indoc! {"
1386 The quick brownˇ
1387 fox jumps over
1388 the lazy dog."},
1389 Mode::HelixNormal,
1390 );
1391
1392 cx.simulate_keystrokes("d");
1393
1394 cx.assert_state(
1395 indoc! {"
1396 The quick brownˇfox jumps over
1397 the lazy dog."},
1398 Mode::HelixNormal,
1399 );
1400 }
1401
1402 // #[gpui::test]
1403 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
1404 // let mut cx = VimTestContext::new(cx, true).await;
1405
1406 // cx.set_state(
1407 // indoc! {"
1408 // The quick brown
1409 // fox jumps over
1410 // the lazy dog.ˇ"},
1411 // Mode::HelixNormal,
1412 // );
1413
1414 // cx.simulate_keystrokes("d");
1415
1416 // cx.assert_state(
1417 // indoc! {"
1418 // The quick brown
1419 // fox jumps over
1420 // the lazy dog.ˇ"},
1421 // Mode::HelixNormal,
1422 // );
1423 // }
1424
1425 #[gpui::test]
1426 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1427 let mut cx = VimTestContext::new(cx, true).await;
1428 cx.enable_helix();
1429
1430 cx.set_state(
1431 indoc! {"
1432 The quˇick brown
1433 fox jumps over
1434 the lazy dog."},
1435 Mode::HelixNormal,
1436 );
1437
1438 cx.simulate_keystrokes("f z");
1439
1440 cx.assert_state(
1441 indoc! {"
1442 The qu«ick brown
1443 fox jumps over
1444 the lazˇ»y dog."},
1445 Mode::HelixNormal,
1446 );
1447
1448 cx.simulate_keystrokes("F e F e");
1449
1450 cx.assert_state(
1451 indoc! {"
1452 The quick brown
1453 fox jumps ov«ˇer
1454 the» lazy dog."},
1455 Mode::HelixNormal,
1456 );
1457
1458 cx.simulate_keystrokes("e 2 F e");
1459
1460 cx.assert_state(
1461 indoc! {"
1462 Th«ˇe quick brown
1463 fox jumps over»
1464 the lazy dog."},
1465 Mode::HelixNormal,
1466 );
1467
1468 cx.simulate_keystrokes("t r t r");
1469
1470 cx.assert_state(
1471 indoc! {"
1472 The quick «brown
1473 fox jumps oveˇ»r
1474 the lazy dog."},
1475 Mode::HelixNormal,
1476 );
1477 }
1478
1479 #[gpui::test]
1480 async fn test_newline_char(cx: &mut gpui::TestAppContext) {
1481 let mut cx = VimTestContext::new(cx, true).await;
1482 cx.enable_helix();
1483
1484 cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
1485
1486 cx.simulate_keystroke("w");
1487
1488 cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
1489
1490 cx.set_state("aa«\nˇ»", Mode::HelixNormal);
1491
1492 cx.simulate_keystroke("b");
1493
1494 cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
1495 }
1496
1497 #[gpui::test]
1498 async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
1499 let mut cx = VimTestContext::new(cx, true).await;
1500 cx.enable_helix();
1501 cx.set_state(
1502 indoc! {"
1503 «The ˇ»quick brown
1504 fox jumps over
1505 the lazy dog."},
1506 Mode::HelixNormal,
1507 );
1508
1509 cx.simulate_keystrokes("i");
1510
1511 cx.assert_state(
1512 indoc! {"
1513 ˇThe quick brown
1514 fox jumps over
1515 the lazy dog."},
1516 Mode::Insert,
1517 );
1518 }
1519
1520 #[gpui::test]
1521 async fn test_append(cx: &mut gpui::TestAppContext) {
1522 let mut cx = VimTestContext::new(cx, true).await;
1523 cx.enable_helix();
1524 // test from the end of the selection
1525 cx.set_state(
1526 indoc! {"
1527 «Theˇ» quick brown
1528 fox jumps over
1529 the lazy dog."},
1530 Mode::HelixNormal,
1531 );
1532
1533 cx.simulate_keystrokes("a");
1534
1535 cx.assert_state(
1536 indoc! {"
1537 Theˇ quick brown
1538 fox jumps over
1539 the lazy dog."},
1540 Mode::Insert,
1541 );
1542
1543 // test from the beginning of the selection
1544 cx.set_state(
1545 indoc! {"
1546 «ˇThe» quick brown
1547 fox jumps over
1548 the lazy dog."},
1549 Mode::HelixNormal,
1550 );
1551
1552 cx.simulate_keystrokes("a");
1553
1554 cx.assert_state(
1555 indoc! {"
1556 Theˇ quick brown
1557 fox jumps over
1558 the lazy dog."},
1559 Mode::Insert,
1560 );
1561 }
1562
1563 #[gpui::test]
1564 async fn test_replace(cx: &mut gpui::TestAppContext) {
1565 let mut cx = VimTestContext::new(cx, true).await;
1566 cx.enable_helix();
1567
1568 // No selection (single character)
1569 cx.set_state("ˇaa", Mode::HelixNormal);
1570
1571 cx.simulate_keystrokes("r x");
1572
1573 cx.assert_state("ˇxa", Mode::HelixNormal);
1574
1575 // Cursor at the beginning
1576 cx.set_state("«ˇaa»", Mode::HelixNormal);
1577
1578 cx.simulate_keystrokes("r x");
1579
1580 cx.assert_state("«ˇxx»", Mode::HelixNormal);
1581
1582 // Cursor at the end
1583 cx.set_state("«aaˇ»", Mode::HelixNormal);
1584
1585 cx.simulate_keystrokes("r x");
1586
1587 cx.assert_state("«xxˇ»", Mode::HelixNormal);
1588 }
1589
1590 #[gpui::test]
1591 async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
1592 let mut cx = VimTestContext::new(cx, true).await;
1593 cx.enable_helix();
1594
1595 // Test yanking current character with no selection
1596 cx.set_state("hello ˇworld", Mode::HelixNormal);
1597 cx.simulate_keystrokes("y");
1598
1599 // Test cursor remains at the same position after yanking single character
1600 cx.assert_state("hello ˇworld", Mode::HelixNormal);
1601 cx.shared_clipboard().assert_eq("w");
1602
1603 // Move cursor and yank another character
1604 cx.simulate_keystrokes("l");
1605 cx.simulate_keystrokes("y");
1606 cx.shared_clipboard().assert_eq("o");
1607
1608 // Test yanking with existing selection
1609 cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
1610 cx.simulate_keystrokes("y");
1611 cx.shared_clipboard().assert_eq("worl");
1612 cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
1613
1614 // Test yanking in select mode character by character
1615 cx.set_state("hello ˇworld", Mode::HelixNormal);
1616 cx.simulate_keystroke("v");
1617 cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
1618 cx.simulate_keystroke("y");
1619 cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
1620 cx.shared_clipboard().assert_eq("w");
1621 }
1622
1623 #[gpui::test]
1624 async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
1625 let mut cx = VimTestContext::new(cx, true).await;
1626 cx.enable_helix();
1627
1628 // First copy some text to clipboard
1629 cx.set_state("«hello worldˇ»", Mode::HelixNormal);
1630 cx.simulate_keystrokes("y");
1631
1632 // Test paste with shift-r on single cursor
1633 cx.set_state("foo ˇbar", Mode::HelixNormal);
1634 cx.simulate_keystrokes("shift-r");
1635
1636 cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
1637
1638 // Test paste with shift-r on selection
1639 cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
1640 cx.simulate_keystrokes("shift-r");
1641
1642 cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
1643 }
1644
1645 #[gpui::test]
1646 async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
1647 let mut cx = VimTestContext::new(cx, true).await;
1648
1649 assert_eq!(cx.mode(), Mode::Normal);
1650 cx.enable_helix();
1651
1652 cx.simulate_keystrokes("v");
1653 assert_eq!(cx.mode(), Mode::HelixSelect);
1654 cx.simulate_keystrokes("escape");
1655 assert_eq!(cx.mode(), Mode::HelixNormal);
1656 }
1657
1658 #[gpui::test]
1659 async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
1660 let mut cx = VimTestContext::new(cx, true).await;
1661 cx.enable_helix();
1662
1663 // Make a modification at a specific location
1664 cx.set_state("ˇhello", Mode::HelixNormal);
1665 assert_eq!(cx.mode(), Mode::HelixNormal);
1666 cx.simulate_keystrokes("i");
1667 assert_eq!(cx.mode(), Mode::Insert);
1668 cx.simulate_keystrokes("escape");
1669 assert_eq!(cx.mode(), Mode::HelixNormal);
1670 }
1671
1672 #[gpui::test]
1673 async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
1674 let mut cx = VimTestContext::new(cx, true).await;
1675 cx.enable_helix();
1676
1677 // Make a modification at a specific location
1678 cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1679 cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1680 cx.simulate_keystrokes("i");
1681 cx.simulate_keystrokes("escape");
1682 cx.simulate_keystrokes("i");
1683 cx.simulate_keystrokes("m o d i f i e d space");
1684 cx.simulate_keystrokes("escape");
1685
1686 // TODO: this fails, because state is no longer helix
1687 cx.assert_state(
1688 "line one\nline modified ˇtwo\nline three",
1689 Mode::HelixNormal,
1690 );
1691
1692 // Move cursor away from the modification
1693 cx.simulate_keystrokes("up");
1694
1695 // Use "g ." to go back to last modification
1696 cx.simulate_keystrokes("g .");
1697
1698 // Verify we're back at the modification location and still in HelixNormal mode
1699 cx.assert_state(
1700 "line one\nline modifiedˇ two\nline three",
1701 Mode::HelixNormal,
1702 );
1703 }
1704
1705 #[gpui::test]
1706 async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
1707 let mut cx = VimTestContext::new(cx, true).await;
1708 cx.set_state(
1709 "line one\nline ˇtwo\nline three\nline four",
1710 Mode::HelixNormal,
1711 );
1712 cx.simulate_keystrokes("2 x");
1713 cx.assert_state(
1714 "line one\n«line two\nline three\nˇ»line four",
1715 Mode::HelixNormal,
1716 );
1717
1718 // Test extending existing line selection
1719 cx.set_state(
1720 indoc! {"
1721 li«ˇne one
1722 li»ne two
1723 line three
1724 line four"},
1725 Mode::HelixNormal,
1726 );
1727 cx.simulate_keystrokes("x");
1728 cx.assert_state(
1729 indoc! {"
1730 «line one
1731 line two
1732 ˇ»line three
1733 line four"},
1734 Mode::HelixNormal,
1735 );
1736
1737 // Pressing x in empty line, select next line (because helix considers cursor a selection)
1738 cx.set_state(
1739 indoc! {"
1740 line one
1741 ˇ
1742 line three
1743 line four
1744 line five
1745 line six"},
1746 Mode::HelixNormal,
1747 );
1748 cx.simulate_keystrokes("x");
1749 cx.assert_state(
1750 indoc! {"
1751 line one
1752 «
1753 line three
1754 ˇ»line four
1755 line five
1756 line six"},
1757 Mode::HelixNormal,
1758 );
1759
1760 // Another x should only select the next line
1761 cx.simulate_keystrokes("x");
1762 cx.assert_state(
1763 indoc! {"
1764 line one
1765 «
1766 line three
1767 line four
1768 ˇ»line five
1769 line six"},
1770 Mode::HelixNormal,
1771 );
1772
1773 // Empty line with count selects extra + count lines
1774 cx.set_state(
1775 indoc! {"
1776 line one
1777 ˇ
1778 line three
1779 line four
1780 line five"},
1781 Mode::HelixNormal,
1782 );
1783 cx.simulate_keystrokes("2 x");
1784 cx.assert_state(
1785 indoc! {"
1786 line one
1787 «
1788 line three
1789 line four
1790 ˇ»line five"},
1791 Mode::HelixNormal,
1792 );
1793
1794 // Compare empty vs non-empty line behavior
1795 cx.set_state(
1796 indoc! {"
1797 ˇnon-empty line
1798 line two
1799 line three"},
1800 Mode::HelixNormal,
1801 );
1802 cx.simulate_keystrokes("x");
1803 cx.assert_state(
1804 indoc! {"
1805 «non-empty line
1806 ˇ»line two
1807 line three"},
1808 Mode::HelixNormal,
1809 );
1810
1811 // Same test but with empty line - should select one extra
1812 cx.set_state(
1813 indoc! {"
1814 ˇ
1815 line two
1816 line three"},
1817 Mode::HelixNormal,
1818 );
1819 cx.simulate_keystrokes("x");
1820 cx.assert_state(
1821 indoc! {"
1822 «
1823 line two
1824 ˇ»line three"},
1825 Mode::HelixNormal,
1826 );
1827
1828 // Test selecting multiple lines with count
1829 cx.set_state(
1830 indoc! {"
1831 ˇline one
1832 line two
1833 line threeˇ
1834 line four
1835 line five"},
1836 Mode::HelixNormal,
1837 );
1838 cx.simulate_keystrokes("x");
1839 cx.assert_state(
1840 indoc! {"
1841 «line one
1842 ˇ»line two
1843 «line three
1844 ˇ»line four
1845 line five"},
1846 Mode::HelixNormal,
1847 );
1848 cx.simulate_keystrokes("x");
1849 // Adjacent line selections stay separate (not merged)
1850 cx.assert_state(
1851 indoc! {"
1852 «line one
1853 line two
1854 ˇ»«line three
1855 line four
1856 ˇ»line five"},
1857 Mode::HelixNormal,
1858 );
1859
1860 // Test selecting with an empty line below the current line
1861 cx.set_state(
1862 indoc! {"
1863 line one
1864 line twoˇ
1865
1866 line four
1867 line five"},
1868 Mode::HelixNormal,
1869 );
1870 cx.simulate_keystrokes("x");
1871 cx.assert_state(
1872 indoc! {"
1873 line one
1874 «line two
1875 ˇ»
1876 line four
1877 line five"},
1878 Mode::HelixNormal,
1879 );
1880 cx.simulate_keystrokes("x");
1881 cx.assert_state(
1882 indoc! {"
1883 line one
1884 «line two
1885
1886 ˇ»line four
1887 line five"},
1888 Mode::HelixNormal,
1889 );
1890 cx.simulate_keystrokes("x");
1891 cx.assert_state(
1892 indoc! {"
1893 line one
1894 «line two
1895
1896 line four
1897 ˇ»line five"},
1898 Mode::HelixNormal,
1899 );
1900 }
1901
1902 #[gpui::test]
1903 async fn test_helix_insert_before_after_select_lines(cx: &mut gpui::TestAppContext) {
1904 let mut cx = VimTestContext::new(cx, true).await;
1905
1906 cx.set_state(
1907 "line one\nline ˇtwo\nline three\nline four",
1908 Mode::HelixNormal,
1909 );
1910 cx.simulate_keystrokes("2 x");
1911 cx.assert_state(
1912 "line one\n«line two\nline three\nˇ»line four",
1913 Mode::HelixNormal,
1914 );
1915 cx.simulate_keystrokes("o");
1916 cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1917
1918 cx.set_state(
1919 "line one\nline ˇtwo\nline three\nline four",
1920 Mode::HelixNormal,
1921 );
1922 cx.simulate_keystrokes("2 x");
1923 cx.assert_state(
1924 "line one\n«line two\nline three\nˇ»line four",
1925 Mode::HelixNormal,
1926 );
1927 cx.simulate_keystrokes("shift-o");
1928 cx.assert_state("line one\nˇ\nline two\nline three\nline four", Mode::Insert);
1929 }
1930
1931 #[gpui::test]
1932 async fn test_helix_insert_before_after_helix_select(cx: &mut gpui::TestAppContext) {
1933 let mut cx = VimTestContext::new(cx, true).await;
1934 cx.enable_helix();
1935
1936 // Test new line in selection direction
1937 cx.set_state(
1938 "ˇline one\nline two\nline three\nline four",
1939 Mode::HelixNormal,
1940 );
1941 cx.simulate_keystrokes("v j j");
1942 cx.assert_state(
1943 "«line one\nline two\nlˇ»ine three\nline four",
1944 Mode::HelixSelect,
1945 );
1946 cx.simulate_keystrokes("o");
1947 cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1948
1949 cx.set_state(
1950 "line one\nline two\nˇline three\nline four",
1951 Mode::HelixNormal,
1952 );
1953 cx.simulate_keystrokes("v k k");
1954 cx.assert_state(
1955 "«ˇline one\nline two\nl»ine three\nline four",
1956 Mode::HelixSelect,
1957 );
1958 cx.simulate_keystrokes("shift-o");
1959 cx.assert_state("ˇ\nline one\nline two\nline three\nline four", Mode::Insert);
1960
1961 // Test new line in opposite selection direction
1962 cx.set_state(
1963 "ˇline one\nline two\nline three\nline four",
1964 Mode::HelixNormal,
1965 );
1966 cx.simulate_keystrokes("v j j");
1967 cx.assert_state(
1968 "«line one\nline two\nlˇ»ine three\nline four",
1969 Mode::HelixSelect,
1970 );
1971 cx.simulate_keystrokes("shift-o");
1972 cx.assert_state("ˇ\nline one\nline two\nline three\nline four", Mode::Insert);
1973
1974 cx.set_state(
1975 "line one\nline two\nˇline three\nline four",
1976 Mode::HelixNormal,
1977 );
1978 cx.simulate_keystrokes("v k k");
1979 cx.assert_state(
1980 "«ˇline one\nline two\nl»ine three\nline four",
1981 Mode::HelixSelect,
1982 );
1983 cx.simulate_keystrokes("o");
1984 cx.assert_state("line one\nline two\nline three\nˇ\nline four", Mode::Insert);
1985 }
1986
1987 #[gpui::test]
1988 async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
1989 let mut cx = VimTestContext::new(cx, true).await;
1990
1991 assert_eq!(cx.mode(), Mode::Normal);
1992 cx.enable_helix();
1993
1994 cx.set_state("ˇhello", Mode::HelixNormal);
1995 cx.simulate_keystrokes("l v l l");
1996 cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
1997 }
1998
1999 #[gpui::test]
2000 async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
2001 let mut cx = VimTestContext::new(cx, true).await;
2002
2003 assert_eq!(cx.mode(), Mode::Normal);
2004 cx.enable_helix();
2005
2006 // Start with multiple cursors (no selections)
2007 cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
2008
2009 // Enter select mode and move right twice
2010 cx.simulate_keystrokes("v l l");
2011
2012 // Each cursor should independently create and extend its own selection
2013 cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
2014 }
2015
2016 #[gpui::test]
2017 async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
2018 let mut cx = VimTestContext::new(cx, true).await;
2019
2020 cx.set_state("ˇone two", Mode::Normal);
2021 cx.simulate_keystrokes("v w");
2022 cx.assert_state("«one tˇ»wo", Mode::Visual);
2023
2024 // In Vim, this selects "t". In helix selections stops just before "t"
2025
2026 cx.enable_helix();
2027 cx.set_state("ˇone two", Mode::HelixNormal);
2028 cx.simulate_keystrokes("v w");
2029 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
2030 }
2031
2032 #[gpui::test]
2033 async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) {
2034 let mut cx = VimTestContext::new(cx, true).await;
2035
2036 cx.set_state("ˇone two", Mode::Normal);
2037 cx.simulate_keystrokes("v w");
2038 cx.assert_state("«one tˇ»wo", Mode::Visual);
2039 cx.simulate_keystrokes("escape");
2040 cx.assert_state("one ˇtwo", Mode::Normal);
2041
2042 cx.enable_helix();
2043 cx.set_state("ˇone two", Mode::HelixNormal);
2044 cx.simulate_keystrokes("v w");
2045 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
2046 cx.simulate_keystrokes("escape");
2047 cx.assert_state("«one ˇ»two", Mode::HelixNormal);
2048 }
2049
2050 #[gpui::test]
2051 async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) {
2052 let mut cx = VimTestContext::new(cx, true).await;
2053 cx.enable_helix();
2054
2055 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
2056 cx.simulate_keystrokes("w");
2057 cx.assert_state("«one ˇ»two three", Mode::HelixSelect);
2058
2059 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
2060 cx.simulate_keystrokes("e");
2061 cx.assert_state("«oneˇ» two three", Mode::HelixSelect);
2062 }
2063
2064 #[gpui::test]
2065 async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) {
2066 let mut cx = VimTestContext::new(cx, true).await;
2067 cx.enable_helix();
2068
2069 cx.set_state("ˇone two three", Mode::HelixNormal);
2070 cx.simulate_keystrokes("l l v h h h");
2071 cx.assert_state("«ˇone» two three", Mode::HelixSelect);
2072 }
2073
2074 #[gpui::test]
2075 async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
2076 let mut cx = VimTestContext::new(cx, true).await;
2077 cx.enable_helix();
2078
2079 cx.set_state("ˇone two one", Mode::HelixNormal);
2080 cx.simulate_keystrokes("x");
2081 cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
2082 cx.simulate_keystrokes("s o n e");
2083 cx.run_until_parked();
2084 cx.simulate_keystrokes("enter");
2085 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
2086
2087 cx.simulate_keystrokes("x");
2088 cx.simulate_keystrokes("s");
2089 cx.run_until_parked();
2090 cx.simulate_keystrokes("enter");
2091 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
2092
2093 // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
2094 // cx.set_state("ˇstuff one two one", Mode::HelixNormal);
2095 // cx.simulate_keystrokes("s o n e enter");
2096 // cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
2097 }
2098
2099 #[gpui::test]
2100 async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
2101 let mut cx = VimTestContext::new(cx, true).await;
2102
2103 cx.set_state("ˇhello two one two one two one", Mode::Visual);
2104 cx.simulate_keystrokes("/ o n e");
2105 cx.simulate_keystrokes("enter");
2106 cx.simulate_keystrokes("n n");
2107 cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
2108
2109 cx.set_state("ˇhello two one two one two one", Mode::Normal);
2110 cx.simulate_keystrokes("/ o n e");
2111 cx.simulate_keystrokes("enter");
2112 cx.simulate_keystrokes("n n");
2113 cx.assert_state("hello two one two one two ˇone", Mode::Normal);
2114
2115 cx.set_state("ˇhello two one two one two one", Mode::Normal);
2116 cx.simulate_keystrokes("/ o n e");
2117 cx.simulate_keystrokes("enter");
2118 cx.simulate_keystrokes("n g n g n");
2119 cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
2120
2121 cx.enable_helix();
2122
2123 cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
2124 cx.simulate_keystrokes("/ o n e");
2125 cx.simulate_keystrokes("enter");
2126 cx.simulate_keystrokes("n n");
2127 cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
2128
2129 cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
2130 cx.simulate_keystrokes("/ o n e");
2131 cx.simulate_keystrokes("enter");
2132 cx.simulate_keystrokes("n n");
2133 cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
2134 }
2135
2136 #[gpui::test]
2137 async fn test_helix_select_next_match_wrapping(cx: &mut gpui::TestAppContext) {
2138 let mut cx = VimTestContext::new(cx, true).await;
2139 cx.enable_helix();
2140
2141 // Three occurrences of "one". After selecting all three with `n n`,
2142 // pressing `n` again wraps the search to the first occurrence.
2143 // The prior selections (at higher offsets) are chained before the
2144 // wrapped selection (at a lower offset), producing unsorted anchors
2145 // that cause `rope::Cursor::summary` to panic with
2146 // "cannot summarize backward".
2147 cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
2148 cx.simulate_keystrokes("/ o n e");
2149 cx.simulate_keystrokes("enter");
2150 cx.simulate_keystrokes("n n n");
2151 // Should not panic; all three occurrences should remain selected.
2152 cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
2153 }
2154
2155 #[gpui::test]
2156 async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
2157 let mut cx = VimTestContext::new(cx, true).await;
2158
2159 cx.set_state("ˇone two", Mode::HelixNormal);
2160 cx.simulate_keystrokes("c");
2161 cx.assert_state("ˇne two", Mode::Insert);
2162
2163 cx.set_state("«oneˇ» two", Mode::HelixNormal);
2164 cx.simulate_keystrokes("c");
2165 cx.assert_state("ˇ two", Mode::Insert);
2166
2167 cx.set_state(
2168 indoc! {"
2169 oneˇ two
2170 three
2171 "},
2172 Mode::HelixNormal,
2173 );
2174 cx.simulate_keystrokes("x c");
2175 cx.assert_state(
2176 indoc! {"
2177 ˇ
2178 three
2179 "},
2180 Mode::Insert,
2181 );
2182
2183 cx.set_state(
2184 indoc! {"
2185 one twoˇ
2186 three
2187 "},
2188 Mode::HelixNormal,
2189 );
2190 cx.simulate_keystrokes("c");
2191 cx.assert_state(
2192 indoc! {"
2193 one twoˇthree
2194 "},
2195 Mode::Insert,
2196 );
2197
2198 // Helix doesn't set the cursor to the first non-blank one when
2199 // replacing lines: it uses language-dependent indent queries instead.
2200 cx.set_state(
2201 indoc! {"
2202 one two
2203 « indented
2204 three not indentedˇ»
2205 "},
2206 Mode::HelixNormal,
2207 );
2208 cx.simulate_keystrokes("c");
2209 cx.set_state(
2210 indoc! {"
2211 one two
2212 ˇ
2213 "},
2214 Mode::Insert,
2215 );
2216 }
2217
2218 #[gpui::test]
2219 async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
2220 let mut cx = VimTestContext::new(cx, true).await;
2221 cx.enable_helix();
2222
2223 // Test g l moves to last character, not after it
2224 cx.set_state("hello ˇworld!", Mode::HelixNormal);
2225 cx.simulate_keystrokes("g l");
2226 cx.assert_state("hello worldˇ!", Mode::HelixNormal);
2227
2228 // Test with Chinese characters, test if work with UTF-8?
2229 cx.set_state("ˇ你好世界", Mode::HelixNormal);
2230 cx.simulate_keystrokes("g l");
2231 cx.assert_state("你好世ˇ界", Mode::HelixNormal);
2232
2233 // Test with end of line
2234 cx.set_state("endˇ", Mode::HelixNormal);
2235 cx.simulate_keystrokes("g l");
2236 cx.assert_state("enˇd", Mode::HelixNormal);
2237
2238 // Test with empty line
2239 cx.set_state(
2240 indoc! {"
2241 hello
2242 ˇ
2243 world"},
2244 Mode::HelixNormal,
2245 );
2246 cx.simulate_keystrokes("g l");
2247 cx.assert_state(
2248 indoc! {"
2249 hello
2250 ˇ
2251 world"},
2252 Mode::HelixNormal,
2253 );
2254
2255 // Test with multiple lines
2256 cx.set_state(
2257 indoc! {"
2258 ˇfirst line
2259 second line
2260 third line"},
2261 Mode::HelixNormal,
2262 );
2263 cx.simulate_keystrokes("g l");
2264 cx.assert_state(
2265 indoc! {"
2266 first linˇe
2267 second line
2268 third line"},
2269 Mode::HelixNormal,
2270 );
2271 }
2272
2273 #[gpui::test]
2274 async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) {
2275 VimTestContext::init(cx);
2276
2277 let fs = FakeFs::new(cx.background_executor.clone());
2278 fs.insert_tree(
2279 path!("/dir"),
2280 json!({
2281 "file_a.rs": "// File A.",
2282 "file_b.rs": "// File B.",
2283 }),
2284 )
2285 .await;
2286
2287 let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
2288 let window_handle =
2289 cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2290 let workspace = window_handle
2291 .read_with(cx, |mw, _| mw.workspace().clone())
2292 .unwrap();
2293
2294 cx.update(|cx| {
2295 VimTestContext::init_keybindings(true, cx);
2296 SettingsStore::update_global(cx, |store, cx| {
2297 store.update_user_settings(cx, |store| store.helix_mode = Some(true));
2298 })
2299 });
2300
2301 let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
2302
2303 workspace.update_in(cx, |workspace, window, cx| {
2304 ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx)
2305 });
2306
2307 let search_view = workspace.update_in(cx, |workspace, _, cx| {
2308 workspace
2309 .active_pane()
2310 .read(cx)
2311 .items()
2312 .find_map(|item| item.downcast::<ProjectSearchView>())
2313 .expect("Project search view should be active")
2314 });
2315
2316 project_search::perform_project_search(&search_view, "File A", cx);
2317
2318 search_view.update(cx, |search_view, cx| {
2319 let vim_mode = search_view
2320 .results_editor()
2321 .read(cx)
2322 .addon::<VimAddon>()
2323 .map(|addon| addon.entity.read(cx).mode);
2324
2325 assert_eq!(vim_mode, Some(Mode::HelixNormal));
2326 });
2327 }
2328
2329 #[gpui::test]
2330 async fn test_scroll_with_selection(cx: &mut gpui::TestAppContext) {
2331 let mut cx = VimTestContext::new(cx, true).await;
2332 cx.enable_helix();
2333
2334 // Start with a selection
2335 cx.set_state(
2336 indoc! {"
2337 «lineˇ» one
2338 line two
2339 line three
2340 line four
2341 line five"},
2342 Mode::HelixNormal,
2343 );
2344
2345 // Scroll down, selection should collapse
2346 cx.simulate_keystrokes("ctrl-d");
2347 cx.assert_state(
2348 indoc! {"
2349 line one
2350 line two
2351 line three
2352 line four
2353 line fiveˇ"},
2354 Mode::HelixNormal,
2355 );
2356
2357 // Make a new selection
2358 cx.simulate_keystroke("b");
2359 cx.assert_state(
2360 indoc! {"
2361 line one
2362 line two
2363 line three
2364 line four
2365 line «ˇfive»"},
2366 Mode::HelixNormal,
2367 );
2368
2369 // And scroll up, once again collapsing the selection.
2370 cx.simulate_keystroke("ctrl-u");
2371 cx.assert_state(
2372 indoc! {"
2373 line one
2374 line two
2375 line three
2376 line ˇfour
2377 line five"},
2378 Mode::HelixNormal,
2379 );
2380
2381 // Enter select mode
2382 cx.simulate_keystroke("v");
2383 cx.assert_state(
2384 indoc! {"
2385 line one
2386 line two
2387 line three
2388 line «fˇ»our
2389 line five"},
2390 Mode::HelixSelect,
2391 );
2392
2393 // And now the selection should be kept/expanded.
2394 cx.simulate_keystroke("ctrl-d");
2395 cx.assert_state(
2396 indoc! {"
2397 line one
2398 line two
2399 line three
2400 line «four
2401 line fiveˇ»"},
2402 Mode::HelixSelect,
2403 );
2404 }
2405
2406 #[gpui::test]
2407 async fn test_helix_insert_end_of_line(cx: &mut gpui::TestAppContext) {
2408 let mut cx = VimTestContext::new(cx, true).await;
2409 cx.enable_helix();
2410
2411 // Ensure that, when lines are selected using `x`, pressing `shift-a`
2412 // actually puts the cursor at the end of the selected lines and not at
2413 // the end of the line below.
2414 cx.set_state(
2415 indoc! {"
2416 line oˇne
2417 line two"},
2418 Mode::HelixNormal,
2419 );
2420
2421 cx.simulate_keystrokes("x");
2422 cx.assert_state(
2423 indoc! {"
2424 «line one
2425 ˇ»line two"},
2426 Mode::HelixNormal,
2427 );
2428
2429 cx.simulate_keystrokes("shift-a");
2430 cx.assert_state(
2431 indoc! {"
2432 line oneˇ
2433 line two"},
2434 Mode::Insert,
2435 );
2436
2437 cx.set_state(
2438 indoc! {"
2439 line «one
2440 lineˇ» two"},
2441 Mode::HelixNormal,
2442 );
2443
2444 cx.simulate_keystrokes("shift-a");
2445 cx.assert_state(
2446 indoc! {"
2447 line one
2448 line twoˇ"},
2449 Mode::Insert,
2450 );
2451 }
2452
2453 #[gpui::test]
2454 async fn test_helix_replace_uses_graphemes(cx: &mut gpui::TestAppContext) {
2455 let mut cx = VimTestContext::new(cx, true).await;
2456 cx.enable_helix();
2457
2458 cx.set_state("«Hällöˇ» Wörld", Mode::HelixNormal);
2459 cx.simulate_keystrokes("r 1");
2460 cx.assert_state("«11111ˇ» Wörld", Mode::HelixNormal);
2461
2462 cx.set_state("«e\u{301}ˇ»", Mode::HelixNormal);
2463 cx.simulate_keystrokes("r 1");
2464 cx.assert_state("«1ˇ»", Mode::HelixNormal);
2465
2466 cx.set_state("«🙂ˇ»", Mode::HelixNormal);
2467 cx.simulate_keystrokes("r 1");
2468 cx.assert_state("«1ˇ»", Mode::HelixNormal);
2469 }
2470}