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