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