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