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