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