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