1use std::sync::Arc;
2
3use collections::HashMap;
4use editor::{
5 display_map::{DisplaySnapshot, ToDisplayPoint},
6 movement,
7 scroll::Autoscroll,
8 Bias, DisplayPoint, Editor, ToOffset,
9};
10use gpui::{actions, ViewContext};
11use language::{Point, Selection, SelectionGoal};
12use multi_buffer::MultiBufferRow;
13use search::BufferSearchBar;
14use util::ResultExt;
15use workspace::searchable::Direction;
16
17use crate::{
18 motion::{first_non_whitespace, next_line_end, start_of_line, Motion},
19 object::Object,
20 state::{Mode, Operator},
21 Vim,
22};
23
24actions!(
25 vim,
26 [
27 ToggleVisual,
28 ToggleVisualLine,
29 ToggleVisualBlock,
30 VisualDelete,
31 VisualDeleteLine,
32 VisualYank,
33 VisualYankLine,
34 OtherEnd,
35 SelectNext,
36 SelectPrevious,
37 SelectNextMatch,
38 SelectPreviousMatch,
39 SelectSmallerSyntaxNode,
40 SelectLargerSyntaxNode,
41 RestoreVisualSelection,
42 VisualInsertEndOfLine,
43 VisualInsertFirstNonWhiteSpace,
44 ]
45);
46
47pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
48 Vim::action(editor, cx, |vim, _: &ToggleVisual, cx| {
49 vim.toggle_mode(Mode::Visual, cx)
50 });
51 Vim::action(editor, cx, |vim, _: &ToggleVisualLine, cx| {
52 vim.toggle_mode(Mode::VisualLine, cx)
53 });
54 Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, cx| {
55 vim.toggle_mode(Mode::VisualBlock, cx)
56 });
57 Vim::action(editor, cx, Vim::other_end);
58 Vim::action(editor, cx, Vim::visual_insert_end_of_line);
59 Vim::action(editor, cx, Vim::visual_insert_first_non_white_space);
60 Vim::action(editor, cx, |vim, _: &VisualDelete, cx| {
61 vim.record_current_action(cx);
62 vim.visual_delete(false, cx);
63 });
64 Vim::action(editor, cx, |vim, _: &VisualDeleteLine, cx| {
65 vim.record_current_action(cx);
66 vim.visual_delete(true, cx);
67 });
68 Vim::action(editor, cx, |vim, _: &VisualYank, cx| vim.visual_yank(cx));
69
70 Vim::action(editor, cx, Vim::select_next);
71 Vim::action(editor, cx, Vim::select_previous);
72 Vim::action(editor, cx, |vim, _: &SelectNextMatch, cx| {
73 vim.select_match(Direction::Next, cx);
74 });
75 Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, cx| {
76 vim.select_match(Direction::Prev, cx);
77 });
78
79 Vim::action(editor, cx, |vim, _: &SelectLargerSyntaxNode, cx| {
80 let count = Vim::take_count(cx).unwrap_or(1);
81 for _ in 0..count {
82 vim.update_editor(cx, |_, editor, cx| {
83 editor.select_larger_syntax_node(&Default::default(), cx);
84 });
85 }
86 });
87
88 Vim::action(editor, cx, |vim, _: &SelectSmallerSyntaxNode, cx| {
89 let count = Vim::take_count(cx).unwrap_or(1);
90 for _ in 0..count {
91 vim.update_editor(cx, |_, editor, cx| {
92 editor.select_smaller_syntax_node(&Default::default(), cx);
93 });
94 }
95 });
96
97 Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, cx| {
98 let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
99 return;
100 };
101 let Some((start, end)) = vim.marks.get("<").zip(vim.marks.get(">")) else {
102 return;
103 };
104 let ranges = start
105 .iter()
106 .zip(end)
107 .zip(reversed)
108 .map(|((start, end), reversed)| (*start, *end, reversed))
109 .collect::<Vec<_>>();
110
111 if vim.mode.is_visual() {
112 vim.create_visual_marks(vim.mode, cx);
113 }
114
115 vim.update_editor(cx, |_, editor, cx| {
116 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
117 let map = s.display_map();
118 let ranges = ranges
119 .into_iter()
120 .map(|(start, end, reversed)| {
121 let new_end = movement::saturating_right(&map, end.to_display_point(&map));
122 Selection {
123 id: s.new_selection_id(),
124 start: start.to_offset(&map.buffer_snapshot),
125 end: new_end.to_offset(&map, Bias::Left),
126 reversed,
127 goal: SelectionGoal::None,
128 }
129 })
130 .collect();
131 s.select(ranges);
132 })
133 });
134 vim.switch_mode(stored_mode, true, cx)
135 });
136}
137
138impl Vim {
139 pub fn visual_motion(
140 &mut self,
141 motion: Motion,
142 times: Option<usize>,
143 cx: &mut ViewContext<Self>,
144 ) {
145 self.update_editor(cx, |vim, editor, cx| {
146 let text_layout_details = editor.text_layout_details(cx);
147 if vim.mode == Mode::VisualBlock
148 && !matches!(
149 motion,
150 Motion::EndOfLine {
151 display_lines: false
152 }
153 )
154 {
155 let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
156 vim.visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
157 motion.move_point(map, point, goal, times, &text_layout_details)
158 })
159 } else {
160 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
161 s.move_with(|map, selection| {
162 let was_reversed = selection.reversed;
163 let mut current_head = selection.head();
164
165 // our motions assume the current character is after the cursor,
166 // but in (forward) visual mode the current character is just
167 // before the end of the selection.
168
169 // If the file ends with a newline (which is common) we don't do this.
170 // so that if you go to the end of such a file you can use "up" to go
171 // to the previous line and have it work somewhat as expected.
172 #[allow(clippy::nonminimal_bool)]
173 if !selection.reversed
174 && !selection.is_empty()
175 && !(selection.end.column() == 0 && selection.end == map.max_point())
176 {
177 current_head = movement::left(map, selection.end)
178 }
179
180 let Some((new_head, goal)) = motion.move_point(
181 map,
182 current_head,
183 selection.goal,
184 times,
185 &text_layout_details,
186 ) else {
187 return;
188 };
189
190 selection.set_head(new_head, goal);
191
192 // ensure the current character is included in the selection.
193 if !selection.reversed {
194 let next_point = if vim.mode == Mode::VisualBlock {
195 movement::saturating_right(map, selection.end)
196 } else {
197 movement::right(map, selection.end)
198 };
199
200 if !(next_point.column() == 0 && next_point == map.max_point()) {
201 selection.end = next_point;
202 }
203 }
204
205 // vim always ensures the anchor character stays selected.
206 // if our selection has reversed, we need to move the opposite end
207 // to ensure the anchor is still selected.
208 if was_reversed && !selection.reversed {
209 selection.start = movement::left(map, selection.start);
210 } else if !was_reversed && selection.reversed {
211 selection.end = movement::right(map, selection.end);
212 }
213 })
214 });
215 }
216 });
217 }
218
219 pub fn visual_block_motion(
220 &mut self,
221 preserve_goal: bool,
222 editor: &mut Editor,
223 cx: &mut ViewContext<Editor>,
224 mut move_selection: impl FnMut(
225 &DisplaySnapshot,
226 DisplayPoint,
227 SelectionGoal,
228 ) -> Option<(DisplayPoint, SelectionGoal)>,
229 ) {
230 let text_layout_details = editor.text_layout_details(cx);
231 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
232 let map = &s.display_map();
233 let mut head = s.newest_anchor().head().to_display_point(map);
234 let mut tail = s.oldest_anchor().tail().to_display_point(map);
235
236 let mut head_x = map.x_for_display_point(head, &text_layout_details);
237 let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
238
239 let (start, end) = match s.newest_anchor().goal {
240 SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
241 SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
242 _ => (tail_x.0, head_x.0),
243 };
244 let mut goal = SelectionGoal::HorizontalRange { start, end };
245
246 let was_reversed = tail_x > head_x;
247 if !was_reversed && !preserve_goal {
248 head = movement::saturating_left(map, head);
249 }
250
251 let Some((new_head, _)) = move_selection(map, head, goal) else {
252 return;
253 };
254 head = new_head;
255 head_x = map.x_for_display_point(head, &text_layout_details);
256
257 let is_reversed = tail_x > head_x;
258 if was_reversed && !is_reversed {
259 tail = movement::saturating_left(map, tail);
260 tail_x = map.x_for_display_point(tail, &text_layout_details);
261 } else if !was_reversed && is_reversed {
262 tail = movement::saturating_right(map, tail);
263 tail_x = map.x_for_display_point(tail, &text_layout_details);
264 }
265 if !is_reversed && !preserve_goal {
266 head = movement::saturating_right(map, head);
267 head_x = map.x_for_display_point(head, &text_layout_details);
268 }
269
270 let positions = if is_reversed {
271 head_x..tail_x
272 } else {
273 tail_x..head_x
274 };
275
276 if !preserve_goal {
277 goal = SelectionGoal::HorizontalRange {
278 start: positions.start.0,
279 end: positions.end.0,
280 };
281 }
282
283 let mut selections = Vec::new();
284 let mut row = tail.row();
285
286 loop {
287 let laid_out_line = map.layout_row(row, &text_layout_details);
288 let start = DisplayPoint::new(
289 row,
290 laid_out_line.closest_index_for_x(positions.start) as u32,
291 );
292 let mut end =
293 DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
294 if end <= start {
295 if start.column() == map.line_len(start.row()) {
296 end = start;
297 } else {
298 end = movement::saturating_right(map, start);
299 }
300 }
301
302 if positions.start <= laid_out_line.width {
303 let selection = Selection {
304 id: s.new_selection_id(),
305 start: start.to_point(map),
306 end: end.to_point(map),
307 reversed: is_reversed,
308 goal,
309 };
310
311 selections.push(selection);
312 }
313 if row == head.row() {
314 break;
315 }
316 if tail.row() > head.row() {
317 row.0 -= 1
318 } else {
319 row.0 += 1
320 }
321 }
322
323 s.select(selections);
324 })
325 }
326
327 pub fn visual_object(&mut self, object: Object, cx: &mut ViewContext<Vim>) {
328 if let Some(Operator::Object { around }) = self.active_operator() {
329 self.pop_operator(cx);
330 let current_mode = self.mode;
331 let target_mode = object.target_visual_mode(current_mode, around);
332 if target_mode != current_mode {
333 self.switch_mode(target_mode, true, cx);
334 }
335
336 self.update_editor(cx, |_, editor, cx| {
337 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
338 s.move_with(|map, selection| {
339 let mut mut_selection = selection.clone();
340
341 // all our motions assume that the current character is
342 // after the cursor; however in the case of a visual selection
343 // the current character is before the cursor.
344 // But this will affect the judgment of the html tag
345 // so the html tag needs to skip this logic.
346 if !selection.reversed && object != Object::Tag {
347 mut_selection.set_head(
348 movement::left(map, mut_selection.head()),
349 mut_selection.goal,
350 );
351 }
352
353 if let Some(range) = object.range(map, mut_selection, around) {
354 if !range.is_empty() {
355 let expand_both_ways = object.always_expands_both_ways()
356 || selection.is_empty()
357 || movement::right(map, selection.start) == selection.end;
358
359 if expand_both_ways {
360 selection.start = range.start;
361 selection.end = range.end;
362 } else if selection.reversed {
363 selection.start = range.start;
364 } else {
365 selection.end = range.end;
366 }
367 }
368
369 // In the visual selection result of a paragraph object, the cursor is
370 // placed at the start of the last line. And in the visual mode, the
371 // selection end is located after the end character. So, adjustment of
372 // selection end is needed.
373 //
374 // We don't do this adjustment for a one-line blank paragraph since the
375 // trailing newline is included in its selection from the beginning.
376 if object == Object::Paragraph && range.start != range.end {
377 let row_of_selection_end_line = selection.end.to_point(map).row;
378 let new_selection_end = if map
379 .buffer_snapshot
380 .line_len(MultiBufferRow(row_of_selection_end_line))
381 == 0
382 {
383 Point::new(row_of_selection_end_line + 1, 0)
384 } else {
385 Point::new(row_of_selection_end_line, 1)
386 };
387 selection.end = new_selection_end.to_display_point(map);
388 }
389 }
390 });
391 });
392 });
393 }
394 }
395
396 fn visual_insert_end_of_line(&mut self, _: &VisualInsertEndOfLine, cx: &mut ViewContext<Self>) {
397 self.update_editor(cx, |_, editor, cx| {
398 editor.split_selection_into_lines(&Default::default(), cx);
399 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
400 s.move_cursors_with(|map, cursor, _| {
401 (next_line_end(map, cursor, 1), SelectionGoal::None)
402 });
403 });
404 });
405
406 self.switch_mode(Mode::Insert, false, cx);
407 }
408
409 fn visual_insert_first_non_white_space(
410 &mut self,
411 _: &VisualInsertFirstNonWhiteSpace,
412 cx: &mut ViewContext<Self>,
413 ) {
414 self.update_editor(cx, |_, editor, cx| {
415 editor.split_selection_into_lines(&Default::default(), cx);
416 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
417 s.move_cursors_with(|map, cursor, _| {
418 (
419 first_non_whitespace(map, false, cursor),
420 SelectionGoal::None,
421 )
422 });
423 });
424 });
425
426 self.switch_mode(Mode::Insert, false, cx);
427 }
428
429 fn toggle_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
430 if self.mode == mode {
431 self.switch_mode(Mode::Normal, false, cx);
432 } else {
433 self.switch_mode(mode, false, cx);
434 }
435 }
436
437 pub fn other_end(&mut self, _: &OtherEnd, cx: &mut ViewContext<Self>) {
438 self.update_editor(cx, |_, editor, cx| {
439 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
440 s.move_with(|_, selection| {
441 selection.reversed = !selection.reversed;
442 })
443 })
444 });
445 }
446
447 pub fn visual_delete(&mut self, line_mode: bool, cx: &mut ViewContext<Self>) {
448 self.store_visual_marks(cx);
449 self.update_editor(cx, |vim, editor, cx| {
450 let mut original_columns: HashMap<_, _> = Default::default();
451 let line_mode = line_mode || editor.selections.line_mode;
452
453 editor.transact(cx, |editor, cx| {
454 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
455 s.move_with(|map, selection| {
456 if line_mode {
457 let mut position = selection.head();
458 if !selection.reversed {
459 position = movement::left(map, position);
460 }
461 original_columns.insert(selection.id, position.to_point(map).column);
462 if vim.mode == Mode::VisualBlock {
463 *selection.end.column_mut() = map.line_len(selection.end.row())
464 } else if vim.mode != Mode::VisualLine {
465 selection.start = DisplayPoint::new(selection.start.row(), 0);
466 if selection.end.row() == map.max_point().row() {
467 selection.end = map.max_point()
468 } else {
469 *selection.end.row_mut() += 1;
470 *selection.end.column_mut() = 0;
471 }
472 }
473 }
474 selection.goal = SelectionGoal::None;
475 });
476 });
477 vim.copy_selections_content(editor, line_mode, cx);
478 editor.insert("", cx);
479
480 // Fixup cursor position after the deletion
481 editor.set_clip_at_line_ends(true, cx);
482 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
483 s.move_with(|map, selection| {
484 let mut cursor = selection.head().to_point(map);
485
486 if let Some(column) = original_columns.get(&selection.id) {
487 cursor.column = *column
488 }
489 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
490 selection.collapse_to(cursor, selection.goal)
491 });
492 if vim.mode == Mode::VisualBlock {
493 s.select_anchors(vec![s.first_anchor()])
494 }
495 });
496 })
497 });
498 self.switch_mode(Mode::Normal, true, cx);
499 }
500
501 pub fn visual_yank(&mut self, cx: &mut ViewContext<Self>) {
502 self.store_visual_marks(cx);
503 self.update_editor(cx, |vim, editor, cx| {
504 let line_mode = editor.selections.line_mode;
505 vim.yank_selections_content(editor, line_mode, cx);
506 editor.change_selections(None, cx, |s| {
507 s.move_with(|map, selection| {
508 if line_mode {
509 selection.start = start_of_line(map, false, selection.start);
510 };
511 selection.collapse_to(selection.start, SelectionGoal::None)
512 });
513 if vim.mode == Mode::VisualBlock {
514 s.select_anchors(vec![s.first_anchor()])
515 }
516 });
517 });
518 self.switch_mode(Mode::Normal, true, cx);
519 }
520
521 pub(crate) fn visual_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
522 self.stop_recording(cx);
523 self.update_editor(cx, |_, editor, cx| {
524 editor.transact(cx, |editor, cx| {
525 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
526
527 // Selections are biased right at the start. So we need to store
528 // anchors that are biased left so that we can restore the selections
529 // after the change
530 let stable_anchors = editor
531 .selections
532 .disjoint_anchors()
533 .iter()
534 .map(|selection| {
535 let start = selection.start.bias_left(&display_map.buffer_snapshot);
536 start..start
537 })
538 .collect::<Vec<_>>();
539
540 let mut edits = Vec::new();
541 for selection in selections.iter() {
542 let selection = selection.clone();
543 for row_range in
544 movement::split_display_range_by_lines(&display_map, selection.range())
545 {
546 let range = row_range.start.to_offset(&display_map, Bias::Right)
547 ..row_range.end.to_offset(&display_map, Bias::Right);
548 let text = text.repeat(range.len());
549 edits.push((range, text));
550 }
551 }
552
553 editor.edit(edits, cx);
554 editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
555 });
556 });
557 self.switch_mode(Mode::Normal, false, cx);
558 }
559
560 pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
561 let count =
562 Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
563 self.update_editor(cx, |_, editor, cx| {
564 editor.set_clip_at_line_ends(false, cx);
565 for _ in 0..count {
566 if editor
567 .select_next(&Default::default(), cx)
568 .log_err()
569 .is_none()
570 {
571 break;
572 }
573 }
574 });
575 }
576
577 pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext<Self>) {
578 let count =
579 Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
580 self.update_editor(cx, |_, editor, cx| {
581 for _ in 0..count {
582 if editor
583 .select_previous(&Default::default(), cx)
584 .log_err()
585 .is_none()
586 {
587 break;
588 }
589 }
590 });
591 }
592
593 pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
594 let count = Vim::take_count(cx).unwrap_or(1);
595 let Some(pane) = self.pane(cx) else {
596 return;
597 };
598 let vim_is_normal = self.mode == Mode::Normal;
599 let mut start_selection = 0usize;
600 let mut end_selection = 0usize;
601
602 self.update_editor(cx, |_, editor, _| {
603 editor.set_collapse_matches(false);
604 });
605 if vim_is_normal {
606 pane.update(cx, |pane, cx| {
607 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
608 {
609 search_bar.update(cx, |search_bar, cx| {
610 if !search_bar.has_active_match() || !search_bar.show(cx) {
611 return;
612 }
613 // without update_match_index there is a bug when the cursor is before the first match
614 search_bar.update_match_index(cx);
615 search_bar.select_match(direction.opposite(), 1, cx);
616 });
617 }
618 });
619 }
620 self.update_editor(cx, |_, editor, cx| {
621 let latest = editor.selections.newest::<usize>(cx);
622 start_selection = latest.start;
623 end_selection = latest.end;
624 });
625
626 let mut match_exists = false;
627 pane.update(cx, |pane, cx| {
628 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
629 search_bar.update(cx, |search_bar, cx| {
630 search_bar.update_match_index(cx);
631 search_bar.select_match(direction, count, cx);
632 match_exists = search_bar.match_exists(cx);
633 });
634 }
635 });
636 if !match_exists {
637 self.clear_operator(cx);
638 self.stop_replaying(cx);
639 return;
640 }
641 self.update_editor(cx, |_, editor, cx| {
642 let latest = editor.selections.newest::<usize>(cx);
643 if vim_is_normal {
644 start_selection = latest.start;
645 end_selection = latest.end;
646 } else {
647 start_selection = start_selection.min(latest.start);
648 end_selection = end_selection.max(latest.end);
649 }
650 if direction == Direction::Prev {
651 std::mem::swap(&mut start_selection, &mut end_selection);
652 }
653 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
654 s.select_ranges([start_selection..end_selection]);
655 });
656 editor.set_collapse_matches(true);
657 });
658
659 match self.maybe_pop_operator() {
660 Some(Operator::Change) => self.substitute(None, false, cx),
661 Some(Operator::Delete) => {
662 self.stop_recording(cx);
663 self.visual_delete(false, cx)
664 }
665 Some(Operator::Yank) => self.visual_yank(cx),
666 _ => {} // Ignoring other operators
667 }
668 }
669}
670#[cfg(test)]
671mod test {
672 use indoc::indoc;
673 use workspace::item::Item;
674
675 use crate::{
676 state::Mode,
677 test::{NeovimBackedTestContext, VimTestContext},
678 };
679
680 #[gpui::test]
681 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
682 let mut cx = NeovimBackedTestContext::new(cx).await;
683
684 cx.set_shared_state(indoc! {
685 "The ˇquick brown
686 fox jumps over
687 the lazy dog"
688 })
689 .await;
690 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
691
692 // entering visual mode should select the character
693 // under cursor
694 cx.simulate_shared_keystrokes("v").await;
695 cx.shared_state()
696 .await
697 .assert_eq(indoc! { "The «qˇ»uick brown
698 fox jumps over
699 the lazy dog"});
700 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
701
702 // forwards motions should extend the selection
703 cx.simulate_shared_keystrokes("w j").await;
704 cx.shared_state().await.assert_eq(indoc! { "The «quick brown
705 fox jumps oˇ»ver
706 the lazy dog"});
707
708 cx.simulate_shared_keystrokes("escape").await;
709 cx.shared_state().await.assert_eq(indoc! { "The quick brown
710 fox jumps ˇover
711 the lazy dog"});
712
713 // motions work backwards
714 cx.simulate_shared_keystrokes("v k b").await;
715 cx.shared_state()
716 .await
717 .assert_eq(indoc! { "The «ˇquick brown
718 fox jumps o»ver
719 the lazy dog"});
720
721 // works on empty lines
722 cx.set_shared_state(indoc! {"
723 a
724 ˇ
725 b
726 "})
727 .await;
728 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
729 cx.simulate_shared_keystrokes("v").await;
730 cx.shared_state().await.assert_eq(indoc! {"
731 a
732 «
733 ˇ»b
734 "});
735 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
736
737 // toggles off again
738 cx.simulate_shared_keystrokes("v").await;
739 cx.shared_state().await.assert_eq(indoc! {"
740 a
741 ˇ
742 b
743 "});
744
745 // works at the end of a document
746 cx.set_shared_state(indoc! {"
747 a
748 b
749 ˇ"})
750 .await;
751
752 cx.simulate_shared_keystrokes("v").await;
753 cx.shared_state().await.assert_eq(indoc! {"
754 a
755 b
756 ˇ"});
757 }
758
759 #[gpui::test]
760 async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
761 let mut cx = VimTestContext::new(cx, true).await;
762
763 cx.set_state(
764 indoc! {
765 "«The quick brown
766 fox jumps over
767 the lazy dogˇ»"
768 },
769 Mode::Visual,
770 );
771 cx.simulate_keystrokes("g shift-i");
772 cx.assert_state(
773 indoc! {
774 "ˇThe quick brown
775 ˇfox jumps over
776 ˇthe lazy dog"
777 },
778 Mode::Insert,
779 );
780 }
781
782 #[gpui::test]
783 async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
784 let mut cx = VimTestContext::new(cx, true).await;
785
786 cx.set_state(
787 indoc! {
788 "«The quick brown
789 fox jumps over
790 the lazy dogˇ»"
791 },
792 Mode::Visual,
793 );
794 cx.simulate_keystrokes("g shift-a");
795 cx.assert_state(
796 indoc! {
797 "The quick brownˇ
798 fox jumps overˇ
799 the lazy dogˇ"
800 },
801 Mode::Insert,
802 );
803 }
804
805 #[gpui::test]
806 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
807 let mut cx = NeovimBackedTestContext::new(cx).await;
808
809 cx.set_shared_state(indoc! {
810 "The ˇquick brown
811 fox jumps over
812 the lazy dog"
813 })
814 .await;
815 cx.simulate_shared_keystrokes("shift-v").await;
816 cx.shared_state()
817 .await
818 .assert_eq(indoc! { "The «qˇ»uick brown
819 fox jumps over
820 the lazy dog"});
821 cx.simulate_shared_keystrokes("x").await;
822 cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
823 the lazy dog"});
824
825 // it should work on empty lines
826 cx.set_shared_state(indoc! {"
827 a
828 ˇ
829 b"})
830 .await;
831 cx.simulate_shared_keystrokes("shift-v").await;
832 cx.shared_state().await.assert_eq(indoc! {"
833 a
834 «
835 ˇ»b"});
836 cx.simulate_shared_keystrokes("x").await;
837 cx.shared_state().await.assert_eq(indoc! {"
838 a
839 ˇb"});
840
841 // it should work at the end of the document
842 cx.set_shared_state(indoc! {"
843 a
844 b
845 ˇ"})
846 .await;
847 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
848 cx.simulate_shared_keystrokes("shift-v").await;
849 cx.shared_state().await.assert_eq(indoc! {"
850 a
851 b
852 ˇ"});
853 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
854 cx.simulate_shared_keystrokes("x").await;
855 cx.shared_state().await.assert_eq(indoc! {"
856 a
857 ˇb"});
858 }
859
860 #[gpui::test]
861 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
862 let mut cx = NeovimBackedTestContext::new(cx).await;
863
864 cx.simulate("v w", "The quick ˇbrown")
865 .await
866 .assert_matches();
867
868 cx.simulate("v w x", "The quick ˇbrown")
869 .await
870 .assert_matches();
871 cx.simulate(
872 "v w j x",
873 indoc! {"
874 The ˇquick brown
875 fox jumps over
876 the lazy dog"},
877 )
878 .await
879 .assert_matches();
880 // Test pasting code copied on delete
881 cx.simulate_shared_keystrokes("j p").await;
882 cx.shared_state().await.assert_matches();
883
884 cx.simulate_at_each_offset(
885 "v w j x",
886 indoc! {"
887 The ˇquick brown
888 fox jumps over
889 the ˇlazy dog"},
890 )
891 .await
892 .assert_matches();
893 cx.simulate_at_each_offset(
894 "v b k x",
895 indoc! {"
896 The ˇquick brown
897 fox jumps ˇover
898 the ˇlazy dog"},
899 )
900 .await
901 .assert_matches();
902 }
903
904 #[gpui::test]
905 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
906 let mut cx = NeovimBackedTestContext::new(cx).await;
907
908 cx.set_shared_state(indoc! {"
909 The quˇick brown
910 fox jumps over
911 the lazy dog"})
912 .await;
913 cx.simulate_shared_keystrokes("shift-v x").await;
914 cx.shared_state().await.assert_matches();
915
916 // Test pasting code copied on delete
917 cx.simulate_shared_keystrokes("p").await;
918 cx.shared_state().await.assert_matches();
919
920 cx.set_shared_state(indoc! {"
921 The quick brown
922 fox jumps over
923 the laˇzy dog"})
924 .await;
925 cx.simulate_shared_keystrokes("shift-v x").await;
926 cx.shared_state().await.assert_matches();
927 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
928
929 cx.set_shared_state(indoc! {"
930 The quˇick brown
931 fox jumps over
932 the lazy dog"})
933 .await;
934 cx.simulate_shared_keystrokes("shift-v j x").await;
935 cx.shared_state().await.assert_matches();
936 // Test pasting code copied on delete
937 cx.simulate_shared_keystrokes("p").await;
938 cx.shared_state().await.assert_matches();
939
940 cx.set_shared_state(indoc! {"
941 The ˇlong line
942 should not
943 crash
944 "})
945 .await;
946 cx.simulate_shared_keystrokes("shift-v $ x").await;
947 cx.shared_state().await.assert_matches();
948 }
949
950 #[gpui::test]
951 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
952 let mut cx = NeovimBackedTestContext::new(cx).await;
953
954 cx.set_shared_state("The quick ˇbrown").await;
955 cx.simulate_shared_keystrokes("v w y").await;
956 cx.shared_state().await.assert_eq("The quick ˇbrown");
957 cx.shared_clipboard().await.assert_eq("brown");
958
959 cx.set_shared_state(indoc! {"
960 The ˇquick brown
961 fox jumps over
962 the lazy dog"})
963 .await;
964 cx.simulate_shared_keystrokes("v w j y").await;
965 cx.shared_state().await.assert_eq(indoc! {"
966 The ˇquick brown
967 fox jumps over
968 the lazy dog"});
969 cx.shared_clipboard().await.assert_eq(indoc! {"
970 quick brown
971 fox jumps o"});
972
973 cx.set_shared_state(indoc! {"
974 The quick brown
975 fox jumps over
976 the ˇlazy dog"})
977 .await;
978 cx.simulate_shared_keystrokes("v w j y").await;
979 cx.shared_state().await.assert_eq(indoc! {"
980 The quick brown
981 fox jumps over
982 the ˇlazy dog"});
983 cx.shared_clipboard().await.assert_eq("lazy d");
984 cx.simulate_shared_keystrokes("shift-v y").await;
985 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
986
987 cx.set_shared_state(indoc! {"
988 The ˇquick brown
989 fox jumps over
990 the lazy dog"})
991 .await;
992 cx.simulate_shared_keystrokes("v b k y").await;
993 cx.shared_state().await.assert_eq(indoc! {"
994 ˇThe quick brown
995 fox jumps over
996 the lazy dog"});
997 assert_eq!(
998 cx.read_from_clipboard()
999 .map(|item| item.text().unwrap().to_string())
1000 .unwrap(),
1001 "The q"
1002 );
1003
1004 cx.set_shared_state(indoc! {"
1005 The quick brown
1006 fox ˇjumps over
1007 the lazy dog"})
1008 .await;
1009 cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
1010 .await;
1011 cx.shared_state().await.assert_eq(indoc! {"
1012 The quick brown
1013 ˇfox jumps over
1014 the lazy dog"});
1015 cx.shared_clipboard()
1016 .await
1017 .assert_eq("fox jumps over\nthe lazy dog\n");
1018 }
1019
1020 #[gpui::test]
1021 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1022 let mut cx = NeovimBackedTestContext::new(cx).await;
1023
1024 cx.set_shared_state(indoc! {
1025 "The ˇquick brown
1026 fox jumps over
1027 the lazy dog"
1028 })
1029 .await;
1030 cx.simulate_shared_keystrokes("ctrl-v").await;
1031 cx.shared_state().await.assert_eq(indoc! {
1032 "The «qˇ»uick brown
1033 fox jumps over
1034 the lazy dog"
1035 });
1036 cx.simulate_shared_keystrokes("2 down").await;
1037 cx.shared_state().await.assert_eq(indoc! {
1038 "The «qˇ»uick brown
1039 fox «jˇ»umps over
1040 the «lˇ»azy dog"
1041 });
1042 cx.simulate_shared_keystrokes("e").await;
1043 cx.shared_state().await.assert_eq(indoc! {
1044 "The «quicˇ»k brown
1045 fox «jumpˇ»s over
1046 the «lazyˇ» dog"
1047 });
1048 cx.simulate_shared_keystrokes("^").await;
1049 cx.shared_state().await.assert_eq(indoc! {
1050 "«ˇThe q»uick brown
1051 «ˇfox j»umps over
1052 «ˇthe l»azy dog"
1053 });
1054 cx.simulate_shared_keystrokes("$").await;
1055 cx.shared_state().await.assert_eq(indoc! {
1056 "The «quick brownˇ»
1057 fox «jumps overˇ»
1058 the «lazy dogˇ»"
1059 });
1060 cx.simulate_shared_keystrokes("shift-f space").await;
1061 cx.shared_state().await.assert_eq(indoc! {
1062 "The «quickˇ» brown
1063 fox «jumpsˇ» over
1064 the «lazy ˇ»dog"
1065 });
1066
1067 // toggling through visual mode works as expected
1068 cx.simulate_shared_keystrokes("v").await;
1069 cx.shared_state().await.assert_eq(indoc! {
1070 "The «quick brown
1071 fox jumps over
1072 the lazy ˇ»dog"
1073 });
1074 cx.simulate_shared_keystrokes("ctrl-v").await;
1075 cx.shared_state().await.assert_eq(indoc! {
1076 "The «quickˇ» brown
1077 fox «jumpsˇ» over
1078 the «lazy ˇ»dog"
1079 });
1080
1081 cx.set_shared_state(indoc! {
1082 "The ˇquick
1083 brown
1084 fox
1085 jumps over the
1086
1087 lazy dog
1088 "
1089 })
1090 .await;
1091 cx.simulate_shared_keystrokes("ctrl-v down down").await;
1092 cx.shared_state().await.assert_eq(indoc! {
1093 "The«ˇ q»uick
1094 bro«ˇwn»
1095 foxˇ
1096 jumps over the
1097
1098 lazy dog
1099 "
1100 });
1101 cx.simulate_shared_keystrokes("down").await;
1102 cx.shared_state().await.assert_eq(indoc! {
1103 "The «qˇ»uick
1104 brow«nˇ»
1105 fox
1106 jump«sˇ» over the
1107
1108 lazy dog
1109 "
1110 });
1111 cx.simulate_shared_keystrokes("left").await;
1112 cx.shared_state().await.assert_eq(indoc! {
1113 "The«ˇ q»uick
1114 bro«ˇwn»
1115 foxˇ
1116 jum«ˇps» over the
1117
1118 lazy dog
1119 "
1120 });
1121 cx.simulate_shared_keystrokes("s o escape").await;
1122 cx.shared_state().await.assert_eq(indoc! {
1123 "Theˇouick
1124 broo
1125 foxo
1126 jumo over the
1127
1128 lazy dog
1129 "
1130 });
1131
1132 // https://github.com/zed-industries/zed/issues/6274
1133 cx.set_shared_state(indoc! {
1134 "Theˇ quick brown
1135
1136 fox jumps over
1137 the lazy dog
1138 "
1139 })
1140 .await;
1141 cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1142 cx.shared_state().await.assert_eq(indoc! {
1143 "The «qˇ»uick brown
1144
1145 fox «jˇ»umps over
1146 the lazy dog
1147 "
1148 });
1149 }
1150
1151 #[gpui::test]
1152 async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1153 let mut cx = NeovimBackedTestContext::new(cx).await;
1154
1155 cx.set_shared_state(indoc! {
1156 "The ˇquick brown
1157 fox jumps over
1158 the lazy dog
1159 "
1160 })
1161 .await;
1162 cx.simulate_shared_keystrokes("ctrl-v right down").await;
1163 cx.shared_state().await.assert_eq(indoc! {
1164 "The «quˇ»ick brown
1165 fox «juˇ»mps over
1166 the lazy dog
1167 "
1168 });
1169 }
1170
1171 #[gpui::test]
1172 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1173 let mut cx = NeovimBackedTestContext::new(cx).await;
1174
1175 cx.set_shared_state(indoc! {
1176 "ˇThe quick brown
1177 fox jumps over
1178 the lazy dog
1179 "
1180 })
1181 .await;
1182 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1183 cx.shared_state().await.assert_eq(indoc! {
1184 "«Tˇ»he quick brown
1185 «fˇ»ox jumps over
1186 «tˇ»he lazy dog
1187 ˇ"
1188 });
1189
1190 cx.simulate_shared_keystrokes("shift-i k escape").await;
1191 cx.shared_state().await.assert_eq(indoc! {
1192 "ˇkThe quick brown
1193 kfox jumps over
1194 kthe lazy dog
1195 k"
1196 });
1197
1198 cx.set_shared_state(indoc! {
1199 "ˇThe quick brown
1200 fox jumps over
1201 the lazy dog
1202 "
1203 })
1204 .await;
1205 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1206 cx.shared_state().await.assert_eq(indoc! {
1207 "«Tˇ»he quick brown
1208 «fˇ»ox jumps over
1209 «tˇ»he lazy dog
1210 ˇ"
1211 });
1212 cx.simulate_shared_keystrokes("c k escape").await;
1213 cx.shared_state().await.assert_eq(indoc! {
1214 "ˇkhe quick brown
1215 kox jumps over
1216 khe lazy dog
1217 k"
1218 });
1219 }
1220
1221 #[gpui::test]
1222 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1223 let mut cx = NeovimBackedTestContext::new(cx).await;
1224
1225 cx.set_shared_state("hello (in [parˇens] o)").await;
1226 cx.simulate_shared_keystrokes("ctrl-v l").await;
1227 cx.simulate_shared_keystrokes("a ]").await;
1228 cx.shared_state()
1229 .await
1230 .assert_eq("hello (in «[parens]ˇ» o)");
1231 cx.simulate_shared_keystrokes("i (").await;
1232 cx.shared_state()
1233 .await
1234 .assert_eq("hello («in [parens] oˇ»)");
1235
1236 cx.set_shared_state("hello in a wˇord again.").await;
1237 cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1238 cx.shared_state()
1239 .await
1240 .assert_eq("hello in a w«ordˇ» again.");
1241 assert_eq!(cx.mode(), Mode::VisualBlock);
1242 cx.simulate_shared_keystrokes("o a s").await;
1243 cx.shared_state()
1244 .await
1245 .assert_eq("«ˇhello in a word» again.");
1246 }
1247
1248 #[gpui::test]
1249 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1250 let mut cx = VimTestContext::new(cx, true).await;
1251
1252 cx.set_state("aˇbc", Mode::Normal);
1253 cx.simulate_keystrokes("ctrl-v");
1254 assert_eq!(cx.mode(), Mode::VisualBlock);
1255 cx.simulate_keystrokes("cmd-shift-p escape");
1256 assert_eq!(cx.mode(), Mode::VisualBlock);
1257 }
1258
1259 #[gpui::test]
1260 async fn test_gn(cx: &mut gpui::TestAppContext) {
1261 let mut cx = NeovimBackedTestContext::new(cx).await;
1262
1263 cx.set_shared_state("aaˇ aa aa aa aa").await;
1264 cx.simulate_shared_keystrokes("/ a a enter").await;
1265 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1266 cx.simulate_shared_keystrokes("g n").await;
1267 cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1268 cx.simulate_shared_keystrokes("g n").await;
1269 cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1270 cx.simulate_shared_keystrokes("escape d g n").await;
1271 cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1272
1273 cx.set_shared_state("aaˇ aa aa aa aa").await;
1274 cx.simulate_shared_keystrokes("/ a a enter").await;
1275 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1276 cx.simulate_shared_keystrokes("3 g n").await;
1277 cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1278
1279 cx.set_shared_state("aaˇ aa aa aa aa").await;
1280 cx.simulate_shared_keystrokes("/ a a enter").await;
1281 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1282 cx.simulate_shared_keystrokes("g shift-n").await;
1283 cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1284 cx.simulate_shared_keystrokes("g shift-n").await;
1285 cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1286 }
1287
1288 #[gpui::test]
1289 async fn test_gl(cx: &mut gpui::TestAppContext) {
1290 let mut cx = VimTestContext::new(cx, true).await;
1291
1292 cx.set_state("aaˇ aa\naa", Mode::Normal);
1293 cx.simulate_keystrokes("g l");
1294 cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1295 cx.simulate_keystrokes("g >");
1296 cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1297 }
1298
1299 #[gpui::test]
1300 async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1301 let mut cx = NeovimBackedTestContext::new(cx).await;
1302
1303 cx.set_shared_state("aaˇ aa aa aa aa").await;
1304 cx.simulate_shared_keystrokes("/ a a enter").await;
1305 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1306 cx.simulate_shared_keystrokes("d g n").await;
1307
1308 cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1309 cx.simulate_shared_keystrokes(".").await;
1310 cx.shared_state().await.assert_eq("aa ˇ aa aa");
1311 cx.simulate_shared_keystrokes(".").await;
1312 cx.shared_state().await.assert_eq("aa ˇ aa");
1313 }
1314
1315 #[gpui::test]
1316 async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1317 let mut cx = NeovimBackedTestContext::new(cx).await;
1318
1319 cx.set_shared_state("aaˇ aa aa aa aa").await;
1320 cx.simulate_shared_keystrokes("/ a a enter").await;
1321 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1322 cx.simulate_shared_keystrokes("c g n x escape").await;
1323 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1324 cx.simulate_shared_keystrokes(".").await;
1325 cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1326 }
1327
1328 #[gpui::test]
1329 async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1330 let mut cx = NeovimBackedTestContext::new(cx).await;
1331
1332 cx.set_shared_state("aaˇ aa aa aa aa").await;
1333 cx.simulate_shared_keystrokes("/ b b enter").await;
1334 cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1335 cx.simulate_shared_keystrokes("c g n x escape").await;
1336 cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1337 cx.simulate_shared_keystrokes(".").await;
1338 cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1339
1340 cx.set_shared_state("aaˇ bb aa aa aa").await;
1341 cx.simulate_shared_keystrokes("/ b b enter").await;
1342 cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1343 cx.simulate_shared_keystrokes("c g n x escape").await;
1344 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1345 cx.simulate_shared_keystrokes(".").await;
1346 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1347 }
1348
1349 #[gpui::test]
1350 async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1351 let mut cx = NeovimBackedTestContext::new(cx).await;
1352
1353 cx.set_shared_state(indoc! {
1354 "The ˇquick brown
1355 fox jumps over
1356 the lazy dog
1357 "
1358 })
1359 .await;
1360 cx.simulate_shared_keystrokes("v down shift-d").await;
1361 cx.shared_state().await.assert_eq(indoc! {
1362 "the ˇlazy dog\n"
1363 });
1364
1365 cx.set_shared_state(indoc! {
1366 "The ˇquick brown
1367 fox jumps over
1368 the lazy dog
1369 "
1370 })
1371 .await;
1372 cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1373 cx.shared_state().await.assert_eq(indoc! {
1374 "Theˇ•
1375 fox•
1376 the lazy dog
1377 "
1378 });
1379 }
1380
1381 #[gpui::test]
1382 async fn test_gv(cx: &mut gpui::TestAppContext) {
1383 let mut cx = NeovimBackedTestContext::new(cx).await;
1384
1385 cx.set_shared_state(indoc! {
1386 "The ˇquick brown"
1387 })
1388 .await;
1389 cx.simulate_shared_keystrokes("v i w escape g v").await;
1390 cx.shared_state().await.assert_eq(indoc! {
1391 "The «quickˇ» brown"
1392 });
1393
1394 cx.simulate_shared_keystrokes("o escape g v").await;
1395 cx.shared_state().await.assert_eq(indoc! {
1396 "The «ˇquick» brown"
1397 });
1398
1399 cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1400 cx.shared_state().await.assert_eq(indoc! {
1401 "«Thˇ»e quick brown"
1402 });
1403 cx.simulate_shared_keystrokes("g v").await;
1404 cx.shared_state().await.assert_eq(indoc! {
1405 "The «ˇquick» brown"
1406 });
1407 cx.simulate_shared_keystrokes("g v").await;
1408 cx.shared_state().await.assert_eq(indoc! {
1409 "«Thˇ»e quick brown"
1410 });
1411
1412 cx.set_state(
1413 indoc! {"
1414 fiˇsh one
1415 fish two
1416 fish red
1417 fish blue
1418 "},
1419 Mode::Normal,
1420 );
1421 cx.simulate_keystrokes("4 g l escape escape g v");
1422 cx.assert_state(
1423 indoc! {"
1424 «fishˇ» one
1425 «fishˇ» two
1426 «fishˇ» red
1427 «fishˇ» blue
1428 "},
1429 Mode::Visual,
1430 );
1431 cx.simulate_keystrokes("y g v");
1432 cx.assert_state(
1433 indoc! {"
1434 «fishˇ» one
1435 «fishˇ» two
1436 «fishˇ» red
1437 «fishˇ» blue
1438 "},
1439 Mode::Visual,
1440 );
1441 }
1442}