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