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