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