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