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