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