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