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