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