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