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