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