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.shared_clipboard().await.assert_eq("jumps o");
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_keystrokes("p").await;
257 cx.shared_state().await.assert_eq(indoc! {"
258 The quick brown
259 fox jumps overjumps ˇo
260 the lazy dog"});
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_keystrokes("shift-p").await;
268 cx.shared_state().await.assert_eq(indoc! {"
269 The quick brown
270 fox jumps ovejumps ˇor
271 the lazy dog"});
272
273 // line mode
274 cx.set_shared_state(indoc! {"
275 The quick brown
276 fox juˇmps over
277 the lazy dog"})
278 .await;
279 cx.simulate_shared_keystrokes("d d").await;
280 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
281 cx.shared_state().await.assert_eq(indoc! {"
282 The quick brown
283 the laˇzy dog"});
284 cx.simulate_shared_keystrokes("p").await;
285 cx.shared_state().await.assert_eq(indoc! {"
286 The quick brown
287 the lazy dog
288 ˇfox jumps over"});
289 cx.simulate_shared_keystrokes("k shift-p").await;
290 cx.shared_state().await.assert_eq(indoc! {"
291 The quick brown
292 ˇfox jumps over
293 the lazy dog
294 fox jumps over"});
295
296 // multiline, cursor to first character of pasted text.
297 cx.set_shared_state(indoc! {"
298 The quick brown
299 fox jumps ˇover
300 the lazy dog"})
301 .await;
302 cx.simulate_shared_keystrokes("v j y").await;
303 cx.shared_clipboard().await.assert_eq("over\nthe lazy do");
304
305 cx.simulate_shared_keystrokes("p").await;
306 cx.shared_state().await.assert_eq(indoc! {"
307 The quick brown
308 fox jumps oˇover
309 the lazy dover
310 the lazy dog"});
311 cx.simulate_shared_keystrokes("u shift-p").await;
312 cx.shared_state().await.assert_eq(indoc! {"
313 The quick brown
314 fox jumps ˇover
315 the lazy doover
316 the lazy dog"});
317 }
318
319 #[gpui::test]
320 async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
321 let mut cx = VimTestContext::new(cx, true).await;
322
323 cx.update_global(|store: &mut SettingsStore, cx| {
324 store.update_user_settings::<VimSettings>(cx, |s| {
325 s.use_system_clipboard = Some(UseSystemClipboard::Never)
326 });
327 });
328
329 cx.set_state(
330 indoc! {"
331 The quick brown
332 fox jˇumps over
333 the lazy dog"},
334 Mode::Normal,
335 );
336 cx.simulate_keystrokes("v i w y");
337 cx.assert_state(
338 indoc! {"
339 The quick brown
340 fox ˇjumps over
341 the lazy dog"},
342 Mode::Normal,
343 );
344 cx.simulate_keystrokes("p");
345 cx.assert_state(
346 indoc! {"
347 The quick brown
348 fox jjumpˇsumps over
349 the lazy dog"},
350 Mode::Normal,
351 );
352 assert_eq!(cx.read_from_clipboard(), None);
353 }
354
355 #[gpui::test]
356 async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
357 let mut cx = VimTestContext::new(cx, true).await;
358
359 cx.update_global(|store: &mut SettingsStore, cx| {
360 store.update_user_settings::<VimSettings>(cx, |s| {
361 s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
362 });
363 });
364
365 // copy in visual mode
366 cx.set_state(
367 indoc! {"
368 The quick brown
369 fox jˇumps over
370 the lazy dog"},
371 Mode::Normal,
372 );
373 cx.simulate_keystrokes("v i w y");
374 cx.assert_state(
375 indoc! {"
376 The quick brown
377 fox ˇjumps over
378 the lazy dog"},
379 Mode::Normal,
380 );
381 cx.simulate_keystrokes("p");
382 cx.assert_state(
383 indoc! {"
384 The quick brown
385 fox jjumpˇsumps over
386 the lazy dog"},
387 Mode::Normal,
388 );
389 assert_eq!(
390 cx.read_from_clipboard().map(|item| item.text().clone()),
391 Some("jumps".into())
392 );
393 cx.simulate_keystrokes("d d p");
394 cx.assert_state(
395 indoc! {"
396 The quick brown
397 the lazy dog
398 ˇfox jjumpsumps over"},
399 Mode::Normal,
400 );
401 assert_eq!(
402 cx.read_from_clipboard().map(|item| item.text().clone()),
403 Some("jumps".into())
404 );
405 cx.write_to_clipboard(ClipboardItem::new("test-copy".to_string()));
406 cx.simulate_keystrokes("shift-p");
407 cx.assert_state(
408 indoc! {"
409 The quick brown
410 the lazy dog
411 test-copˇyfox jjumpsumps over"},
412 Mode::Normal,
413 );
414 }
415
416 #[gpui::test]
417 async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
418 let mut cx = NeovimBackedTestContext::new(cx).await;
419
420 // copy in visual mode
421 cx.set_shared_state(indoc! {"
422 The quick brown
423 fox jˇumps over
424 the lazy dog"})
425 .await;
426 cx.simulate_shared_keystrokes("v i w y").await;
427 cx.shared_state().await.assert_eq(indoc! {"
428 The quick brown
429 fox ˇjumps over
430 the lazy dog"});
431 // paste in visual mode
432 cx.simulate_shared_keystrokes("w v i w p").await;
433 cx.shared_state().await.assert_eq(indoc! {"
434 The quick brown
435 fox jumps jumpˇs
436 the lazy dog"});
437 cx.shared_clipboard().await.assert_eq("over");
438 // paste in visual line mode
439 cx.simulate_shared_keystrokes("up shift-v shift-p").await;
440 cx.shared_state().await.assert_eq(indoc! {"
441 ˇover
442 fox jumps jumps
443 the lazy dog"});
444 cx.shared_clipboard().await.assert_eq("over");
445 // paste in visual block mode
446 cx.simulate_shared_keystrokes("ctrl-v down down p").await;
447 cx.shared_state().await.assert_eq(indoc! {"
448 oveˇrver
449 overox jumps jumps
450 overhe lazy dog"});
451
452 // copy in visual line mode
453 cx.set_shared_state(indoc! {"
454 The quick brown
455 fox juˇmps over
456 the lazy dog"})
457 .await;
458 cx.simulate_shared_keystrokes("shift-v d").await;
459 cx.shared_state().await.assert_eq(indoc! {"
460 The quick brown
461 the laˇzy dog"});
462 // paste in visual mode
463 cx.simulate_shared_keystrokes("v i w p").await;
464 cx.shared_state().await.assert_eq(&indoc! {"
465 The quick brown
466 the•
467 ˇfox jumps over
468 dog"});
469 cx.shared_clipboard().await.assert_eq("lazy");
470 cx.set_shared_state(indoc! {"
471 The quick brown
472 fox juˇmps over
473 the lazy dog"})
474 .await;
475 cx.simulate_shared_keystrokes("shift-v d").await;
476 cx.shared_state().await.assert_eq(indoc! {"
477 The quick brown
478 the laˇzy dog"});
479 // paste in visual line mode
480 cx.simulate_shared_keystrokes("k shift-v p").await;
481 cx.shared_state().await.assert_eq(indoc! {"
482 ˇfox jumps over
483 the lazy dog"});
484 cx.shared_clipboard().await.assert_eq("The quick brown\n");
485 }
486
487 #[gpui::test]
488 async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
489 let mut cx = NeovimBackedTestContext::new(cx).await;
490 // copy in visual block mode
491 cx.set_shared_state(indoc! {"
492 The ˇquick brown
493 fox jumps over
494 the lazy dog"})
495 .await;
496 cx.simulate_shared_keystrokes("ctrl-v 2 j y").await;
497 cx.shared_clipboard().await.assert_eq("q\nj\nl");
498 cx.simulate_shared_keystrokes("p").await;
499 cx.shared_state().await.assert_eq(indoc! {"
500 The qˇquick brown
501 fox jjumps over
502 the llazy dog"});
503 cx.simulate_shared_keystrokes("v i w shift-p").await;
504 cx.shared_state().await.assert_eq(indoc! {"
505 The ˇq brown
506 fox jjjumps over
507 the lllazy dog"});
508 cx.simulate_shared_keystrokes("v i w shift-p").await;
509
510 cx.set_shared_state(indoc! {"
511 The ˇquick brown
512 fox jumps over
513 the lazy dog"})
514 .await;
515 cx.simulate_shared_keystrokes("ctrl-v j y").await;
516 cx.shared_clipboard().await.assert_eq("q\nj");
517 cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await;
518 cx.shared_state().await.assert_eq(indoc! {"
519 The qˇqick brown
520 fox jjmps over
521 the lzy dog"});
522
523 cx.simulate_shared_keystrokes("shift-v p").await;
524 cx.shared_state().await.assert_eq(indoc! {"
525 ˇq
526 j
527 fox jjmps over
528 the lzy dog"});
529 }
530
531 #[gpui::test]
532 async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
533 let mut cx = VimTestContext::new_typescript(cx).await;
534
535 cx.set_state(
536 indoc! {"
537 class A {ˇ
538 }
539 "},
540 Mode::Normal,
541 );
542 cx.simulate_keystrokes("o a ( ) { escape");
543 cx.assert_state(
544 indoc! {"
545 class A {
546 a()ˇ{}
547 }
548 "},
549 Mode::Normal,
550 );
551 // cursor goes to the first non-blank character in the line;
552 cx.simulate_keystrokes("y y p");
553 cx.assert_state(
554 indoc! {"
555 class A {
556 a(){}
557 ˇa(){}
558 }
559 "},
560 Mode::Normal,
561 );
562 // indentation is preserved when pasting
563 cx.simulate_keystrokes("u shift-v up y shift-p");
564 cx.assert_state(
565 indoc! {"
566 ˇclass A {
567 a(){}
568 class A {
569 a(){}
570 }
571 "},
572 Mode::Normal,
573 );
574 }
575
576 #[gpui::test]
577 async fn test_paste_count(cx: &mut gpui::TestAppContext) {
578 let mut cx = NeovimBackedTestContext::new(cx).await;
579
580 cx.set_shared_state(indoc! {"
581 onˇe
582 two
583 three
584 "})
585 .await;
586 cx.simulate_shared_keystrokes("y y 3 p").await;
587 cx.shared_state().await.assert_eq(indoc! {"
588 one
589 ˇone
590 one
591 one
592 two
593 three
594 "});
595
596 cx.set_shared_state(indoc! {"
597 one
598 ˇtwo
599 three
600 "})
601 .await;
602 cx.simulate_shared_keystrokes("y $ $ 3 p").await;
603 cx.shared_state().await.assert_eq(indoc! {"
604 one
605 twotwotwotwˇo
606 three
607 "});
608 }
609}