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