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