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