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