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