1use std::sync::Arc;
2
3use collections::HashMap;
4use editor::{
5 display_map::{DisplayRow, 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 selection.end =
467 map.next_line_boundary(selection.end.to_point(map)).1;
468 if selection.end.row() == map.max_point().row() {
469 selection.end = map.max_point();
470 if selection.start == selection.end {
471 let prev_row =
472 DisplayRow(selection.start.row().0.saturating_sub(1));
473 selection.start =
474 DisplayPoint::new(prev_row, map.line_len(prev_row));
475 }
476 } else {
477 *selection.end.row_mut() += 1;
478 *selection.end.column_mut() = 0;
479 }
480 }
481 }
482 selection.goal = SelectionGoal::None;
483 });
484 });
485 vim.copy_selections_content(editor, line_mode, cx);
486 editor.insert("", cx);
487
488 // Fixup cursor position after the deletion
489 editor.set_clip_at_line_ends(true, cx);
490 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
491 s.move_with(|map, selection| {
492 let mut cursor = selection.head().to_point(map);
493
494 if let Some(column) = original_columns.get(&selection.id) {
495 cursor.column = *column
496 }
497 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
498 selection.collapse_to(cursor, selection.goal)
499 });
500 if vim.mode == Mode::VisualBlock {
501 s.select_anchors(vec![s.first_anchor()])
502 }
503 });
504 })
505 });
506 self.switch_mode(Mode::Normal, true, cx);
507 }
508
509 pub fn visual_yank(&mut self, cx: &mut ViewContext<Self>) {
510 self.store_visual_marks(cx);
511 self.update_editor(cx, |vim, editor, cx| {
512 let line_mode = editor.selections.line_mode;
513 vim.yank_selections_content(editor, line_mode, cx);
514 editor.change_selections(None, cx, |s| {
515 s.move_with(|map, selection| {
516 if line_mode {
517 selection.start = start_of_line(map, false, selection.start);
518 };
519 selection.collapse_to(selection.start, SelectionGoal::None)
520 });
521 if vim.mode == Mode::VisualBlock {
522 s.select_anchors(vec![s.first_anchor()])
523 }
524 });
525 });
526 self.switch_mode(Mode::Normal, true, cx);
527 }
528
529 pub(crate) fn visual_replace(&mut self, text: Arc<str>, cx: &mut ViewContext<Self>) {
530 self.stop_recording(cx);
531 self.update_editor(cx, |_, editor, cx| {
532 editor.transact(cx, |editor, cx| {
533 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
534
535 // Selections are biased right at the start. So we need to store
536 // anchors that are biased left so that we can restore the selections
537 // after the change
538 let stable_anchors = editor
539 .selections
540 .disjoint_anchors()
541 .iter()
542 .map(|selection| {
543 let start = selection.start.bias_left(&display_map.buffer_snapshot);
544 start..start
545 })
546 .collect::<Vec<_>>();
547
548 let mut edits = Vec::new();
549 for selection in selections.iter() {
550 let selection = selection.clone();
551 for row_range in
552 movement::split_display_range_by_lines(&display_map, selection.range())
553 {
554 let range = row_range.start.to_offset(&display_map, Bias::Right)
555 ..row_range.end.to_offset(&display_map, Bias::Right);
556 let text = text.repeat(range.len());
557 edits.push((range, text));
558 }
559 }
560
561 editor.edit(edits, cx);
562 editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
563 });
564 });
565 self.switch_mode(Mode::Normal, false, cx);
566 }
567
568 pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
569 let count =
570 Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
571 self.update_editor(cx, |_, editor, cx| {
572 editor.set_clip_at_line_ends(false, cx);
573 for _ in 0..count {
574 if editor
575 .select_next(&Default::default(), cx)
576 .log_err()
577 .is_none()
578 {
579 break;
580 }
581 }
582 });
583 }
584
585 pub fn select_previous(&mut self, _: &SelectPrevious, cx: &mut ViewContext<Self>) {
586 let count =
587 Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
588 self.update_editor(cx, |_, editor, cx| {
589 for _ in 0..count {
590 if editor
591 .select_previous(&Default::default(), cx)
592 .log_err()
593 .is_none()
594 {
595 break;
596 }
597 }
598 });
599 }
600
601 pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
602 let count = Vim::take_count(cx).unwrap_or(1);
603 let Some(pane) = self.pane(cx) else {
604 return;
605 };
606 let vim_is_normal = self.mode == Mode::Normal;
607 let mut start_selection = 0usize;
608 let mut end_selection = 0usize;
609
610 self.update_editor(cx, |_, editor, _| {
611 editor.set_collapse_matches(false);
612 });
613 if vim_is_normal {
614 pane.update(cx, |pane, cx| {
615 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
616 {
617 search_bar.update(cx, |search_bar, cx| {
618 if !search_bar.has_active_match() || !search_bar.show(cx) {
619 return;
620 }
621 // without update_match_index there is a bug when the cursor is before the first match
622 search_bar.update_match_index(cx);
623 search_bar.select_match(direction.opposite(), 1, cx);
624 });
625 }
626 });
627 }
628 self.update_editor(cx, |_, editor, cx| {
629 let latest = editor.selections.newest::<usize>(cx);
630 start_selection = latest.start;
631 end_selection = latest.end;
632 });
633
634 let mut match_exists = false;
635 pane.update(cx, |pane, cx| {
636 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
637 search_bar.update(cx, |search_bar, cx| {
638 search_bar.update_match_index(cx);
639 search_bar.select_match(direction, count, cx);
640 match_exists = search_bar.match_exists(cx);
641 });
642 }
643 });
644 if !match_exists {
645 self.clear_operator(cx);
646 self.stop_replaying(cx);
647 return;
648 }
649 self.update_editor(cx, |_, editor, cx| {
650 let latest = editor.selections.newest::<usize>(cx);
651 if vim_is_normal {
652 start_selection = latest.start;
653 end_selection = latest.end;
654 } else {
655 start_selection = start_selection.min(latest.start);
656 end_selection = end_selection.max(latest.end);
657 }
658 if direction == Direction::Prev {
659 std::mem::swap(&mut start_selection, &mut end_selection);
660 }
661 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
662 s.select_ranges([start_selection..end_selection]);
663 });
664 editor.set_collapse_matches(true);
665 });
666
667 match self.maybe_pop_operator() {
668 Some(Operator::Change) => self.substitute(None, false, cx),
669 Some(Operator::Delete) => {
670 self.stop_recording(cx);
671 self.visual_delete(false, cx)
672 }
673 Some(Operator::Yank) => self.visual_yank(cx),
674 _ => {} // Ignoring other operators
675 }
676 }
677}
678#[cfg(test)]
679mod test {
680 use indoc::indoc;
681 use workspace::item::Item;
682
683 use crate::{
684 state::Mode,
685 test::{NeovimBackedTestContext, VimTestContext},
686 };
687
688 #[gpui::test]
689 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
690 let mut cx = NeovimBackedTestContext::new(cx).await;
691
692 cx.set_shared_state(indoc! {
693 "The ˇquick brown
694 fox jumps over
695 the lazy dog"
696 })
697 .await;
698 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
699
700 // entering visual mode should select the character
701 // under cursor
702 cx.simulate_shared_keystrokes("v").await;
703 cx.shared_state()
704 .await
705 .assert_eq(indoc! { "The «qˇ»uick brown
706 fox jumps over
707 the lazy dog"});
708 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
709
710 // forwards motions should extend the selection
711 cx.simulate_shared_keystrokes("w j").await;
712 cx.shared_state().await.assert_eq(indoc! { "The «quick brown
713 fox jumps oˇ»ver
714 the lazy dog"});
715
716 cx.simulate_shared_keystrokes("escape").await;
717 cx.shared_state().await.assert_eq(indoc! { "The quick brown
718 fox jumps ˇover
719 the lazy dog"});
720
721 // motions work backwards
722 cx.simulate_shared_keystrokes("v k b").await;
723 cx.shared_state()
724 .await
725 .assert_eq(indoc! { "The «ˇquick brown
726 fox jumps o»ver
727 the lazy dog"});
728
729 // works on empty lines
730 cx.set_shared_state(indoc! {"
731 a
732 ˇ
733 b
734 "})
735 .await;
736 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
737 cx.simulate_shared_keystrokes("v").await;
738 cx.shared_state().await.assert_eq(indoc! {"
739 a
740 «
741 ˇ»b
742 "});
743 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
744
745 // toggles off again
746 cx.simulate_shared_keystrokes("v").await;
747 cx.shared_state().await.assert_eq(indoc! {"
748 a
749 ˇ
750 b
751 "});
752
753 // works at the end of a document
754 cx.set_shared_state(indoc! {"
755 a
756 b
757 ˇ"})
758 .await;
759
760 cx.simulate_shared_keystrokes("v").await;
761 cx.shared_state().await.assert_eq(indoc! {"
762 a
763 b
764 ˇ"});
765 }
766
767 #[gpui::test]
768 async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
769 let mut cx = VimTestContext::new(cx, true).await;
770
771 cx.set_state(
772 indoc! {
773 "«The quick brown
774 fox jumps over
775 the lazy dogˇ»"
776 },
777 Mode::Visual,
778 );
779 cx.simulate_keystrokes("g shift-i");
780 cx.assert_state(
781 indoc! {
782 "ˇThe quick brown
783 ˇfox jumps over
784 ˇthe lazy dog"
785 },
786 Mode::Insert,
787 );
788 }
789
790 #[gpui::test]
791 async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
792 let mut cx = VimTestContext::new(cx, true).await;
793
794 cx.set_state(
795 indoc! {
796 "«The quick brown
797 fox jumps over
798 the lazy dogˇ»"
799 },
800 Mode::Visual,
801 );
802 cx.simulate_keystrokes("g shift-a");
803 cx.assert_state(
804 indoc! {
805 "The quick brownˇ
806 fox jumps overˇ
807 the lazy dogˇ"
808 },
809 Mode::Insert,
810 );
811 }
812
813 #[gpui::test]
814 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
815 let mut cx = NeovimBackedTestContext::new(cx).await;
816
817 cx.set_shared_state(indoc! {
818 "The ˇquick brown
819 fox jumps over
820 the lazy dog"
821 })
822 .await;
823 cx.simulate_shared_keystrokes("shift-v").await;
824 cx.shared_state()
825 .await
826 .assert_eq(indoc! { "The «qˇ»uick brown
827 fox jumps over
828 the lazy dog"});
829 cx.simulate_shared_keystrokes("x").await;
830 cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
831 the lazy dog"});
832
833 // it should work on empty lines
834 cx.set_shared_state(indoc! {"
835 a
836 ˇ
837 b"})
838 .await;
839 cx.simulate_shared_keystrokes("shift-v").await;
840 cx.shared_state().await.assert_eq(indoc! {"
841 a
842 «
843 ˇ»b"});
844 cx.simulate_shared_keystrokes("x").await;
845 cx.shared_state().await.assert_eq(indoc! {"
846 a
847 ˇb"});
848
849 // it should work at the end of the document
850 cx.set_shared_state(indoc! {"
851 a
852 b
853 ˇ"})
854 .await;
855 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
856 cx.simulate_shared_keystrokes("shift-v").await;
857 cx.shared_state().await.assert_eq(indoc! {"
858 a
859 b
860 ˇ"});
861 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
862 cx.simulate_shared_keystrokes("x").await;
863 cx.shared_state().await.assert_eq(indoc! {"
864 a
865 ˇb"});
866 }
867
868 #[gpui::test]
869 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
870 let mut cx = NeovimBackedTestContext::new(cx).await;
871
872 cx.simulate("v w", "The quick ˇbrown")
873 .await
874 .assert_matches();
875
876 cx.simulate("v w x", "The quick ˇbrown")
877 .await
878 .assert_matches();
879 cx.simulate(
880 "v w j x",
881 indoc! {"
882 The ˇquick brown
883 fox jumps over
884 the lazy dog"},
885 )
886 .await
887 .assert_matches();
888 // Test pasting code copied on delete
889 cx.simulate_shared_keystrokes("j p").await;
890 cx.shared_state().await.assert_matches();
891
892 cx.simulate_at_each_offset(
893 "v w j x",
894 indoc! {"
895 The ˇquick brown
896 fox jumps over
897 the ˇlazy dog"},
898 )
899 .await
900 .assert_matches();
901 cx.simulate_at_each_offset(
902 "v b k x",
903 indoc! {"
904 The ˇquick brown
905 fox jumps ˇover
906 the ˇlazy dog"},
907 )
908 .await
909 .assert_matches();
910 }
911
912 #[gpui::test]
913 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
914 let mut cx = NeovimBackedTestContext::new(cx).await;
915
916 cx.set_shared_state(indoc! {"
917 The quˇick brown
918 fox jumps over
919 the lazy dog"})
920 .await;
921 cx.simulate_shared_keystrokes("shift-v x").await;
922 cx.shared_state().await.assert_matches();
923
924 // Test pasting code copied on delete
925 cx.simulate_shared_keystrokes("p").await;
926 cx.shared_state().await.assert_matches();
927
928 cx.set_shared_state(indoc! {"
929 The quick brown
930 fox jumps over
931 the laˇzy dog"})
932 .await;
933 cx.simulate_shared_keystrokes("shift-v x").await;
934 cx.shared_state().await.assert_matches();
935 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
936
937 cx.set_shared_state(indoc! {"
938 The quˇick brown
939 fox jumps over
940 the lazy dog"})
941 .await;
942 cx.simulate_shared_keystrokes("shift-v j x").await;
943 cx.shared_state().await.assert_matches();
944 // Test pasting code copied on delete
945 cx.simulate_shared_keystrokes("p").await;
946 cx.shared_state().await.assert_matches();
947
948 cx.set_shared_state(indoc! {"
949 The ˇlong line
950 should not
951 crash
952 "})
953 .await;
954 cx.simulate_shared_keystrokes("shift-v $ x").await;
955 cx.shared_state().await.assert_matches();
956 }
957
958 #[gpui::test]
959 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
960 let mut cx = NeovimBackedTestContext::new(cx).await;
961
962 cx.set_shared_state("The quick ˇbrown").await;
963 cx.simulate_shared_keystrokes("v w y").await;
964 cx.shared_state().await.assert_eq("The quick ˇbrown");
965 cx.shared_clipboard().await.assert_eq("brown");
966
967 cx.set_shared_state(indoc! {"
968 The ˇquick brown
969 fox jumps over
970 the lazy dog"})
971 .await;
972 cx.simulate_shared_keystrokes("v w j y").await;
973 cx.shared_state().await.assert_eq(indoc! {"
974 The ˇquick brown
975 fox jumps over
976 the lazy dog"});
977 cx.shared_clipboard().await.assert_eq(indoc! {"
978 quick brown
979 fox jumps o"});
980
981 cx.set_shared_state(indoc! {"
982 The quick brown
983 fox jumps over
984 the ˇlazy dog"})
985 .await;
986 cx.simulate_shared_keystrokes("v w j y").await;
987 cx.shared_state().await.assert_eq(indoc! {"
988 The quick brown
989 fox jumps over
990 the ˇlazy dog"});
991 cx.shared_clipboard().await.assert_eq("lazy d");
992 cx.simulate_shared_keystrokes("shift-v y").await;
993 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
994
995 cx.set_shared_state(indoc! {"
996 The ˇquick brown
997 fox jumps over
998 the lazy dog"})
999 .await;
1000 cx.simulate_shared_keystrokes("v b k y").await;
1001 cx.shared_state().await.assert_eq(indoc! {"
1002 ˇThe quick brown
1003 fox jumps over
1004 the lazy dog"});
1005 assert_eq!(
1006 cx.read_from_clipboard()
1007 .map(|item| item.text().unwrap().to_string())
1008 .unwrap(),
1009 "The q"
1010 );
1011
1012 cx.set_shared_state(indoc! {"
1013 The quick brown
1014 fox ˇjumps over
1015 the lazy dog"})
1016 .await;
1017 cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
1018 .await;
1019 cx.shared_state().await.assert_eq(indoc! {"
1020 The quick brown
1021 ˇfox jumps over
1022 the lazy dog"});
1023 cx.shared_clipboard()
1024 .await
1025 .assert_eq("fox jumps over\nthe lazy dog\n");
1026 }
1027
1028 #[gpui::test]
1029 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1030 let mut cx = NeovimBackedTestContext::new(cx).await;
1031
1032 cx.set_shared_state(indoc! {
1033 "The ˇquick brown
1034 fox jumps over
1035 the lazy dog"
1036 })
1037 .await;
1038 cx.simulate_shared_keystrokes("ctrl-v").await;
1039 cx.shared_state().await.assert_eq(indoc! {
1040 "The «qˇ»uick brown
1041 fox jumps over
1042 the lazy dog"
1043 });
1044 cx.simulate_shared_keystrokes("2 down").await;
1045 cx.shared_state().await.assert_eq(indoc! {
1046 "The «qˇ»uick brown
1047 fox «jˇ»umps over
1048 the «lˇ»azy dog"
1049 });
1050 cx.simulate_shared_keystrokes("e").await;
1051 cx.shared_state().await.assert_eq(indoc! {
1052 "The «quicˇ»k brown
1053 fox «jumpˇ»s over
1054 the «lazyˇ» dog"
1055 });
1056 cx.simulate_shared_keystrokes("^").await;
1057 cx.shared_state().await.assert_eq(indoc! {
1058 "«ˇThe q»uick brown
1059 «ˇfox j»umps over
1060 «ˇthe l»azy dog"
1061 });
1062 cx.simulate_shared_keystrokes("$").await;
1063 cx.shared_state().await.assert_eq(indoc! {
1064 "The «quick brownˇ»
1065 fox «jumps overˇ»
1066 the «lazy dogˇ»"
1067 });
1068 cx.simulate_shared_keystrokes("shift-f space").await;
1069 cx.shared_state().await.assert_eq(indoc! {
1070 "The «quickˇ» brown
1071 fox «jumpsˇ» over
1072 the «lazy ˇ»dog"
1073 });
1074
1075 // toggling through visual mode works as expected
1076 cx.simulate_shared_keystrokes("v").await;
1077 cx.shared_state().await.assert_eq(indoc! {
1078 "The «quick brown
1079 fox jumps over
1080 the lazy ˇ»dog"
1081 });
1082 cx.simulate_shared_keystrokes("ctrl-v").await;
1083 cx.shared_state().await.assert_eq(indoc! {
1084 "The «quickˇ» brown
1085 fox «jumpsˇ» over
1086 the «lazy ˇ»dog"
1087 });
1088
1089 cx.set_shared_state(indoc! {
1090 "The ˇquick
1091 brown
1092 fox
1093 jumps over the
1094
1095 lazy dog
1096 "
1097 })
1098 .await;
1099 cx.simulate_shared_keystrokes("ctrl-v down down").await;
1100 cx.shared_state().await.assert_eq(indoc! {
1101 "The«ˇ q»uick
1102 bro«ˇwn»
1103 foxˇ
1104 jumps over the
1105
1106 lazy dog
1107 "
1108 });
1109 cx.simulate_shared_keystrokes("down").await;
1110 cx.shared_state().await.assert_eq(indoc! {
1111 "The «qˇ»uick
1112 brow«nˇ»
1113 fox
1114 jump«sˇ» over the
1115
1116 lazy dog
1117 "
1118 });
1119 cx.simulate_shared_keystrokes("left").await;
1120 cx.shared_state().await.assert_eq(indoc! {
1121 "The«ˇ q»uick
1122 bro«ˇwn»
1123 foxˇ
1124 jum«ˇps» over the
1125
1126 lazy dog
1127 "
1128 });
1129 cx.simulate_shared_keystrokes("s o escape").await;
1130 cx.shared_state().await.assert_eq(indoc! {
1131 "Theˇouick
1132 broo
1133 foxo
1134 jumo over the
1135
1136 lazy dog
1137 "
1138 });
1139
1140 // https://github.com/zed-industries/zed/issues/6274
1141 cx.set_shared_state(indoc! {
1142 "Theˇ quick brown
1143
1144 fox jumps over
1145 the lazy dog
1146 "
1147 })
1148 .await;
1149 cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1150 cx.shared_state().await.assert_eq(indoc! {
1151 "The «qˇ»uick brown
1152
1153 fox «jˇ»umps over
1154 the lazy dog
1155 "
1156 });
1157 }
1158
1159 #[gpui::test]
1160 async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1161 let mut cx = NeovimBackedTestContext::new(cx).await;
1162
1163 cx.set_shared_state(indoc! {
1164 "The ˇquick brown
1165 fox jumps over
1166 the lazy dog
1167 "
1168 })
1169 .await;
1170 cx.simulate_shared_keystrokes("ctrl-v right down").await;
1171 cx.shared_state().await.assert_eq(indoc! {
1172 "The «quˇ»ick brown
1173 fox «juˇ»mps over
1174 the lazy dog
1175 "
1176 });
1177 }
1178
1179 #[gpui::test]
1180 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1181 let mut cx = NeovimBackedTestContext::new(cx).await;
1182
1183 cx.set_shared_state(indoc! {
1184 "ˇThe quick brown
1185 fox jumps over
1186 the lazy dog
1187 "
1188 })
1189 .await;
1190 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1191 cx.shared_state().await.assert_eq(indoc! {
1192 "«Tˇ»he quick brown
1193 «fˇ»ox jumps over
1194 «tˇ»he lazy dog
1195 ˇ"
1196 });
1197
1198 cx.simulate_shared_keystrokes("shift-i k escape").await;
1199 cx.shared_state().await.assert_eq(indoc! {
1200 "ˇkThe quick brown
1201 kfox jumps over
1202 kthe lazy dog
1203 k"
1204 });
1205
1206 cx.set_shared_state(indoc! {
1207 "ˇThe quick brown
1208 fox jumps over
1209 the lazy dog
1210 "
1211 })
1212 .await;
1213 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1214 cx.shared_state().await.assert_eq(indoc! {
1215 "«Tˇ»he quick brown
1216 «fˇ»ox jumps over
1217 «tˇ»he lazy dog
1218 ˇ"
1219 });
1220 cx.simulate_shared_keystrokes("c k escape").await;
1221 cx.shared_state().await.assert_eq(indoc! {
1222 "ˇkhe quick brown
1223 kox jumps over
1224 khe lazy dog
1225 k"
1226 });
1227 }
1228
1229 #[gpui::test]
1230 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1231 let mut cx = NeovimBackedTestContext::new(cx).await;
1232
1233 cx.set_shared_state("hello (in [parˇens] o)").await;
1234 cx.simulate_shared_keystrokes("ctrl-v l").await;
1235 cx.simulate_shared_keystrokes("a ]").await;
1236 cx.shared_state()
1237 .await
1238 .assert_eq("hello (in «[parens]ˇ» o)");
1239 cx.simulate_shared_keystrokes("i (").await;
1240 cx.shared_state()
1241 .await
1242 .assert_eq("hello («in [parens] oˇ»)");
1243
1244 cx.set_shared_state("hello in a wˇord again.").await;
1245 cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1246 cx.shared_state()
1247 .await
1248 .assert_eq("hello in a w«ordˇ» again.");
1249 assert_eq!(cx.mode(), Mode::VisualBlock);
1250 cx.simulate_shared_keystrokes("o a s").await;
1251 cx.shared_state()
1252 .await
1253 .assert_eq("«ˇhello in a word» again.");
1254 }
1255
1256 #[gpui::test]
1257 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1258 let mut cx = VimTestContext::new(cx, true).await;
1259
1260 cx.set_state("aˇbc", Mode::Normal);
1261 cx.simulate_keystrokes("ctrl-v");
1262 assert_eq!(cx.mode(), Mode::VisualBlock);
1263 cx.simulate_keystrokes("cmd-shift-p escape");
1264 assert_eq!(cx.mode(), Mode::VisualBlock);
1265 }
1266
1267 #[gpui::test]
1268 async fn test_gn(cx: &mut gpui::TestAppContext) {
1269 let mut cx = NeovimBackedTestContext::new(cx).await;
1270
1271 cx.set_shared_state("aaˇ aa aa aa aa").await;
1272 cx.simulate_shared_keystrokes("/ a a enter").await;
1273 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1274 cx.simulate_shared_keystrokes("g n").await;
1275 cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1276 cx.simulate_shared_keystrokes("g n").await;
1277 cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1278 cx.simulate_shared_keystrokes("escape d g n").await;
1279 cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1280
1281 cx.set_shared_state("aaˇ aa aa aa aa").await;
1282 cx.simulate_shared_keystrokes("/ a a enter").await;
1283 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1284 cx.simulate_shared_keystrokes("3 g n").await;
1285 cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1286
1287 cx.set_shared_state("aaˇ aa aa aa aa").await;
1288 cx.simulate_shared_keystrokes("/ a a enter").await;
1289 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1290 cx.simulate_shared_keystrokes("g shift-n").await;
1291 cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1292 cx.simulate_shared_keystrokes("g shift-n").await;
1293 cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1294 }
1295
1296 #[gpui::test]
1297 async fn test_gl(cx: &mut gpui::TestAppContext) {
1298 let mut cx = VimTestContext::new(cx, true).await;
1299
1300 cx.set_state("aaˇ aa\naa", Mode::Normal);
1301 cx.simulate_keystrokes("g l");
1302 cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1303 cx.simulate_keystrokes("g >");
1304 cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1305 }
1306
1307 #[gpui::test]
1308 async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1309 let mut cx = NeovimBackedTestContext::new(cx).await;
1310
1311 cx.set_shared_state("aaˇ aa aa aa aa").await;
1312 cx.simulate_shared_keystrokes("/ a a enter").await;
1313 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1314 cx.simulate_shared_keystrokes("d g n").await;
1315
1316 cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1317 cx.simulate_shared_keystrokes(".").await;
1318 cx.shared_state().await.assert_eq("aa ˇ aa aa");
1319 cx.simulate_shared_keystrokes(".").await;
1320 cx.shared_state().await.assert_eq("aa ˇ aa");
1321 }
1322
1323 #[gpui::test]
1324 async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1325 let mut cx = NeovimBackedTestContext::new(cx).await;
1326
1327 cx.set_shared_state("aaˇ aa aa aa aa").await;
1328 cx.simulate_shared_keystrokes("/ a a enter").await;
1329 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1330 cx.simulate_shared_keystrokes("c g n x escape").await;
1331 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1332 cx.simulate_shared_keystrokes(".").await;
1333 cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1334 }
1335
1336 #[gpui::test]
1337 async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1338 let mut cx = NeovimBackedTestContext::new(cx).await;
1339
1340 cx.set_shared_state("aaˇ aa aa aa aa").await;
1341 cx.simulate_shared_keystrokes("/ b b enter").await;
1342 cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1343 cx.simulate_shared_keystrokes("c g n x escape").await;
1344 cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1345 cx.simulate_shared_keystrokes(".").await;
1346 cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1347
1348 cx.set_shared_state("aaˇ bb aa aa aa").await;
1349 cx.simulate_shared_keystrokes("/ b b enter").await;
1350 cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1351 cx.simulate_shared_keystrokes("c g n x escape").await;
1352 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1353 cx.simulate_shared_keystrokes(".").await;
1354 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1355 }
1356
1357 #[gpui::test]
1358 async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1359 let mut cx = NeovimBackedTestContext::new(cx).await;
1360
1361 cx.set_shared_state(indoc! {
1362 "The ˇquick brown
1363 fox jumps over
1364 the lazy dog
1365 "
1366 })
1367 .await;
1368 cx.simulate_shared_keystrokes("v down shift-d").await;
1369 cx.shared_state().await.assert_eq(indoc! {
1370 "the ˇlazy dog\n"
1371 });
1372
1373 cx.set_shared_state(indoc! {
1374 "The ˇquick brown
1375 fox jumps over
1376 the lazy dog
1377 "
1378 })
1379 .await;
1380 cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1381 cx.shared_state().await.assert_eq(indoc! {
1382 "Theˇ•
1383 fox•
1384 the lazy dog
1385 "
1386 });
1387 }
1388
1389 #[gpui::test]
1390 async fn test_gv(cx: &mut gpui::TestAppContext) {
1391 let mut cx = NeovimBackedTestContext::new(cx).await;
1392
1393 cx.set_shared_state(indoc! {
1394 "The ˇquick brown"
1395 })
1396 .await;
1397 cx.simulate_shared_keystrokes("v i w escape g v").await;
1398 cx.shared_state().await.assert_eq(indoc! {
1399 "The «quickˇ» brown"
1400 });
1401
1402 cx.simulate_shared_keystrokes("o escape g v").await;
1403 cx.shared_state().await.assert_eq(indoc! {
1404 "The «ˇquick» brown"
1405 });
1406
1407 cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1408 cx.shared_state().await.assert_eq(indoc! {
1409 "«Thˇ»e quick brown"
1410 });
1411 cx.simulate_shared_keystrokes("g v").await;
1412 cx.shared_state().await.assert_eq(indoc! {
1413 "The «ˇquick» brown"
1414 });
1415 cx.simulate_shared_keystrokes("g v").await;
1416 cx.shared_state().await.assert_eq(indoc! {
1417 "«Thˇ»e quick brown"
1418 });
1419
1420 cx.set_state(
1421 indoc! {"
1422 fiˇsh one
1423 fish two
1424 fish red
1425 fish blue
1426 "},
1427 Mode::Normal,
1428 );
1429 cx.simulate_keystrokes("4 g l escape escape g v");
1430 cx.assert_state(
1431 indoc! {"
1432 «fishˇ» one
1433 «fishˇ» two
1434 «fishˇ» red
1435 «fishˇ» blue
1436 "},
1437 Mode::Visual,
1438 );
1439 cx.simulate_keystrokes("y g v");
1440 cx.assert_state(
1441 indoc! {"
1442 «fishˇ» one
1443 «fishˇ» two
1444 «fishˇ» red
1445 «fishˇ» blue
1446 "},
1447 Mode::Visual,
1448 );
1449 }
1450}