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