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