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