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