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