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