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