1use anyhow::Result;
2use std::sync::Arc;
3
4use collections::HashMap;
5use editor::{
6 display_map::{DisplaySnapshot, ToDisplayPoint},
7 movement,
8 scroll::Autoscroll,
9 Bias, DisplayPoint, Editor,
10};
11use gpui::{actions, 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, yank_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 register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
38 workspace.register_action(|_, _: &ToggleVisual, cx: &mut ViewContext<Workspace>| {
39 toggle_mode(Mode::Visual, cx)
40 });
41 workspace.register_action(|_, _: &ToggleVisualLine, cx: &mut ViewContext<Workspace>| {
42 toggle_mode(Mode::VisualLine, cx)
43 });
44 workspace.register_action(
45 |_, _: &ToggleVisualBlock, cx: &mut ViewContext<Workspace>| {
46 toggle_mode(Mode::VisualBlock, cx)
47 },
48 );
49 workspace.register_action(other_end);
50 workspace.register_action(delete);
51 workspace.register_action(yank);
52
53 workspace.register_action(|workspace, action, cx| {
54 select_next(workspace, action, cx).ok();
55 });
56 workspace.register_action(|workspace, action, cx| {
57 select_previous(workspace, action, cx).ok();
58 });
59}
60
61pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
62 Vim::update(cx, |vim, cx| {
63 vim.update_active_editor(cx, |vim, editor, cx| {
64 let text_layout_details = editor.text_layout_details(cx);
65 if vim.state().mode == Mode::VisualBlock
66 && !matches!(
67 motion,
68 Motion::EndOfLine {
69 display_lines: false
70 }
71 )
72 {
73 let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. });
74 visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| {
75 motion.move_point(map, point, goal, times, &text_layout_details)
76 })
77 } else {
78 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
79 s.move_with(|map, selection| {
80 let was_reversed = selection.reversed;
81 let mut current_head = selection.head();
82
83 // our motions assume the current character is after the cursor,
84 // but in (forward) visual mode the current character is just
85 // before the end of the selection.
86
87 // If the file ends with a newline (which is common) we don't do this.
88 // so that if you go to the end of such a file you can use "up" to go
89 // to the previous line and have it work somewhat as expected.
90 #[allow(clippy::nonminimal_bool)]
91 if !selection.reversed
92 && !selection.is_empty()
93 && !(selection.end.column() == 0 && selection.end == map.max_point())
94 {
95 current_head = movement::left(map, selection.end)
96 }
97
98 let Some((new_head, goal)) = motion.move_point(
99 map,
100 current_head,
101 selection.goal,
102 times,
103 &text_layout_details,
104 ) else {
105 return;
106 };
107
108 selection.set_head(new_head, goal);
109
110 // ensure the current character is included in the selection.
111 if !selection.reversed {
112 let next_point = if vim.state().mode == Mode::VisualBlock {
113 movement::saturating_right(map, selection.end)
114 } else {
115 movement::right(map, selection.end)
116 };
117
118 if !(next_point.column() == 0 && next_point == map.max_point()) {
119 selection.end = next_point;
120 }
121 }
122
123 // vim always ensures the anchor character stays selected.
124 // if our selection has reversed, we need to move the opposite end
125 // to ensure the anchor is still selected.
126 if was_reversed && !selection.reversed {
127 selection.start = movement::left(map, selection.start);
128 } else if !was_reversed && selection.reversed {
129 selection.end = movement::right(map, selection.end);
130 }
131 })
132 });
133 }
134 });
135 });
136}
137
138pub fn visual_block_motion(
139 preserve_goal: bool,
140 editor: &mut Editor,
141 cx: &mut ViewContext<Editor>,
142 mut move_selection: impl FnMut(
143 &DisplaySnapshot,
144 DisplayPoint,
145 SelectionGoal,
146 ) -> Option<(DisplayPoint, SelectionGoal)>,
147) {
148 let text_layout_details = editor.text_layout_details(cx);
149 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
150 let map = &s.display_map();
151 let mut head = s.newest_anchor().head().to_display_point(map);
152 let mut tail = s.oldest_anchor().tail().to_display_point(map);
153
154 let mut head_x = map.x_for_display_point(head, &text_layout_details);
155 let mut tail_x = map.x_for_display_point(tail, &text_layout_details);
156
157 let (start, end) = match s.newest_anchor().goal {
158 SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end),
159 SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start),
160 _ => (tail_x.0, head_x.0),
161 };
162 let mut goal = SelectionGoal::HorizontalRange { start, end };
163
164 let was_reversed = tail_x > head_x;
165 if !was_reversed && !preserve_goal {
166 head = movement::saturating_left(map, head);
167 }
168
169 let Some((new_head, _)) = move_selection(&map, head, goal) else {
170 return;
171 };
172 head = new_head;
173 head_x = map.x_for_display_point(head, &text_layout_details);
174
175 let is_reversed = tail_x > head_x;
176 if was_reversed && !is_reversed {
177 tail = movement::saturating_left(map, tail);
178 tail_x = map.x_for_display_point(tail, &text_layout_details);
179 } else if !was_reversed && is_reversed {
180 tail = movement::saturating_right(map, tail);
181 tail_x = map.x_for_display_point(tail, &text_layout_details);
182 }
183 if !is_reversed && !preserve_goal {
184 head = movement::saturating_right(map, head);
185 head_x = map.x_for_display_point(head, &text_layout_details);
186 }
187
188 let positions = if is_reversed {
189 head_x..tail_x
190 } else {
191 tail_x..head_x
192 };
193
194 if !preserve_goal {
195 goal = SelectionGoal::HorizontalRange {
196 start: positions.start.0,
197 end: positions.end.0,
198 };
199 }
200
201 let mut selections = Vec::new();
202 let mut row = tail.row();
203
204 loop {
205 let laid_out_line = map.layout_row(row, &text_layout_details);
206 let start = DisplayPoint::new(
207 row,
208 laid_out_line.closest_index_for_x(positions.start) as u32,
209 );
210 let mut end =
211 DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32);
212 if end <= start {
213 if start.column() == map.line_len(start.row()) {
214 end = start;
215 } else {
216 end = movement::saturating_right(map, start);
217 }
218 }
219
220 if positions.start <= laid_out_line.width {
221 let selection = Selection {
222 id: s.new_selection_id(),
223 start: start.to_point(map),
224 end: end.to_point(map),
225 reversed: is_reversed,
226 goal,
227 };
228
229 selections.push(selection);
230 }
231 if row == head.row() {
232 break;
233 }
234 if tail.row() > head.row() {
235 row -= 1
236 } else {
237 row += 1
238 }
239 }
240
241 s.select(selections);
242 })
243}
244
245pub fn visual_object(object: Object, cx: &mut WindowContext) {
246 Vim::update(cx, |vim, cx| {
247 if let Some(Operator::Object { around }) = vim.active_operator() {
248 vim.pop_operator(cx);
249 let current_mode = vim.state().mode;
250 let target_mode = object.target_visual_mode(current_mode);
251 if target_mode != current_mode {
252 vim.switch_mode(target_mode, true, cx);
253 }
254
255 vim.update_active_editor(cx, |_, editor, cx| {
256 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
257 s.move_with(|map, selection| {
258 let mut head = selection.head();
259
260 // all our motions assume that the current character is
261 // after the cursor; however in the case of a visual selection
262 // the current character is before the cursor.
263 if !selection.reversed {
264 head = movement::left(map, head);
265 }
266
267 if let Some(range) = object.range(map, head, around) {
268 if !range.is_empty() {
269 let expand_both_ways = object.always_expands_both_ways()
270 || selection.is_empty()
271 || movement::right(map, selection.start) == selection.end;
272
273 if expand_both_ways {
274 selection.start = range.start;
275 selection.end = range.end;
276 } else if selection.reversed {
277 selection.start = range.start;
278 } else {
279 selection.end = range.end;
280 }
281 }
282 }
283 });
284 });
285 });
286 }
287 });
288}
289
290fn toggle_mode(mode: Mode, cx: &mut ViewContext<Workspace>) {
291 Vim::update(cx, |vim, cx| {
292 if vim.state().mode == mode {
293 vim.switch_mode(Mode::Normal, false, cx);
294 } else {
295 vim.switch_mode(mode, false, cx);
296 }
297 })
298}
299
300pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace>) {
301 Vim::update(cx, |vim, cx| {
302 vim.update_active_editor(cx, |_, editor, cx| {
303 editor.change_selections(None, cx, |s| {
304 s.move_with(|_, selection| {
305 selection.reversed = !selection.reversed;
306 })
307 })
308 })
309 });
310}
311
312pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
313 Vim::update(cx, |vim, cx| {
314 vim.record_current_action(cx);
315 vim.update_active_editor(cx, |vim, editor, cx| {
316 let mut original_columns: HashMap<_, _> = Default::default();
317 let line_mode = editor.selections.line_mode;
318
319 editor.transact(cx, |editor, cx| {
320 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
321 s.move_with(|map, selection| {
322 if line_mode {
323 let mut position = selection.head();
324 if !selection.reversed {
325 position = movement::left(map, position);
326 }
327 original_columns.insert(selection.id, position.to_point(map).column);
328 }
329 selection.goal = SelectionGoal::None;
330 });
331 });
332 copy_selections_content(vim, editor, line_mode, cx);
333 editor.insert("", cx);
334
335 // Fixup cursor position after the deletion
336 editor.set_clip_at_line_ends(true, cx);
337 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
338 s.move_with(|map, selection| {
339 let mut cursor = selection.head().to_point(map);
340
341 if let Some(column) = original_columns.get(&selection.id) {
342 cursor.column = *column
343 }
344 let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left);
345 selection.collapse_to(cursor, selection.goal)
346 });
347 if vim.state().mode == Mode::VisualBlock {
348 s.select_anchors(vec![s.first_anchor()])
349 }
350 });
351 })
352 });
353 vim.switch_mode(Mode::Normal, true, cx);
354 });
355}
356
357pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>) {
358 Vim::update(cx, |vim, cx| {
359 vim.update_active_editor(cx, |vim, editor, cx| {
360 let line_mode = editor.selections.line_mode;
361 yank_selections_content(vim, editor, line_mode, cx);
362 editor.change_selections(None, cx, |s| {
363 s.move_with(|map, selection| {
364 if line_mode {
365 selection.start = start_of_line(map, false, selection.start);
366 };
367 selection.collapse_to(selection.start, SelectionGoal::None)
368 });
369 if vim.state().mode == Mode::VisualBlock {
370 s.select_anchors(vec![s.first_anchor()])
371 }
372 });
373 });
374 vim.switch_mode(Mode::Normal, true, cx);
375 });
376}
377
378pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
379 Vim::update(cx, |vim, cx| {
380 vim.stop_recording();
381 vim.update_active_editor(cx, |_, editor, cx| {
382 editor.transact(cx, |editor, cx| {
383 let (display_map, selections) = editor.selections.all_adjusted_display(cx);
384
385 // Selections are biased right at the start. So we need to store
386 // anchors that are biased left so that we can restore the selections
387 // after the change
388 let stable_anchors = editor
389 .selections
390 .disjoint_anchors()
391 .into_iter()
392 .map(|selection| {
393 let start = selection.start.bias_left(&display_map.buffer_snapshot);
394 start..start
395 })
396 .collect::<Vec<_>>();
397
398 let mut edits = Vec::new();
399 for selection in selections.iter() {
400 let selection = selection.clone();
401 for row_range in
402 movement::split_display_range_by_lines(&display_map, selection.range())
403 {
404 let range = row_range.start.to_offset(&display_map, Bias::Right)
405 ..row_range.end.to_offset(&display_map, Bias::Right);
406 let text = text.repeat(range.len());
407 edits.push((range, text));
408 }
409 }
410
411 editor.buffer().update(cx, |buffer, cx| {
412 buffer.edit(edits, None, cx);
413 });
414 editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
415 });
416 });
417 vim.switch_mode(Mode::Normal, false, cx);
418 });
419}
420
421pub fn select_next(
422 _: &mut Workspace,
423 _: &SelectNext,
424 cx: &mut ViewContext<Workspace>,
425) -> Result<()> {
426 Vim::update(cx, |vim, cx| {
427 let count =
428 vim.take_count(cx)
429 .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
430 vim.update_active_editor(cx, |_, editor, cx| {
431 for _ in 0..count {
432 match editor.select_next(&Default::default(), cx) {
433 Err(a) => return Err(a),
434 _ => {}
435 }
436 }
437 Ok(())
438 })
439 })
440 .unwrap_or(Ok(()))
441}
442
443pub fn select_previous(
444 _: &mut Workspace,
445 _: &SelectPrevious,
446 cx: &mut ViewContext<Workspace>,
447) -> Result<()> {
448 Vim::update(cx, |vim, cx| {
449 let count =
450 vim.take_count(cx)
451 .unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
452 vim.update_active_editor(cx, |_, editor, cx| {
453 for _ in 0..count {
454 match editor.select_previous(&Default::default(), cx) {
455 Err(a) => return Err(a),
456 _ => {}
457 }
458 }
459 Ok(())
460 })
461 })
462 .unwrap_or(Ok(()))
463}
464
465#[cfg(test)]
466mod test {
467 use indoc::indoc;
468 use workspace::item::Item;
469
470 use crate::{
471 state::Mode,
472 test::{NeovimBackedTestContext, VimTestContext},
473 };
474
475 #[gpui::test]
476 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
477 let mut cx = NeovimBackedTestContext::new(cx).await;
478
479 cx.set_shared_state(indoc! {
480 "The ˇquick brown
481 fox jumps over
482 the lazy dog"
483 })
484 .await;
485 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
486
487 // entering visual mode should select the character
488 // under cursor
489 cx.simulate_shared_keystrokes(["v"]).await;
490 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
491 fox jumps over
492 the lazy dog"})
493 .await;
494 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
495
496 // forwards motions should extend the selection
497 cx.simulate_shared_keystrokes(["w", "j"]).await;
498 cx.assert_shared_state(indoc! { "The «quick brown
499 fox jumps oˇ»ver
500 the lazy dog"})
501 .await;
502
503 cx.simulate_shared_keystrokes(["escape"]).await;
504 assert_eq!(Mode::Normal, cx.neovim_mode().await);
505 cx.assert_shared_state(indoc! { "The quick brown
506 fox jumps ˇover
507 the lazy dog"})
508 .await;
509
510 // motions work backwards
511 cx.simulate_shared_keystrokes(["v", "k", "b"]).await;
512 cx.assert_shared_state(indoc! { "The «ˇquick brown
513 fox jumps o»ver
514 the lazy dog"})
515 .await;
516
517 // works on empty lines
518 cx.set_shared_state(indoc! {"
519 a
520 ˇ
521 b
522 "})
523 .await;
524 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
525 cx.simulate_shared_keystrokes(["v"]).await;
526 cx.assert_shared_state(indoc! {"
527 a
528 «
529 ˇ»b
530 "})
531 .await;
532 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
533
534 // toggles off again
535 cx.simulate_shared_keystrokes(["v"]).await;
536 cx.assert_shared_state(indoc! {"
537 a
538 ˇ
539 b
540 "})
541 .await;
542
543 // works at the end of a document
544 cx.set_shared_state(indoc! {"
545 a
546 b
547 ˇ"})
548 .await;
549
550 cx.simulate_shared_keystrokes(["v"]).await;
551 cx.assert_shared_state(indoc! {"
552 a
553 b
554 ˇ"})
555 .await;
556 assert_eq!(cx.mode(), cx.neovim_mode().await);
557 }
558
559 #[gpui::test]
560 async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) {
561 let mut cx = NeovimBackedTestContext::new(cx).await;
562
563 cx.set_shared_state(indoc! {
564 "The ˇquick brown
565 fox jumps over
566 the lazy dog"
567 })
568 .await;
569 cx.simulate_shared_keystrokes(["shift-v"]).await;
570 cx.assert_shared_state(indoc! { "The «qˇ»uick brown
571 fox jumps over
572 the lazy dog"})
573 .await;
574 assert_eq!(cx.mode(), cx.neovim_mode().await);
575 cx.simulate_shared_keystrokes(["x"]).await;
576 cx.assert_shared_state(indoc! { "fox ˇjumps over
577 the lazy dog"})
578 .await;
579
580 // it should work on empty lines
581 cx.set_shared_state(indoc! {"
582 a
583 ˇ
584 b"})
585 .await;
586 cx.simulate_shared_keystrokes(["shift-v"]).await;
587 cx.assert_shared_state(indoc! { "
588 a
589 «
590 ˇ»b"})
591 .await;
592 cx.simulate_shared_keystrokes(["x"]).await;
593 cx.assert_shared_state(indoc! { "
594 a
595 ˇb"})
596 .await;
597
598 // it should work at the end of the document
599 cx.set_shared_state(indoc! {"
600 a
601 b
602 ˇ"})
603 .await;
604 let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx));
605 cx.simulate_shared_keystrokes(["shift-v"]).await;
606 cx.assert_shared_state(indoc! {"
607 a
608 b
609 ˇ"})
610 .await;
611 assert_eq!(cx.mode(), cx.neovim_mode().await);
612 cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx)));
613 cx.simulate_shared_keystrokes(["x"]).await;
614 cx.assert_shared_state(indoc! {"
615 a
616 ˇb"})
617 .await;
618 }
619
620 #[gpui::test]
621 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
622 let mut cx = NeovimBackedTestContext::new(cx).await;
623
624 cx.assert_binding_matches(["v", "w"], "The quick ˇbrown")
625 .await;
626
627 cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown")
628 .await;
629 cx.assert_binding_matches(
630 ["v", "w", "j", "x"],
631 indoc! {"
632 The ˇquick brown
633 fox jumps over
634 the lazy dog"},
635 )
636 .await;
637 // Test pasting code copied on delete
638 cx.simulate_shared_keystrokes(["j", "p"]).await;
639 cx.assert_state_matches().await;
640
641 let mut cx = cx.binding(["v", "w", "j", "x"]);
642 cx.assert_all(indoc! {"
643 The ˇquick brown
644 fox jumps over
645 the ˇlazy dog"})
646 .await;
647 let mut cx = cx.binding(["v", "b", "k", "x"]);
648 cx.assert_all(indoc! {"
649 The ˇquick brown
650 fox jumps ˇover
651 the ˇlazy dog"})
652 .await;
653 }
654
655 #[gpui::test]
656 async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) {
657 let mut cx = NeovimBackedTestContext::new(cx).await;
658
659 cx.set_shared_state(indoc! {"
660 The quˇick brown
661 fox jumps over
662 the lazy dog"})
663 .await;
664 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
665 cx.assert_state_matches().await;
666
667 // Test pasting code copied on delete
668 cx.simulate_shared_keystroke("p").await;
669 cx.assert_state_matches().await;
670
671 cx.set_shared_state(indoc! {"
672 The quick brown
673 fox jumps over
674 the laˇzy dog"})
675 .await;
676 cx.simulate_shared_keystrokes(["shift-v", "x"]).await;
677 cx.assert_state_matches().await;
678 cx.assert_shared_clipboard("the lazy dog\n").await;
679
680 for marked_text in cx.each_marked_position(indoc! {"
681 The quˇick brown
682 fox jumps over
683 the lazy dog"})
684 {
685 cx.set_shared_state(&marked_text).await;
686 cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await;
687 cx.assert_state_matches().await;
688 // Test pasting code copied on delete
689 cx.simulate_shared_keystroke("p").await;
690 cx.assert_state_matches().await;
691 }
692
693 cx.set_shared_state(indoc! {"
694 The ˇlong line
695 should not
696 crash
697 "})
698 .await;
699 cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await;
700 cx.assert_state_matches().await;
701 }
702
703 #[gpui::test]
704 async fn test_visual_yank(cx: &mut gpui::TestAppContext) {
705 let mut cx = NeovimBackedTestContext::new(cx).await;
706
707 cx.set_shared_state("The quick ˇbrown").await;
708 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
709 cx.assert_shared_state("The quick ˇbrown").await;
710 cx.assert_shared_clipboard("brown").await;
711
712 cx.set_shared_state(indoc! {"
713 The ˇquick brown
714 fox jumps over
715 the lazy dog"})
716 .await;
717 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
718 cx.assert_shared_state(indoc! {"
719 The ˇquick brown
720 fox jumps over
721 the lazy dog"})
722 .await;
723 cx.assert_shared_clipboard(indoc! {"
724 quick brown
725 fox jumps o"})
726 .await;
727
728 cx.set_shared_state(indoc! {"
729 The quick brown
730 fox jumps over
731 the ˇlazy dog"})
732 .await;
733 cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await;
734 cx.assert_shared_state(indoc! {"
735 The quick brown
736 fox jumps over
737 the ˇlazy dog"})
738 .await;
739 cx.assert_shared_clipboard("lazy d").await;
740 cx.simulate_shared_keystrokes(["shift-v", "y"]).await;
741 cx.assert_shared_clipboard("the lazy dog\n").await;
742
743 let mut cx = cx.binding(["v", "b", "k", "y"]);
744 cx.set_shared_state(indoc! {"
745 The ˇquick brown
746 fox jumps over
747 the lazy dog"})
748 .await;
749 cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await;
750 cx.assert_shared_state(indoc! {"
751 ˇThe quick brown
752 fox jumps over
753 the lazy dog"})
754 .await;
755 assert_eq!(
756 cx.read_from_clipboard()
757 .map(|item| item.text().clone())
758 .unwrap(),
759 "The q"
760 );
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/zed/issues/6274
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 cx.simulate_shared_keystrokes(["i", "("]).await;
1010 cx.assert_shared_state("hello («in [parens] oˇ»)").await;
1011
1012 cx.set_shared_state("hello in a wˇord again.").await;
1013 cx.simulate_shared_keystrokes(["ctrl-v", "l", "i", "w"])
1014 .await;
1015 cx.assert_shared_state("hello in a w«ordˇ» again.").await;
1016 assert_eq!(cx.mode(), Mode::VisualBlock);
1017 cx.simulate_shared_keystrokes(["o", "a", "s"]).await;
1018 cx.assert_shared_state("«ˇhello in a word» again.").await;
1019 }
1020
1021 #[gpui::test]
1022 async fn test_mode_across_command(cx: &mut gpui::TestAppContext) {
1023 let mut cx = VimTestContext::new(cx, true).await;
1024
1025 cx.set_state("aˇbc", Mode::Normal);
1026 cx.simulate_keystrokes(["ctrl-v"]);
1027 assert_eq!(cx.mode(), Mode::VisualBlock);
1028 cx.simulate_keystrokes(["cmd-shift-p", "escape"]);
1029 assert_eq!(cx.mode(), Mode::VisualBlock);
1030 }
1031}