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