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.record_current_action(cx);
281 vim.update_active_editor(cx, |editor, cx| {
282 let mut original_columns: HashMap<_, _> = Default::default();
283 let line_mode = editor.selections.line_mode;
284
285 editor.transact(cx, |editor, cx| {
286 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
287 s.move_with(|map, selection| {
288 if line_mode {
289 let mut position = selection.head();
290 if !selection.reversed {
291 position = movement::left(map, position);
292 }
293 original_columns.insert(selection.id, position.to_point(map).column);
294 }
295 selection.goal = SelectionGoal::None;
296 });
297 });
298 copy_selections_content(editor, line_mode, cx);
299 editor.insert("", cx);
300
301 // Fixup cursor position after the deletion
302 editor.set_clip_at_line_ends(true, cx);
303 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
304 s.move_with(|map, selection| {
305 let mut cursor = selection.head().to_point(map);
306
307 if let Some(column) = original_columns.get(&selection.id) {
308 cursor.column = *column
309 }
310 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
311 selection.collapse_to(cursor, selection.goal)
312 });
313 if vim.state().mode == Mode::VisualBlock {
314 s.select_anchors(vec![s.first_anchor()])
315 }
316 });
317 })
318 });
319 vim.switch_mode(Mode::Normal, true, cx);
320 });
321}
322
323pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
324 Vim::update(cx, |vim, cx| {
325 vim.update_active_editor(cx, |editor, cx| {
326 let line_mode = editor.selections.line_mode;
327 copy_selections_content(editor, line_mode, cx);
328 editor.change_selections(None, cx, |s| {
329 s.move_with(|_, selection| {
330 selection.collapse_to(selection.start, SelectionGoal::None)
331 });
332 if vim.state().mode == Mode::VisualBlock {
333 s.select_anchors(vec![s.first_anchor()])
334 }
335 });
336 });
337 vim.switch_mode(Mode::Normal, true, cx);
338 });
339}
340
341pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
342 Vim::update(cx, |vim, cx| {
343 vim.stop_recording();
344 vim.update_active_editor(cx, |editor, cx| {
345 editor.transact(cx, |editor, cx| {
346 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
347
348 // Selections are biased right at the start. So we need to store
349 // anchors that are biased left so that we can restore the selections
350 // after the change
351 let stable_anchors = editor
352 .selections
353 .disjoint_anchors()
354 .into_iter()
355 .map(|selection| {
356 let start = selection.start.bias_left(&display_map.buffer_snapshot);
357 start..start
358 })
359 .collect::<Vec<_>>();
360
361 let mut edits = Vec::new();
362 for selection in selections.iter() {
363 let selection = selection.clone();
364 for row_range in
365 movement::split_display_range_by_lines(&display_map, selection.range())
366 {
367 let range = row_range.start.to_offset(&display_map, Bias::Right)
368 ..row_range.end.to_offset(&display_map, Bias::Right);
369 let text = text.repeat(range.len());
370 edits.push((range, text));
371 }
372 }
373
374 editor.buffer().update(cx, |buffer, cx| {
375 buffer.edit(edits, None, cx);
376 });
377 editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
378 });
379 });
380 vim.switch_mode(Mode::Normal, false, cx);
381 });
382}
383
384#[cfg(test)]
385mod test {
386 use indoc::indoc;
387 use workspace::item::Item;
388
389 use crate::{
390 state::Mode,
391 test::{NeovimBackedTestContext, VimTestContext},
392 };
393
394 #[gpui::test]
395 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
396 let mut cx = NeovimBackedTestContext::new(cx).await;
397
398 cx.set_shared_state(indoc! {
399 "The ˇquick brown
400 fox jumps over
401 the lazy dog"
402 })
403 .await;
404 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
405
406 // entering visual mode should select the character
407 // under cursor
408 cx.simulate_shared_keystrokes(["v"]).await;
409 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
410 fox jumps over
411 the lazy dog"})
412 .await;
413 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
414
415 // forwards motions should extend the selection
416 cx.simulate_shared_keystrokes(["w", "j"]).await;
417 cx.assert_shared_state(indoc! { "The «quick brown
418 fox jumps oˇ»ver
419 the lazy dog"})
420 .await;
421
422 cx.simulate_shared_keystrokes(["escape"]).await;
423 assert_eq!(Mode::Normal, cx.neovim_mode().await);
424 cx.assert_shared_state(indoc! { "The quick brown
425 fox jumps ˇover
426 the lazy dog"})
427 .await;
428
429 // motions work backwards
430 cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
431 cx.assert_shared_state(indoc! { "The «ˇquick brown
432 fox jumps o»ver
433 the lazy dog"})
434 .await;
435
436 // works on empty lines
437 cx.set_shared_state(indoc! {"
438 a
439 ˇ
440 b
441 "})
442 .await;
443 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
444 cx.simulate_shared_keystrokes(["v"]).await;
445 cx.assert_shared_state(indoc! {"
446 a
447 «
448 ˇ»b
449 "})
450 .await;
451 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
452
453 // toggles off again
454 cx.simulate_shared_keystrokes(["v"]).await;
455 cx.assert_shared_state(indoc! {"
456 a
457 ˇ
458 b
459 "})
460 .await;
461
462 // works at the end of a document
463 cx.set_shared_state(indoc! {"
464 a
465 b
466 ˇ"})
467 .await;
468
469 cx.simulate_shared_keystrokes(["v"]).await;
470 cx.assert_shared_state(indoc! {"
471 a
472 b
473 ˇ"})
474 .await;
475 assert_eq!(cx.mode(), cx.neovim_mode().await);
476 }
477
478 #[gpui::test]
479 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
480 let mut cx = NeovimBackedTestContext::new(cx).await;
481
482 cx.set_shared_state(indoc! {
483 "The ˇquick brown
484 fox jumps over
485 the lazy dog"
486 })
487 .await;
488 cx.simulate_shared_keystrokes(["shift-v"]).await;
489 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
490 fox jumps over
491 the lazy dog"})
492 .await;
493 assert_eq!(cx.mode(), cx.neovim_mode().await);
494 cx.simulate_shared_keystrokes(["x"]).await;
495 cx.assert_shared_state(indoc! { "fox ˇjumps over
496 the lazy dog"})
497 .await;
498
499 // it should work on empty lines
500 cx.set_shared_state(indoc! {"
501 a
502 ˇ
503 b"})
504 .await;
505 cx.simulate_shared_keystrokes(["shift-v"]).await;
506 cx.assert_shared_state(indoc! { "
507 a
508 «
509 ˇ»b"})
510 .await;
511 cx.simulate_shared_keystrokes(["x"]).await;
512 cx.assert_shared_state(indoc! { "
513 a
514 ˇb"})
515 .await;
516
517 // it should work at the end of the document
518 cx.set_shared_state(indoc! {"
519 a
520 b
521 ˇ"})
522 .await;
523 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
524 cx.simulate_shared_keystrokes(["shift-v"]).await;
525 cx.assert_shared_state(indoc! {"
526 a
527 b
528 ˇ"})
529 .await;
530 assert_eq!(cx.mode(), cx.neovim_mode().await);
531 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
532 cx.simulate_shared_keystrokes(["x"]).await;
533 cx.assert_shared_state(indoc! {"
534 a
535 ˇb"})
536 .await;
537 }
538
539 #[gpui::test]
540 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
541 let mut cx = NeovimBackedTestContext::new(cx).await;
542
543 cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
544 .await;
545
546 cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
547 .await;
548 cx.assert_binding_matches(
549 ["v", "w", "j", "x"],
550 indoc! {"
551 The ˇquick brown
552 fox jumps over
553 the lazy dog"},
554 )
555 .await;
556 // Test pasting code copied on delete
557 cx.simulate_shared_keystrokes(["j", "p"]).await;
558 cx.assert_state_matches().await;
559
560 let mut cx = cx.binding(["v", "w", "j", "x"]);
561 cx.assert_all(indoc! {"
562 The ˇquick brown
563 fox jumps over
564 the ˇlazy dog"})
565 .await;
566 let mut cx = cx.binding(["v", "b", "k", "x"]);
567 cx.assert_all(indoc! {"
568 The ˇquick brown
569 fox jumps ˇover
570 the ˇlazy dog"})
571 .await;
572 }
573
574 #[gpui::test]
575 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
576 let mut cx = NeovimBackedTestContext::new(cx).await;
577
578 cx.set_shared_state(indoc! {"
579 The quˇick brown
580 fox jumps over
581 the lazy dog"})
582 .await;
583 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
584 cx.assert_state_matches().await;
585
586 // Test pasting code copied on delete
587 cx.simulate_shared_keystroke("p").await;
588 cx.assert_state_matches().await;
589
590 cx.set_shared_state(indoc! {"
591 The quick brown
592 fox jumps over
593 the laˇzy dog"})
594 .await;
595 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
596 cx.assert_state_matches().await;
597 cx.assert_shared_clipboard("the lazy dog\n").await;
598
599 for marked_text in cx.each_marked_position(indoc! {"
600 The quˇick brown
601 fox jumps over
602 the lazy dog"})
603 {
604 cx.set_shared_state(&marked_text).await;
605 cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
606 cx.assert_state_matches().await;
607 // Test pasting code copied on delete
608 cx.simulate_shared_keystroke("p").await;
609 cx.assert_state_matches().await;
610 }
611
612 cx.set_shared_state(indoc! {"
613 The ˇlong line
614 should not
615 crash
616 "})
617 .await;
618 cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
619 cx.assert_state_matches().await;
620 }
621
622 #[gpui::test]
623 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
624 let mut cx = NeovimBackedTestContext::new(cx).await;
625
626 cx.set_shared_state("The quick ˇbrown").await;
627 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
628 cx.assert_shared_state("The quick ˇbrown").await;
629 cx.assert_shared_clipboard("brown").await;
630
631 cx.set_shared_state(indoc! {"
632 The ˇquick brown
633 fox jumps over
634 the lazy dog"})
635 .await;
636 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
637 cx.assert_shared_state(indoc! {"
638 The ˇquick brown
639 fox jumps over
640 the lazy dog"})
641 .await;
642 cx.assert_shared_clipboard(indoc! {"
643 quick brown
644 fox jumps o"})
645 .await;
646
647 cx.set_shared_state(indoc! {"
648 The quick brown
649 fox jumps over
650 the ˇlazy dog"})
651 .await;
652 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
653 cx.assert_shared_state(indoc! {"
654 The quick brown
655 fox jumps over
656 the ˇlazy dog"})
657 .await;
658 cx.assert_shared_clipboard("lazy d").await;
659 cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
660 cx.assert_shared_clipboard("the lazy dog\n").await;
661
662 let mut cx = cx.binding(["v", "b", "k", "y"]);
663 cx.set_shared_state(indoc! {"
664 The ˇquick brown
665 fox jumps over
666 the lazy dog"})
667 .await;
668 cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
669 cx.assert_shared_state(indoc! {"
670 ˇThe quick brown
671 fox jumps over
672 the lazy dog"})
673 .await;
674 cx.assert_clipboard_content(Some("The q"));
675 }
676
677 #[gpui::test]
678 async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) {
679 let mut cx = NeovimBackedTestContext::new(cx).await;
680
681 cx.set_shared_state(indoc! {
682 "The ˇquick brown
683 fox jumps over
684 the lazy dog"
685 })
686 .await;
687 cx.simulate_shared_keystrokes(["ctrl-v"]).await;
688 cx.assert_shared_state(indoc! {
689 "The «qˇ»uick brown
690 fox jumps over
691 the lazy dog"
692 })
693 .await;
694 cx.simulate_shared_keystrokes(["2", "down"]).await;
695 cx.assert_shared_state(indoc! {
696 "The «qˇ»uick brown
697 fox «jˇ»umps over
698 the «lˇ»azy dog"
699 })
700 .await;
701 cx.simulate_shared_keystrokes(["e"]).await;
702 cx.assert_shared_state(indoc! {
703 "The «quicˇ»k brown
704 fox «jumpˇ»s over
705 the «lazyˇ» dog"
706 })
707 .await;
708 cx.simulate_shared_keystrokes(["^"]).await;
709 cx.assert_shared_state(indoc! {
710 "«ˇThe q»uick brown
711 «ˇfox j»umps over
712 «ˇthe l»azy dog"
713 })
714 .await;
715 cx.simulate_shared_keystrokes(["$"]).await;
716 cx.assert_shared_state(indoc! {
717 "The «quick brownˇ»
718 fox «jumps overˇ»
719 the «lazy dogˇ»"
720 })
721 .await;
722 cx.simulate_shared_keystrokes(["shift-f", " "]).await;
723 cx.assert_shared_state(indoc! {
724 "The «quickˇ» brown
725 fox «jumpsˇ» over
726 the «lazy ˇ»dog"
727 })
728 .await;
729
730 // toggling through visual mode works as expected
731 cx.simulate_shared_keystrokes(["v"]).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(["ctrl-v"]).await;
739 cx.assert_shared_state(indoc! {
740 "The «quickˇ» brown
741 fox «jumpsˇ» over
742 the «lazy ˇ»dog"
743 })
744 .await;
745
746 cx.set_shared_state(indoc! {
747 "The ˇquick
748 brown
749 fox
750 jumps over the
751
752 lazy dog
753 "
754 })
755 .await;
756 cx.simulate_shared_keystrokes(["ctrl-v", "down", "down"])
757 .await;
758 cx.assert_shared_state(indoc! {
759 "The«ˇ q»uick
760 bro«ˇwn»
761 foxˇ
762 jumps over the
763
764 lazy dog
765 "
766 })
767 .await;
768 cx.simulate_shared_keystrokes(["down"]).await;
769 cx.assert_shared_state(indoc! {
770 "The «qˇ»uick
771 brow«nˇ»
772 fox
773 jump«sˇ» over the
774
775 lazy dog
776 "
777 })
778 .await;
779 cx.simulate_shared_keystroke("left").await;
780 cx.assert_shared_state(indoc! {
781 "The«ˇ q»uick
782 bro«ˇwn»
783 foxˇ
784 jum«ˇps» over the
785
786 lazy dog
787 "
788 })
789 .await;
790 cx.simulate_shared_keystrokes(["s", "o", "escape"]).await;
791 cx.assert_shared_state(indoc! {
792 "Theˇouick
793 broo
794 foxo
795 jumo over the
796
797 lazy dog
798 "
799 })
800 .await;
801
802 //https://github.com/zed-industries/community/issues/1950
803 cx.set_shared_state(indoc! {
804 "Theˇ quick brown
805
806 fox jumps over
807 the lazy dog
808 "
809 })
810 .await;
811 cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"])
812 .await;
813 cx.assert_shared_state(indoc! {
814 "The «qˇ»uick brown
815
816 fox «jˇ»umps over
817 the lazy dog
818 "
819 })
820 .await;
821 }
822
823 #[gpui::test]
824 async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) {
825 let mut cx = NeovimBackedTestContext::new(cx).await;
826
827 cx.set_shared_state(indoc! {
828 "ˇThe quick brown
829 fox jumps over
830 the lazy dog
831 "
832 })
833 .await;
834 cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
835 cx.assert_shared_state(indoc! {
836 "«Tˇ»he quick brown
837 «fˇ»ox jumps over
838 «tˇ»he lazy dog
839 ˇ"
840 })
841 .await;
842
843 cx.simulate_shared_keystrokes(["shift-i", "k", "escape"])
844 .await;
845 cx.assert_shared_state(indoc! {
846 "ˇkThe quick brown
847 kfox jumps over
848 kthe lazy dog
849 k"
850 })
851 .await;
852
853 cx.set_shared_state(indoc! {
854 "ˇThe quick brown
855 fox jumps over
856 the lazy dog
857 "
858 })
859 .await;
860 cx.simulate_shared_keystrokes(["ctrl-v", "9", "down"]).await;
861 cx.assert_shared_state(indoc! {
862 "«Tˇ»he quick brown
863 «fˇ»ox jumps over
864 «tˇ»he lazy dog
865 ˇ"
866 })
867 .await;
868 cx.simulate_shared_keystrokes(["c", "k", "escape"]).await;
869 cx.assert_shared_state(indoc! {
870 "ˇkhe quick brown
871 kox jumps over
872 khe lazy dog
873 k"
874 })
875 .await;
876 }
877
878 #[gpui::test]
879 async fn test_visual_object(cx: &mut gpui::TestAppContext) {
880 let mut cx = NeovimBackedTestContext::new(cx).await;
881
882 cx.set_shared_state("hello (in [parˇens] o)").await;
883 cx.simulate_shared_keystrokes(["ctrl-v", "l"]).await;
884 cx.simulate_shared_keystrokes(["a", "]"]).await;
885 cx.assert_shared_state("hello (in «[parens]ˇ» o)").await;
886 assert_eq!(cx.mode(), Mode::Visual);
887 cx.simulate_shared_keystrokes(["i", "("]).await;
888 cx.assert_shared_state("hello («in [parens] oˇ»)").await;
889
890 cx.set_shared_state("hello in a wˇord again.").await;
891 cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
892 .await;
893 cx.assert_shared_state("hello in a w«ordˇ» again.").await;
894 assert_eq!(cx.mode(), Mode::VisualBlock);
895 cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
896 cx.assert_shared_state("«ˇhello in a word» again.").await;
897 assert_eq!(cx.mode(), Mode::Visual);
898 }
899
900 #[gpui::test]
901 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
902 let mut cx = VimTestContext::new(cx, true).await;
903
904 cx.set_state("aˇbc", Mode::Normal);
905 cx.simulate_keystrokes(["ctrl-v"]);
906 assert_eq!(cx.mode(), Mode::VisualBlock);
907 cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
908 assert_eq!(cx.mode(), Mode::VisualBlock);
909 }
910}