1use editor::{
2 DisplayPoint, MultiBufferOffset, RowExt, SelectionEffects, display_map::ToDisplayPoint,
3 movement,
4};
5use gpui::{Action, Context, Window};
6use language::{Bias, SelectionGoal};
7use schemars::JsonSchema;
8use serde::Deserialize;
9use settings::Settings;
10use std::cmp;
11use vim_mode_setting::HelixModeSetting;
12
13use crate::{
14 Vim,
15 motion::{Motion, MotionKind},
16 object::Object,
17 state::{Mode, Register},
18};
19
20/// Pastes text from the specified register at the cursor position.
21#[derive(Clone, Default, Deserialize, JsonSchema, PartialEq, Action)]
22#[action(namespace = vim)]
23#[serde(deny_unknown_fields)]
24pub struct Paste {
25 #[serde(default)]
26 before: bool,
27 #[serde(default)]
28 preserve_clipboard: bool,
29}
30
31impl Vim {
32 pub fn paste(&mut self, action: &Paste, window: &mut Window, cx: &mut Context<Self>) {
33 self.record_current_action(cx);
34 self.store_visual_marks(window, cx);
35 let count = Vim::take_count(cx).unwrap_or(1);
36 Vim::take_forced_motion(cx);
37
38 self.update_editor(cx, |vim, editor, cx| {
39 let text_layout_details = editor.text_layout_details(window, cx);
40 editor.transact(window, cx, |editor, window, cx| {
41 editor.set_clip_at_line_ends(false, cx);
42
43 let selected_register = vim.selected_register.take();
44
45 let Some(Register {
46 text,
47 clipboard_selections,
48 }) = Vim::update_globals(cx, |globals, cx| {
49 globals.read_register(selected_register, Some(editor), cx)
50 })
51 .filter(|reg| !reg.text.is_empty())
52 else {
53 return;
54 };
55 let clipboard_selections = clipboard_selections
56 .filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine);
57
58 if !action.preserve_clipboard && vim.mode.is_visual() {
59 vim.copy_selections_content(editor, MotionKind::for_mode(vim.mode), window, cx);
60 }
61
62 let display_map = editor.display_snapshot(cx);
63 let current_selections = editor.selections.all_adjusted_display(&display_map);
64
65 // unlike zed, if you have a multi-cursor selection from vim block mode,
66 // pasting it will paste it on subsequent lines, even if you don't yet
67 // have a cursor there.
68 let mut selections_to_process = Vec::new();
69 let mut i = 0;
70 while i < current_selections.len() {
71 selections_to_process
72 .push((current_selections[i].start..current_selections[i].end, true));
73 i += 1;
74 }
75 if let Some(clipboard_selections) = clipboard_selections.as_ref() {
76 let left = current_selections
77 .iter()
78 .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
79 .min()
80 .unwrap();
81 let mut row = current_selections.last().unwrap().end.row().next_row();
82 while i < clipboard_selections.len() {
83 let cursor =
84 display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
85 selections_to_process.push((cursor..cursor, false));
86 i += 1;
87 row.0 += 1;
88 }
89 }
90
91 let first_selection_indent_column =
92 clipboard_selections.as_ref().and_then(|zed_selections| {
93 zed_selections
94 .first()
95 .map(|selection| selection.first_line_indent)
96 });
97 let before = action.before || vim.mode == Mode::VisualLine;
98
99 let mut edits = Vec::new();
100 let mut new_selections = Vec::new();
101 let mut original_indent_columns = Vec::new();
102 let mut start_offset = 0;
103
104 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
105 let (mut to_insert, original_indent_column) =
106 if let Some(clipboard_selections) = &clipboard_selections {
107 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
108 let end_offset = start_offset + clipboard_selection.len;
109 let text = text[start_offset..end_offset].to_string();
110 start_offset = if clipboard_selection.is_entire_line {
111 end_offset
112 } else {
113 end_offset + 1
114 };
115 (text, Some(clipboard_selection.first_line_indent))
116 } else {
117 ("".to_string(), first_selection_indent_column)
118 }
119 } else {
120 (text.to_string(), first_selection_indent_column)
121 };
122 let line_mode = to_insert.ends_with('\n');
123 let is_multiline = to_insert.contains('\n');
124
125 if line_mode && !before {
126 if selection.is_empty() {
127 to_insert =
128 "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
129 } else {
130 to_insert = "\n".to_owned() + &to_insert;
131 }
132 } else if line_mode && vim.mode == Mode::VisualLine {
133 to_insert.pop();
134 }
135
136 let display_range = if !selection.is_empty() {
137 // If vim is in VISUAL LINE mode and the column for the
138 // selection's end point is 0, that means that the
139 // cursor is at the newline character (\n) at the end of
140 // the line. In this situation we'll want to move one
141 // position to the left, ensuring we don't join the last
142 // line of the selection with the line directly below.
143 let end_point =
144 if vim.mode == Mode::VisualLine && selection.end.column() == 0 {
145 movement::left(&display_map, selection.end)
146 } else {
147 selection.end
148 };
149
150 selection.start..end_point
151 } else if line_mode {
152 let point = if before {
153 movement::line_beginning(&display_map, selection.start, false)
154 } else {
155 movement::line_end(&display_map, selection.start, false)
156 };
157 point..point
158 } else {
159 let point = if before {
160 selection.start
161 } else {
162 movement::saturating_right(&display_map, selection.start)
163 };
164 point..point
165 };
166
167 let point_range = display_range.start.to_point(&display_map)
168 ..display_range.end.to_point(&display_map);
169 let anchor = if is_multiline || vim.mode == Mode::VisualLine {
170 display_map
171 .buffer_snapshot()
172 .anchor_before(point_range.start)
173 } else {
174 display_map.buffer_snapshot().anchor_after(point_range.end)
175 };
176
177 if *preserve {
178 new_selections.push((anchor, line_mode, is_multiline));
179 }
180 edits.push((point_range, to_insert.repeat(count)));
181 original_indent_columns.push(original_indent_column);
182 }
183
184 let cursor_offset = editor
185 .selections
186 .last::<MultiBufferOffset>(&display_map)
187 .head();
188 if editor
189 .buffer()
190 .read(cx)
191 .snapshot(cx)
192 .language_settings_at(cursor_offset, cx)
193 .auto_indent_on_paste
194 {
195 editor.edit_with_block_indent(edits, original_indent_columns, cx);
196 } else {
197 editor.edit(edits, cx);
198 }
199
200 // in line_mode vim will insert the new text on the next (or previous if before) line
201 // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
202 // otherwise vim will insert the next text at (or before) the current cursor position,
203 // the cursor will go to the last (or first, if is_multiline) inserted character.
204 editor.change_selections(Default::default(), window, cx, |s| {
205 s.replace_cursors_with(|map| {
206 let mut cursors = Vec::new();
207 for (anchor, line_mode, is_multiline) in &new_selections {
208 let mut cursor = anchor.to_display_point(map);
209 if *line_mode {
210 if !before {
211 cursor = movement::down(
212 map,
213 cursor,
214 SelectionGoal::None,
215 false,
216 &text_layout_details,
217 )
218 .0;
219 }
220 cursor = movement::indented_line_beginning(map, cursor, true, true);
221 } else if !is_multiline && !vim.temp_mode {
222 cursor = movement::saturating_left(map, cursor)
223 }
224 cursors.push(cursor);
225 if vim.mode == Mode::VisualBlock {
226 break;
227 }
228 }
229
230 cursors
231 });
232 })
233 });
234 });
235
236 if HelixModeSetting::get_global(cx).0 {
237 self.switch_mode(Mode::HelixNormal, true, window, cx);
238 } else {
239 self.switch_mode(Mode::Normal, true, window, cx);
240 }
241 }
242
243 pub fn replace_with_register_object(
244 &mut self,
245 object: Object,
246 around: bool,
247 window: &mut Window,
248 cx: &mut Context<Self>,
249 ) {
250 self.stop_recording(cx);
251 let selected_register = self.selected_register.take();
252 self.update_editor(cx, |_, editor, cx| {
253 editor.transact(window, cx, |editor, window, cx| {
254 editor.set_clip_at_line_ends(false, cx);
255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
256 s.move_with(&mut |map, selection| {
257 object.expand_selection(map, selection, around, None);
258 });
259 });
260
261 let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| {
262 globals.read_register(selected_register, Some(editor), cx)
263 })
264 .filter(|reg| !reg.text.is_empty()) else {
265 return;
266 };
267 editor.insert(&text, window, cx);
268 editor.set_clip_at_line_ends(true, cx);
269 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
270 s.move_with(&mut |map, selection| {
271 selection.start = map.clip_point(selection.start, Bias::Left);
272 selection.end = selection.start
273 })
274 })
275 });
276 });
277 }
278
279 pub fn replace_with_register_motion(
280 &mut self,
281 motion: Motion,
282 times: Option<usize>,
283 forced_motion: bool,
284 window: &mut Window,
285 cx: &mut Context<Self>,
286 ) {
287 self.stop_recording(cx);
288 let selected_register = self.selected_register.take();
289 self.update_editor(cx, |_, editor, cx| {
290 let text_layout_details = editor.text_layout_details(window, cx);
291 editor.transact(window, cx, |editor, window, cx| {
292 editor.set_clip_at_line_ends(false, cx);
293 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
294 s.move_with(&mut |map, selection| {
295 motion.expand_selection(
296 map,
297 selection,
298 times,
299 &text_layout_details,
300 forced_motion,
301 );
302 });
303 });
304
305 let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| {
306 globals.read_register(selected_register, Some(editor), cx)
307 })
308 .filter(|reg| !reg.text.is_empty()) else {
309 return;
310 };
311 editor.insert(&text, window, cx);
312 editor.set_clip_at_line_ends(true, cx);
313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
314 s.move_with(&mut |map, selection| {
315 selection.start = map.clip_point(selection.start, Bias::Left);
316 selection.end = selection.start
317 })
318 })
319 });
320 });
321 }
322}
323
324#[cfg(test)]
325mod test {
326 use crate::{
327 state::{Mode, Register},
328 test::{NeovimBackedTestContext, VimTestContext},
329 };
330 use gpui::ClipboardItem;
331 use indoc::indoc;
332 use language::{LanguageName, language_settings::LanguageSettingsContent};
333 use settings::{SettingsStore, UseSystemClipboard};
334
335 #[gpui::test]
336 async fn test_paste(cx: &mut gpui::TestAppContext) {
337 let mut cx = NeovimBackedTestContext::new(cx).await;
338
339 // single line
340 cx.set_shared_state(indoc! {"
341 The quick brown
342 fox ˇjumps over
343 the lazy dog"})
344 .await;
345 cx.simulate_shared_keystrokes("v w y").await;
346 cx.shared_clipboard().await.assert_eq("jumps o");
347 cx.set_shared_state(indoc! {"
348 The quick brown
349 fox jumps oveˇr
350 the lazy dog"})
351 .await;
352 cx.simulate_shared_keystrokes("p").await;
353 cx.shared_state().await.assert_eq(indoc! {"
354 The quick brown
355 fox jumps overjumps ˇo
356 the lazy dog"});
357
358 cx.set_shared_state(indoc! {"
359 The quick brown
360 fox jumps oveˇr
361 the lazy dog"})
362 .await;
363 cx.simulate_shared_keystrokes("shift-p").await;
364 cx.shared_state().await.assert_eq(indoc! {"
365 The quick brown
366 fox jumps ovejumps ˇor
367 the lazy dog"});
368
369 // line mode
370 cx.set_shared_state(indoc! {"
371 The quick brown
372 fox juˇmps over
373 the lazy dog"})
374 .await;
375 cx.simulate_shared_keystrokes("d d").await;
376 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
377 cx.shared_state().await.assert_eq(indoc! {"
378 The quick brown
379 the laˇzy dog"});
380 cx.simulate_shared_keystrokes("p").await;
381 cx.shared_state().await.assert_eq(indoc! {"
382 The quick brown
383 the lazy dog
384 ˇfox jumps over"});
385 cx.simulate_shared_keystrokes("k shift-p").await;
386 cx.shared_state().await.assert_eq(indoc! {"
387 The quick brown
388 ˇfox jumps over
389 the lazy dog
390 fox jumps over"});
391
392 // multiline, cursor to first character of pasted text.
393 cx.set_shared_state(indoc! {"
394 The quick brown
395 fox jumps ˇover
396 the lazy dog"})
397 .await;
398 cx.simulate_shared_keystrokes("v j y").await;
399 cx.shared_clipboard().await.assert_eq("over\nthe lazy do");
400
401 cx.simulate_shared_keystrokes("p").await;
402 cx.shared_state().await.assert_eq(indoc! {"
403 The quick brown
404 fox jumps oˇover
405 the lazy dover
406 the lazy dog"});
407 cx.simulate_shared_keystrokes("u shift-p").await;
408 cx.shared_state().await.assert_eq(indoc! {"
409 The quick brown
410 fox jumps ˇover
411 the lazy doover
412 the lazy dog"});
413 }
414
415 #[gpui::test]
416 async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
417 let mut cx = VimTestContext::new(cx, true).await;
418
419 cx.update_global(|store: &mut SettingsStore, cx| {
420 store.update_user_settings(cx, |s| {
421 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
422 });
423 });
424
425 cx.set_state(
426 indoc! {"
427 The quick brown
428 fox jˇumps over
429 the lazy dog"},
430 Mode::Normal,
431 );
432 cx.simulate_keystrokes("v i w y");
433 cx.assert_state(
434 indoc! {"
435 The quick brown
436 fox ˇjumps over
437 the lazy dog"},
438 Mode::Normal,
439 );
440 cx.simulate_keystrokes("p");
441 cx.assert_state(
442 indoc! {"
443 The quick brown
444 fox jjumpˇsumps over
445 the lazy dog"},
446 Mode::Normal,
447 );
448 assert_eq!(cx.read_from_clipboard(), None);
449 }
450
451 #[gpui::test]
452 async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
453 let mut cx = VimTestContext::new(cx, true).await;
454
455 cx.update_global(|store: &mut SettingsStore, cx| {
456 store.update_user_settings(cx, |s| {
457 s.vim.get_or_insert_default().use_system_clipboard =
458 Some(UseSystemClipboard::OnYank)
459 });
460 });
461
462 // copy in visual mode
463 cx.set_state(
464 indoc! {"
465 The quick brown
466 fox jˇumps over
467 the lazy dog"},
468 Mode::Normal,
469 );
470 cx.simulate_keystrokes("v i w y");
471 cx.assert_state(
472 indoc! {"
473 The quick brown
474 fox ˇjumps over
475 the lazy dog"},
476 Mode::Normal,
477 );
478 cx.simulate_keystrokes("p");
479 cx.assert_state(
480 indoc! {"
481 The quick brown
482 fox jjumpˇsumps over
483 the lazy dog"},
484 Mode::Normal,
485 );
486 assert_eq!(
487 cx.read_from_clipboard().map(|item| item.text().unwrap()),
488 Some("jumps".into())
489 );
490 cx.simulate_keystrokes("d d p");
491 cx.assert_state(
492 indoc! {"
493 The quick brown
494 the lazy dog
495 ˇfox jjumpsumps over"},
496 Mode::Normal,
497 );
498 assert_eq!(
499 cx.read_from_clipboard().map(|item| item.text().unwrap()),
500 Some("jumps".into())
501 );
502 cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string()));
503 cx.simulate_keystrokes("shift-p");
504 cx.assert_state(
505 indoc! {"
506 The quick brown
507 the lazy dog
508 test-copˇyfox jjumpsumps over"},
509 Mode::Normal,
510 );
511 }
512
513 #[gpui::test]
514 async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
515 let mut cx = NeovimBackedTestContext::new(cx).await;
516
517 // copy in visual mode
518 cx.set_shared_state(indoc! {"
519 The quick brown
520 fox jˇumps over
521 the lazy dog"})
522 .await;
523 cx.simulate_shared_keystrokes("v i w y").await;
524 cx.shared_state().await.assert_eq(indoc! {"
525 The quick brown
526 fox ˇjumps over
527 the lazy dog"});
528 // paste in visual mode
529 cx.simulate_shared_keystrokes("w v i w p").await;
530 cx.shared_state().await.assert_eq(indoc! {"
531 The quick brown
532 fox jumps jumpˇs
533 the lazy dog"});
534 cx.shared_clipboard().await.assert_eq("over");
535 // paste in visual line mode
536 cx.simulate_shared_keystrokes("up shift-v shift-p").await;
537 cx.shared_state().await.assert_eq(indoc! {"
538 ˇover
539 fox jumps jumps
540 the lazy dog"});
541 cx.shared_clipboard().await.assert_eq("over");
542 // paste in visual block mode
543 cx.simulate_shared_keystrokes("ctrl-v down down p").await;
544 cx.shared_state().await.assert_eq(indoc! {"
545 oveˇrver
546 overox jumps jumps
547 overhe lazy dog"});
548
549 // copy in visual line mode
550 cx.set_shared_state(indoc! {"
551 The quick brown
552 fox juˇmps over
553 the lazy dog"})
554 .await;
555 cx.simulate_shared_keystrokes("shift-v d").await;
556 cx.shared_state().await.assert_eq(indoc! {"
557 The quick brown
558 the laˇzy dog"});
559 // paste in visual mode
560 cx.simulate_shared_keystrokes("v i w p").await;
561 cx.shared_state().await.assert_eq(indoc! {"
562 The quick brown
563 the•
564 ˇfox jumps over
565 dog"});
566 cx.shared_clipboard().await.assert_eq("lazy");
567 cx.set_shared_state(indoc! {"
568 The quick brown
569 fox juˇmps over
570 the lazy dog"})
571 .await;
572 cx.simulate_shared_keystrokes("shift-v d").await;
573 cx.shared_state().await.assert_eq(indoc! {"
574 The quick brown
575 the laˇzy dog"});
576 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
577 // paste in visual line mode
578 cx.simulate_shared_keystrokes("k shift-v p").await;
579 cx.shared_state().await.assert_eq(indoc! {"
580 ˇfox jumps over
581 the lazy dog"});
582 cx.shared_clipboard().await.assert_eq("The quick brown\n");
583
584 // Copy line and paste in visual mode, with cursor on newline character.
585 cx.set_shared_state(indoc! {"
586 ˇThe quick brown
587 fox jumps over
588 the lazy dog"})
589 .await;
590 cx.simulate_shared_keystrokes("y y shift-v j $ p").await;
591 cx.shared_state().await.assert_eq(indoc! {"
592 ˇThe quick brown
593 the lazy dog"});
594 }
595
596 #[gpui::test]
597 async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
598 let mut cx = NeovimBackedTestContext::new(cx).await;
599 // copy in visual block mode
600 cx.set_shared_state(indoc! {"
601 The ˇquick brown
602 fox jumps over
603 the lazy dog"})
604 .await;
605 cx.simulate_shared_keystrokes("ctrl-v 2 j y").await;
606 cx.shared_clipboard().await.assert_eq("q\nj\nl");
607 cx.simulate_shared_keystrokes("p").await;
608 cx.shared_state().await.assert_eq(indoc! {"
609 The qˇquick brown
610 fox jjumps over
611 the llazy dog"});
612 cx.simulate_shared_keystrokes("v i w shift-p").await;
613 cx.shared_state().await.assert_eq(indoc! {"
614 The ˇq brown
615 fox jjjumps over
616 the lllazy dog"});
617 cx.simulate_shared_keystrokes("v i w shift-p").await;
618
619 cx.set_shared_state(indoc! {"
620 The ˇquick brown
621 fox jumps over
622 the lazy dog"})
623 .await;
624 cx.simulate_shared_keystrokes("ctrl-v j y").await;
625 cx.shared_clipboard().await.assert_eq("q\nj");
626 cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await;
627 cx.shared_state().await.assert_eq(indoc! {"
628 The qˇqick brown
629 fox jjmps over
630 the lzy dog"});
631
632 cx.simulate_shared_keystrokes("shift-v p").await;
633 cx.shared_state().await.assert_eq(indoc! {"
634 ˇq
635 j
636 fox jjmps over
637 the lzy dog"});
638 }
639
640 #[gpui::test]
641 async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
642 let mut cx = VimTestContext::new_typescript(cx).await;
643
644 cx.set_state(
645 indoc! {"
646 class A {ˇ
647 }
648 "},
649 Mode::Normal,
650 );
651 cx.simulate_keystrokes("o a ( ) { escape");
652 cx.assert_state(
653 indoc! {"
654 class A {
655 a()ˇ{}
656 }
657 "},
658 Mode::Normal,
659 );
660 // cursor goes to the first non-blank character in the line;
661 cx.simulate_keystrokes("y y p");
662 cx.assert_state(
663 indoc! {"
664 class A {
665 a(){}
666 ˇa(){}
667 }
668 "},
669 Mode::Normal,
670 );
671 // indentation is preserved when pasting
672 cx.simulate_keystrokes("u shift-v up y shift-p");
673 cx.assert_state(
674 indoc! {"
675 ˇclass A {
676 a(){}
677 class A {
678 a(){}
679 }
680 "},
681 Mode::Normal,
682 );
683 }
684
685 #[gpui::test]
686 async fn test_paste_auto_indent(cx: &mut gpui::TestAppContext) {
687 let mut cx = VimTestContext::new(cx, true).await;
688
689 cx.set_state(
690 indoc! {"
691 mod some_module {
692 ˇfn main() {
693 }
694 }
695 "},
696 Mode::Normal,
697 );
698 // default auto indentation
699 cx.simulate_keystrokes("y y p");
700 cx.assert_state(
701 indoc! {"
702 mod some_module {
703 fn main() {
704 ˇfn main() {
705 }
706 }
707 "},
708 Mode::Normal,
709 );
710 // back to previous state
711 cx.simulate_keystrokes("u u");
712 cx.assert_state(
713 indoc! {"
714 mod some_module {
715 ˇfn main() {
716 }
717 }
718 "},
719 Mode::Normal,
720 );
721 cx.update_global(|store: &mut SettingsStore, cx| {
722 store.update_user_settings(cx, |settings| {
723 settings.project.all_languages.languages.0.insert(
724 LanguageName::new_static("Rust").0.to_string(),
725 LanguageSettingsContent {
726 auto_indent_on_paste: Some(false),
727 ..Default::default()
728 },
729 );
730 });
731 });
732 // auto indentation turned off
733 cx.simulate_keystrokes("y y p");
734 cx.assert_state(
735 indoc! {"
736 mod some_module {
737 fn main() {
738 ˇfn main() {
739 }
740 }
741 "},
742 Mode::Normal,
743 );
744 }
745
746 #[gpui::test]
747 async fn test_paste_count(cx: &mut gpui::TestAppContext) {
748 let mut cx = NeovimBackedTestContext::new(cx).await;
749
750 cx.set_shared_state(indoc! {"
751 onˇe
752 two
753 three
754 "})
755 .await;
756 cx.simulate_shared_keystrokes("y y 3 p").await;
757 cx.shared_state().await.assert_eq(indoc! {"
758 one
759 ˇone
760 one
761 one
762 two
763 three
764 "});
765
766 cx.set_shared_state(indoc! {"
767 one
768 ˇtwo
769 three
770 "})
771 .await;
772 cx.simulate_shared_keystrokes("y $ $ 3 p").await;
773 cx.shared_state().await.assert_eq(indoc! {"
774 one
775 twotwotwotwˇo
776 three
777 "});
778 }
779
780 #[gpui::test]
781 async fn test_paste_system_clipboard_never(cx: &mut gpui::TestAppContext) {
782 let mut cx = VimTestContext::new(cx, true).await;
783
784 cx.update_global(|store: &mut SettingsStore, cx| {
785 store.update_user_settings(cx, |s| {
786 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
787 });
788 });
789
790 cx.set_state(
791 indoc! {"
792 ˇThe quick brown
793 fox jumps over
794 the lazy dog"},
795 Mode::Normal,
796 );
797
798 cx.write_to_clipboard(ClipboardItem::new_string("something else".to_string()));
799
800 cx.simulate_keystrokes("d d");
801 cx.assert_state(
802 indoc! {"
803 ˇfox jumps over
804 the lazy dog"},
805 Mode::Normal,
806 );
807
808 cx.simulate_keystrokes("shift-v p");
809 cx.assert_state(
810 indoc! {"
811 ˇThe quick brown
812 the lazy dog"},
813 Mode::Normal,
814 );
815
816 cx.simulate_keystrokes("shift-v");
817 cx.dispatch_action(editor::actions::Paste);
818 cx.assert_state(
819 indoc! {"
820 ˇsomething else
821 the lazy dog"},
822 Mode::Normal,
823 );
824 }
825
826 #[gpui::test]
827 async fn test_numbered_registers(cx: &mut gpui::TestAppContext) {
828 let mut cx = NeovimBackedTestContext::new(cx).await;
829
830 cx.update_global(|store: &mut SettingsStore, cx| {
831 store.update_user_settings(cx, |s| {
832 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
833 });
834 });
835
836 cx.set_shared_state(indoc! {"
837 The quick brown
838 fox jˇumps over
839 the lazy dog"})
840 .await;
841 cx.simulate_shared_keystrokes("y y \" 0 p").await;
842 cx.shared_register('0').await.assert_eq("fox jumps over\n");
843 cx.shared_register('"').await.assert_eq("fox jumps over\n");
844
845 cx.shared_state().await.assert_eq(indoc! {"
846 The quick brown
847 fox jumps over
848 ˇfox jumps over
849 the lazy dog"});
850 cx.simulate_shared_keystrokes("k k d d").await;
851 cx.shared_register('0').await.assert_eq("fox jumps over\n");
852 cx.shared_register('1').await.assert_eq("The quick brown\n");
853 cx.shared_register('"').await.assert_eq("The quick brown\n");
854
855 cx.simulate_shared_keystrokes("d d shift-g d d").await;
856 cx.shared_register('0').await.assert_eq("fox jumps over\n");
857 cx.shared_register('3').await.assert_eq("The quick brown\n");
858 cx.shared_register('2').await.assert_eq("fox jumps over\n");
859 cx.shared_register('1').await.assert_eq("the lazy dog\n");
860
861 cx.shared_state().await.assert_eq(indoc! {"
862 ˇfox jumps over"});
863
864 cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await;
865 cx.set_shared_state(indoc! {"
866 The quick brown
867 fox jumps over
868 ˇthe lazy dog"})
869 .await;
870 }
871
872 #[gpui::test]
873 async fn test_named_registers(cx: &mut gpui::TestAppContext) {
874 let mut cx = NeovimBackedTestContext::new(cx).await;
875
876 cx.update_global(|store: &mut SettingsStore, cx| {
877 store.update_user_settings(cx, |s| {
878 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
879 });
880 });
881
882 cx.set_shared_state(indoc! {"
883 The quick brown
884 fox jˇumps over
885 the lazy dog"})
886 .await;
887 cx.simulate_shared_keystrokes("\" a d a w").await;
888 cx.shared_register('a').await.assert_eq("jumps ");
889 cx.simulate_shared_keystrokes("\" shift-a d i w").await;
890 cx.shared_register('a').await.assert_eq("jumps over");
891 cx.shared_register('"').await.assert_eq("jumps over");
892 cx.simulate_shared_keystrokes("\" a p").await;
893 cx.shared_state().await.assert_eq(indoc! {"
894 The quick brown
895 fox jumps oveˇr
896 the lazy dog"});
897 cx.simulate_shared_keystrokes("\" a d a w").await;
898 cx.shared_register('a').await.assert_eq(" over");
899 }
900
901 #[gpui::test]
902 async fn test_special_registers(cx: &mut gpui::TestAppContext) {
903 let mut cx = NeovimBackedTestContext::new(cx).await;
904
905 cx.update_global(|store: &mut SettingsStore, cx| {
906 store.update_user_settings(cx, |s| {
907 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
908 });
909 });
910
911 cx.set_shared_state(indoc! {"
912 The quick brown
913 fox jˇumps over
914 the lazy dog"})
915 .await;
916 cx.simulate_shared_keystrokes("d i w").await;
917 cx.shared_register('-').await.assert_eq("jumps");
918 cx.simulate_shared_keystrokes("\" _ d d").await;
919 cx.shared_register('_').await.assert_eq("");
920
921 cx.simulate_shared_keystrokes("shift-v \" _ y w").await;
922 cx.shared_register('"').await.assert_eq("jumps");
923
924 cx.shared_state().await.assert_eq(indoc! {"
925 The quick brown
926 the ˇlazy dog"});
927 cx.simulate_shared_keystrokes("\" \" d ^").await;
928 cx.shared_register('0').await.assert_eq("the ");
929 cx.shared_register('"').await.assert_eq("the ");
930
931 cx.simulate_shared_keystrokes("^ \" + d $").await;
932 cx.shared_clipboard().await.assert_eq("lazy dog");
933 cx.shared_register('"').await.assert_eq("lazy dog");
934
935 cx.simulate_shared_keystrokes("/ d o g enter").await;
936 cx.shared_register('/').await.assert_eq("dog");
937 cx.simulate_shared_keystrokes("\" / shift-p").await;
938 cx.shared_state().await.assert_eq(indoc! {"
939 The quick brown
940 doˇg"});
941
942 // not testing nvim as it doesn't have a filename
943 cx.simulate_keystrokes("\" % p");
944 #[cfg(not(target_os = "windows"))]
945 cx.assert_state(
946 indoc! {"
947 The quick brown
948 dogdir/file.rˇs"},
949 Mode::Normal,
950 );
951 #[cfg(target_os = "windows")]
952 cx.assert_state(
953 indoc! {"
954 The quick brown
955 dogdir\\file.rˇs"},
956 Mode::Normal,
957 );
958 }
959
960 #[gpui::test]
961 async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) {
962 let mut cx = VimTestContext::new(cx, true).await;
963
964 cx.update_global(|store: &mut SettingsStore, cx| {
965 store.update_user_settings(cx, |s| {
966 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
967 });
968 });
969
970 cx.set_state(
971 indoc! {"
972 ˇfish one
973 fish two
974 fish red
975 fish blue
976 "},
977 Mode::Normal,
978 );
979 cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p");
980 cx.assert_state(
981 indoc! {"
982 onˇefish•
983 twˇofish•
984 reˇdfish•
985 bluˇefish•
986 "},
987 Mode::Normal,
988 );
989 }
990
991 #[gpui::test]
992 async fn test_replace_with_register(cx: &mut gpui::TestAppContext) {
993 let mut cx = VimTestContext::new(cx, true).await;
994
995 cx.set_state(
996 indoc! {"
997 ˇfish one
998 two three
999 "},
1000 Mode::Normal,
1001 );
1002 cx.simulate_keystrokes("y i w");
1003 cx.simulate_keystrokes("w");
1004 cx.simulate_keystrokes("g shift-r i w");
1005 cx.assert_state(
1006 indoc! {"
1007 fish fisˇh
1008 two three
1009 "},
1010 Mode::Normal,
1011 );
1012 cx.simulate_keystrokes("j b g shift-r e");
1013 cx.assert_state(
1014 indoc! {"
1015 fish fish
1016 two fisˇh
1017 "},
1018 Mode::Normal,
1019 );
1020 let clipboard: Register = cx.read_from_clipboard().unwrap().into();
1021 assert_eq!(clipboard.text, "fish");
1022
1023 cx.set_state(
1024 indoc! {"
1025 ˇfish one
1026 two three
1027 "},
1028 Mode::Normal,
1029 );
1030 cx.simulate_keystrokes("y i w");
1031 cx.simulate_keystrokes("w");
1032 cx.simulate_keystrokes("v i w g shift-r");
1033 cx.assert_state(
1034 indoc! {"
1035 fish fisˇh
1036 two three
1037 "},
1038 Mode::Normal,
1039 );
1040 cx.simulate_keystrokes("g shift-r r");
1041 cx.assert_state(
1042 indoc! {"
1043 fisˇh
1044 two three
1045 "},
1046 Mode::Normal,
1047 );
1048 cx.simulate_keystrokes("j w g shift-r $");
1049 cx.assert_state(
1050 indoc! {"
1051 fish
1052 two fisˇh
1053 "},
1054 Mode::Normal,
1055 );
1056 let clipboard: Register = cx.read_from_clipboard().unwrap().into();
1057 assert_eq!(clipboard.text, "fish");
1058 }
1059
1060 #[gpui::test]
1061 async fn test_replace_with_register_dot_repeat(cx: &mut gpui::TestAppContext) {
1062 let mut cx = VimTestContext::new(cx, true).await;
1063
1064 cx.set_state(
1065 indoc! {"
1066 ˇfish one
1067 two three
1068 "},
1069 Mode::Normal,
1070 );
1071 cx.simulate_keystrokes("y i w");
1072 cx.simulate_keystrokes("w");
1073 cx.simulate_keystrokes("g shift-r i w");
1074 cx.assert_state(
1075 indoc! {"
1076 fish fisˇh
1077 two three
1078 "},
1079 Mode::Normal,
1080 );
1081 cx.simulate_keystrokes("j .");
1082 cx.assert_state(
1083 indoc! {"
1084 fish fish
1085 two fisˇh
1086 "},
1087 Mode::Normal,
1088 );
1089 }
1090
1091 #[gpui::test]
1092 async fn test_paste_entire_line_from_editor_copy(cx: &mut gpui::TestAppContext) {
1093 let mut cx = VimTestContext::new(cx, true).await;
1094
1095 cx.set_state(
1096 indoc! {"
1097 ˇline one
1098 line two
1099 line three"},
1100 Mode::Normal,
1101 );
1102
1103 // Simulate what the editor's do_copy produces for two entire-line selections:
1104 // entire-line selections are NOT separated by an extra newline in the clipboard text.
1105 let clipboard_text = "line one\nline two\n".to_string();
1106 let clipboard_selections = vec![
1107 editor::ClipboardSelection {
1108 len: "line one\n".len(),
1109 is_entire_line: true,
1110 first_line_indent: 0,
1111 file_path: None,
1112 line_range: None,
1113 },
1114 editor::ClipboardSelection {
1115 len: "line two\n".len(),
1116 is_entire_line: true,
1117 first_line_indent: 0,
1118 file_path: None,
1119 line_range: None,
1120 },
1121 ];
1122 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1123 clipboard_text,
1124 clipboard_selections,
1125 ));
1126
1127 cx.simulate_keystrokes("p");
1128 cx.assert_state(
1129 indoc! {"
1130 line one
1131 ˇline one
1132 line two
1133 line two
1134 line three"},
1135 Mode::Normal,
1136 );
1137 }
1138}