1use std::sync::Arc;
2
3use collections::HashMap;
4use editor::{
5 Bias, DisplayPoint, Editor, ToOffset,
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 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 if selection.start == range.start
390 && selection.end == range.end
391 && object.always_expands_both_ways()
392 {
393 if let Some(range) =
394 object.range(map, selection.clone(), around)
395 {
396 selection.start = range.start;
397 selection.end = range.end;
398 }
399 } else {
400 selection.start = range.start;
401 selection.end = range.end;
402 }
403 } else if selection.reversed {
404 selection.start = range.start;
405 } else {
406 selection.end = range.end;
407 }
408 }
409
410 // In the visual selection result of a paragraph object, the cursor is
411 // placed at the start of the last line. And in the visual mode, the
412 // selection end is located after the end character. So, adjustment of
413 // selection end is needed.
414 //
415 // We don't do this adjustment for a one-line blank paragraph since the
416 // trailing newline is included in its selection from the beginning.
417 if object == Object::Paragraph && range.start != range.end {
418 let row_of_selection_end_line = selection.end.to_point(map).row;
419 let new_selection_end = if map
420 .buffer_snapshot
421 .line_len(MultiBufferRow(row_of_selection_end_line))
422 == 0
423 {
424 Point::new(row_of_selection_end_line + 1, 0)
425 } else {
426 Point::new(row_of_selection_end_line, 1)
427 };
428 selection.end = new_selection_end.to_display_point(map);
429 }
430 }
431 });
432 });
433 });
434 }
435 }
436
437 fn visual_insert_end_of_line(
438 &mut self,
439 _: &VisualInsertEndOfLine,
440 window: &mut Window,
441 cx: &mut Context<Self>,
442 ) {
443 self.update_editor(window, cx, |_, editor, window, cx| {
444 editor.split_selection_into_lines(&Default::default(), window, cx);
445 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
446 s.move_cursors_with(|map, cursor, _| {
447 (next_line_end(map, cursor, 1), SelectionGoal::None)
448 });
449 });
450 });
451
452 self.switch_mode(Mode::Insert, false, window, cx);
453 }
454
455 fn visual_insert_first_non_white_space(
456 &mut self,
457 _: &VisualInsertFirstNonWhiteSpace,
458 window: &mut Window,
459 cx: &mut Context<Self>,
460 ) {
461 self.update_editor(window, cx, |_, editor, window, cx| {
462 editor.split_selection_into_lines(&Default::default(), window, cx);
463 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
464 s.move_cursors_with(|map, cursor, _| {
465 (
466 first_non_whitespace(map, false, cursor),
467 SelectionGoal::None,
468 )
469 });
470 });
471 });
472
473 self.switch_mode(Mode::Insert, false, window, cx);
474 }
475
476 fn toggle_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context<Self>) {
477 if self.mode == mode {
478 self.switch_mode(Mode::Normal, false, window, cx);
479 } else {
480 self.switch_mode(mode, false, window, cx);
481 }
482 }
483
484 pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context<Self>) {
485 self.update_editor(window, cx, |_, editor, window, cx| {
486 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
487 s.move_with(|_, selection| {
488 selection.reversed = !selection.reversed;
489 });
490 })
491 });
492 }
493
494 pub fn other_end_row_aware(
495 &mut self,
496 _: &OtherEndRowAware,
497 window: &mut Window,
498 cx: &mut Context<Self>,
499 ) {
500 let mode = self.mode;
501 self.update_editor(window, cx, |_, editor, window, cx| {
502 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
503 s.move_with(|_, selection| {
504 selection.reversed = !selection.reversed;
505 });
506 if mode == Mode::VisualBlock {
507 s.reverse_selections();
508 }
509 })
510 });
511 }
512
513 pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
514 self.store_visual_marks(window, cx);
515 self.update_editor(window, cx, |vim, editor, window, cx| {
516 let mut original_columns: HashMap<_, _> = Default::default();
517 let line_mode = line_mode || editor.selections.line_mode;
518 editor.selections.line_mode = false;
519
520 editor.transact(window, cx, |editor, window, cx| {
521 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
522 s.move_with(|map, selection| {
523 if line_mode {
524 let mut position = selection.head();
525 if !selection.reversed {
526 position = movement::left(map, position);
527 }
528 original_columns.insert(selection.id, position.to_point(map).column);
529 if vim.mode == Mode::VisualBlock {
530 *selection.end.column_mut() = map.line_len(selection.end.row())
531 } else {
532 let start = selection.start.to_point(map);
533 let end = selection.end.to_point(map);
534 selection.start = map.prev_line_boundary(start).1;
535 if end.column == 0 && end > start {
536 let row = end.row.saturating_sub(1);
537 selection.end = Point::new(
538 row,
539 map.buffer_snapshot.line_len(MultiBufferRow(row)),
540 )
541 .to_display_point(map)
542 } else {
543 selection.end = map.next_line_boundary(end).1;
544 }
545 }
546 }
547 selection.goal = SelectionGoal::None;
548 });
549 });
550 let kind = if line_mode {
551 MotionKind::Linewise
552 } else {
553 MotionKind::Exclusive
554 };
555 vim.copy_selections_content(editor, kind, window, cx);
556
557 if line_mode && vim.mode != Mode::VisualBlock {
558 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
559 s.move_with(|map, selection| {
560 let end = selection.end.to_point(map);
561 let start = selection.start.to_point(map);
562 if end.row < map.buffer_snapshot.max_point().row {
563 selection.end = Point::new(end.row + 1, 0).to_display_point(map)
564 } else if start.row > 0 {
565 selection.start = Point::new(
566 start.row - 1,
567 map.buffer_snapshot.line_len(MultiBufferRow(start.row - 1)),
568 )
569 .to_display_point(map)
570 }
571 });
572 });
573 }
574 editor.insert("", window, cx);
575
576 // Fixup cursor position after the deletion
577 editor.set_clip_at_line_ends(true, cx);
578 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
579 s.move_with(|map, selection| {
580 let mut cursor = selection.head().to_point(map);
581
582 if let Some(column) = original_columns.get(&selection.id) {
583 cursor.column = *column
584 }
585 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
586 selection.collapse_to(cursor, selection.goal)
587 });
588 if vim.mode == Mode::VisualBlock {
589 s.select_anchors(vec![s.first_anchor()])
590 }
591 });
592 })
593 });
594 self.switch_mode(Mode::Normal, true, window, cx);
595 }
596
597 pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context<Self>) {
598 self.store_visual_marks(window, cx);
599 self.update_editor(window, cx, |vim, editor, window, cx| {
600 let line_mode = line_mode || editor.selections.line_mode;
601 editor.selections.line_mode = line_mode;
602 let kind = if line_mode {
603 MotionKind::Linewise
604 } else {
605 MotionKind::Exclusive
606 };
607 vim.yank_selections_content(editor, kind, window, cx);
608 editor.change_selections(None, window, cx, |s| {
609 s.move_with(|map, selection| {
610 if line_mode {
611 selection.start = start_of_line(map, false, selection.start);
612 };
613 selection.collapse_to(selection.start, SelectionGoal::None)
614 });
615 if vim.mode == Mode::VisualBlock {
616 s.select_anchors(vec![s.first_anchor()])
617 }
618 });
619 });
620 self.switch_mode(Mode::Normal, true, window, cx);
621 }
622
623 pub(crate) fn visual_replace(
624 &mut self,
625 text: Arc<str>,
626 window: &mut Window,
627 cx: &mut Context<Self>,
628 ) {
629 self.stop_recording(cx);
630 self.update_editor(window, cx, |_, editor, window, cx| {
631 editor.transact(window, cx, |editor, window, cx| {
632 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
633
634 // Selections are biased right at the start. So we need to store
635 // anchors that are biased left so that we can restore the selections
636 // after the change
637 let stable_anchors = editor
638 .selections
639 .disjoint_anchors()
640 .iter()
641 .map(|selection| {
642 let start = selection.start.bias_left(&display_map.buffer_snapshot);
643 start..start
644 })
645 .collect::<Vec<_>>();
646
647 let mut edits = Vec::new();
648 for selection in selections.iter() {
649 let selection = selection.clone();
650 for row_range in
651 movement::split_display_range_by_lines(&display_map, selection.range())
652 {
653 let range = row_range.start.to_offset(&display_map, Bias::Right)
654 ..row_range.end.to_offset(&display_map, Bias::Right);
655 let text = text.repeat(range.len());
656 edits.push((range, text));
657 }
658 }
659
660 editor.edit(edits, cx);
661 editor.change_selections(None, window, cx, |s| s.select_ranges(stable_anchors));
662 });
663 });
664 self.switch_mode(Mode::Normal, false, window, cx);
665 }
666
667 pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
668 let count =
669 Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
670 self.update_editor(window, cx, |_, editor, window, cx| {
671 editor.set_clip_at_line_ends(false, cx);
672 for _ in 0..count {
673 if editor
674 .select_next(&Default::default(), window, cx)
675 .log_err()
676 .is_none()
677 {
678 break;
679 }
680 }
681 });
682 }
683
684 pub fn select_previous(
685 &mut self,
686 _: &SelectPrevious,
687 window: &mut Window,
688 cx: &mut Context<Self>,
689 ) {
690 let count =
691 Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 });
692 self.update_editor(window, cx, |_, editor, window, cx| {
693 for _ in 0..count {
694 if editor
695 .select_previous(&Default::default(), window, cx)
696 .log_err()
697 .is_none()
698 {
699 break;
700 }
701 }
702 });
703 }
704
705 pub fn select_match(
706 &mut self,
707 direction: Direction,
708 window: &mut Window,
709 cx: &mut Context<Self>,
710 ) {
711 let count = Vim::take_count(cx).unwrap_or(1);
712 let Some(pane) = self.pane(window, cx) else {
713 return;
714 };
715 let vim_is_normal = self.mode == Mode::Normal;
716 let mut start_selection = 0usize;
717 let mut end_selection = 0usize;
718
719 self.update_editor(window, cx, |_, editor, _, _| {
720 editor.set_collapse_matches(false);
721 });
722 if vim_is_normal {
723 pane.update(cx, |pane, cx| {
724 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
725 {
726 search_bar.update(cx, |search_bar, cx| {
727 if !search_bar.has_active_match() || !search_bar.show(window, cx) {
728 return;
729 }
730 // without update_match_index there is a bug when the cursor is before the first match
731 search_bar.update_match_index(window, cx);
732 search_bar.select_match(direction.opposite(), 1, window, cx);
733 });
734 }
735 });
736 }
737 self.update_editor(window, cx, |_, editor, _, cx| {
738 let latest = editor.selections.newest::<usize>(cx);
739 start_selection = latest.start;
740 end_selection = latest.end;
741 });
742
743 let mut match_exists = false;
744 pane.update(cx, |pane, cx| {
745 if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
746 search_bar.update(cx, |search_bar, cx| {
747 search_bar.update_match_index(window, cx);
748 search_bar.select_match(direction, count, window, cx);
749 match_exists = search_bar.match_exists(window, cx);
750 });
751 }
752 });
753 if !match_exists {
754 self.clear_operator(window, cx);
755 self.stop_replaying(cx);
756 return;
757 }
758 self.update_editor(window, cx, |_, editor, window, cx| {
759 let latest = editor.selections.newest::<usize>(cx);
760 if vim_is_normal {
761 start_selection = latest.start;
762 end_selection = latest.end;
763 } else {
764 start_selection = start_selection.min(latest.start);
765 end_selection = end_selection.max(latest.end);
766 }
767 if direction == Direction::Prev {
768 std::mem::swap(&mut start_selection, &mut end_selection);
769 }
770 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
771 s.select_ranges([start_selection..end_selection]);
772 });
773 editor.set_collapse_matches(true);
774 });
775
776 match self.maybe_pop_operator() {
777 Some(Operator::Change) => self.substitute(None, false, window, cx),
778 Some(Operator::Delete) => {
779 self.stop_recording(cx);
780 self.visual_delete(false, window, cx)
781 }
782 Some(Operator::Yank) => self.visual_yank(false, window, cx),
783 _ => {} // Ignoring other operators
784 }
785 }
786}
787#[cfg(test)]
788mod test {
789 use indoc::indoc;
790 use workspace::item::Item;
791
792 use crate::{
793 state::Mode,
794 test::{NeovimBackedTestContext, VimTestContext},
795 };
796
797 #[gpui::test]
798 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
799 let mut cx = NeovimBackedTestContext::new(cx).await;
800
801 cx.set_shared_state(indoc! {
802 "The ˇquick brown
803 fox jumps over
804 the lazy dog"
805 })
806 .await;
807 let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
808
809 // entering visual mode should select the character
810 // under cursor
811 cx.simulate_shared_keystrokes("v").await;
812 cx.shared_state()
813 .await
814 .assert_eq(indoc! { "The «qˇ»uick brown
815 fox jumps over
816 the lazy dog"});
817 cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
818
819 // forwards motions should extend the selection
820 cx.simulate_shared_keystrokes("w j").await;
821 cx.shared_state().await.assert_eq(indoc! { "The «quick brown
822 fox jumps oˇ»ver
823 the lazy dog"});
824
825 cx.simulate_shared_keystrokes("escape").await;
826 cx.shared_state().await.assert_eq(indoc! { "The quick brown
827 fox jumps ˇover
828 the lazy dog"});
829
830 // motions work backwards
831 cx.simulate_shared_keystrokes("v k b").await;
832 cx.shared_state()
833 .await
834 .assert_eq(indoc! { "The «ˇquick brown
835 fox jumps o»ver
836 the lazy dog"});
837
838 // works on empty lines
839 cx.set_shared_state(indoc! {"
840 a
841 ˇ
842 b
843 "})
844 .await;
845 let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
846 cx.simulate_shared_keystrokes("v").await;
847 cx.shared_state().await.assert_eq(indoc! {"
848 a
849 «
850 ˇ»b
851 "});
852 cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
853
854 // toggles off again
855 cx.simulate_shared_keystrokes("v").await;
856 cx.shared_state().await.assert_eq(indoc! {"
857 a
858 ˇ
859 b
860 "});
861
862 // works at the end of a document
863 cx.set_shared_state(indoc! {"
864 a
865 b
866 ˇ"})
867 .await;
868
869 cx.simulate_shared_keystrokes("v").await;
870 cx.shared_state().await.assert_eq(indoc! {"
871 a
872 b
873 ˇ"});
874 }
875
876 #[gpui::test]
877 async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) {
878 let mut cx = VimTestContext::new(cx, true).await;
879
880 cx.set_state(
881 indoc! {
882 "«The quick brown
883 fox jumps over
884 the lazy dogˇ»"
885 },
886 Mode::Visual,
887 );
888 cx.simulate_keystrokes("g shift-i");
889 cx.assert_state(
890 indoc! {
891 "ˇThe quick brown
892 ˇfox jumps over
893 ˇthe lazy dog"
894 },
895 Mode::Insert,
896 );
897 }
898
899 #[gpui::test]
900 async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) {
901 let mut cx = VimTestContext::new(cx, true).await;
902
903 cx.set_state(
904 indoc! {
905 "«The quick brown
906 fox jumps over
907 the lazy dogˇ»"
908 },
909 Mode::Visual,
910 );
911 cx.simulate_keystrokes("g shift-a");
912 cx.assert_state(
913 indoc! {
914 "The quick brownˇ
915 fox jumps overˇ
916 the lazy dogˇ"
917 },
918 Mode::Insert,
919 );
920 }
921
922 #[gpui::test]
923 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
924 let mut cx = NeovimBackedTestContext::new(cx).await;
925
926 cx.set_shared_state(indoc! {
927 "The ˇquick brown
928 fox jumps over
929 the lazy dog"
930 })
931 .await;
932 cx.simulate_shared_keystrokes("shift-v").await;
933 cx.shared_state()
934 .await
935 .assert_eq(indoc! { "The «qˇ»uick brown
936 fox jumps over
937 the lazy dog"});
938 cx.simulate_shared_keystrokes("x").await;
939 cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over
940 the lazy dog"});
941
942 // it should work on empty lines
943 cx.set_shared_state(indoc! {"
944 a
945 ˇ
946 b"})
947 .await;
948 cx.simulate_shared_keystrokes("shift-v").await;
949 cx.shared_state().await.assert_eq(indoc! {"
950 a
951 «
952 ˇ»b"});
953 cx.simulate_shared_keystrokes("x").await;
954 cx.shared_state().await.assert_eq(indoc! {"
955 a
956 ˇb"});
957
958 // it should work at the end of the document
959 cx.set_shared_state(indoc! {"
960 a
961 b
962 ˇ"})
963 .await;
964 let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx));
965 cx.simulate_shared_keystrokes("shift-v").await;
966 cx.shared_state().await.assert_eq(indoc! {"
967 a
968 b
969 ˇ"});
970 cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
971 cx.simulate_shared_keystrokes("x").await;
972 cx.shared_state().await.assert_eq(indoc! {"
973 a
974 ˇb"});
975 }
976
977 #[gpui::test]
978 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
979 let mut cx = NeovimBackedTestContext::new(cx).await;
980
981 cx.simulate("v w", "The quick ˇbrown")
982 .await
983 .assert_matches();
984
985 cx.simulate("v w x", "The quick ˇbrown")
986 .await
987 .assert_matches();
988 cx.simulate(
989 "v w j x",
990 indoc! {"
991 The ˇquick brown
992 fox jumps over
993 the lazy dog"},
994 )
995 .await
996 .assert_matches();
997 // Test pasting code copied on delete
998 cx.simulate_shared_keystrokes("j p").await;
999 cx.shared_state().await.assert_matches();
1000
1001 cx.simulate_at_each_offset(
1002 "v w j x",
1003 indoc! {"
1004 The ˇquick brown
1005 fox jumps over
1006 the ˇlazy dog"},
1007 )
1008 .await
1009 .assert_matches();
1010 cx.simulate_at_each_offset(
1011 "v b k x",
1012 indoc! {"
1013 The ˇquick brown
1014 fox jumps ˇover
1015 the ˇlazy dog"},
1016 )
1017 .await
1018 .assert_matches();
1019 }
1020
1021 #[gpui::test]
1022 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
1023 let mut cx = NeovimBackedTestContext::new(cx).await;
1024
1025 cx.set_shared_state(indoc! {"
1026 The quˇick brown
1027 fox jumps over
1028 the lazy dog"})
1029 .await;
1030 cx.simulate_shared_keystrokes("shift-v x").await;
1031 cx.shared_state().await.assert_matches();
1032
1033 // Test pasting code copied on delete
1034 cx.simulate_shared_keystrokes("p").await;
1035 cx.shared_state().await.assert_matches();
1036
1037 cx.set_shared_state(indoc! {"
1038 The quick brown
1039 fox jumps over
1040 the laˇzy dog"})
1041 .await;
1042 cx.simulate_shared_keystrokes("shift-v x").await;
1043 cx.shared_state().await.assert_matches();
1044 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1045
1046 cx.set_shared_state(indoc! {"
1047 The quˇick brown
1048 fox jumps over
1049 the lazy dog"})
1050 .await;
1051 cx.simulate_shared_keystrokes("shift-v j x").await;
1052 cx.shared_state().await.assert_matches();
1053 // Test pasting code copied on delete
1054 cx.simulate_shared_keystrokes("p").await;
1055 cx.shared_state().await.assert_matches();
1056
1057 cx.set_shared_state(indoc! {"
1058 The ˇlong line
1059 should not
1060 crash
1061 "})
1062 .await;
1063 cx.simulate_shared_keystrokes("shift-v $ x").await;
1064 cx.shared_state().await.assert_matches();
1065 }
1066
1067 #[gpui::test]
1068 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
1069 let mut cx = NeovimBackedTestContext::new(cx).await;
1070
1071 cx.set_shared_state("The quick ˇbrown").await;
1072 cx.simulate_shared_keystrokes("v w y").await;
1073 cx.shared_state().await.assert_eq("The quick ˇbrown");
1074 cx.shared_clipboard().await.assert_eq("brown");
1075
1076 cx.set_shared_state(indoc! {"
1077 The ˇquick brown
1078 fox jumps over
1079 the lazy dog"})
1080 .await;
1081 cx.simulate_shared_keystrokes("v w j y").await;
1082 cx.shared_state().await.assert_eq(indoc! {"
1083 The ˇquick brown
1084 fox jumps over
1085 the lazy dog"});
1086 cx.shared_clipboard().await.assert_eq(indoc! {"
1087 quick brown
1088 fox jumps o"});
1089
1090 cx.set_shared_state(indoc! {"
1091 The quick brown
1092 fox jumps over
1093 the ˇlazy dog"})
1094 .await;
1095 cx.simulate_shared_keystrokes("v w j y").await;
1096 cx.shared_state().await.assert_eq(indoc! {"
1097 The quick brown
1098 fox jumps over
1099 the ˇlazy dog"});
1100 cx.shared_clipboard().await.assert_eq("lazy d");
1101 cx.simulate_shared_keystrokes("shift-v y").await;
1102 cx.shared_clipboard().await.assert_eq("the lazy dog\n");
1103
1104 cx.set_shared_state(indoc! {"
1105 The ˇquick brown
1106 fox jumps over
1107 the lazy dog"})
1108 .await;
1109 cx.simulate_shared_keystrokes("v b k y").await;
1110 cx.shared_state().await.assert_eq(indoc! {"
1111 ˇThe quick brown
1112 fox jumps over
1113 the lazy dog"});
1114 assert_eq!(
1115 cx.read_from_clipboard()
1116 .map(|item| item.text().unwrap().to_string())
1117 .unwrap(),
1118 "The q"
1119 );
1120
1121 cx.set_shared_state(indoc! {"
1122 The quick brown
1123 fox ˇjumps over
1124 the lazy dog"})
1125 .await;
1126 cx.simulate_shared_keystrokes("shift-v shift-g shift-y")
1127 .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()
1133 .await
1134 .assert_eq("fox jumps over\nthe lazy dog\n");
1135 }
1136
1137 #[gpui::test]
1138 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
1139 let mut cx = NeovimBackedTestContext::new(cx).await;
1140
1141 cx.set_shared_state(indoc! {
1142 "The ˇquick brown
1143 fox jumps over
1144 the lazy dog"
1145 })
1146 .await;
1147 cx.simulate_shared_keystrokes("ctrl-v").await;
1148 cx.shared_state().await.assert_eq(indoc! {
1149 "The «qˇ»uick brown
1150 fox jumps over
1151 the lazy dog"
1152 });
1153 cx.simulate_shared_keystrokes("2 down").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("e").await;
1160 cx.shared_state().await.assert_eq(indoc! {
1161 "The «quicˇ»k brown
1162 fox «jumpˇ»s over
1163 the «lazyˇ» dog"
1164 });
1165 cx.simulate_shared_keystrokes("^").await;
1166 cx.shared_state().await.assert_eq(indoc! {
1167 "«ˇThe q»uick brown
1168 «ˇfox j»umps over
1169 «ˇthe l»azy dog"
1170 });
1171 cx.simulate_shared_keystrokes("$").await;
1172 cx.shared_state().await.assert_eq(indoc! {
1173 "The «quick brownˇ»
1174 fox «jumps overˇ»
1175 the «lazy dogˇ»"
1176 });
1177 cx.simulate_shared_keystrokes("shift-f space").await;
1178 cx.shared_state().await.assert_eq(indoc! {
1179 "The «quickˇ» brown
1180 fox «jumpsˇ» over
1181 the «lazy ˇ»dog"
1182 });
1183
1184 // toggling through visual mode works as expected
1185 cx.simulate_shared_keystrokes("v").await;
1186 cx.shared_state().await.assert_eq(indoc! {
1187 "The «quick brown
1188 fox jumps over
1189 the lazy ˇ»dog"
1190 });
1191 cx.simulate_shared_keystrokes("ctrl-v").await;
1192 cx.shared_state().await.assert_eq(indoc! {
1193 "The «quickˇ» brown
1194 fox «jumpsˇ» over
1195 the «lazy ˇ»dog"
1196 });
1197
1198 cx.set_shared_state(indoc! {
1199 "The ˇquick
1200 brown
1201 fox
1202 jumps over the
1203
1204 lazy dog
1205 "
1206 })
1207 .await;
1208 cx.simulate_shared_keystrokes("ctrl-v down down").await;
1209 cx.shared_state().await.assert_eq(indoc! {
1210 "The«ˇ q»uick
1211 bro«ˇwn»
1212 foxˇ
1213 jumps over the
1214
1215 lazy dog
1216 "
1217 });
1218 cx.simulate_shared_keystrokes("down").await;
1219 cx.shared_state().await.assert_eq(indoc! {
1220 "The «qˇ»uick
1221 brow«nˇ»
1222 fox
1223 jump«sˇ» over the
1224
1225 lazy dog
1226 "
1227 });
1228 cx.simulate_shared_keystrokes("left").await;
1229 cx.shared_state().await.assert_eq(indoc! {
1230 "The«ˇ q»uick
1231 bro«ˇwn»
1232 foxˇ
1233 jum«ˇps» over the
1234
1235 lazy dog
1236 "
1237 });
1238 cx.simulate_shared_keystrokes("s o escape").await;
1239 cx.shared_state().await.assert_eq(indoc! {
1240 "Theˇouick
1241 broo
1242 foxo
1243 jumo over the
1244
1245 lazy dog
1246 "
1247 });
1248
1249 // https://github.com/zed-industries/zed/issues/6274
1250 cx.set_shared_state(indoc! {
1251 "Theˇ quick brown
1252
1253 fox jumps over
1254 the lazy dog
1255 "
1256 })
1257 .await;
1258 cx.simulate_shared_keystrokes("l ctrl-v j j").await;
1259 cx.shared_state().await.assert_eq(indoc! {
1260 "The «qˇ»uick brown
1261
1262 fox «jˇ»umps over
1263 the lazy dog
1264 "
1265 });
1266 }
1267
1268 #[gpui::test]
1269 async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) {
1270 let mut cx = NeovimBackedTestContext::new(cx).await;
1271
1272 cx.set_shared_state(indoc! {
1273 "The ˇquick brown
1274 fox jumps over
1275 the lazy dog
1276 "
1277 })
1278 .await;
1279 cx.simulate_shared_keystrokes("ctrl-v right down").await;
1280 cx.shared_state().await.assert_eq(indoc! {
1281 "The «quˇ»ick brown
1282 fox «juˇ»mps over
1283 the lazy dog
1284 "
1285 });
1286 }
1287 #[gpui::test]
1288 async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) {
1289 let mut cx = NeovimBackedTestContext::new(cx).await;
1290 cx.set_shared_state(indoc! {"
1291 The ˇquick brown
1292 fox jumps over
1293 the lazy dog"})
1294 .await;
1295 cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await;
1296 cx.shared_state().await.assert_eq(indoc! {"
1297 The «quick ˇ»brown
1298 fox «jumps ˇ»over
1299 the lazy dog"});
1300 }
1301
1302 #[gpui::test]
1303 async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) {
1304 let mut cx = NeovimBackedTestContext::new(cx).await;
1305 cx.set_shared_state(indoc! {"
1306 The quick brown
1307 fox jumpsˇ over
1308 the lazy dog"})
1309 .await;
1310 cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await;
1311 cx.shared_state().await.assert_eq(indoc! {"
1312 The «ˇquick »brown
1313 fox «ˇjumps »over
1314 the lazy dog"});
1315 }
1316
1317 #[gpui::test]
1318 async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) {
1319 let mut cx = NeovimBackedTestContext::new(cx).await;
1320 cx.set_shared_state(indoc! {"
1321 The quick brown
1322 fox jˇumps over
1323 the lazy dog"})
1324 .await;
1325 cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1326 cx.shared_state().await.assert_eq(indoc! {"
1327 The quick brown
1328 fox j«umps ˇ»over
1329 the l«azy dˇ»og"});
1330 cx.simulate_shared_keystrokes("o k").await;
1331 cx.shared_state().await.assert_eq(indoc! {"
1332 The q«ˇuick »brown
1333 fox j«ˇumps »over
1334 the l«ˇazy d»og"});
1335 }
1336
1337 #[gpui::test]
1338 async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) {
1339 let mut cx = NeovimBackedTestContext::new(cx).await;
1340 cx.set_shared_state(indoc! {"
1341 The quick brown
1342 fox jˇumps over
1343 the lazy dog"})
1344 .await;
1345 cx.simulate_shared_keystrokes("ctrl-v l l l l j").await;
1346 cx.shared_state().await.assert_eq(indoc! {"
1347 The quick brown
1348 fox j«umps ˇ»over
1349 the l«azy dˇ»og"});
1350 cx.simulate_shared_keystrokes("shift-o k").await;
1351 cx.shared_state().await.assert_eq(indoc! {"
1352 The quick brown
1353 fox j«ˇumps »over
1354 the lazy dog"});
1355 }
1356
1357 #[gpui::test]
1358 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
1359 let mut cx = NeovimBackedTestContext::new(cx).await;
1360
1361 cx.set_shared_state(indoc! {
1362 "ˇThe quick brown
1363 fox jumps over
1364 the lazy dog
1365 "
1366 })
1367 .await;
1368 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1369 cx.shared_state().await.assert_eq(indoc! {
1370 "«Tˇ»he quick brown
1371 «fˇ»ox jumps over
1372 «tˇ»he lazy dog
1373 ˇ"
1374 });
1375
1376 cx.simulate_shared_keystrokes("shift-i k escape").await;
1377 cx.shared_state().await.assert_eq(indoc! {
1378 "ˇkThe quick brown
1379 kfox jumps over
1380 kthe lazy dog
1381 k"
1382 });
1383
1384 cx.set_shared_state(indoc! {
1385 "ˇThe quick brown
1386 fox jumps over
1387 the lazy dog
1388 "
1389 })
1390 .await;
1391 cx.simulate_shared_keystrokes("ctrl-v 9 down").await;
1392 cx.shared_state().await.assert_eq(indoc! {
1393 "«Tˇ»he quick brown
1394 «fˇ»ox jumps over
1395 «tˇ»he lazy dog
1396 ˇ"
1397 });
1398 cx.simulate_shared_keystrokes("c k escape").await;
1399 cx.shared_state().await.assert_eq(indoc! {
1400 "ˇkhe quick brown
1401 kox jumps over
1402 khe lazy dog
1403 k"
1404 });
1405 }
1406
1407 #[gpui::test]
1408 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
1409 let mut cx = NeovimBackedTestContext::new(cx).await;
1410
1411 cx.set_shared_state("hello (in [parˇens] o)").await;
1412 cx.simulate_shared_keystrokes("ctrl-v l").await;
1413 cx.simulate_shared_keystrokes("a ]").await;
1414 cx.shared_state()
1415 .await
1416 .assert_eq("hello (in «[parens]ˇ» o)");
1417 cx.simulate_shared_keystrokes("i (").await;
1418 cx.shared_state()
1419 .await
1420 .assert_eq("hello («in [parens] oˇ»)");
1421
1422 cx.set_shared_state("hello in a wˇord again.").await;
1423 cx.simulate_shared_keystrokes("ctrl-v l i w").await;
1424 cx.shared_state()
1425 .await
1426 .assert_eq("hello in a w«ordˇ» again.");
1427 assert_eq!(cx.mode(), Mode::VisualBlock);
1428 cx.simulate_shared_keystrokes("o a s").await;
1429 cx.shared_state()
1430 .await
1431 .assert_eq("«ˇhello in a word» again.");
1432 }
1433
1434 #[gpui::test]
1435 async fn test_visual_object_expands(cx: &mut gpui::TestAppContext) {
1436 let mut cx = NeovimBackedTestContext::new(cx).await;
1437
1438 cx.set_shared_state(indoc! {
1439 "{
1440 {
1441 ˇ }
1442 }
1443 {
1444 }
1445 "
1446 })
1447 .await;
1448 cx.simulate_shared_keystrokes("v l").await;
1449 cx.shared_state().await.assert_eq(indoc! {
1450 "{
1451 {
1452 « }ˇ»
1453 }
1454 {
1455 }
1456 "
1457 });
1458 cx.simulate_shared_keystrokes("a {").await;
1459 cx.shared_state().await.assert_eq(indoc! {
1460 "{
1461 «{
1462 }ˇ»
1463 }
1464 {
1465 }
1466 "
1467 });
1468 cx.simulate_shared_keystrokes("a {").await;
1469 cx.shared_state().await.assert_eq(indoc! {
1470 "«{
1471 {
1472 }
1473 }ˇ»
1474 {
1475 }
1476 "
1477 });
1478 // cx.simulate_shared_keystrokes("a {").await;
1479 // cx.shared_state().await.assert_eq(indoc! {
1480 // "{
1481 // «{
1482 // }ˇ»
1483 // }
1484 // {
1485 // }
1486 // "
1487 // });
1488 }
1489
1490 #[gpui::test]
1491 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1492 let mut cx = VimTestContext::new(cx, true).await;
1493
1494 cx.set_state("aˇbc", Mode::Normal);
1495 cx.simulate_keystrokes("ctrl-v");
1496 assert_eq!(cx.mode(), Mode::VisualBlock);
1497 cx.simulate_keystrokes("cmd-shift-p escape");
1498 assert_eq!(cx.mode(), Mode::VisualBlock);
1499 }
1500
1501 #[gpui::test]
1502 async fn test_gn(cx: &mut gpui::TestAppContext) {
1503 let mut cx = NeovimBackedTestContext::new(cx).await;
1504
1505 cx.set_shared_state("aaˇ aa aa aa aa").await;
1506 cx.simulate_shared_keystrokes("/ a a enter").await;
1507 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1508 cx.simulate_shared_keystrokes("g n").await;
1509 cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa");
1510 cx.simulate_shared_keystrokes("g n").await;
1511 cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa");
1512 cx.simulate_shared_keystrokes("escape d g n").await;
1513 cx.shared_state().await.assert_eq("aa aa ˇ aa aa");
1514
1515 cx.set_shared_state("aaˇ aa aa aa aa").await;
1516 cx.simulate_shared_keystrokes("/ a a enter").await;
1517 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1518 cx.simulate_shared_keystrokes("3 g n").await;
1519 cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa");
1520
1521 cx.set_shared_state("aaˇ aa aa aa aa").await;
1522 cx.simulate_shared_keystrokes("/ a a enter").await;
1523 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1524 cx.simulate_shared_keystrokes("g shift-n").await;
1525 cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa");
1526 cx.simulate_shared_keystrokes("g shift-n").await;
1527 cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
1528 }
1529
1530 #[gpui::test]
1531 async fn test_gl(cx: &mut gpui::TestAppContext) {
1532 let mut cx = VimTestContext::new(cx, true).await;
1533
1534 cx.set_state("aaˇ aa\naa", Mode::Normal);
1535 cx.simulate_keystrokes("g l");
1536 cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
1537 cx.simulate_keystrokes("g >");
1538 cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
1539 }
1540
1541 #[gpui::test]
1542 async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
1543 let mut cx = NeovimBackedTestContext::new(cx).await;
1544
1545 cx.set_shared_state("aaˇ aa aa aa aa").await;
1546 cx.simulate_shared_keystrokes("/ a a enter").await;
1547 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1548 cx.simulate_shared_keystrokes("d g n").await;
1549
1550 cx.shared_state().await.assert_eq("aa ˇ aa aa aa");
1551 cx.simulate_shared_keystrokes(".").await;
1552 cx.shared_state().await.assert_eq("aa ˇ aa aa");
1553 cx.simulate_shared_keystrokes(".").await;
1554 cx.shared_state().await.assert_eq("aa ˇ aa");
1555 }
1556
1557 #[gpui::test]
1558 async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) {
1559 let mut cx = NeovimBackedTestContext::new(cx).await;
1560
1561 cx.set_shared_state("aaˇ aa aa aa aa").await;
1562 cx.simulate_shared_keystrokes("/ a a enter").await;
1563 cx.shared_state().await.assert_eq("aa ˇaa aa aa aa");
1564 cx.simulate_shared_keystrokes("c g n x escape").await;
1565 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1566 cx.simulate_shared_keystrokes(".").await;
1567 cx.shared_state().await.assert_eq("aa x ˇx aa aa");
1568 }
1569
1570 #[gpui::test]
1571 async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) {
1572 let mut cx = NeovimBackedTestContext::new(cx).await;
1573
1574 cx.set_shared_state("aaˇ aa aa aa aa").await;
1575 cx.simulate_shared_keystrokes("/ b b enter").await;
1576 cx.shared_state().await.assert_eq("aaˇ aa aa aa aa");
1577 cx.simulate_shared_keystrokes("c g n x escape").await;
1578 cx.shared_state().await.assert_eq("aaˇaa aa aa aa");
1579 cx.simulate_shared_keystrokes(".").await;
1580 cx.shared_state().await.assert_eq("aaˇa aa aa aa");
1581
1582 cx.set_shared_state("aaˇ bb aa aa aa").await;
1583 cx.simulate_shared_keystrokes("/ b b enter").await;
1584 cx.shared_state().await.assert_eq("aa ˇbb aa aa aa");
1585 cx.simulate_shared_keystrokes("c g n x escape").await;
1586 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1587 cx.simulate_shared_keystrokes(".").await;
1588 cx.shared_state().await.assert_eq("aa ˇx aa aa aa");
1589 }
1590
1591 #[gpui::test]
1592 async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) {
1593 let mut cx = NeovimBackedTestContext::new(cx).await;
1594
1595 cx.set_shared_state(indoc! {
1596 "The ˇquick brown
1597 fox jumps over
1598 the lazy dog
1599 "
1600 })
1601 .await;
1602 cx.simulate_shared_keystrokes("v down shift-d").await;
1603 cx.shared_state().await.assert_eq(indoc! {
1604 "the ˇlazy dog\n"
1605 });
1606
1607 cx.set_shared_state(indoc! {
1608 "The ˇquick brown
1609 fox jumps over
1610 the lazy dog
1611 "
1612 })
1613 .await;
1614 cx.simulate_shared_keystrokes("ctrl-v down shift-d").await;
1615 cx.shared_state().await.assert_eq(indoc! {
1616 "Theˇ•
1617 fox•
1618 the lazy dog
1619 "
1620 });
1621 }
1622
1623 #[gpui::test]
1624 async fn test_shift_y(cx: &mut gpui::TestAppContext) {
1625 let mut cx = NeovimBackedTestContext::new(cx).await;
1626
1627 cx.set_shared_state(indoc! {
1628 "The ˇquick brown\n"
1629 })
1630 .await;
1631 cx.simulate_shared_keystrokes("v i w shift-y").await;
1632 cx.shared_clipboard().await.assert_eq(indoc! {
1633 "The quick brown\n"
1634 });
1635 }
1636
1637 #[gpui::test]
1638 async fn test_gv(cx: &mut gpui::TestAppContext) {
1639 let mut cx = NeovimBackedTestContext::new(cx).await;
1640
1641 cx.set_shared_state(indoc! {
1642 "The ˇquick brown"
1643 })
1644 .await;
1645 cx.simulate_shared_keystrokes("v i w escape g v").await;
1646 cx.shared_state().await.assert_eq(indoc! {
1647 "The «quickˇ» brown"
1648 });
1649
1650 cx.simulate_shared_keystrokes("o escape g v").await;
1651 cx.shared_state().await.assert_eq(indoc! {
1652 "The «ˇquick» brown"
1653 });
1654
1655 cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
1656 cx.shared_state().await.assert_eq(indoc! {
1657 "«Thˇ»e quick brown"
1658 });
1659 cx.simulate_shared_keystrokes("g v").await;
1660 cx.shared_state().await.assert_eq(indoc! {
1661 "The «ˇquick» brown"
1662 });
1663 cx.simulate_shared_keystrokes("g v").await;
1664 cx.shared_state().await.assert_eq(indoc! {
1665 "«Thˇ»e quick brown"
1666 });
1667
1668 cx.set_state(
1669 indoc! {"
1670 fiˇsh one
1671 fish two
1672 fish red
1673 fish blue
1674 "},
1675 Mode::Normal,
1676 );
1677 cx.simulate_keystrokes("4 g l escape escape g v");
1678 cx.assert_state(
1679 indoc! {"
1680 «fishˇ» one
1681 «fishˇ» two
1682 «fishˇ» red
1683 «fishˇ» blue
1684 "},
1685 Mode::Visual,
1686 );
1687 cx.simulate_keystrokes("y g v");
1688 cx.assert_state(
1689 indoc! {"
1690 «fishˇ» one
1691 «fishˇ» two
1692 «fishˇ» red
1693 «fishˇ» blue
1694 "},
1695 Mode::Visual,
1696 );
1697 }
1698}