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