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