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