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