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