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