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