1use std::cmp;
2
3use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, DisplayPoint, RowExt};
4use gpui::{impl_actions, ViewContext};
5use language::{Bias, SelectionGoal};
6use serde::Deserialize;
7use workspace::Workspace;
8
9use crate::{
10 normal::yank::copy_selections_content,
11 state::{Mode, Register},
12 Vim,
13};
14
15#[derive(Clone, Deserialize, PartialEq)]
16#[serde(rename_all = "camelCase")]
17struct Paste {
18 #[serde(default)]
19 before: bool,
20 #[serde(default)]
21 preserve_clipboard: bool,
22}
23
24impl_actions!(vim, [Paste]);
25
26pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
27 workspace.register_action(paste);
28}
29
30fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
31 Vim::update(cx, |vim, cx| {
32 vim.record_current_action(cx);
33 let count = vim.take_count(cx).unwrap_or(1);
34 vim.update_active_editor(cx, |vim, editor, cx| {
35 let text_layout_details = editor.text_layout_details(cx);
36 editor.transact(cx, |editor, cx| {
37 editor.set_clip_at_line_ends(false, cx);
38
39 let selected_register = vim.update_state(|state| state.selected_register.take());
40
41 let Some(Register {
42 text,
43 clipboard_selections,
44 }) = vim
45 .read_register(selected_register, Some(editor), cx)
46 .filter(|reg| !reg.text.is_empty())
47 else {
48 return;
49 };
50 let clipboard_selections = clipboard_selections
51 .filter(|sel| sel.len() > 1 && vim.state().mode != Mode::VisualLine);
52
53 if !action.preserve_clipboard && vim.state().mode.is_visual() {
54 copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx);
55 }
56
57 let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
58
59 // unlike zed, if you have a multi-cursor selection from vim block mode,
60 // pasting it will paste it on subsequent lines, even if you don't yet
61 // have a cursor there.
62 let mut selections_to_process = Vec::new();
63 let mut i = 0;
64 while i < current_selections.len() {
65 selections_to_process
66 .push((current_selections[i].start..current_selections[i].end, true));
67 i += 1;
68 }
69 if let Some(clipboard_selections) = clipboard_selections.as_ref() {
70 let left = current_selections
71 .iter()
72 .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
73 .min()
74 .unwrap();
75 let mut row = current_selections.last().unwrap().end.row().next_row();
76 while i < clipboard_selections.len() {
77 let cursor =
78 display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
79 selections_to_process.push((cursor..cursor, false));
80 i += 1;
81 row.0 += 1;
82 }
83 }
84
85 let first_selection_indent_column =
86 clipboard_selections.as_ref().and_then(|zed_selections| {
87 zed_selections
88 .first()
89 .map(|selection| selection.first_line_indent)
90 });
91 let before = action.before || vim.state().mode == Mode::VisualLine;
92
93 let mut edits = Vec::new();
94 let mut new_selections = Vec::new();
95 let mut original_indent_columns = Vec::new();
96 let mut start_offset = 0;
97
98 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
99 let (mut to_insert, original_indent_column) =
100 if let Some(clipboard_selections) = &clipboard_selections {
101 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
102 let end_offset = start_offset + clipboard_selection.len;
103 let text = text[start_offset..end_offset].to_string();
104 start_offset = end_offset + 1;
105 (text, Some(clipboard_selection.first_line_indent))
106 } else {
107 ("".to_string(), first_selection_indent_column)
108 }
109 } else {
110 (text.to_string(), first_selection_indent_column)
111 };
112 let line_mode = to_insert.ends_with('\n');
113 let is_multiline = to_insert.contains('\n');
114
115 if line_mode && !before {
116 if selection.is_empty() {
117 to_insert =
118 "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
119 } else {
120 to_insert = "\n".to_owned() + &to_insert;
121 }
122 } else if !line_mode && vim.state().mode == Mode::VisualLine {
123 to_insert = to_insert + "\n";
124 }
125
126 let display_range = if !selection.is_empty() {
127 selection.start..selection.end
128 } else if line_mode {
129 let point = if before {
130 movement::line_beginning(&display_map, selection.start, false)
131 } else {
132 movement::line_end(&display_map, selection.start, false)
133 };
134 point..point
135 } else {
136 let point = if before {
137 selection.start
138 } else {
139 movement::saturating_right(&display_map, selection.start)
140 };
141 point..point
142 };
143
144 let point_range = display_range.start.to_point(&display_map)
145 ..display_range.end.to_point(&display_map);
146 let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
147 display_map.buffer_snapshot.anchor_before(point_range.start)
148 } else {
149 display_map.buffer_snapshot.anchor_after(point_range.end)
150 };
151
152 if *preserve {
153 new_selections.push((anchor, line_mode, is_multiline));
154 }
155 edits.push((point_range, to_insert.repeat(count)));
156 original_indent_columns.extend(original_indent_column);
157 }
158
159 editor.edit_with_block_indent(edits, original_indent_columns, cx);
160
161 // in line_mode vim will insert the new text on the next (or previous if before) line
162 // 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).
163 // otherwise vim will insert the next text at (or before) the current cursor position,
164 // the cursor will go to the last (or first, if is_multiline) inserted character.
165 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
166 s.replace_cursors_with(|map| {
167 let mut cursors = Vec::new();
168 for (anchor, line_mode, is_multiline) in &new_selections {
169 let mut cursor = anchor.to_display_point(map);
170 if *line_mode {
171 if !before {
172 cursor = movement::down(
173 map,
174 cursor,
175 SelectionGoal::None,
176 false,
177 &text_layout_details,
178 )
179 .0;
180 }
181 cursor = movement::indented_line_beginning(map, cursor, true);
182 } else if !is_multiline {
183 cursor = movement::saturating_left(map, cursor)
184 }
185 cursors.push(cursor);
186 if vim.state().mode == Mode::VisualBlock {
187 break;
188 }
189 }
190
191 cursors
192 });
193 })
194 });
195 });
196 vim.switch_mode(Mode::Normal, true, cx);
197 });
198}
199
200#[cfg(test)]
201mod test {
202 use crate::{
203 state::Mode,
204 test::{NeovimBackedTestContext, VimTestContext},
205 UseSystemClipboard, VimSettings,
206 };
207 use gpui::ClipboardItem;
208 use indoc::indoc;
209 use settings::SettingsStore;
210
211 #[gpui::test]
212 async fn test_paste(cx: &mut gpui::TestAppContext) {
213 let mut cx = NeovimBackedTestContext::new(cx).await;
214
215 // single line
216 cx.set_shared_state(indoc! {"
217 The quick brown
218 fox ˇjumps over
219 the lazy dog"})
220 .await;
221 cx.simulate_shared_keystrokes("v w y").await;
222 cx.shared_clipboard().await.assert_eq("jumps o");
223 cx.set_shared_state(indoc! {"
224 The quick brown
225 fox jumps oveˇr
226 the lazy dog"})
227 .await;
228 cx.simulate_shared_keystrokes("p").await;
229 cx.shared_state().await.assert_eq(indoc! {"
230 The quick brown
231 fox jumps overjumps ˇo
232 the lazy dog"});
233
234 cx.set_shared_state(indoc! {"
235 The quick brown
236 fox jumps oveˇr
237 the lazy dog"})
238 .await;
239 cx.simulate_shared_keystrokes("shift-p").await;
240 cx.shared_state().await.assert_eq(indoc! {"
241 The quick brown
242 fox jumps ovejumps ˇor
243 the lazy dog"});
244
245 // line mode
246 cx.set_shared_state(indoc! {"
247 The quick brown
248 fox juˇmps over
249 the lazy dog"})
250 .await;
251 cx.simulate_shared_keystrokes("d d").await;
252 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
253 cx.shared_state().await.assert_eq(indoc! {"
254 The quick brown
255 the laˇzy dog"});
256 cx.simulate_shared_keystrokes("p").await;
257 cx.shared_state().await.assert_eq(indoc! {"
258 The quick brown
259 the lazy dog
260 ˇfox jumps over"});
261 cx.simulate_shared_keystrokes("k shift-p").await;
262 cx.shared_state().await.assert_eq(indoc! {"
263 The quick brown
264 ˇfox jumps over
265 the lazy dog
266 fox jumps over"});
267
268 // multiline, cursor to first character of pasted text.
269 cx.set_shared_state(indoc! {"
270 The quick brown
271 fox jumps ˇover
272 the lazy dog"})
273 .await;
274 cx.simulate_shared_keystrokes("v j y").await;
275 cx.shared_clipboard().await.assert_eq("over\nthe lazy do");
276
277 cx.simulate_shared_keystrokes("p").await;
278 cx.shared_state().await.assert_eq(indoc! {"
279 The quick brown
280 fox jumps oˇover
281 the lazy dover
282 the lazy dog"});
283 cx.simulate_shared_keystrokes("u shift-p").await;
284 cx.shared_state().await.assert_eq(indoc! {"
285 The quick brown
286 fox jumps ˇover
287 the lazy doover
288 the lazy dog"});
289 }
290
291 #[gpui::test]
292 async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
293 let mut cx = VimTestContext::new(cx, true).await;
294
295 cx.update_global(|store: &mut SettingsStore, cx| {
296 store.update_user_settings::<VimSettings>(cx, |s| {
297 s.use_system_clipboard = Some(UseSystemClipboard::Never)
298 });
299 });
300
301 cx.set_state(
302 indoc! {"
303 The quick brown
304 fox jˇumps over
305 the lazy dog"},
306 Mode::Normal,
307 );
308 cx.simulate_keystrokes("v i w y");
309 cx.assert_state(
310 indoc! {"
311 The quick brown
312 fox ˇjumps over
313 the lazy dog"},
314 Mode::Normal,
315 );
316 cx.simulate_keystrokes("p");
317 cx.assert_state(
318 indoc! {"
319 The quick brown
320 fox jjumpˇsumps over
321 the lazy dog"},
322 Mode::Normal,
323 );
324 assert_eq!(cx.read_from_clipboard(), None);
325 }
326
327 #[gpui::test]
328 async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
329 let mut cx = VimTestContext::new(cx, true).await;
330
331 cx.update_global(|store: &mut SettingsStore, cx| {
332 store.update_user_settings::<VimSettings>(cx, |s| {
333 s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
334 });
335 });
336
337 // copy in visual mode
338 cx.set_state(
339 indoc! {"
340 The quick brown
341 fox jˇumps over
342 the lazy dog"},
343 Mode::Normal,
344 );
345 cx.simulate_keystrokes("v i w y");
346 cx.assert_state(
347 indoc! {"
348 The quick brown
349 fox ˇjumps over
350 the lazy dog"},
351 Mode::Normal,
352 );
353 cx.simulate_keystrokes("p");
354 cx.assert_state(
355 indoc! {"
356 The quick brown
357 fox jjumpˇsumps over
358 the lazy dog"},
359 Mode::Normal,
360 );
361 assert_eq!(
362 cx.read_from_clipboard().map(|item| item.text().clone()),
363 Some("jumps".into())
364 );
365 cx.simulate_keystrokes("d d p");
366 cx.assert_state(
367 indoc! {"
368 The quick brown
369 the lazy dog
370 ˇfox jjumpsumps over"},
371 Mode::Normal,
372 );
373 assert_eq!(
374 cx.read_from_clipboard().map(|item| item.text().clone()),
375 Some("jumps".into())
376 );
377 cx.write_to_clipboard(ClipboardItem::new("test-copy".to_string()));
378 cx.simulate_keystrokes("shift-p");
379 cx.assert_state(
380 indoc! {"
381 The quick brown
382 the lazy dog
383 test-copˇyfox jjumpsumps over"},
384 Mode::Normal,
385 );
386 }
387
388 #[gpui::test]
389 async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
390 let mut cx = NeovimBackedTestContext::new(cx).await;
391
392 // copy in visual mode
393 cx.set_shared_state(indoc! {"
394 The quick brown
395 fox jˇumps over
396 the lazy dog"})
397 .await;
398 cx.simulate_shared_keystrokes("v i w y").await;
399 cx.shared_state().await.assert_eq(indoc! {"
400 The quick brown
401 fox ˇjumps over
402 the lazy dog"});
403 // paste in visual mode
404 cx.simulate_shared_keystrokes("w v i w p").await;
405 cx.shared_state().await.assert_eq(indoc! {"
406 The quick brown
407 fox jumps jumpˇs
408 the lazy dog"});
409 cx.shared_clipboard().await.assert_eq("over");
410 // paste in visual line mode
411 cx.simulate_shared_keystrokes("up shift-v shift-p").await;
412 cx.shared_state().await.assert_eq(indoc! {"
413 ˇover
414 fox jumps jumps
415 the lazy dog"});
416 cx.shared_clipboard().await.assert_eq("over");
417 // paste in visual block mode
418 cx.simulate_shared_keystrokes("ctrl-v down down p").await;
419 cx.shared_state().await.assert_eq(indoc! {"
420 oveˇrver
421 overox jumps jumps
422 overhe lazy dog"});
423
424 // copy in visual line mode
425 cx.set_shared_state(indoc! {"
426 The quick brown
427 fox juˇmps over
428 the lazy dog"})
429 .await;
430 cx.simulate_shared_keystrokes("shift-v d").await;
431 cx.shared_state().await.assert_eq(indoc! {"
432 The quick brown
433 the laˇzy dog"});
434 // paste in visual mode
435 cx.simulate_shared_keystrokes("v i w p").await;
436 cx.shared_state().await.assert_eq(&indoc! {"
437 The quick brown
438 the•
439 ˇfox jumps over
440 dog"});
441 cx.shared_clipboard().await.assert_eq("lazy");
442 cx.set_shared_state(indoc! {"
443 The quick brown
444 fox juˇmps over
445 the lazy dog"})
446 .await;
447 cx.simulate_shared_keystrokes("shift-v d").await;
448 cx.shared_state().await.assert_eq(indoc! {"
449 The quick brown
450 the laˇzy dog"});
451 // paste in visual line mode
452 cx.simulate_shared_keystrokes("k shift-v p").await;
453 cx.shared_state().await.assert_eq(indoc! {"
454 ˇfox jumps over
455 the lazy dog"});
456 cx.shared_clipboard().await.assert_eq("The quick brown\n");
457 }
458
459 #[gpui::test]
460 async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
461 let mut cx = NeovimBackedTestContext::new(cx).await;
462 // copy in visual block mode
463 cx.set_shared_state(indoc! {"
464 The ˇquick brown
465 fox jumps over
466 the lazy dog"})
467 .await;
468 cx.simulate_shared_keystrokes("ctrl-v 2 j y").await;
469 cx.shared_clipboard().await.assert_eq("q\nj\nl");
470 cx.simulate_shared_keystrokes("p").await;
471 cx.shared_state().await.assert_eq(indoc! {"
472 The qˇquick brown
473 fox jjumps over
474 the llazy dog"});
475 cx.simulate_shared_keystrokes("v i w shift-p").await;
476 cx.shared_state().await.assert_eq(indoc! {"
477 The ˇq brown
478 fox jjjumps over
479 the lllazy dog"});
480 cx.simulate_shared_keystrokes("v i w shift-p").await;
481
482 cx.set_shared_state(indoc! {"
483 The ˇquick brown
484 fox jumps over
485 the lazy dog"})
486 .await;
487 cx.simulate_shared_keystrokes("ctrl-v j y").await;
488 cx.shared_clipboard().await.assert_eq("q\nj");
489 cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await;
490 cx.shared_state().await.assert_eq(indoc! {"
491 The qˇqick brown
492 fox jjmps over
493 the lzy dog"});
494
495 cx.simulate_shared_keystrokes("shift-v p").await;
496 cx.shared_state().await.assert_eq(indoc! {"
497 ˇq
498 j
499 fox jjmps over
500 the lzy dog"});
501 }
502
503 #[gpui::test]
504 async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
505 let mut cx = VimTestContext::new_typescript(cx).await;
506
507 cx.set_state(
508 indoc! {"
509 class A {ˇ
510 }
511 "},
512 Mode::Normal,
513 );
514 cx.simulate_keystrokes("o a ( ) { escape");
515 cx.assert_state(
516 indoc! {"
517 class A {
518 a()ˇ{}
519 }
520 "},
521 Mode::Normal,
522 );
523 // cursor goes to the first non-blank character in the line;
524 cx.simulate_keystrokes("y y p");
525 cx.assert_state(
526 indoc! {"
527 class A {
528 a(){}
529 ˇa(){}
530 }
531 "},
532 Mode::Normal,
533 );
534 // indentation is preserved when pasting
535 cx.simulate_keystrokes("u shift-v up y shift-p");
536 cx.assert_state(
537 indoc! {"
538 ˇclass A {
539 a(){}
540 class A {
541 a(){}
542 }
543 "},
544 Mode::Normal,
545 );
546 }
547
548 #[gpui::test]
549 async fn test_paste_count(cx: &mut gpui::TestAppContext) {
550 let mut cx = NeovimBackedTestContext::new(cx).await;
551
552 cx.set_shared_state(indoc! {"
553 onˇe
554 two
555 three
556 "})
557 .await;
558 cx.simulate_shared_keystrokes("y y 3 p").await;
559 cx.shared_state().await.assert_eq(indoc! {"
560 one
561 ˇone
562 one
563 one
564 two
565 three
566 "});
567
568 cx.set_shared_state(indoc! {"
569 one
570 ˇtwo
571 three
572 "})
573 .await;
574 cx.simulate_shared_keystrokes("y $ $ 3 p").await;
575 cx.shared_state().await.assert_eq(indoc! {"
576 one
577 twotwotwotwˇo
578 three
579 "});
580 }
581
582 #[gpui::test]
583 async fn test_numbered_registers(cx: &mut gpui::TestAppContext) {
584 let mut cx = NeovimBackedTestContext::new(cx).await;
585
586 cx.update_global(|store: &mut SettingsStore, cx| {
587 store.update_user_settings::<VimSettings>(cx, |s| {
588 s.use_system_clipboard = Some(UseSystemClipboard::Never)
589 });
590 });
591
592 cx.set_shared_state(indoc! {"
593 The quick brown
594 fox jˇumps over
595 the lazy dog"})
596 .await;
597 cx.simulate_shared_keystrokes("y y \" 0 p").await;
598 cx.shared_register('0').await.assert_eq("fox jumps over\n");
599 cx.shared_register('"').await.assert_eq("fox jumps over\n");
600
601 cx.shared_state().await.assert_eq(indoc! {"
602 The quick brown
603 fox jumps over
604 ˇfox jumps over
605 the lazy dog"});
606 cx.simulate_shared_keystrokes("k k d d").await;
607 cx.shared_register('0').await.assert_eq("fox jumps over\n");
608 cx.shared_register('1').await.assert_eq("The quick brown\n");
609 cx.shared_register('"').await.assert_eq("The quick brown\n");
610
611 cx.simulate_shared_keystrokes("d d shift-g d d").await;
612 cx.shared_register('0').await.assert_eq("fox jumps over\n");
613 cx.shared_register('3').await.assert_eq("The quick brown\n");
614 cx.shared_register('2').await.assert_eq("fox jumps over\n");
615 cx.shared_register('1').await.assert_eq("the lazy dog\n");
616
617 cx.shared_state().await.assert_eq(indoc! {"
618 ˇfox jumps over"});
619
620 cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await;
621 cx.set_shared_state(indoc! {"
622 The quick brown
623 fox jumps over
624 ˇthe lazy dog"})
625 .await;
626 }
627
628 #[gpui::test]
629 async fn test_named_registers(cx: &mut gpui::TestAppContext) {
630 let mut cx = NeovimBackedTestContext::new(cx).await;
631
632 cx.update_global(|store: &mut SettingsStore, cx| {
633 store.update_user_settings::<VimSettings>(cx, |s| {
634 s.use_system_clipboard = Some(UseSystemClipboard::Never)
635 });
636 });
637
638 cx.set_shared_state(indoc! {"
639 The quick brown
640 fox jˇumps over
641 the lazy dog"})
642 .await;
643 cx.simulate_shared_keystrokes("\" a d a w").await;
644 cx.shared_register('a').await.assert_eq("jumps ");
645 cx.simulate_shared_keystrokes("\" shift-a d i w").await;
646 cx.shared_register('a').await.assert_eq("jumps over");
647 cx.shared_register('"').await.assert_eq("jumps over");
648 cx.simulate_shared_keystrokes("\" a p").await;
649 cx.shared_state().await.assert_eq(indoc! {"
650 The quick brown
651 fox jumps oveˇr
652 the lazy dog"});
653 cx.simulate_shared_keystrokes("\" a d a w").await;
654 cx.shared_register('a').await.assert_eq(" over");
655 }
656
657 #[gpui::test]
658 async fn test_special_registers(cx: &mut gpui::TestAppContext) {
659 let mut cx = NeovimBackedTestContext::new(cx).await;
660
661 cx.update_global(|store: &mut SettingsStore, cx| {
662 store.update_user_settings::<VimSettings>(cx, |s| {
663 s.use_system_clipboard = Some(UseSystemClipboard::Never)
664 });
665 });
666
667 cx.set_shared_state(indoc! {"
668 The quick brown
669 fox jˇumps over
670 the lazy dog"})
671 .await;
672 cx.simulate_shared_keystrokes("d i w").await;
673 cx.shared_register('-').await.assert_eq("jumps");
674 cx.simulate_shared_keystrokes("\" _ d d").await;
675 cx.shared_register('_').await.assert_eq("");
676
677 cx.shared_state().await.assert_eq(indoc! {"
678 The quick brown
679 the ˇlazy dog"});
680 cx.simulate_shared_keystrokes("\" \" d ^").await;
681 cx.shared_register('0').await.assert_eq("the ");
682 cx.shared_register('"').await.assert_eq("the ");
683
684 cx.simulate_shared_keystrokes("^ \" + d $").await;
685 cx.shared_clipboard().await.assert_eq("lazy dog");
686 cx.shared_register('"').await.assert_eq("lazy dog");
687
688 cx.simulate_shared_keystrokes("/ d o g enter").await;
689 cx.shared_register('/').await.assert_eq("dog");
690 cx.simulate_shared_keystrokes("\" / shift-p").await;
691 cx.shared_state().await.assert_eq(indoc! {"
692 The quick brown
693 doˇg"});
694
695 // not testing nvim as it doesn't have a filename
696 cx.simulate_keystrokes("\" % p");
697 cx.assert_state(
698 indoc! {"
699 The quick brown
700 dogdir/file.rˇs"},
701 Mode::Normal,
702 );
703 }
704
705 #[gpui::test]
706 async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) {
707 let mut cx = VimTestContext::new(cx, true).await;
708
709 cx.update_global(|store: &mut SettingsStore, cx| {
710 store.update_user_settings::<VimSettings>(cx, |s| {
711 s.use_system_clipboard = Some(UseSystemClipboard::Never)
712 });
713 });
714
715 cx.set_state(
716 indoc! {"
717 ˇfish one
718 fish two
719 fish red
720 fish blue
721 "},
722 Mode::Normal,
723 );
724 cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p");
725 cx.assert_state(
726 indoc! {"
727 onˇefish•
728 twˇofish•
729 reˇdfish•
730 bluˇefish•
731 "},
732 Mode::Normal,
733 );
734 }
735}