1mod boundary;
2mod duplicate;
3mod object;
4mod paste;
5mod select;
6mod surround;
7
8use editor::display_map::DisplaySnapshot;
9use editor::{
10 DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset,
11 SelectionEffects, ToOffset, ToPoint, movement,
12};
13use gpui::actions;
14use gpui::{Context, Window};
15use language::{CharClassifier, CharKind, Point};
16use search::{BufferSearchBar, SearchOptions};
17use settings::Settings;
18use text::{Bias, SelectionGoal};
19use workspace::searchable::FilteredSearchRange;
20use workspace::searchable::{self, Direction};
21
22use crate::motion::{self, MotionKind};
23use crate::state::{Operator, SearchState};
24use crate::{
25 PushHelixSurroundAdd, PushHelixSurroundDelete, PushHelixSurroundReplace, Vim,
26 motion::{Motion, right},
27 state::Mode,
28};
29
30actions!(
31 vim,
32 [
33 /// Yanks the current selection or character if no selection.
34 HelixYank,
35 /// Inserts at the beginning of the selection.
36 HelixInsert,
37 /// Appends at the end of the selection.
38 HelixAppend,
39 /// Inserts at the end of the current Helix cursor line.
40 HelixInsertEndOfLine,
41 /// Goes to the location of the last modification.
42 HelixGotoLastModification,
43 /// Select entire line or multiple lines, extending downwards.
44 HelixSelectLine,
45 /// Select all matches of a given pattern within the current selection.
46 HelixSelectRegex,
47 /// Removes all but the one selection that was created last.
48 /// `Newest` can eventually be `Primary`.
49 HelixKeepNewestSelection,
50 /// Copies all selections below.
51 HelixDuplicateBelow,
52 /// Copies all selections above.
53 HelixDuplicateAbove,
54 /// Delete the selection and enter edit mode.
55 HelixSubstitute,
56 /// Delete the selection and enter edit mode, without yanking the selection.
57 HelixSubstituteNoYank,
58 /// Select the next match for the current search query.
59 HelixSelectNext,
60 /// Select the previous match for the current search query.
61 HelixSelectPrevious,
62 ]
63);
64
65pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
66 Vim::action(editor, cx, Vim::helix_select_lines);
67 Vim::action(editor, cx, Vim::helix_insert);
68 Vim::action(editor, cx, Vim::helix_append);
69 Vim::action(editor, cx, Vim::helix_insert_end_of_line);
70 Vim::action(editor, cx, Vim::helix_yank);
71 Vim::action(editor, cx, Vim::helix_goto_last_modification);
72 Vim::action(editor, cx, Vim::helix_paste);
73 Vim::action(editor, cx, Vim::helix_select_regex);
74 Vim::action(editor, cx, Vim::helix_keep_newest_selection);
75 Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| {
76 let times = Vim::take_count(cx);
77 vim.helix_duplicate_selections_below(times, window, cx);
78 });
79 Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| {
80 let times = Vim::take_count(cx);
81 vim.helix_duplicate_selections_above(times, window, cx);
82 });
83 Vim::action(editor, cx, Vim::helix_substitute);
84 Vim::action(editor, cx, Vim::helix_substitute_no_yank);
85 Vim::action(editor, cx, Vim::helix_select_next);
86 Vim::action(editor, cx, Vim::helix_select_previous);
87 Vim::action(editor, cx, |vim, _: &PushHelixSurroundAdd, window, cx| {
88 vim.clear_operator(window, cx);
89 vim.push_operator(Operator::HelixSurroundAdd, window, cx);
90 });
91 Vim::action(
92 editor,
93 cx,
94 |vim, _: &PushHelixSurroundReplace, window, cx| {
95 vim.clear_operator(window, cx);
96 vim.push_operator(
97 Operator::HelixSurroundReplace {
98 replaced_char: None,
99 },
100 window,
101 cx,
102 );
103 },
104 );
105 Vim::action(
106 editor,
107 cx,
108 |vim, _: &PushHelixSurroundDelete, window, cx| {
109 vim.clear_operator(window, cx);
110 vim.push_operator(Operator::HelixSurroundDelete, window, cx);
111 },
112 );
113}
114
115impl Vim {
116 pub fn helix_normal_motion(
117 &mut self,
118 motion: Motion,
119 times: Option<usize>,
120 window: &mut Window,
121 cx: &mut Context<Self>,
122 ) {
123 self.helix_move_cursor(motion, times, window, cx);
124 }
125
126 pub fn helix_select_motion(
127 &mut self,
128 motion: Motion,
129 times: Option<usize>,
130 window: &mut Window,
131 cx: &mut Context<Self>,
132 ) {
133 self.update_editor(cx, |_, editor, cx| {
134 let text_layout_details = editor.text_layout_details(window, cx);
135 editor.change_selections(Default::default(), window, cx, |s| {
136 if let Motion::ZedSearchResult { new_selections, .. } = &motion {
137 s.select_anchor_ranges(new_selections.clone());
138 return;
139 };
140
141 s.move_with(&mut |map, selection| {
142 let was_reversed = selection.reversed;
143 let mut current_head = selection.head();
144
145 // our motions assume the current character is after the cursor,
146 // but in (forward) visual mode the current character is just
147 // before the end of the selection.
148
149 // If the file ends with a newline (which is common) we don't do this.
150 // so that if you go to the end of such a file you can use "up" to go
151 // to the previous line and have it work somewhat as expected.
152 if !selection.reversed
153 && !selection.is_empty()
154 && !(selection.end.column() == 0 && selection.end == map.max_point())
155 {
156 current_head = movement::left(map, selection.end)
157 }
158
159 let (new_head, goal) = match motion {
160 // Going to next word start is special cased
161 // since Vim differs from Helix in that motion
162 // Vim: `w` goes to the first character of a word
163 // Helix: `w` goes to the character before a word
164 Motion::NextWordStart { ignore_punctuation } => {
165 let mut head = movement::right(map, current_head);
166 let classifier =
167 map.buffer_snapshot().char_classifier_at(head.to_point(map));
168 for _ in 0..times.unwrap_or(1) {
169 let (_, new_head) =
170 movement::find_boundary_trail(map, head, &mut |left, right| {
171 Self::is_boundary_right(ignore_punctuation)(
172 left,
173 right,
174 &classifier,
175 )
176 });
177 head = new_head;
178 }
179 head = movement::left(map, head);
180 (head, SelectionGoal::None)
181 }
182 _ => motion
183 .move_point(
184 map,
185 current_head,
186 selection.goal,
187 times,
188 &text_layout_details,
189 )
190 .unwrap_or((current_head, selection.goal)),
191 };
192
193 selection.set_head(new_head, goal);
194
195 // ensure the current character is included in the selection.
196 if !selection.reversed {
197 let next_point = movement::right(map, selection.end);
198
199 if !(next_point.column() == 0 && next_point == map.max_point()) {
200 selection.end = next_point;
201 }
202 }
203
204 // vim always ensures the anchor character stays selected.
205 // if our selection has reversed, we need to move the opposite end
206 // to ensure the anchor is still selected.
207 if was_reversed && !selection.reversed {
208 selection.start = movement::left(map, selection.start);
209 } else if !was_reversed && selection.reversed {
210 selection.end = movement::right(map, selection.end);
211 }
212 })
213 });
214 });
215 }
216
217 /// Updates all selections based on where the cursors are.
218 fn helix_new_selections(
219 &mut self,
220 window: &mut Window,
221 cx: &mut Context<Self>,
222 change: &mut dyn FnMut(
223 // the start of the cursor
224 DisplayPoint,
225 &DisplaySnapshot,
226 ) -> Option<(DisplayPoint, DisplayPoint)>,
227 ) {
228 self.update_editor(cx, |_, editor, cx| {
229 editor.change_selections(Default::default(), window, cx, |s| {
230 s.move_with(&mut |map, selection| {
231 let cursor_start = if selection.reversed || selection.is_empty() {
232 selection.head()
233 } else {
234 movement::left(map, selection.head())
235 };
236 let Some((head, tail)) = change(cursor_start, map) else {
237 return;
238 };
239
240 selection.set_head_tail(head, tail, SelectionGoal::None);
241 });
242 });
243 });
244 }
245
246 fn helix_find_range_forward(
247 &mut self,
248 times: Option<usize>,
249 window: &mut Window,
250 cx: &mut Context<Self>,
251 is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool,
252 ) {
253 let times = times.unwrap_or(1);
254 self.helix_new_selections(window, cx, &mut |cursor, map| {
255 let mut head = movement::right(map, cursor);
256 let mut tail = cursor;
257 let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map));
258 if head == map.max_point() {
259 return None;
260 }
261 for _ in 0..times {
262 let (maybe_next_tail, next_head) =
263 movement::find_boundary_trail(map, head, &mut |left, right| {
264 is_boundary(left, right, &classifier)
265 });
266
267 if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
268 break;
269 }
270
271 head = next_head;
272 if let Some(next_tail) = maybe_next_tail {
273 tail = next_tail;
274 }
275 }
276 Some((head, tail))
277 });
278 }
279
280 fn helix_find_range_backward(
281 &mut self,
282 times: Option<usize>,
283 window: &mut Window,
284 cx: &mut Context<Self>,
285 is_boundary: &mut dyn FnMut(char, char, &CharClassifier) -> bool,
286 ) {
287 let times = times.unwrap_or(1);
288 self.helix_new_selections(window, cx, &mut |cursor, map| {
289 let mut head = cursor;
290 // The original cursor was one character wide,
291 // but the search starts from the left side of it,
292 // so to include that space the selection must end one character to the right.
293 let mut tail = movement::right(map, cursor);
294 let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map));
295 if head == DisplayPoint::zero() {
296 return None;
297 }
298 for _ in 0..times {
299 let (maybe_next_tail, next_head) =
300 movement::find_preceding_boundary_trail(map, head, &mut |left, right| {
301 is_boundary(left, right, &classifier)
302 });
303
304 if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
305 break;
306 }
307
308 head = next_head;
309 if let Some(next_tail) = maybe_next_tail {
310 tail = next_tail;
311 }
312 }
313 Some((head, tail))
314 });
315 }
316
317 pub fn helix_move_and_collapse(
318 &mut self,
319 motion: Motion,
320 times: Option<usize>,
321 window: &mut Window,
322 cx: &mut Context<Self>,
323 ) {
324 self.update_editor(cx, |_, editor, cx| {
325 let text_layout_details = editor.text_layout_details(window, cx);
326 editor.change_selections(Default::default(), window, cx, |s| {
327 s.move_with(&mut |map, selection| {
328 let goal = selection.goal;
329 let cursor = if selection.is_empty() || selection.reversed {
330 selection.head()
331 } else {
332 movement::left(map, selection.head())
333 };
334
335 let (point, goal) = motion
336 .move_point(map, cursor, selection.goal, times, &text_layout_details)
337 .unwrap_or((cursor, goal));
338
339 selection.collapse_to(point, goal)
340 })
341 });
342 });
343 }
344
345 fn is_boundary_right(
346 ignore_punctuation: bool,
347 ) -> impl FnMut(char, char, &CharClassifier) -> bool {
348 move |left, right, classifier| {
349 let left_kind = classifier.kind_with(left, ignore_punctuation);
350 let right_kind = classifier.kind_with(right, ignore_punctuation);
351 let at_newline = (left == '\n') ^ (right == '\n');
352
353 (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline
354 }
355 }
356
357 fn is_boundary_left(
358 ignore_punctuation: bool,
359 ) -> impl FnMut(char, char, &CharClassifier) -> bool {
360 move |left, right, classifier| {
361 let left_kind = classifier.kind_with(left, ignore_punctuation);
362 let right_kind = classifier.kind_with(right, ignore_punctuation);
363 let at_newline = (left == '\n') ^ (right == '\n');
364
365 (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline
366 }
367 }
368
369 pub fn helix_move_cursor(
370 &mut self,
371 motion: Motion,
372 times: Option<usize>,
373 window: &mut Window,
374 cx: &mut Context<Self>,
375 ) {
376 match motion {
377 Motion::NextWordStart { ignore_punctuation } => {
378 let mut is_boundary = Self::is_boundary_right(ignore_punctuation);
379 self.helix_find_range_forward(times, window, cx, &mut is_boundary)
380 }
381 Motion::NextWordEnd { ignore_punctuation } => {
382 let mut is_boundary = Self::is_boundary_left(ignore_punctuation);
383 self.helix_find_range_forward(times, window, cx, &mut is_boundary)
384 }
385 Motion::PreviousWordStart { ignore_punctuation } => {
386 let mut is_boundary = Self::is_boundary_left(ignore_punctuation);
387 self.helix_find_range_backward(times, window, cx, &mut is_boundary)
388 }
389 Motion::PreviousWordEnd { ignore_punctuation } => {
390 let mut is_boundary = Self::is_boundary_right(ignore_punctuation);
391 self.helix_find_range_backward(times, window, cx, &mut is_boundary)
392 }
393 Motion::EndOfLine { .. } => {
394 // In Helix mode, EndOfLine should position cursor ON the last character,
395 // not after it. We therefore need special handling for it.
396 self.update_editor(cx, |_, editor, cx| {
397 let text_layout_details = editor.text_layout_details(window, cx);
398 editor.change_selections(Default::default(), window, cx, |s| {
399 s.move_with(&mut |map, selection| {
400 let goal = selection.goal;
401 let cursor = if selection.is_empty() || selection.reversed {
402 selection.head()
403 } else {
404 movement::left(map, selection.head())
405 };
406
407 let (point, _goal) = motion
408 .move_point(map, cursor, goal, times, &text_layout_details)
409 .unwrap_or((cursor, goal));
410
411 // Move left by one character to position on the last character
412 let adjusted_point = movement::saturating_left(map, point);
413 selection.collapse_to(adjusted_point, SelectionGoal::None)
414 })
415 });
416 });
417 }
418 Motion::FindForward {
419 before,
420 char,
421 mode,
422 smartcase,
423 } => {
424 self.helix_new_selections(window, cx, &mut |cursor, map| {
425 let start = cursor;
426 let mut last_boundary = start;
427 for _ in 0..times.unwrap_or(1) {
428 last_boundary = movement::find_boundary(
429 map,
430 movement::right(map, last_boundary),
431 mode,
432 &mut |left, right| {
433 let current_char = if before { right } else { left };
434 motion::is_character_match(char, current_char, smartcase)
435 },
436 );
437 }
438 Some((last_boundary, start))
439 });
440 }
441 Motion::FindBackward {
442 after,
443 char,
444 mode,
445 smartcase,
446 } => {
447 self.helix_new_selections(window, cx, &mut |cursor, map| {
448 let start = cursor;
449 let mut last_boundary = start;
450 for _ in 0..times.unwrap_or(1) {
451 last_boundary = movement::find_preceding_boundary_display_point(
452 map,
453 last_boundary,
454 mode,
455 &mut |left, right| {
456 let current_char = if after { left } else { right };
457 motion::is_character_match(char, current_char, smartcase)
458 },
459 );
460 }
461 // The original cursor was one character wide,
462 // but the search started from the left side of it,
463 // so to include that space the selection must end one character to the right.
464 Some((last_boundary, movement::right(map, start)))
465 });
466 }
467 _ => self.helix_move_and_collapse(motion, times, window, cx),
468 }
469 }
470
471 pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context<Self>) {
472 self.update_editor(cx, |vim, editor, cx| {
473 let has_selection = editor
474 .selections
475 .all_adjusted(&editor.display_snapshot(cx))
476 .iter()
477 .any(|selection| !selection.is_empty());
478
479 if !has_selection {
480 // If no selection, expand to current character (like 'v' does)
481 editor.change_selections(Default::default(), window, cx, |s| {
482 s.move_with(&mut |map, selection| {
483 let head = selection.head();
484 let new_head = movement::saturating_right(map, head);
485 selection.set_tail(head, SelectionGoal::None);
486 selection.set_head(new_head, SelectionGoal::None);
487 });
488 });
489 vim.yank_selections_content(
490 editor,
491 crate::motion::MotionKind::Exclusive,
492 window,
493 cx,
494 );
495 editor.change_selections(Default::default(), window, cx, |s| {
496 s.move_with(&mut |_map, selection| {
497 selection.collapse_to(selection.start, SelectionGoal::None);
498 });
499 });
500 } else {
501 // Yank the selection(s)
502 vim.yank_selections_content(
503 editor,
504 crate::motion::MotionKind::Exclusive,
505 window,
506 cx,
507 );
508 }
509 });
510
511 // Drop back to normal mode after yanking
512 self.switch_mode(Mode::HelixNormal, true, window, cx);
513 }
514
515 fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context<Self>) {
516 self.start_recording(cx);
517 self.update_editor(cx, |_, editor, cx| {
518 editor.change_selections(Default::default(), window, cx, |s| {
519 s.move_with(&mut |_map, selection| {
520 // In helix normal mode, move cursor to start of selection and collapse
521 if !selection.is_empty() {
522 selection.collapse_to(selection.start, SelectionGoal::None);
523 }
524 });
525 });
526 });
527 self.switch_mode(Mode::Insert, false, window, cx);
528 }
529
530 fn helix_select_regex(
531 &mut self,
532 _: &HelixSelectRegex,
533 window: &mut Window,
534 cx: &mut Context<Self>,
535 ) {
536 Vim::take_forced_motion(cx);
537 let Some(pane) = self.pane(window, cx) else {
538 return;
539 };
540 let prior_selections = self.editor_selections(window, cx);
541 pane.update(cx, |pane, cx| {
542 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
543 search_bar.update(cx, |search_bar, cx| {
544 if !search_bar.show(window, cx) {
545 return;
546 }
547
548 search_bar.select_query(window, cx);
549 cx.focus_self(window);
550
551 search_bar.set_replacement(None, cx);
552 let mut options = SearchOptions::NONE;
553 options |= SearchOptions::REGEX;
554 if EditorSettings::get_global(cx).search.case_sensitive {
555 options |= SearchOptions::CASE_SENSITIVE;
556 }
557 search_bar.set_search_options(options, cx);
558 if let Some(search) = search_bar.set_search_within_selection(
559 Some(FilteredSearchRange::Selection),
560 window,
561 cx,
562 ) {
563 cx.spawn_in(window, async move |search_bar, cx| {
564 if search.await.is_ok() {
565 search_bar.update_in(cx, |search_bar, window, cx| {
566 search_bar.activate_current_match(window, cx)
567 })
568 } else {
569 Ok(())
570 }
571 })
572 .detach_and_log_err(cx);
573 }
574 self.search = SearchState {
575 direction: searchable::Direction::Next,
576 count: 1,
577 prior_selections,
578 prior_operator: self.operator_stack.last().cloned(),
579 prior_mode: self.mode,
580 helix_select: true,
581 _dismiss_subscription: None,
582 }
583 });
584 }
585 });
586 self.start_recording(cx);
587 }
588
589 fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context<Self>) {
590 self.start_recording(cx);
591 self.switch_mode(Mode::Insert, false, window, cx);
592 self.update_editor(cx, |_, editor, cx| {
593 editor.change_selections(Default::default(), window, cx, |s| {
594 s.move_with(&mut |map, selection| {
595 let point = if selection.is_empty() {
596 right(map, selection.head(), 1)
597 } else {
598 selection.end
599 };
600 selection.collapse_to(point, SelectionGoal::None);
601 });
602 });
603 });
604 }
605
606 /// Helix-specific implementation of `shift-a` that accounts for Helix's
607 /// selection model, where selecting a line with `x` creates a selection
608 /// from column 0 of the current row to column 0 of the next row, so the
609 /// default [`vim::normal::InsertEndOfLine`] would move the cursor to the
610 /// end of the wrong line.
611 fn helix_insert_end_of_line(
612 &mut self,
613 _: &HelixInsertEndOfLine,
614 window: &mut Window,
615 cx: &mut Context<Self>,
616 ) {
617 self.start_recording(cx);
618 self.switch_mode(Mode::Insert, false, window, cx);
619 self.update_editor(cx, |_, editor, cx| {
620 editor.change_selections(Default::default(), window, cx, |s| {
621 s.move_with(&mut |map, selection| {
622 let cursor = if !selection.is_empty() && !selection.reversed {
623 movement::left(map, selection.head())
624 } else {
625 selection.head()
626 };
627 selection
628 .collapse_to(motion::next_line_end(map, cursor, 1), SelectionGoal::None);
629 });
630 });
631 });
632 }
633
634 pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
635 self.update_editor(cx, |_, editor, cx| {
636 editor.transact(window, cx, |editor, window, cx| {
637 let display_map = editor.display_snapshot(cx);
638 let selections = editor.selections.all_display(&display_map);
639
640 // Store selection info for positioning after edit
641 let selection_info: Vec<_> = selections
642 .iter()
643 .map(|selection| {
644 let range = selection.range();
645 let start_offset = range.start.to_offset(&display_map, Bias::Left);
646 let end_offset = range.end.to_offset(&display_map, Bias::Left);
647 let was_empty = range.is_empty();
648 let was_reversed = selection.reversed;
649 (
650 display_map.buffer_snapshot().anchor_before(start_offset),
651 end_offset - start_offset,
652 was_empty,
653 was_reversed,
654 )
655 })
656 .collect();
657
658 let mut edits = Vec::new();
659 for selection in &selections {
660 let mut range = selection.range();
661
662 // For empty selections, extend to replace one character
663 if range.is_empty() {
664 range.end = movement::saturating_right(&display_map, range.start);
665 }
666
667 let byte_range = range.start.to_offset(&display_map, Bias::Left)
668 ..range.end.to_offset(&display_map, Bias::Left);
669
670 if !byte_range.is_empty() {
671 let replacement_text = text.repeat(byte_range.end - byte_range.start);
672 edits.push((byte_range, replacement_text));
673 }
674 }
675
676 editor.edit(edits, cx);
677
678 // Restore selections based on original info
679 let snapshot = editor.buffer().read(cx).snapshot(cx);
680 let ranges: Vec<_> = selection_info
681 .into_iter()
682 .map(|(start_anchor, original_len, was_empty, was_reversed)| {
683 let start_point = start_anchor.to_point(&snapshot);
684 if was_empty {
685 // For cursor-only, collapse to start
686 start_point..start_point
687 } else {
688 // For selections, span the replaced text
689 let replacement_len = text.len() * original_len;
690 let end_offset = start_anchor.to_offset(&snapshot) + replacement_len;
691 let end_point = snapshot.offset_to_point(end_offset);
692 if was_reversed {
693 end_point..start_point
694 } else {
695 start_point..end_point
696 }
697 }
698 })
699 .collect();
700
701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
702 s.select_ranges(ranges);
703 });
704 });
705 });
706 self.switch_mode(Mode::HelixNormal, true, window, cx);
707 }
708
709 pub fn helix_goto_last_modification(
710 &mut self,
711 _: &HelixGotoLastModification,
712 window: &mut Window,
713 cx: &mut Context<Self>,
714 ) {
715 self.jump(".".into(), false, false, window, cx);
716 }
717
718 pub fn helix_select_lines(
719 &mut self,
720 _: &HelixSelectLine,
721 window: &mut Window,
722 cx: &mut Context<Self>,
723 ) {
724 let count = Vim::take_count(cx).unwrap_or(1);
725 self.update_editor(cx, |_, editor, cx| {
726 editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
727 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
728 let mut selections = editor.selections.all::<Point>(&display_map);
729 let max_point = display_map.buffer_snapshot().max_point();
730 let buffer_snapshot = &display_map.buffer_snapshot();
731
732 for selection in &mut selections {
733 // Start always goes to column 0 of the first selected line
734 let start_row = selection.start.row;
735 let current_end_row = selection.end.row;
736
737 // Check if cursor is on empty line by checking first character
738 let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0));
739 let first_char = buffer_snapshot.chars_at(line_start_offset).next();
740 let extra_line = if first_char == Some('\n') && selection.is_empty() {
741 1
742 } else {
743 0
744 };
745
746 let end_row = current_end_row + count as u32 + extra_line;
747
748 selection.start = Point::new(start_row, 0);
749 selection.end = if end_row > max_point.row {
750 max_point
751 } else {
752 Point::new(end_row, 0)
753 };
754 selection.reversed = false;
755 }
756
757 editor.change_selections(Default::default(), window, cx, |s| {
758 s.select(selections);
759 });
760 });
761 }
762
763 fn helix_keep_newest_selection(
764 &mut self,
765 _: &HelixKeepNewestSelection,
766 window: &mut Window,
767 cx: &mut Context<Self>,
768 ) {
769 self.update_editor(cx, |_, editor, cx| {
770 let newest = editor
771 .selections
772 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx));
773 editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest]));
774 });
775 }
776
777 fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context<Self>) {
778 self.update_editor(cx, |vim, editor, cx| {
779 editor.set_clip_at_line_ends(false, cx);
780 editor.transact(window, cx, |editor, window, cx| {
781 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
782 s.move_with(&mut |map, selection| {
783 if selection.start == selection.end {
784 selection.end = movement::right(map, selection.end);
785 }
786
787 // If the selection starts and ends on a newline, we exclude the last one.
788 if !selection.is_empty()
789 && selection.start.column() == 0
790 && selection.end.column() == 0
791 {
792 selection.end = movement::left(map, selection.end);
793 }
794 })
795 });
796 if yank {
797 vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
798 }
799 let selections = editor
800 .selections
801 .all::<Point>(&editor.display_snapshot(cx))
802 .into_iter();
803 let edits = selections.map(|selection| (selection.start..selection.end, ""));
804 editor.edit(edits, cx);
805 });
806 });
807 self.switch_mode(Mode::Insert, true, window, cx);
808 }
809
810 fn helix_substitute(
811 &mut self,
812 _: &HelixSubstitute,
813 window: &mut Window,
814 cx: &mut Context<Self>,
815 ) {
816 self.do_helix_substitute(true, window, cx);
817 }
818
819 fn helix_substitute_no_yank(
820 &mut self,
821 _: &HelixSubstituteNoYank,
822 window: &mut Window,
823 cx: &mut Context<Self>,
824 ) {
825 self.do_helix_substitute(false, window, cx);
826 }
827
828 fn helix_select_next(
829 &mut self,
830 _: &HelixSelectNext,
831 window: &mut Window,
832 cx: &mut Context<Self>,
833 ) {
834 self.do_helix_select(Direction::Next, window, cx);
835 }
836
837 fn helix_select_previous(
838 &mut self,
839 _: &HelixSelectPrevious,
840 window: &mut Window,
841 cx: &mut Context<Self>,
842 ) {
843 self.do_helix_select(Direction::Prev, window, cx);
844 }
845
846 fn do_helix_select(
847 &mut self,
848 direction: searchable::Direction,
849 window: &mut Window,
850 cx: &mut Context<Self>,
851 ) {
852 let Some(pane) = self.pane(window, cx) else {
853 return;
854 };
855 let count = Vim::take_count(cx).unwrap_or(1);
856 Vim::take_forced_motion(cx);
857 let prior_selections = self.editor_selections(window, cx);
858
859 let success = pane.update(cx, |pane, cx| {
860 let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
861 return false;
862 };
863 search_bar.update(cx, |search_bar, cx| {
864 if !search_bar.has_active_match() || !search_bar.show(window, cx) {
865 return false;
866 }
867 search_bar.select_match(direction, count, window, cx);
868 true
869 })
870 });
871
872 if !success {
873 return;
874 }
875 if self.mode == Mode::HelixSelect {
876 self.update_editor(cx, |_vim, editor, cx| {
877 let snapshot = editor.snapshot(window, cx);
878 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
879 s.select_anchor_ranges(
880 prior_selections
881 .iter()
882 .cloned()
883 .chain(s.all_anchors(&snapshot).iter().map(|s| s.range())),
884 );
885 })
886 });
887 }
888 }
889}
890
891#[cfg(test)]
892mod test {
893 use gpui::{UpdateGlobal, VisualTestContext};
894 use indoc::indoc;
895 use project::FakeFs;
896 use search::{ProjectSearchView, project_search};
897 use serde_json::json;
898 use settings::SettingsStore;
899 use util::path;
900 use workspace::{DeploySearch, MultiWorkspace};
901
902 use crate::{VimAddon, state::Mode, test::VimTestContext};
903
904 #[gpui::test]
905 async fn test_word_motions(cx: &mut gpui::TestAppContext) {
906 let mut cx = VimTestContext::new(cx, true).await;
907 cx.enable_helix();
908 // «
909 // ˇ
910 // »
911 cx.set_state(
912 indoc! {"
913 Th«e quiˇ»ck brown
914 fox jumps over
915 the lazy dog."},
916 Mode::HelixNormal,
917 );
918
919 cx.simulate_keystrokes("w");
920
921 cx.assert_state(
922 indoc! {"
923 The qu«ick ˇ»brown
924 fox jumps over
925 the lazy dog."},
926 Mode::HelixNormal,
927 );
928
929 cx.simulate_keystrokes("w");
930
931 cx.assert_state(
932 indoc! {"
933 The quick «brownˇ»
934 fox jumps over
935 the lazy dog."},
936 Mode::HelixNormal,
937 );
938
939 cx.simulate_keystrokes("2 b");
940
941 cx.assert_state(
942 indoc! {"
943 The «ˇquick »brown
944 fox jumps over
945 the lazy dog."},
946 Mode::HelixNormal,
947 );
948
949 cx.simulate_keystrokes("down e up");
950
951 cx.assert_state(
952 indoc! {"
953 The quicˇk brown
954 fox jumps over
955 the lazy dog."},
956 Mode::HelixNormal,
957 );
958
959 cx.set_state("aa\n «ˇbb»", Mode::HelixNormal);
960
961 cx.simulate_keystroke("b");
962
963 cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal);
964 }
965
966 #[gpui::test]
967 async fn test_delete(cx: &mut gpui::TestAppContext) {
968 let mut cx = VimTestContext::new(cx, true).await;
969 cx.enable_helix();
970
971 // test delete a selection
972 cx.set_state(
973 indoc! {"
974 The qu«ick ˇ»brown
975 fox jumps over
976 the lazy dog."},
977 Mode::HelixNormal,
978 );
979
980 cx.simulate_keystrokes("d");
981
982 cx.assert_state(
983 indoc! {"
984 The quˇbrown
985 fox jumps over
986 the lazy dog."},
987 Mode::HelixNormal,
988 );
989
990 // test deleting a single character
991 cx.simulate_keystrokes("d");
992
993 cx.assert_state(
994 indoc! {"
995 The quˇrown
996 fox jumps over
997 the lazy dog."},
998 Mode::HelixNormal,
999 );
1000 }
1001
1002 #[gpui::test]
1003 async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
1004 let mut cx = VimTestContext::new(cx, true).await;
1005
1006 cx.set_state(
1007 indoc! {"
1008 The quick brownˇ
1009 fox jumps over
1010 the lazy dog."},
1011 Mode::HelixNormal,
1012 );
1013
1014 cx.simulate_keystrokes("d");
1015
1016 cx.assert_state(
1017 indoc! {"
1018 The quick brownˇfox jumps over
1019 the lazy dog."},
1020 Mode::HelixNormal,
1021 );
1022 }
1023
1024 // #[gpui::test]
1025 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
1026 // let mut cx = VimTestContext::new(cx, true).await;
1027
1028 // cx.set_state(
1029 // indoc! {"
1030 // The quick brown
1031 // fox jumps over
1032 // the lazy dog.ˇ"},
1033 // Mode::HelixNormal,
1034 // );
1035
1036 // cx.simulate_keystrokes("d");
1037
1038 // cx.assert_state(
1039 // indoc! {"
1040 // The quick brown
1041 // fox jumps over
1042 // the lazy dog.ˇ"},
1043 // Mode::HelixNormal,
1044 // );
1045 // }
1046
1047 #[gpui::test]
1048 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
1049 let mut cx = VimTestContext::new(cx, true).await;
1050 cx.enable_helix();
1051
1052 cx.set_state(
1053 indoc! {"
1054 The quˇick brown
1055 fox jumps over
1056 the lazy dog."},
1057 Mode::HelixNormal,
1058 );
1059
1060 cx.simulate_keystrokes("f z");
1061
1062 cx.assert_state(
1063 indoc! {"
1064 The qu«ick brown
1065 fox jumps over
1066 the lazˇ»y dog."},
1067 Mode::HelixNormal,
1068 );
1069
1070 cx.simulate_keystrokes("F e F e");
1071
1072 cx.assert_state(
1073 indoc! {"
1074 The quick brown
1075 fox jumps ov«ˇer
1076 the» lazy dog."},
1077 Mode::HelixNormal,
1078 );
1079
1080 cx.simulate_keystrokes("e 2 F e");
1081
1082 cx.assert_state(
1083 indoc! {"
1084 Th«ˇe quick brown
1085 fox jumps over»
1086 the lazy dog."},
1087 Mode::HelixNormal,
1088 );
1089
1090 cx.simulate_keystrokes("t r t r");
1091
1092 cx.assert_state(
1093 indoc! {"
1094 The quick «brown
1095 fox jumps oveˇ»r
1096 the lazy dog."},
1097 Mode::HelixNormal,
1098 );
1099 }
1100
1101 #[gpui::test]
1102 async fn test_newline_char(cx: &mut gpui::TestAppContext) {
1103 let mut cx = VimTestContext::new(cx, true).await;
1104 cx.enable_helix();
1105
1106 cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
1107
1108 cx.simulate_keystroke("w");
1109
1110 cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
1111
1112 cx.set_state("aa«\nˇ»", Mode::HelixNormal);
1113
1114 cx.simulate_keystroke("b");
1115
1116 cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
1117 }
1118
1119 #[gpui::test]
1120 async fn test_insert_selected(cx: &mut gpui::TestAppContext) {
1121 let mut cx = VimTestContext::new(cx, true).await;
1122 cx.enable_helix();
1123 cx.set_state(
1124 indoc! {"
1125 «The ˇ»quick brown
1126 fox jumps over
1127 the lazy dog."},
1128 Mode::HelixNormal,
1129 );
1130
1131 cx.simulate_keystrokes("i");
1132
1133 cx.assert_state(
1134 indoc! {"
1135 ˇThe quick brown
1136 fox jumps over
1137 the lazy dog."},
1138 Mode::Insert,
1139 );
1140 }
1141
1142 #[gpui::test]
1143 async fn test_append(cx: &mut gpui::TestAppContext) {
1144 let mut cx = VimTestContext::new(cx, true).await;
1145 cx.enable_helix();
1146 // test from the end of the selection
1147 cx.set_state(
1148 indoc! {"
1149 «Theˇ» quick brown
1150 fox jumps over
1151 the lazy dog."},
1152 Mode::HelixNormal,
1153 );
1154
1155 cx.simulate_keystrokes("a");
1156
1157 cx.assert_state(
1158 indoc! {"
1159 Theˇ quick brown
1160 fox jumps over
1161 the lazy dog."},
1162 Mode::Insert,
1163 );
1164
1165 // test from the beginning of the selection
1166 cx.set_state(
1167 indoc! {"
1168 «ˇThe» quick brown
1169 fox jumps over
1170 the lazy dog."},
1171 Mode::HelixNormal,
1172 );
1173
1174 cx.simulate_keystrokes("a");
1175
1176 cx.assert_state(
1177 indoc! {"
1178 Theˇ quick brown
1179 fox jumps over
1180 the lazy dog."},
1181 Mode::Insert,
1182 );
1183 }
1184
1185 #[gpui::test]
1186 async fn test_replace(cx: &mut gpui::TestAppContext) {
1187 let mut cx = VimTestContext::new(cx, true).await;
1188 cx.enable_helix();
1189
1190 // No selection (single character)
1191 cx.set_state("ˇaa", Mode::HelixNormal);
1192
1193 cx.simulate_keystrokes("r x");
1194
1195 cx.assert_state("ˇxa", Mode::HelixNormal);
1196
1197 // Cursor at the beginning
1198 cx.set_state("«ˇaa»", Mode::HelixNormal);
1199
1200 cx.simulate_keystrokes("r x");
1201
1202 cx.assert_state("«ˇxx»", Mode::HelixNormal);
1203
1204 // Cursor at the end
1205 cx.set_state("«aaˇ»", Mode::HelixNormal);
1206
1207 cx.simulate_keystrokes("r x");
1208
1209 cx.assert_state("«xxˇ»", Mode::HelixNormal);
1210 }
1211
1212 #[gpui::test]
1213 async fn test_helix_yank(cx: &mut gpui::TestAppContext) {
1214 let mut cx = VimTestContext::new(cx, true).await;
1215 cx.enable_helix();
1216
1217 // Test yanking current character with no selection
1218 cx.set_state("hello ˇworld", Mode::HelixNormal);
1219 cx.simulate_keystrokes("y");
1220
1221 // Test cursor remains at the same position after yanking single character
1222 cx.assert_state("hello ˇworld", Mode::HelixNormal);
1223 cx.shared_clipboard().assert_eq("w");
1224
1225 // Move cursor and yank another character
1226 cx.simulate_keystrokes("l");
1227 cx.simulate_keystrokes("y");
1228 cx.shared_clipboard().assert_eq("o");
1229
1230 // Test yanking with existing selection
1231 cx.set_state("hello «worlˇ»d", Mode::HelixNormal);
1232 cx.simulate_keystrokes("y");
1233 cx.shared_clipboard().assert_eq("worl");
1234 cx.assert_state("hello «worlˇ»d", Mode::HelixNormal);
1235
1236 // Test yanking in select mode character by character
1237 cx.set_state("hello ˇworld", Mode::HelixNormal);
1238 cx.simulate_keystroke("v");
1239 cx.assert_state("hello «wˇ»orld", Mode::HelixSelect);
1240 cx.simulate_keystroke("y");
1241 cx.assert_state("hello «wˇ»orld", Mode::HelixNormal);
1242 cx.shared_clipboard().assert_eq("w");
1243 }
1244
1245 #[gpui::test]
1246 async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) {
1247 let mut cx = VimTestContext::new(cx, true).await;
1248 cx.enable_helix();
1249
1250 // First copy some text to clipboard
1251 cx.set_state("«hello worldˇ»", Mode::HelixNormal);
1252 cx.simulate_keystrokes("y");
1253
1254 // Test paste with shift-r on single cursor
1255 cx.set_state("foo ˇbar", Mode::HelixNormal);
1256 cx.simulate_keystrokes("shift-r");
1257
1258 cx.assert_state("foo hello worldˇbar", Mode::HelixNormal);
1259
1260 // Test paste with shift-r on selection
1261 cx.set_state("foo «barˇ» baz", Mode::HelixNormal);
1262 cx.simulate_keystrokes("shift-r");
1263
1264 cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal);
1265 }
1266
1267 #[gpui::test]
1268 async fn test_helix_select_mode(cx: &mut gpui::TestAppContext) {
1269 let mut cx = VimTestContext::new(cx, true).await;
1270
1271 assert_eq!(cx.mode(), Mode::Normal);
1272 cx.enable_helix();
1273
1274 cx.simulate_keystrokes("v");
1275 assert_eq!(cx.mode(), Mode::HelixSelect);
1276 cx.simulate_keystrokes("escape");
1277 assert_eq!(cx.mode(), Mode::HelixNormal);
1278 }
1279
1280 #[gpui::test]
1281 async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) {
1282 let mut cx = VimTestContext::new(cx, true).await;
1283 cx.enable_helix();
1284
1285 // Make a modification at a specific location
1286 cx.set_state("ˇhello", Mode::HelixNormal);
1287 assert_eq!(cx.mode(), Mode::HelixNormal);
1288 cx.simulate_keystrokes("i");
1289 assert_eq!(cx.mode(), Mode::Insert);
1290 cx.simulate_keystrokes("escape");
1291 assert_eq!(cx.mode(), Mode::HelixNormal);
1292 }
1293
1294 #[gpui::test]
1295 async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) {
1296 let mut cx = VimTestContext::new(cx, true).await;
1297 cx.enable_helix();
1298
1299 // Make a modification at a specific location
1300 cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1301 cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal);
1302 cx.simulate_keystrokes("i");
1303 cx.simulate_keystrokes("escape");
1304 cx.simulate_keystrokes("i");
1305 cx.simulate_keystrokes("m o d i f i e d space");
1306 cx.simulate_keystrokes("escape");
1307
1308 // TODO: this fails, because state is no longer helix
1309 cx.assert_state(
1310 "line one\nline modified ˇtwo\nline three",
1311 Mode::HelixNormal,
1312 );
1313
1314 // Move cursor away from the modification
1315 cx.simulate_keystrokes("up");
1316
1317 // Use "g ." to go back to last modification
1318 cx.simulate_keystrokes("g .");
1319
1320 // Verify we're back at the modification location and still in HelixNormal mode
1321 cx.assert_state(
1322 "line one\nline modifiedˇ two\nline three",
1323 Mode::HelixNormal,
1324 );
1325 }
1326
1327 #[gpui::test]
1328 async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) {
1329 let mut cx = VimTestContext::new(cx, true).await;
1330 cx.set_state(
1331 "line one\nline ˇtwo\nline three\nline four",
1332 Mode::HelixNormal,
1333 );
1334 cx.simulate_keystrokes("2 x");
1335 cx.assert_state(
1336 "line one\n«line two\nline three\nˇ»line four",
1337 Mode::HelixNormal,
1338 );
1339
1340 // Test extending existing line selection
1341 cx.set_state(
1342 indoc! {"
1343 li«ˇne one
1344 li»ne two
1345 line three
1346 line four"},
1347 Mode::HelixNormal,
1348 );
1349 cx.simulate_keystrokes("x");
1350 cx.assert_state(
1351 indoc! {"
1352 «line one
1353 line two
1354 ˇ»line three
1355 line four"},
1356 Mode::HelixNormal,
1357 );
1358
1359 // Pressing x in empty line, select next line (because helix considers cursor a selection)
1360 cx.set_state(
1361 indoc! {"
1362 line one
1363 ˇ
1364 line three
1365 line four
1366 line five
1367 line six"},
1368 Mode::HelixNormal,
1369 );
1370 cx.simulate_keystrokes("x");
1371 cx.assert_state(
1372 indoc! {"
1373 line one
1374 «
1375 line three
1376 ˇ»line four
1377 line five
1378 line six"},
1379 Mode::HelixNormal,
1380 );
1381
1382 // Another x should only select the next line
1383 cx.simulate_keystrokes("x");
1384 cx.assert_state(
1385 indoc! {"
1386 line one
1387 «
1388 line three
1389 line four
1390 ˇ»line five
1391 line six"},
1392 Mode::HelixNormal,
1393 );
1394
1395 // Empty line with count selects extra + count lines
1396 cx.set_state(
1397 indoc! {"
1398 line one
1399 ˇ
1400 line three
1401 line four
1402 line five"},
1403 Mode::HelixNormal,
1404 );
1405 cx.simulate_keystrokes("2 x");
1406 cx.assert_state(
1407 indoc! {"
1408 line one
1409 «
1410 line three
1411 line four
1412 ˇ»line five"},
1413 Mode::HelixNormal,
1414 );
1415
1416 // Compare empty vs non-empty line behavior
1417 cx.set_state(
1418 indoc! {"
1419 ˇnon-empty line
1420 line two
1421 line three"},
1422 Mode::HelixNormal,
1423 );
1424 cx.simulate_keystrokes("x");
1425 cx.assert_state(
1426 indoc! {"
1427 «non-empty line
1428 ˇ»line two
1429 line three"},
1430 Mode::HelixNormal,
1431 );
1432
1433 // Same test but with empty line - should select one extra
1434 cx.set_state(
1435 indoc! {"
1436 ˇ
1437 line two
1438 line three"},
1439 Mode::HelixNormal,
1440 );
1441 cx.simulate_keystrokes("x");
1442 cx.assert_state(
1443 indoc! {"
1444 «
1445 line two
1446 ˇ»line three"},
1447 Mode::HelixNormal,
1448 );
1449
1450 // Test selecting multiple lines with count
1451 cx.set_state(
1452 indoc! {"
1453 ˇline one
1454 line two
1455 line threeˇ
1456 line four
1457 line five"},
1458 Mode::HelixNormal,
1459 );
1460 cx.simulate_keystrokes("x");
1461 cx.assert_state(
1462 indoc! {"
1463 «line one
1464 ˇ»line two
1465 «line three
1466 ˇ»line four
1467 line five"},
1468 Mode::HelixNormal,
1469 );
1470 cx.simulate_keystrokes("x");
1471 // Adjacent line selections stay separate (not merged)
1472 cx.assert_state(
1473 indoc! {"
1474 «line one
1475 line two
1476 ˇ»«line three
1477 line four
1478 ˇ»line five"},
1479 Mode::HelixNormal,
1480 );
1481
1482 // Test selecting with an empty line below the current line
1483 cx.set_state(
1484 indoc! {"
1485 line one
1486 line twoˇ
1487
1488 line four
1489 line five"},
1490 Mode::HelixNormal,
1491 );
1492 cx.simulate_keystrokes("x");
1493 cx.assert_state(
1494 indoc! {"
1495 line one
1496 «line two
1497 ˇ»
1498 line four
1499 line five"},
1500 Mode::HelixNormal,
1501 );
1502 cx.simulate_keystrokes("x");
1503 cx.assert_state(
1504 indoc! {"
1505 line one
1506 «line two
1507
1508 ˇ»line four
1509 line five"},
1510 Mode::HelixNormal,
1511 );
1512 cx.simulate_keystrokes("x");
1513 cx.assert_state(
1514 indoc! {"
1515 line one
1516 «line two
1517
1518 line four
1519 ˇ»line five"},
1520 Mode::HelixNormal,
1521 );
1522 }
1523
1524 #[gpui::test]
1525 async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) {
1526 let mut cx = VimTestContext::new(cx, true).await;
1527
1528 assert_eq!(cx.mode(), Mode::Normal);
1529 cx.enable_helix();
1530
1531 cx.set_state("ˇhello", Mode::HelixNormal);
1532 cx.simulate_keystrokes("l v l l");
1533 cx.assert_state("h«ellˇ»o", Mode::HelixSelect);
1534 }
1535
1536 #[gpui::test]
1537 async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) {
1538 let mut cx = VimTestContext::new(cx, true).await;
1539
1540 assert_eq!(cx.mode(), Mode::Normal);
1541 cx.enable_helix();
1542
1543 // Start with multiple cursors (no selections)
1544 cx.set_state("ˇhello\nˇworld", Mode::HelixNormal);
1545
1546 // Enter select mode and move right twice
1547 cx.simulate_keystrokes("v l l");
1548
1549 // Each cursor should independently create and extend its own selection
1550 cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect);
1551 }
1552
1553 #[gpui::test]
1554 async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) {
1555 let mut cx = VimTestContext::new(cx, true).await;
1556
1557 cx.set_state("ˇone two", Mode::Normal);
1558 cx.simulate_keystrokes("v w");
1559 cx.assert_state("«one tˇ»wo", Mode::Visual);
1560
1561 // In Vim, this selects "t". In helix selections stops just before "t"
1562
1563 cx.enable_helix();
1564 cx.set_state("ˇone two", Mode::HelixNormal);
1565 cx.simulate_keystrokes("v w");
1566 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
1567 }
1568
1569 #[gpui::test]
1570 async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) {
1571 let mut cx = VimTestContext::new(cx, true).await;
1572
1573 cx.set_state("ˇone two", Mode::Normal);
1574 cx.simulate_keystrokes("v w");
1575 cx.assert_state("«one tˇ»wo", Mode::Visual);
1576 cx.simulate_keystrokes("escape");
1577 cx.assert_state("one ˇtwo", Mode::Normal);
1578
1579 cx.enable_helix();
1580 cx.set_state("ˇone two", Mode::HelixNormal);
1581 cx.simulate_keystrokes("v w");
1582 cx.assert_state("«one ˇ»two", Mode::HelixSelect);
1583 cx.simulate_keystrokes("escape");
1584 cx.assert_state("«one ˇ»two", Mode::HelixNormal);
1585 }
1586
1587 #[gpui::test]
1588 async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) {
1589 let mut cx = VimTestContext::new(cx, true).await;
1590 cx.enable_helix();
1591
1592 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
1593 cx.simulate_keystrokes("w");
1594 cx.assert_state("«one ˇ»two three", Mode::HelixSelect);
1595
1596 cx.set_state("«ˇ»one two three", Mode::HelixSelect);
1597 cx.simulate_keystrokes("e");
1598 cx.assert_state("«oneˇ» two three", Mode::HelixSelect);
1599 }
1600
1601 #[gpui::test]
1602 async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) {
1603 let mut cx = VimTestContext::new(cx, true).await;
1604 cx.enable_helix();
1605
1606 cx.set_state("ˇone two three", Mode::HelixNormal);
1607 cx.simulate_keystrokes("l l v h h h");
1608 cx.assert_state("«ˇone» two three", Mode::HelixSelect);
1609 }
1610
1611 #[gpui::test]
1612 async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) {
1613 let mut cx = VimTestContext::new(cx, true).await;
1614 cx.enable_helix();
1615
1616 cx.set_state("ˇone two one", Mode::HelixNormal);
1617 cx.simulate_keystrokes("x");
1618 cx.assert_state("«one two oneˇ»", Mode::HelixNormal);
1619 cx.simulate_keystrokes("s o n e");
1620 cx.run_until_parked();
1621 cx.simulate_keystrokes("enter");
1622 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1623
1624 cx.simulate_keystrokes("x");
1625 cx.simulate_keystrokes("s");
1626 cx.run_until_parked();
1627 cx.simulate_keystrokes("enter");
1628 cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal);
1629
1630 // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection
1631 // cx.set_state("ˇstuff one two one", Mode::HelixNormal);
1632 // cx.simulate_keystrokes("s o n e enter");
1633 // cx.assert_state("ˇstuff one two one", Mode::HelixNormal);
1634 }
1635
1636 #[gpui::test]
1637 async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) {
1638 let mut cx = VimTestContext::new(cx, true).await;
1639
1640 cx.set_state("ˇhello two one two one two one", Mode::Visual);
1641 cx.simulate_keystrokes("/ o n e");
1642 cx.simulate_keystrokes("enter");
1643 cx.simulate_keystrokes("n n");
1644 cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual);
1645
1646 cx.set_state("ˇhello two one two one two one", Mode::Normal);
1647 cx.simulate_keystrokes("/ o n e");
1648 cx.simulate_keystrokes("enter");
1649 cx.simulate_keystrokes("n n");
1650 cx.assert_state("hello two one two one two ˇone", Mode::Normal);
1651
1652 cx.set_state("ˇhello two one two one two one", Mode::Normal);
1653 cx.simulate_keystrokes("/ o n e");
1654 cx.simulate_keystrokes("enter");
1655 cx.simulate_keystrokes("n g n g n");
1656 cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual);
1657
1658 cx.enable_helix();
1659
1660 cx.set_state("ˇhello two one two one two one", Mode::HelixNormal);
1661 cx.simulate_keystrokes("/ o n e");
1662 cx.simulate_keystrokes("enter");
1663 cx.simulate_keystrokes("n n");
1664 cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal);
1665
1666 cx.set_state("ˇhello two one two one two one", Mode::HelixSelect);
1667 cx.simulate_keystrokes("/ o n e");
1668 cx.simulate_keystrokes("enter");
1669 cx.simulate_keystrokes("n n");
1670 cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect);
1671 }
1672
1673 #[gpui::test]
1674 async fn test_helix_substitute(cx: &mut gpui::TestAppContext) {
1675 let mut cx = VimTestContext::new(cx, true).await;
1676
1677 cx.set_state("ˇone two", Mode::HelixNormal);
1678 cx.simulate_keystrokes("c");
1679 cx.assert_state("ˇne two", Mode::Insert);
1680
1681 cx.set_state("«oneˇ» two", Mode::HelixNormal);
1682 cx.simulate_keystrokes("c");
1683 cx.assert_state("ˇ two", Mode::Insert);
1684
1685 cx.set_state(
1686 indoc! {"
1687 oneˇ two
1688 three
1689 "},
1690 Mode::HelixNormal,
1691 );
1692 cx.simulate_keystrokes("x c");
1693 cx.assert_state(
1694 indoc! {"
1695 ˇ
1696 three
1697 "},
1698 Mode::Insert,
1699 );
1700
1701 cx.set_state(
1702 indoc! {"
1703 one twoˇ
1704 three
1705 "},
1706 Mode::HelixNormal,
1707 );
1708 cx.simulate_keystrokes("c");
1709 cx.assert_state(
1710 indoc! {"
1711 one twoˇthree
1712 "},
1713 Mode::Insert,
1714 );
1715
1716 // Helix doesn't set the cursor to the first non-blank one when
1717 // replacing lines: it uses language-dependent indent queries instead.
1718 cx.set_state(
1719 indoc! {"
1720 one two
1721 « indented
1722 three not indentedˇ»
1723 "},
1724 Mode::HelixNormal,
1725 );
1726 cx.simulate_keystrokes("c");
1727 cx.set_state(
1728 indoc! {"
1729 one two
1730 ˇ
1731 "},
1732 Mode::Insert,
1733 );
1734 }
1735
1736 #[gpui::test]
1737 async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) {
1738 let mut cx = VimTestContext::new(cx, true).await;
1739 cx.enable_helix();
1740
1741 // Test g l moves to last character, not after it
1742 cx.set_state("hello ˇworld!", Mode::HelixNormal);
1743 cx.simulate_keystrokes("g l");
1744 cx.assert_state("hello worldˇ!", Mode::HelixNormal);
1745
1746 // Test with Chinese characters, test if work with UTF-8?
1747 cx.set_state("ˇ你好世界", Mode::HelixNormal);
1748 cx.simulate_keystrokes("g l");
1749 cx.assert_state("你好世ˇ界", Mode::HelixNormal);
1750
1751 // Test with end of line
1752 cx.set_state("endˇ", Mode::HelixNormal);
1753 cx.simulate_keystrokes("g l");
1754 cx.assert_state("enˇd", Mode::HelixNormal);
1755
1756 // Test with empty line
1757 cx.set_state(
1758 indoc! {"
1759 hello
1760 ˇ
1761 world"},
1762 Mode::HelixNormal,
1763 );
1764 cx.simulate_keystrokes("g l");
1765 cx.assert_state(
1766 indoc! {"
1767 hello
1768 ˇ
1769 world"},
1770 Mode::HelixNormal,
1771 );
1772
1773 // Test with multiple lines
1774 cx.set_state(
1775 indoc! {"
1776 ˇfirst line
1777 second line
1778 third line"},
1779 Mode::HelixNormal,
1780 );
1781 cx.simulate_keystrokes("g l");
1782 cx.assert_state(
1783 indoc! {"
1784 first linˇe
1785 second line
1786 third line"},
1787 Mode::HelixNormal,
1788 );
1789 }
1790
1791 #[gpui::test]
1792 async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) {
1793 VimTestContext::init(cx);
1794
1795 let fs = FakeFs::new(cx.background_executor.clone());
1796 fs.insert_tree(
1797 path!("/dir"),
1798 json!({
1799 "file_a.rs": "// File A.",
1800 "file_b.rs": "// File B.",
1801 }),
1802 )
1803 .await;
1804
1805 let project = project::Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
1806 let window_handle =
1807 cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1808 let workspace = window_handle
1809 .read_with(cx, |mw, _| mw.workspace().clone())
1810 .unwrap();
1811
1812 cx.update(|cx| {
1813 VimTestContext::init_keybindings(true, cx);
1814 SettingsStore::update_global(cx, |store, cx| {
1815 store.update_user_settings(cx, |store| store.helix_mode = Some(true));
1816 })
1817 });
1818
1819 let cx = &mut VisualTestContext::from_window(window_handle.into(), cx);
1820
1821 workspace.update_in(cx, |workspace, window, cx| {
1822 ProjectSearchView::deploy_search(workspace, &DeploySearch::default(), window, cx)
1823 });
1824
1825 let search_view = workspace.update_in(cx, |workspace, _, cx| {
1826 workspace
1827 .active_pane()
1828 .read(cx)
1829 .items()
1830 .find_map(|item| item.downcast::<ProjectSearchView>())
1831 .expect("Project search view should be active")
1832 });
1833
1834 project_search::perform_project_search(&search_view, "File A", cx);
1835
1836 search_view.update(cx, |search_view, cx| {
1837 let vim_mode = search_view
1838 .results_editor()
1839 .read(cx)
1840 .addon::<VimAddon>()
1841 .map(|addon| addon.entity.read(cx).mode);
1842
1843 assert_eq!(vim_mode, Some(Mode::HelixNormal));
1844 });
1845 }
1846
1847 #[gpui::test]
1848 async fn test_scroll_with_selection(cx: &mut gpui::TestAppContext) {
1849 let mut cx = VimTestContext::new(cx, true).await;
1850 cx.enable_helix();
1851
1852 // Start with a selection
1853 cx.set_state(
1854 indoc! {"
1855 «lineˇ» one
1856 line two
1857 line three
1858 line four
1859 line five"},
1860 Mode::HelixNormal,
1861 );
1862
1863 // Scroll down, selection should collapse
1864 cx.simulate_keystrokes("ctrl-d");
1865 cx.assert_state(
1866 indoc! {"
1867 line one
1868 line two
1869 line three
1870 line four
1871 line fiveˇ"},
1872 Mode::HelixNormal,
1873 );
1874
1875 // Make a new selection
1876 cx.simulate_keystroke("b");
1877 cx.assert_state(
1878 indoc! {"
1879 line one
1880 line two
1881 line three
1882 line four
1883 line «ˇfive»"},
1884 Mode::HelixNormal,
1885 );
1886
1887 // And scroll up, once again collapsing the selection.
1888 cx.simulate_keystroke("ctrl-u");
1889 cx.assert_state(
1890 indoc! {"
1891 line one
1892 line two
1893 line three
1894 line ˇfour
1895 line five"},
1896 Mode::HelixNormal,
1897 );
1898
1899 // Enter select mode
1900 cx.simulate_keystroke("v");
1901 cx.assert_state(
1902 indoc! {"
1903 line one
1904 line two
1905 line three
1906 line «fˇ»our
1907 line five"},
1908 Mode::HelixSelect,
1909 );
1910
1911 // And now the selection should be kept/expanded.
1912 cx.simulate_keystroke("ctrl-d");
1913 cx.assert_state(
1914 indoc! {"
1915 line one
1916 line two
1917 line three
1918 line «four
1919 line fiveˇ»"},
1920 Mode::HelixSelect,
1921 );
1922 }
1923
1924 #[gpui::test]
1925 async fn test_helix_insert_end_of_line(cx: &mut gpui::TestAppContext) {
1926 let mut cx = VimTestContext::new(cx, true).await;
1927 cx.enable_helix();
1928
1929 // Ensure that, when lines are selected using `x`, pressing `shift-a`
1930 // actually puts the cursor at the end of the selected lines and not at
1931 // the end of the line below.
1932 cx.set_state(
1933 indoc! {"
1934 line oˇne
1935 line two"},
1936 Mode::HelixNormal,
1937 );
1938
1939 cx.simulate_keystrokes("x");
1940 cx.assert_state(
1941 indoc! {"
1942 «line one
1943 ˇ»line two"},
1944 Mode::HelixNormal,
1945 );
1946
1947 cx.simulate_keystrokes("shift-a");
1948 cx.assert_state(
1949 indoc! {"
1950 line oneˇ
1951 line two"},
1952 Mode::Insert,
1953 );
1954
1955 cx.set_state(
1956 indoc! {"
1957 line «one
1958 lineˇ» two"},
1959 Mode::HelixNormal,
1960 );
1961
1962 cx.simulate_keystrokes("shift-a");
1963 cx.assert_state(
1964 indoc! {"
1965 line one
1966 line twoˇ"},
1967 Mode::Insert,
1968 );
1969 }
1970}