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, cx| editor.pixel_position_of_cursor(cx));
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, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
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, cx| editor.pixel_position_of_cursor(cx));
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, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
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, cx| editor.pixel_position_of_cursor(cx));
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, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
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).await;
567
568 cx.set_shared_state(indoc! {"
569 The quˇick brown
570 fox jumps over
571 the lazy dog"})
572 .await;
573 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
574 cx.assert_state_matches().await;
575
576 // Test pasting code copied on delete
577 cx.simulate_shared_keystroke("p").await;
578 cx.assert_state_matches().await;
579
580 cx.set_shared_state(indoc! {"
581 The quick brown
582 fox jumps over
583 the laˇzy dog"})
584 .await;
585 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
586 cx.assert_state_matches().await;
587 cx.assert_shared_clipboard("the lazy dog\n").await;
588
589 for marked_text in cx.each_marked_position(indoc! {"
590 The quˇick brown
591 fox jumps over
592 the lazy dog"})
593 {
594 cx.set_shared_state(&marked_text).await;
595 cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
596 cx.assert_state_matches().await;
597 // Test pasting code copied on delete
598 cx.simulate_shared_keystroke("p").await;
599 cx.assert_state_matches().await;
600 }
601
602 cx.set_shared_state(indoc! {"
603 The ˇlong line
604 should not
605 crash
606 "})
607 .await;
608 cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
609 cx.assert_state_matches().await;
610 }
611
612 #[gpui::test]
613 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
614 let mut cx = NeovimBackedTestContext::new(cx).await;
615
616 cx.set_shared_state("The quick ˇbrown").await;
617 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
618 cx.assert_shared_state("The quick ˇbrown").await;
619 cx.assert_shared_clipboard("brown").await;
620
621 cx.set_shared_state(indoc! {"
622 The ˇquick brown
623 fox jumps over
624 the lazy dog"})
625 .await;
626 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
627 cx.assert_shared_state(indoc! {"
628 The ˇquick brown
629 fox jumps over
630 the lazy dog"})
631 .await;
632 cx.assert_shared_clipboard(indoc! {"
633 quick brown
634 fox jumps o"})
635 .await;
636
637 cx.set_shared_state(indoc! {"
638 The quick brown
639 fox jumps over
640 the ˇlazy dog"})
641 .await;
642 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
643 cx.assert_shared_state(indoc! {"
644 The quick brown
645 fox jumps over
646 the ˇlazy dog"})
647 .await;
648 cx.assert_shared_clipboard("lazy d").await;
649 cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
650 cx.assert_shared_clipboard("the lazy dog\n").await;
651
652 let mut cx = cx.binding(["v", "b", "k", "y"]);
653 cx.set_shared_state(indoc! {"
654 The ˇquick brown
655 fox jumps over
656 the lazy dog"})
657 .await;
658 cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
659 cx.assert_shared_state(indoc! {"
660 ˇThe quick brown
661 fox jumps over
662 the lazy dog"})
663 .await;
664 cx.assert_clipboard_content(Some("The q"));
665 }
666
667 #[gpui::test]
668 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
669 let mut cx = NeovimBackedTestContext::new(cx).await;
670
671 cx.set_shared_state(indoc! {
672 "The ˇquick brown
673 fox jumps over
674 the lazy dog"
675 })
676 .await;
677 cx.simulate_shared_keystrokes(["ctrl-v"]).await;
678 cx.assert_shared_state(indoc! {
679 "The «qˇ»uick brown
680 fox jumps over
681 the lazy dog"
682 })
683 .await;
684 cx.simulate_shared_keystrokes(["2", "down"]).await;
685 cx.assert_shared_state(indoc! {
686 "The «qˇ»uick brown
687 fox «jˇ»umps over
688 the «lˇ»azy dog"
689 })
690 .await;
691 cx.simulate_shared_keystrokes(["e"]).await;
692 cx.assert_shared_state(indoc! {
693 "The «quicˇ»k brown
694 fox «jumpˇ»s over
695 the «lazyˇ» dog"
696 })
697 .await;
698 cx.simulate_shared_keystrokes(["^"]).await;
699 cx.assert_shared_state(indoc! {
700 "«ˇThe q»uick brown
701 «ˇfox j»umps over
702 «ˇthe l»azy dog"
703 })
704 .await;
705 cx.simulate_shared_keystrokes(["$"]).await;
706 cx.assert_shared_state(indoc! {
707 "The «quick brownˇ»
708 fox «jumps overˇ»
709 the «lazy dogˇ»"
710 })
711 .await;
712 cx.simulate_shared_keystrokes(["shift-f", " "]).await;
713 cx.assert_shared_state(indoc! {
714 "The «quickˇ» brown
715 fox «jumpsˇ» over
716 the «lazy ˇ»dog"
717 })
718 .await;
719
720 // toggling through visual mode works as expected
721 cx.simulate_shared_keystrokes(["v"]).await;
722 cx.assert_shared_state(indoc! {
723 "The «quick brown
724 fox jumps over
725 the lazy ˇ»dog"
726 })
727 .await;
728 cx.simulate_shared_keystrokes(["ctrl-v"]).await;
729 cx.assert_shared_state(indoc! {
730 "The «quickˇ» brown
731 fox «jumpsˇ» over
732 the «lazy ˇ»dog"
733 })
734 .await;
735
736 cx.set_shared_state(indoc! {
737 "The ˇquick
738 brown
739 fox
740 jumps over the
741
742 lazy dog
743 "
744 })
745 .await;
746 cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
747 .await;
748 cx.assert_shared_state(indoc! {
749 "The«ˇ q»uick
750 bro«ˇwn»
751 foxˇ
752 jumps over the
753
754 lazy dog
755 "
756 })
757 .await;
758 cx.simulate_shared_keystrokes(["down"]).await;
759 cx.assert_shared_state(indoc! {
760 "The «qˇ»uick
761 brow«nˇ»
762 fox
763 jump«sˇ» over the
764
765 lazy dog
766 "
767 })
768 .await;
769 cx.simulate_shared_keystroke("left").await;
770 cx.assert_shared_state(indoc! {
771 "The«ˇ q»uick
772 bro«ˇwn»
773 foxˇ
774 jum«ˇps» over the
775
776 lazy dog
777 "
778 })
779 .await;
780 cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
781 cx.assert_shared_state(indoc! {
782 "Theˇouick
783 broo
784 foxo
785 jumo over the
786
787 lazy dog
788 "
789 })
790 .await;
791 }
792
793 #[gpui::test]
794 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
795 let mut cx = NeovimBackedTestContext::new(cx).await;
796
797 cx.set_shared_state(indoc! {
798 "ˇThe quick brown
799 fox jumps over
800 the lazy dog
801 "
802 })
803 .await;
804 cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
805 cx.assert_shared_state(indoc! {
806 "«Tˇ»he quick brown
807 «fˇ»ox jumps over
808 «tˇ»he lazy dog
809 ˇ"
810 })
811 .await;
812
813 cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
814 .await;
815 cx.assert_shared_state(indoc! {
816 "ˇkThe quick brown
817 kfox jumps over
818 kthe lazy dog
819 k"
820 })
821 .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 cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
839 cx.assert_shared_state(indoc! {
840 "ˇkhe quick brown
841 kox jumps over
842 khe lazy dog
843 k"
844 })
845 .await;
846 }
847
848 #[gpui::test]
849 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
850 let mut cx = NeovimBackedTestContext::new(cx).await;
851
852 cx.set_shared_state("hello (in [parˇens] o)").await;
853 cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
854 cx.simulate_shared_keystrokes(["a", "]"]).await;
855 cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
856 assert_eq!(cx.mode(), Mode::Visual);
857 cx.simulate_shared_keystrokes(["i", "("]).await;
858 cx.assert_shared_state("hello («in [parens] oˇ»)").await;
859
860 cx.set_shared_state("hello in a wˇord again.").await;
861 cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
862 .await;
863 cx.assert_shared_state("hello in a w«ordˇ» again.").await;
864 assert_eq!(cx.mode(), Mode::VisualBlock);
865 cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
866 cx.assert_shared_state("«ˇhello in a word» again.").await;
867 assert_eq!(cx.mode(), Mode::Visual);
868 }
869
870 #[gpui::test]
871 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
872 let mut cx = VimTestContext::new(cx, true).await;
873
874 cx.set_state("aˇbc", Mode::Normal);
875 cx.simulate_keystrokes(["ctrl-v"]);
876 assert_eq!(cx.mode(), Mode::VisualBlock);
877 cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
878 assert_eq!(cx.mode(), Mode::VisualBlock);
879 }
880}