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::{state::Mode, utils::copy_selections_content, UseSystemClipboard, Vim, VimSettings};
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 system_clipboard_is_newer(vim: &Vim, cx: &mut AppContext) -> bool {
31 cx.read_from_clipboard().is_some_and(|item| {
32 if let Some(last_state) = vim.workspace_state.registers.get(".system.") {
33 last_state != item.text()
34 } else {
35 true
36 }
37 })
38}
39
40fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
41 Vim::update(cx, |vim, cx| {
42 vim.record_current_action(cx);
43 let count = vim.take_count(cx).unwrap_or(1);
44 vim.update_active_editor(cx, |vim, editor, cx| {
45 let text_layout_details = editor.text_layout_details(cx);
46 editor.transact(cx, |editor, cx| {
47 editor.set_clip_at_line_ends(false, cx);
48
49 let (clipboard_text, clipboard_selections): (String, Option<_>) =
50 if VimSettings::get_global(cx).use_system_clipboard == UseSystemClipboard::Never
51 || VimSettings::get_global(cx).use_system_clipboard
52 == UseSystemClipboard::OnYank
53 && !system_clipboard_is_newer(vim, cx)
54 {
55 (
56 vim.workspace_state
57 .registers
58 .get("\"")
59 .cloned()
60 .unwrap_or_else(|| "".to_string()),
61 None,
62 )
63 } else {
64 if let Some(item) = cx.read_from_clipboard() {
65 let clipboard_selections = item
66 .metadata::<Vec<ClipboardSelection>>()
67 .filter(|clipboard_selections| {
68 clipboard_selections.len() > 1
69 && vim.state().mode != Mode::VisualLine
70 });
71 (item.text().clone(), clipboard_selections)
72 } else {
73 ("".into(), None)
74 }
75 };
76
77 if clipboard_text.is_empty() {
78 return;
79 }
80
81 if !action.preserve_clipboard && vim.state().mode.is_visual() {
82 copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx);
83 }
84
85 let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
86
87 // unlike zed, if you have a multi-cursor selection from vim block mode,
88 // pasting it will paste it on subsequent lines, even if you don't yet
89 // have a cursor there.
90 let mut selections_to_process = Vec::new();
91 let mut i = 0;
92 while i < current_selections.len() {
93 selections_to_process
94 .push((current_selections[i].start..current_selections[i].end, true));
95 i += 1;
96 }
97 if let Some(clipboard_selections) = clipboard_selections.as_ref() {
98 let left = current_selections
99 .iter()
100 .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
101 .min()
102 .unwrap();
103 let mut row = current_selections.last().unwrap().end.row().next_row();
104 while i < clipboard_selections.len() {
105 let cursor =
106 display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
107 selections_to_process.push((cursor..cursor, false));
108 i += 1;
109 row.0 += 1;
110 }
111 }
112
113 let first_selection_indent_column =
114 clipboard_selections.as_ref().and_then(|zed_selections| {
115 zed_selections
116 .first()
117 .map(|selection| selection.first_line_indent)
118 });
119 let before = action.before || vim.state().mode == Mode::VisualLine;
120
121 let mut edits = Vec::new();
122 let mut new_selections = Vec::new();
123 let mut original_indent_columns = Vec::new();
124 let mut start_offset = 0;
125
126 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
127 let (mut to_insert, original_indent_column) =
128 if let Some(clipboard_selections) = &clipboard_selections {
129 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
130 let end_offset = start_offset + clipboard_selection.len;
131 let text = clipboard_text[start_offset..end_offset].to_string();
132 start_offset = end_offset + 1;
133 (text, Some(clipboard_selection.first_line_indent))
134 } else {
135 ("".to_string(), first_selection_indent_column)
136 }
137 } else {
138 (clipboard_text.to_string(), first_selection_indent_column)
139 };
140 let line_mode = to_insert.ends_with('\n');
141 let is_multiline = to_insert.contains('\n');
142
143 if line_mode && !before {
144 if selection.is_empty() {
145 to_insert =
146 "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
147 } else {
148 to_insert = "\n".to_owned() + &to_insert;
149 }
150 } else if !line_mode && vim.state().mode == Mode::VisualLine {
151 to_insert = to_insert + "\n";
152 }
153
154 let display_range = if !selection.is_empty() {
155 selection.start..selection.end
156 } else if line_mode {
157 let point = if before {
158 movement::line_beginning(&display_map, selection.start, false)
159 } else {
160 movement::line_end(&display_map, selection.start, false)
161 };
162 point..point
163 } else {
164 let point = if before {
165 selection.start
166 } else {
167 movement::saturating_right(&display_map, selection.start)
168 };
169 point..point
170 };
171
172 let point_range = display_range.start.to_point(&display_map)
173 ..display_range.end.to_point(&display_map);
174 let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
175 display_map.buffer_snapshot.anchor_before(point_range.start)
176 } else {
177 display_map.buffer_snapshot.anchor_after(point_range.end)
178 };
179
180 if *preserve {
181 new_selections.push((anchor, line_mode, is_multiline));
182 }
183 edits.push((point_range, to_insert.repeat(count)));
184 original_indent_columns.extend(original_indent_column);
185 }
186
187 editor.edit_with_block_indent(edits, original_indent_columns, cx);
188
189 // in line_mode vim will insert the new text on the next (or previous if before) line
190 // 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).
191 // otherwise vim will insert the next text at (or before) the current cursor position,
192 // the cursor will go to the last (or first, if is_multiline) inserted character.
193 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
194 s.replace_cursors_with(|map| {
195 let mut cursors = Vec::new();
196 for (anchor, line_mode, is_multiline) in &new_selections {
197 let mut cursor = anchor.to_display_point(map);
198 if *line_mode {
199 if !before {
200 cursor = movement::down(
201 map,
202 cursor,
203 SelectionGoal::None,
204 false,
205 &text_layout_details,
206 )
207 .0;
208 }
209 cursor = movement::indented_line_beginning(map, cursor, true);
210 } else if !is_multiline {
211 cursor = movement::saturating_left(map, cursor)
212 }
213 cursors.push(cursor);
214 if vim.state().mode == Mode::VisualBlock {
215 break;
216 }
217 }
218
219 cursors
220 });
221 })
222 });
223 });
224 vim.switch_mode(Mode::Normal, true, cx);
225 });
226}
227
228#[cfg(test)]
229mod test {
230 use crate::{
231 state::Mode,
232 test::{NeovimBackedTestContext, VimTestContext},
233 UseSystemClipboard, VimSettings,
234 };
235 use gpui::ClipboardItem;
236 use indoc::indoc;
237 use settings::SettingsStore;
238
239 #[gpui::test]
240 async fn test_paste(cx: &mut gpui::TestAppContext) {
241 let mut cx = NeovimBackedTestContext::new(cx).await;
242
243 // single line
244 cx.set_shared_state(indoc! {"
245 The quick brown
246 fox ˇjumps over
247 the lazy dog"})
248 .await;
249 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
250 cx.assert_shared_clipboard("jumps o").await;
251 cx.set_shared_state(indoc! {"
252 The quick brown
253 fox jumps oveˇr
254 the lazy dog"})
255 .await;
256 cx.simulate_shared_keystroke("p").await;
257 cx.assert_shared_state(indoc! {"
258 The quick brown
259 fox jumps overjumps ˇo
260 the lazy dog"})
261 .await;
262
263 cx.set_shared_state(indoc! {"
264 The quick brown
265 fox jumps oveˇr
266 the lazy dog"})
267 .await;
268 cx.simulate_shared_keystroke("shift-p").await;
269 cx.assert_shared_state(indoc! {"
270 The quick brown
271 fox jumps ovejumps ˇor
272 the lazy dog"})
273 .await;
274
275 // line mode
276 cx.set_shared_state(indoc! {"
277 The quick brown
278 fox juˇmps over
279 the lazy dog"})
280 .await;
281 cx.simulate_shared_keystrokes(["d", "d"]).await;
282 cx.assert_shared_clipboard("fox jumps over\n").await;
283 cx.assert_shared_state(indoc! {"
284 The quick brown
285 the laˇzy dog"})
286 .await;
287 cx.simulate_shared_keystroke("p").await;
288 cx.assert_shared_state(indoc! {"
289 The quick brown
290 the lazy dog
291 ˇfox jumps over"})
292 .await;
293 cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
294 cx.assert_shared_state(indoc! {"
295 The quick brown
296 ˇfox jumps over
297 the lazy dog
298 fox jumps over"})
299 .await;
300
301 // multiline, cursor to first character of pasted text.
302 cx.set_shared_state(indoc! {"
303 The quick brown
304 fox jumps ˇover
305 the lazy dog"})
306 .await;
307 cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
308 cx.assert_shared_clipboard("over\nthe lazy do").await;
309
310 cx.simulate_shared_keystroke("p").await;
311 cx.assert_shared_state(indoc! {"
312 The quick brown
313 fox jumps oˇover
314 the lazy dover
315 the lazy dog"})
316 .await;
317 cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
318 cx.assert_shared_state(indoc! {"
319 The quick brown
320 fox jumps ˇover
321 the lazy doover
322 the lazy dog"})
323 .await;
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_keystroke("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_keystroke("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_keystroke("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.assert_shared_state(indoc! {"
435 The quick brown
436 fox ˇjumps over
437 the lazy dog"})
438 .await;
439 // paste in visual mode
440 cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
441 .await;
442 cx.assert_shared_state(indoc! {"
443 The quick brown
444 fox jumps jumpˇs
445 the lazy dog"})
446 .await;
447 cx.assert_shared_clipboard("over").await;
448 // paste in visual line mode
449 cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
450 .await;
451 cx.assert_shared_state(indoc! {"
452 ˇover
453 fox jumps jumps
454 the lazy dog"})
455 .await;
456 cx.assert_shared_clipboard("over").await;
457 // paste in visual block mode
458 cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
459 .await;
460 cx.assert_shared_state(indoc! {"
461 oveˇrver
462 overox jumps jumps
463 overhe lazy dog"})
464 .await;
465
466 // copy in visual line mode
467 cx.set_shared_state(indoc! {"
468 The quick brown
469 fox juˇmps over
470 the lazy dog"})
471 .await;
472 cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
473 cx.assert_shared_state(indoc! {"
474 The quick brown
475 the laˇzy dog"})
476 .await;
477 // paste in visual mode
478 cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
479 cx.assert_shared_state(
480 &indoc! {"
481 The quick brown
482 the_
483 ˇfox jumps over
484 _dog"}
485 .replace('_', " "), // Hack for trailing whitespace
486 )
487 .await;
488 cx.assert_shared_clipboard("lazy").await;
489 cx.set_shared_state(indoc! {"
490 The quick brown
491 fox juˇmps over
492 the lazy dog"})
493 .await;
494 cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
495 cx.assert_shared_state(indoc! {"
496 The quick brown
497 the laˇzy dog"})
498 .await;
499 // paste in visual line mode
500 cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
501 cx.assert_shared_state(indoc! {"
502 ˇfox jumps over
503 the lazy dog"})
504 .await;
505 cx.assert_shared_clipboard("The quick brown\n").await;
506 }
507
508 #[gpui::test]
509 async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
510 let mut cx = NeovimBackedTestContext::new(cx).await;
511 // copy in visual block mode
512 cx.set_shared_state(indoc! {"
513 The ˇquick brown
514 fox jumps over
515 the lazy dog"})
516 .await;
517 cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
518 .await;
519 cx.assert_shared_clipboard("q\nj\nl").await;
520 cx.simulate_shared_keystrokes(["p"]).await;
521 cx.assert_shared_state(indoc! {"
522 The qˇquick brown
523 fox jjumps over
524 the llazy dog"})
525 .await;
526 cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
527 .await;
528 cx.assert_shared_state(indoc! {"
529 The ˇq brown
530 fox jjjumps over
531 the lllazy dog"})
532 .await;
533 cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
534 .await;
535
536 cx.set_shared_state(indoc! {"
537 The ˇquick brown
538 fox jumps over
539 the lazy dog"})
540 .await;
541 cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
542 cx.assert_shared_clipboard("q\nj").await;
543 cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
544 .await;
545 cx.assert_shared_state(indoc! {"
546 The qˇqick brown
547 fox jjmps over
548 the lzy dog"})
549 .await;
550
551 cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
552 cx.assert_shared_state(indoc! {"
553 ˇq
554 j
555 fox jjmps over
556 the lzy dog"})
557 .await;
558 }
559
560 #[gpui::test]
561 async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
562 let mut cx = VimTestContext::new_typescript(cx).await;
563
564 cx.set_state(
565 indoc! {"
566 class A {ˇ
567 }
568 "},
569 Mode::Normal,
570 );
571 cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
572 cx.assert_state(
573 indoc! {"
574 class A {
575 a()ˇ{}
576 }
577 "},
578 Mode::Normal,
579 );
580 // cursor goes to the first non-blank character in the line;
581 cx.simulate_keystrokes(["y", "y", "p"]);
582 cx.assert_state(
583 indoc! {"
584 class A {
585 a(){}
586 ˇa(){}
587 }
588 "},
589 Mode::Normal,
590 );
591 // indentation is preserved when pasting
592 cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
593 cx.assert_state(
594 indoc! {"
595 ˇclass A {
596 a(){}
597 class A {
598 a(){}
599 }
600 "},
601 Mode::Normal,
602 );
603 }
604
605 #[gpui::test]
606 async fn test_paste_count(cx: &mut gpui::TestAppContext) {
607 let mut cx = NeovimBackedTestContext::new(cx).await;
608
609 cx.set_shared_state(indoc! {"
610 onˇe
611 two
612 three
613 "})
614 .await;
615 cx.simulate_shared_keystrokes(["y", "y", "3", "p"]).await;
616 cx.assert_shared_state(indoc! {"
617 one
618 ˇone
619 one
620 one
621 two
622 three
623 "})
624 .await;
625
626 cx.set_shared_state(indoc! {"
627 one
628 ˇtwo
629 three
630 "})
631 .await;
632 cx.simulate_shared_keystrokes(["y", "$", "$", "3", "p"])
633 .await;
634 cx.assert_shared_state(indoc! {"
635 one
636 twotwotwotwˇo
637 three
638 "})
639 .await;
640 }
641}