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