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