1use std::{cmp, sync::Arc};
2
3use collections::HashMap;
4use editor::{
5 display_map::{DisplaySnapshot, ToDisplayPoint},
6 movement,
7 scroll::autoscroll::Autoscroll,
8 Bias, DisplayPoint, Editor,
9};
10use gpui::{actions, AppContext, ViewContext, WindowContext};
11use language::{Selection, SelectionGoal};
12use workspace::Workspace;
13
14use crate::{
15 motion::Motion,
16 object::Object,
17 state::{Mode, Operator},
18 utils::copy_selections_content,
19 Vim,
20};
21
22actions!(
23 vim,
24 [
25 ToggleVisual,
26 ToggleVisualLine,
27 ToggleVisualBlock,
28 VisualDelete,
29 VisualYank,
30 OtherEnd,
31 ]
32);
33
34pub fn init(cx: &mut AppContext) {
35 cx.add_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
36 toggle_mode(Mode::Visual, cx)
37 });
38 cx.add_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
39 toggle_mode(Mode::VisualLine, cx)
40 });
41 cx.add_action(
42 |_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
43 toggle_mode(Mode::VisualBlock, cx)
44 },
45 );
46 cx.add_action(other_end);
47 cx.add_action(delete);
48 cx.add_action(yank);
49}
50
51pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
52 Vim::update(cx, |vim, cx| {
53 vim.update_active_editor(cx, |editor, cx| {
54 if vim.state().mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) {
55 let is_up_or_down = matches!(motion, Motion::Up | Motion::Down);
56 visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
57 motion.move_point(map, point, goal, times)
58 })
59 } else {
60 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
61 s.move_with(|map, selection| {
62 let was_reversed = selection.reversed;
63 let mut current_head = selection.head();
64
65 // our motions assume the current character is after the cursor,
66 // but in (forward) visual mode the current character is just
67 // before the end of the selection.
68
69 // If the file ends with a newline (which is common) we don't do this.
70 // so that if you go to the end of such a file you can use "up" to go
71 // to the previous line and have it work somewhat as expected.
72 if !selection.reversed
73 && !selection.is_empty()
74 && !(selection.end.column() == 0 && selection.end == map.max_point())
75 {
76 current_head = movement::left(map, selection.end)
77 }
78
79 let Some((new_head, goal)) =
80 motion.move_point(map, current_head, selection.goal, times) else { return };
81
82 selection.set_head(new_head, goal);
83
84 // ensure the current character is included in the selection.
85 if !selection.reversed {
86 let next_point = if vim.state().mode == Mode::VisualBlock {
87 movement::saturating_right(map, selection.end)
88 } else {
89 movement::right(map, selection.end)
90 };
91
92 if !(next_point.column() == 0 && next_point == map.max_point()) {
93 selection.end = next_point;
94 }
95 }
96
97 // vim always ensures the anchor character stays selected.
98 // if our selection has reversed, we need to move the opposite end
99 // to ensure the anchor is still selected.
100 if was_reversed && !selection.reversed {
101 selection.start = movement::left(map, selection.start);
102 } else if !was_reversed && selection.reversed {
103 selection.end = movement::right(map, selection.end);
104 }
105 })
106 });
107 }
108 });
109 });
110}
111
112pub fn visual_block_motion(
113 preserve_goal: bool,
114 editor: &mut Editor,
115 cx: &mut ViewContext<Editor>,
116 mut move_selection: impl FnMut(
117 &DisplaySnapshot,
118 DisplayPoint,
119 SelectionGoal,
120 ) -> Option<(DisplayPoint, SelectionGoal)>,
121) {
122 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
123 let map = &s.display_map();
124 let mut head = s.newest_anchor().head().to_display_point(map);
125 let mut tail = s.oldest_anchor().tail().to_display_point(map);
126 let mut goal = s.newest_anchor().goal;
127
128 let was_reversed = tail.column() > head.column();
129
130 if !was_reversed && !preserve_goal {
131 head = movement::saturating_left(map, head);
132 }
133
134 let Some((new_head, _)) = move_selection(&map, head, goal) else {
135 return
136 };
137 head = new_head;
138
139 let is_reversed = tail.column() > head.column();
140 if was_reversed && !is_reversed {
141 tail = movement::left(map, tail)
142 } else if !was_reversed && is_reversed {
143 tail = movement::right(map, tail)
144 }
145 if !is_reversed && !preserve_goal {
146 head = movement::saturating_right(map, head)
147 }
148
149 let (start, end) = match goal {
150 SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end),
151 SelectionGoal::Column(start) if preserve_goal => (start, start + 1),
152 _ => (tail.column(), head.column()),
153 };
154 goal = SelectionGoal::ColumnRange { start, end };
155
156 let columns = if is_reversed {
157 head.column()..tail.column()
158 } else if head.column() == tail.column() {
159 head.column()..(head.column() + 1)
160 } else {
161 tail.column()..head.column()
162 };
163
164 let mut selections = Vec::new();
165 let mut row = tail.row();
166
167 loop {
168 let start = map.clip_point(DisplayPoint::new(row, columns.start), Bias::Left);
169 let end = map.clip_point(DisplayPoint::new(row, columns.end), Bias::Left);
170 if columns.start <= map.line_len(row) {
171 let selection = Selection {
172 id: s.new_selection_id(),
173 start: start.to_point(map),
174 end: end.to_point(map),
175 reversed: is_reversed,
176 goal: goal.clone(),
177 };
178
179 selections.push(selection);
180 }
181 if row == head.row() {
182 break;
183 }
184 if tail.row() > head.row() {
185 row -= 1
186 } else {
187 row += 1
188 }
189 }
190
191 s.select(selections);
192 })
193}
194
195pub fn visual_object(object: Object, cx: &mut WindowContext) {
196 Vim::update(cx, |vim, cx| {
197 if let Some(Operator::Object { around }) = vim.active_operator() {
198 vim.pop_operator(cx);
199 let current_mode = vim.state().mode;
200 let target_mode = object.target_visual_mode(current_mode);
201 if target_mode != current_mode {
202 vim.switch_mode(target_mode, true, cx);
203 }
204
205 vim.update_active_editor(cx, |editor, cx| {
206 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
207 s.move_with(|map, selection| {
208 let mut head = selection.head();
209
210 // all our motions assume that the current character is
211 // after the cursor; however in the case of a visual selection
212 // the current character is before the cursor.
213 if !selection.reversed {
214 head = movement::left(map, head);
215 }
216
217 if let Some(range) = object.range(map, head, around) {
218 if !range.is_empty() {
219 let expand_both_ways =
220 if object.always_expands_both_ways() || selection.is_empty() {
221 true
222 // contains only one character
223 } else if let Some((_, start)) =
224 map.reverse_chars_at(selection.end).next()
225 {
226 selection.start == start
227 } else {
228 false
229 };
230
231 if expand_both_ways {
232 selection.start = cmp::min(selection.start, range.start);
233 selection.end = cmp::max(selection.end, range.end);
234 } else if selection.reversed {
235 selection.start = range.start;
236 } else {
237 selection.end = range.end;
238 }
239 }
240 }
241 });
242 });
243 });
244 }
245 });
246}
247
248fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
249 Vim::update(cx, |vim, cx| {
250 if vim.state().mode == mode {
251 vim.switch_mode(Mode::Normal, false, cx);
252 } else {
253 vim.switch_mode(mode, false, cx);
254 }
255 })
256}
257
258pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
259 Vim::update(cx, |vim, cx| {
260 vim.update_active_editor(cx, |editor, cx| {
261 editor.change_selections(None, cx, |s| {
262 s.move_with(|_, selection| {
263 selection.reversed = !selection.reversed;
264 })
265 })
266 })
267 });
268}
269
270pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
271 Vim::update(cx, |vim, cx| {
272 vim.update_active_editor(cx, |editor, cx| {
273 let mut original_columns: HashMap<_, _> = Default::default();
274 let line_mode = editor.selections.line_mode;
275
276 editor.transact(cx, |editor, cx| {
277 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
278 s.move_with(|map, selection| {
279 if line_mode {
280 let mut position = selection.head();
281 if !selection.reversed {
282 position = movement::left(map, position);
283 }
284 original_columns.insert(selection.id, position.to_point(map).column);
285 }
286 selection.goal = SelectionGoal::None;
287 });
288 });
289 copy_selections_content(editor, line_mode, cx);
290 editor.insert("", cx);
291
292 // Fixup cursor position after the deletion
293 editor.set_clip_at_line_ends(true, cx);
294 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
295 s.move_with(|map, selection| {
296 let mut cursor = selection.head().to_point(map);
297
298 if let Some(column) = original_columns.get(&selection.id) {
299 cursor.column = *column
300 }
301 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
302 selection.collapse_to(cursor, selection.goal)
303 });
304 if vim.state().mode == Mode::VisualBlock {
305 s.select_anchors(vec![s.first_anchor()])
306 }
307 });
308 })
309 });
310 vim.switch_mode(Mode::Normal, true, cx);
311 });
312}
313
314pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
315 Vim::update(cx, |vim, cx| {
316 vim.update_active_editor(cx, |editor, cx| {
317 let line_mode = editor.selections.line_mode;
318 copy_selections_content(editor, line_mode, cx);
319 editor.change_selections(None, cx, |s| {
320 s.move_with(|_, selection| {
321 selection.collapse_to(selection.start, SelectionGoal::None)
322 });
323 if vim.state().mode == Mode::VisualBlock {
324 s.select_anchors(vec![s.first_anchor()])
325 }
326 });
327 });
328 vim.switch_mode(Mode::Normal, true, cx);
329 });
330}
331
332pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
333 Vim::update(cx, |vim, cx| {
334 vim.update_active_editor(cx, |editor, cx| {
335 editor.transact(cx, |editor, cx| {
336 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
337
338 // Selections are biased right at the start. So we need to store
339 // anchors that are biased left so that we can restore the selections
340 // after the change
341 let stable_anchors = editor
342 .selections
343 .disjoint_anchors()
344 .into_iter()
345 .map(|selection| {
346 let start = selection.start.bias_left(&display_map.buffer_snapshot);
347 start..start
348 })
349 .collect::<Vec<_>>();
350
351 let mut edits = Vec::new();
352 for selection in selections.iter() {
353 let selection = selection.clone();
354 for row_range in
355 movement::split_display_range_by_lines(&display_map, selection.range())
356 {
357 let range = row_range.start.to_offset(&display_map, Bias::Right)
358 ..row_range.end.to_offset(&display_map, Bias::Right);
359 let text = text.repeat(range.len());
360 edits.push((range, text));
361 }
362 }
363
364 editor.buffer().update(cx, |buffer, cx| {
365 buffer.edit(edits, None, cx);
366 });
367 editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
368 });
369 });
370 vim.switch_mode(Mode::Normal, false, cx);
371 });
372}
373
374#[cfg(test)]
375mod test {
376 use indoc::indoc;
377 use workspace::item::Item;
378
379 use crate::{
380 state::Mode,
381 test::{NeovimBackedTestContext, VimTestContext},
382 };
383
384 #[gpui::test]
385 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
386 let mut cx = NeovimBackedTestContext::new(cx).await;
387
388 cx.set_shared_state(indoc! {
389 "The ˇquick brown
390 fox jumps over
391 the lazy dog"
392 })
393 .await;
394 let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
395
396 // entering visual mode should select the character
397 // under cursor
398 cx.simulate_shared_keystrokes(["v"]).await;
399 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
400 fox jumps over
401 the lazy dog"})
402 .await;
403 cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
404
405 // forwards motions should extend the selection
406 cx.simulate_shared_keystrokes(["w", "j"]).await;
407 cx.assert_shared_state(indoc! { "The «quick brown
408 fox jumps oˇ»ver
409 the lazy dog"})
410 .await;
411
412 cx.simulate_shared_keystrokes(["escape"]).await;
413 assert_eq!(Mode::Normal, cx.neovim_mode().await);
414 cx.assert_shared_state(indoc! { "The quick brown
415 fox jumps ˇover
416 the lazy dog"})
417 .await;
418
419 // motions work backwards
420 cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
421 cx.assert_shared_state(indoc! { "The «ˇquick brown
422 fox jumps o»ver
423 the lazy dog"})
424 .await;
425
426 // works on empty lines
427 cx.set_shared_state(indoc! {"
428 a
429 ˇ
430 b
431 "})
432 .await;
433 let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
434 cx.simulate_shared_keystrokes(["v"]).await;
435 cx.assert_shared_state(indoc! {"
436 a
437 «
438 ˇ»b
439 "})
440 .await;
441 cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
442
443 // toggles off again
444 cx.simulate_shared_keystrokes(["v"]).await;
445 cx.assert_shared_state(indoc! {"
446 a
447 ˇ
448 b
449 "})
450 .await;
451
452 // works at the end of a document
453 cx.set_shared_state(indoc! {"
454 a
455 b
456 ˇ"})
457 .await;
458
459 cx.simulate_shared_keystrokes(["v"]).await;
460 cx.assert_shared_state(indoc! {"
461 a
462 b
463 ˇ"})
464 .await;
465 assert_eq!(cx.mode(), cx.neovim_mode().await);
466 }
467
468 #[gpui::test]
469 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
470 let mut cx = NeovimBackedTestContext::new(cx).await;
471
472 cx.set_shared_state(indoc! {
473 "The ˇquick brown
474 fox jumps over
475 the lazy dog"
476 })
477 .await;
478 cx.simulate_shared_keystrokes(["shift-v"]).await;
479 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
480 fox jumps over
481 the lazy dog"})
482 .await;
483 assert_eq!(cx.mode(), cx.neovim_mode().await);
484 cx.simulate_shared_keystrokes(["x"]).await;
485 cx.assert_shared_state(indoc! { "fox ˇjumps over
486 the lazy dog"})
487 .await;
488
489 // it should work on empty lines
490 cx.set_shared_state(indoc! {"
491 a
492 ˇ
493 b"})
494 .await;
495 cx.simulate_shared_keystrokes(["shift-v"]).await;
496 cx.assert_shared_state(indoc! { "
497 a
498 «
499 ˇ»b"})
500 .await;
501 cx.simulate_shared_keystrokes(["x"]).await;
502 cx.assert_shared_state(indoc! { "
503 a
504 ˇb"})
505 .await;
506
507 // it should work at the end of the document
508 cx.set_shared_state(indoc! {"
509 a
510 b
511 ˇ"})
512 .await;
513 let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor());
514 cx.simulate_shared_keystrokes(["shift-v"]).await;
515 cx.assert_shared_state(indoc! {"
516 a
517 b
518 ˇ"})
519 .await;
520 assert_eq!(cx.mode(), cx.neovim_mode().await);
521 cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor()));
522 cx.simulate_shared_keystrokes(["x"]).await;
523 cx.assert_shared_state(indoc! {"
524 a
525 ˇb"})
526 .await;
527 }
528
529 #[gpui::test]
530 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
531 let mut cx = NeovimBackedTestContext::new(cx).await;
532
533 cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
534 .await;
535
536 cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
537 .await;
538 cx.assert_binding_matches(
539 ["v", "w", "j", "x"],
540 indoc! {"
541 The ˇquick brown
542 fox jumps over
543 the lazy dog"},
544 )
545 .await;
546 // Test pasting code copied on delete
547 cx.simulate_shared_keystrokes(["j", "p"]).await;
548 cx.assert_state_matches().await;
549
550 let mut cx = cx.binding(["v", "w", "j", "x"]);
551 cx.assert_all(indoc! {"
552 The ˇquick brown
553 fox jumps over
554 the ˇlazy dog"})
555 .await;
556 let mut cx = cx.binding(["v", "b", "k", "x"]);
557 cx.assert_all(indoc! {"
558 The ˇquick brown
559 fox jumps ˇover
560 the ˇlazy dog"})
561 .await;
562 }
563
564 #[gpui::test]
565 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
566 let mut cx = NeovimBackedTestContext::new(cx)
567 .await
568 .binding(["shift-v", "x"]);
569 cx.assert(indoc! {"
570 The quˇick brown
571 fox jumps over
572 the lazy dog"})
573 .await;
574 // Test pasting code copied on delete
575 cx.simulate_shared_keystroke("p").await;
576 cx.assert_state_matches().await;
577
578 cx.assert_all(indoc! {"
579 The quick brown
580 fox juˇmps over
581 the laˇzy dog"})
582 .await;
583 let mut cx = cx.binding(["shift-v", "j", "x"]);
584 cx.assert(indoc! {"
585 The quˇick brown
586 fox jumps over
587 the lazy dog"})
588 .await;
589 // Test pasting code copied on delete
590 cx.simulate_shared_keystroke("p").await;
591 cx.assert_state_matches().await;
592
593 cx.assert_all(indoc! {"
594 The quick brown
595 fox juˇmps over
596 the laˇzy dog"})
597 .await;
598
599 cx.set_shared_state(indoc! {"
600 The ˇlong line
601 should not
602 crash
603 "})
604 .await;
605 cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
606 cx.assert_state_matches().await;
607 }
608
609 #[gpui::test]
610 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
611 let cx = VimTestContext::new(cx, true).await;
612 let mut cx = cx.binding(["v", "w", "y"]);
613 cx.assert("The quick ˇbrown", "The quick ˇbrown");
614 cx.assert_clipboard_content(Some("brown"));
615 let mut cx = cx.binding(["v", "w", "j", "y"]);
616 cx.assert(
617 indoc! {"
618 The ˇquick brown
619 fox jumps over
620 the lazy dog"},
621 indoc! {"
622 The ˇquick brown
623 fox jumps over
624 the lazy dog"},
625 );
626 cx.assert_clipboard_content(Some(indoc! {"
627 quick brown
628 fox jumps o"}));
629 cx.assert(
630 indoc! {"
631 The quick brown
632 fox jumps over
633 the ˇlazy dog"},
634 indoc! {"
635 The quick brown
636 fox jumps over
637 the ˇlazy dog"},
638 );
639 cx.assert_clipboard_content(Some("lazy d"));
640 cx.assert(
641 indoc! {"
642 The quick brown
643 fox jumps ˇover
644 the lazy dog"},
645 indoc! {"
646 The quick brown
647 fox jumps ˇover
648 the lazy dog"},
649 );
650 cx.assert_clipboard_content(Some(indoc! {"
651 over
652 t"}));
653 let mut cx = cx.binding(["v", "b", "k", "y"]);
654 cx.assert(
655 indoc! {"
656 The ˇquick brown
657 fox jumps over
658 the lazy dog"},
659 indoc! {"
660 ˇThe quick brown
661 fox jumps over
662 the lazy dog"},
663 );
664 cx.assert_clipboard_content(Some("The q"));
665 cx.assert(
666 indoc! {"
667 The quick brown
668 fox jumps over
669 the ˇlazy dog"},
670 indoc! {"
671 The quick brown
672 ˇfox jumps over
673 the lazy dog"},
674 );
675 cx.assert_clipboard_content(Some(indoc! {"
676 fox jumps over
677 the l"}));
678 cx.assert(
679 indoc! {"
680 The quick brown
681 fox jumps ˇover
682 the lazy dog"},
683 indoc! {"
684 The ˇquick brown
685 fox jumps over
686 the lazy dog"},
687 );
688 cx.assert_clipboard_content(Some(indoc! {"
689 quick brown
690 fox jumps o"}));
691 }
692
693 #[gpui::test]
694 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
695 let mut cx = NeovimBackedTestContext::new(cx).await;
696
697 cx.set_shared_state(indoc! {
698 "The ˇquick brown
699 fox jumps over
700 the lazy dog"
701 })
702 .await;
703 cx.simulate_shared_keystrokes(["ctrl-v"]).await;
704 cx.assert_shared_state(indoc! {
705 "The «qˇ»uick brown
706 fox jumps over
707 the lazy dog"
708 })
709 .await;
710 cx.simulate_shared_keystrokes(["2", "down"]).await;
711 cx.assert_shared_state(indoc! {
712 "The «qˇ»uick brown
713 fox «jˇ»umps over
714 the «lˇ»azy dog"
715 })
716 .await;
717 cx.simulate_shared_keystrokes(["e"]).await;
718 cx.assert_shared_state(indoc! {
719 "The «quicˇ»k brown
720 fox «jumpˇ»s over
721 the «lazyˇ» dog"
722 })
723 .await;
724 cx.simulate_shared_keystrokes(["^"]).await;
725 cx.assert_shared_state(indoc! {
726 "«ˇThe q»uick brown
727 «ˇfox j»umps over
728 «ˇthe l»azy dog"
729 })
730 .await;
731 cx.simulate_shared_keystrokes(["$"]).await;
732 cx.assert_shared_state(indoc! {
733 "The «quick brownˇ»
734 fox «jumps overˇ»
735 the «lazy dogˇ»"
736 })
737 .await;
738 cx.simulate_shared_keystrokes(["shift-f", " "]).await;
739 cx.assert_shared_state(indoc! {
740 "The «quickˇ» brown
741 fox «jumpsˇ» over
742 the «lazy ˇ»dog"
743 })
744 .await;
745
746 // toggling through visual mode works as expected
747 cx.simulate_shared_keystrokes(["v"]).await;
748 cx.assert_shared_state(indoc! {
749 "The «quick brown
750 fox jumps over
751 the lazy ˇ»dog"
752 })
753 .await;
754 cx.simulate_shared_keystrokes(["ctrl-v"]).await;
755 cx.assert_shared_state(indoc! {
756 "The «quickˇ» brown
757 fox «jumpsˇ» over
758 the «lazy ˇ»dog"
759 })
760 .await;
761
762 cx.set_shared_state(indoc! {
763 "The ˇquick
764 brown
765 fox
766 jumps over the
767
768 lazy dog
769 "
770 })
771 .await;
772 cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
773 .await;
774 cx.assert_shared_state(indoc! {
775 "The«ˇ q»uick
776 bro«ˇwn»
777 foxˇ
778 jumps over the
779
780 lazy dog
781 "
782 })
783 .await;
784 cx.simulate_shared_keystrokes(["down"]).await;
785 cx.assert_shared_state(indoc! {
786 "The «qˇ»uick
787 brow«nˇ»
788 fox
789 jump«sˇ» over the
790
791 lazy dog
792 "
793 })
794 .await;
795 cx.simulate_shared_keystroke("left").await;
796 cx.assert_shared_state(indoc! {
797 "The«ˇ q»uick
798 bro«ˇwn»
799 foxˇ
800 jum«ˇps» over the
801
802 lazy dog
803 "
804 })
805 .await;
806 cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
807 cx.assert_shared_state(indoc! {
808 "Theˇouick
809 broo
810 foxo
811 jumo over the
812
813 lazy dog
814 "
815 })
816 .await;
817 }
818
819 #[gpui::test]
820 async fn test_visual_block_insert(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 })
829 .await;
830 cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
831 cx.assert_shared_state(indoc! {
832 "«Tˇ»he quick brown
833 «fˇ»ox jumps over
834 «tˇ»he lazy dog
835 ˇ"
836 })
837 .await;
838
839 cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
840 .await;
841 cx.assert_shared_state(indoc! {
842 "ˇkThe quick brown
843 kfox jumps over
844 kthe lazy dog
845 k"
846 })
847 .await;
848
849 cx.set_shared_state(indoc! {
850 "ˇThe quick brown
851 fox jumps over
852 the lazy dog
853 "
854 })
855 .await;
856 cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
857 cx.assert_shared_state(indoc! {
858 "«Tˇ»he quick brown
859 «fˇ»ox jumps over
860 «tˇ»he lazy dog
861 ˇ"
862 })
863 .await;
864 cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
865 cx.assert_shared_state(indoc! {
866 "ˇkhe quick brown
867 kox jumps over
868 khe lazy dog
869 k"
870 })
871 .await;
872 }
873
874 #[gpui::test]
875 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
876 let mut cx = NeovimBackedTestContext::new(cx).await;
877
878 cx.set_shared_state("hello (in [parˇens] o)").await;
879 cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
880 cx.simulate_shared_keystrokes(["a", "]"]).await;
881 cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
882 assert_eq!(cx.mode(), Mode::Visual);
883 cx.simulate_shared_keystrokes(["i", "("]).await;
884 cx.assert_shared_state("hello («in [parens] oˇ»)").await;
885
886 cx.set_shared_state("hello in a wˇord again.").await;
887 cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
888 .await;
889 cx.assert_shared_state("hello in a w«ordˇ» again.").await;
890 assert_eq!(cx.mode(), Mode::VisualBlock);
891 cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
892 cx.assert_shared_state("«ˇhello in a word» again.").await;
893 assert_eq!(cx.mode(), Mode::Visual);
894 }
895
896 #[gpui::test]
897 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
898 let mut cx = VimTestContext::new(cx, true).await;
899
900 cx.set_state("aˇbc", Mode::Normal);
901 cx.simulate_keystrokes(["ctrl-v"]);
902 assert_eq!(cx.mode(), Mode::VisualBlock);
903 cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
904 assert_eq!(cx.mode(), Mode::VisualBlock);
905 }
906}