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