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