1use std::{borrow::Cow, cmp};
2
3use editor::{
4 display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint,
5};
6use gpui::{impl_actions, ViewContext};
7use language::{Bias, SelectionGoal};
8use serde::Deserialize;
9use workspace::Workspace;
10
11use crate::{state::Mode, utils::copy_selections_content, Vim};
12
13#[derive(Clone, Deserialize, PartialEq)]
14#[serde(rename_all = "camelCase")]
15struct Paste {
16 #[serde(default)]
17 before: bool,
18 #[serde(default)]
19 preserve_clipboard: bool,
20}
21
22impl_actions!(vim, [Paste]);
23
24pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
25 workspace.register_action(paste);
26}
27
28fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
29 Vim::update(cx, |vim, cx| {
30 vim.record_current_action(cx);
31 vim.update_active_editor(cx, |editor, cx| {
32 let text_layout_details = editor.text_layout_details(cx);
33 editor.transact(cx, |editor, cx| {
34 editor.set_clip_at_line_ends(false, cx);
35
36 let Some(item) = cx.read_from_clipboard() else {
37 return;
38 };
39 let clipboard_text = Cow::Borrowed(item.text());
40 if clipboard_text.is_empty() {
41 return;
42 }
43
44 if !action.preserve_clipboard && vim.state().mode.is_visual() {
45 copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
46 }
47
48 // if we are copying from multi-cursor (of visual block mode), we want
49 // to
50 let clipboard_selections =
51 item.metadata::<Vec<ClipboardSelection>>()
52 .filter(|clipboard_selections| {
53 clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
54 });
55
56 let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
57
58 // unlike zed, if you have a multi-cursor selection from vim block mode,
59 // pasting it will paste it on subsequent lines, even if you don't yet
60 // have a cursor there.
61 let mut selections_to_process = Vec::new();
62 let mut i = 0;
63 while i < current_selections.len() {
64 selections_to_process
65 .push((current_selections[i].start..current_selections[i].end, true));
66 i += 1;
67 }
68 if let Some(clipboard_selections) = clipboard_selections.as_ref() {
69 let left = current_selections
70 .iter()
71 .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
72 .min()
73 .unwrap();
74 let mut row = current_selections.last().unwrap().end.row() + 1;
75 while i < clipboard_selections.len() {
76 let cursor =
77 display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
78 selections_to_process.push((cursor..cursor, false));
79 i += 1;
80 row += 1;
81 }
82 }
83
84 let first_selection_indent_column =
85 clipboard_selections.as_ref().and_then(|zed_selections| {
86 zed_selections
87 .first()
88 .map(|selection| selection.first_line_indent)
89 });
90 let before = action.before || vim.state().mode == Mode::VisualLine;
91
92 let mut edits = Vec::new();
93 let mut new_selections = Vec::new();
94 let mut original_indent_columns = Vec::new();
95 let mut start_offset = 0;
96
97 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
98 let (mut to_insert, original_indent_column) =
99 if let Some(clipboard_selections) = &clipboard_selections {
100 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
101 let end_offset = start_offset + clipboard_selection.len;
102 let text = clipboard_text[start_offset..end_offset].to_string();
103 start_offset = end_offset + 1;
104 (text, Some(clipboard_selection.first_line_indent))
105 } else {
106 ("".to_string(), first_selection_indent_column)
107 }
108 } else {
109 (clipboard_text.to_string(), first_selection_indent_column)
110 };
111 let line_mode = to_insert.ends_with("\n");
112 let is_multiline = to_insert.contains("\n");
113
114 if line_mode && !before {
115 if selection.is_empty() {
116 to_insert =
117 "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
118 } else {
119 to_insert = "\n".to_owned() + &to_insert;
120 }
121 } else if !line_mode && vim.state().mode == Mode::VisualLine {
122 to_insert = to_insert + "\n";
123 }
124
125 let display_range = if !selection.is_empty() {
126 selection.start..selection.end
127 } else if line_mode {
128 let point = if before {
129 movement::line_beginning(&display_map, selection.start, false)
130 } else {
131 movement::line_end(&display_map, selection.start, false)
132 };
133 point..point
134 } else {
135 let point = if before {
136 selection.start
137 } else {
138 movement::saturating_right(&display_map, selection.start)
139 };
140 point..point
141 };
142
143 let point_range = display_range.start.to_point(&display_map)
144 ..display_range.end.to_point(&display_map);
145 let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
146 display_map.buffer_snapshot.anchor_before(point_range.start)
147 } else {
148 display_map.buffer_snapshot.anchor_after(point_range.end)
149 };
150
151 if *preserve {
152 new_selections.push((anchor, line_mode, is_multiline));
153 }
154 edits.push((point_range, to_insert));
155 original_indent_columns.extend(original_indent_column);
156 }
157
158 editor.edit_with_block_indent(edits, original_indent_columns, cx);
159
160 // in line_mode vim will insert the new text on the next (or previous if before) line
161 // 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).
162 // otherwise vim will insert the next text at (or before) the current cursor position,
163 // the cursor will go to the last (or first, if is_multiline) inserted character.
164 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
165 s.replace_cursors_with(|map| {
166 let mut cursors = Vec::new();
167 for (anchor, line_mode, is_multiline) in &new_selections {
168 let mut cursor = anchor.to_display_point(map);
169 if *line_mode {
170 if !before {
171 cursor = movement::down(
172 map,
173 cursor,
174 SelectionGoal::None,
175 false,
176 &text_layout_details,
177 )
178 .0;
179 }
180 cursor = movement::indented_line_beginning(map, cursor, true);
181 } else if !is_multiline {
182 cursor = movement::saturating_left(map, cursor)
183 }
184 cursors.push(cursor);
185 if vim.state().mode == Mode::VisualBlock {
186 break;
187 }
188 }
189
190 cursors
191 });
192 })
193 });
194 });
195 vim.switch_mode(Mode::Normal, true, cx);
196 });
197}
198
199#[cfg(test)]
200mod test {
201 use crate::{
202 state::Mode,
203 test::{NeovimBackedTestContext, VimTestContext},
204 };
205 use indoc::indoc;
206
207 #[gpui::test]
208 async fn test_paste(cx: &mut gpui::TestAppContext) {
209 let mut cx = NeovimBackedTestContext::new(cx).await;
210
211 // single line
212 cx.set_shared_state(indoc! {"
213 The quick brown
214 fox ˇjumps over
215 the lazy dog"})
216 .await;
217 cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
218 cx.assert_shared_clipboard("jumps o").await;
219 cx.set_shared_state(indoc! {"
220 The quick brown
221 fox jumps oveˇr
222 the lazy dog"})
223 .await;
224 cx.simulate_shared_keystroke("p").await;
225 cx.assert_shared_state(indoc! {"
226 The quick brown
227 fox jumps overjumps ˇo
228 the lazy dog"})
229 .await;
230
231 cx.set_shared_state(indoc! {"
232 The quick brown
233 fox jumps oveˇr
234 the lazy dog"})
235 .await;
236 cx.simulate_shared_keystroke("shift-p").await;
237 cx.assert_shared_state(indoc! {"
238 The quick brown
239 fox jumps ovejumps ˇor
240 the lazy dog"})
241 .await;
242
243 // line mode
244 cx.set_shared_state(indoc! {"
245 The quick brown
246 fox juˇmps over
247 the lazy dog"})
248 .await;
249 cx.simulate_shared_keystrokes(["d", "d"]).await;
250 cx.assert_shared_clipboard("fox jumps over\n").await;
251 cx.assert_shared_state(indoc! {"
252 The quick brown
253 the laˇzy dog"})
254 .await;
255 cx.simulate_shared_keystroke("p").await;
256 cx.assert_shared_state(indoc! {"
257 The quick brown
258 the lazy dog
259 ˇfox jumps over"})
260 .await;
261 cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
262 cx.assert_shared_state(indoc! {"
263 The quick brown
264 ˇfox jumps over
265 the lazy dog
266 fox jumps over"})
267 .await;
268
269 // multiline, cursor to first character of pasted text.
270 cx.set_shared_state(indoc! {"
271 The quick brown
272 fox jumps ˇover
273 the lazy dog"})
274 .await;
275 cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
276 cx.assert_shared_clipboard("over\nthe lazy do").await;
277
278 cx.simulate_shared_keystroke("p").await;
279 cx.assert_shared_state(indoc! {"
280 The quick brown
281 fox jumps oˇover
282 the lazy dover
283 the lazy dog"})
284 .await;
285 cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
286 cx.assert_shared_state(indoc! {"
287 The quick brown
288 fox jumps ˇover
289 the lazy doover
290 the lazy dog"})
291 .await;
292 }
293
294 #[gpui::test]
295 async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
296 let mut cx = NeovimBackedTestContext::new(cx).await;
297
298 // copy in visual mode
299 cx.set_shared_state(indoc! {"
300 The quick brown
301 fox jˇumps over
302 the lazy dog"})
303 .await;
304 cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
305 cx.assert_shared_state(indoc! {"
306 The quick brown
307 fox ˇjumps over
308 the lazy dog"})
309 .await;
310 // paste in visual mode
311 cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
312 .await;
313 cx.assert_shared_state(indoc! {"
314 The quick brown
315 fox jumps jumpˇs
316 the lazy dog"})
317 .await;
318 cx.assert_shared_clipboard("over").await;
319 // paste in visual line mode
320 cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
321 .await;
322 cx.assert_shared_state(indoc! {"
323 ˇover
324 fox jumps jumps
325 the lazy dog"})
326 .await;
327 cx.assert_shared_clipboard("over").await;
328 // paste in visual block mode
329 cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
330 .await;
331 cx.assert_shared_state(indoc! {"
332 oveˇrver
333 overox jumps jumps
334 overhe lazy dog"})
335 .await;
336
337 // copy in visual line mode
338 cx.set_shared_state(indoc! {"
339 The quick brown
340 fox juˇmps over
341 the lazy dog"})
342 .await;
343 cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
344 cx.assert_shared_state(indoc! {"
345 The quick brown
346 the laˇzy dog"})
347 .await;
348 // paste in visual mode
349 cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
350 cx.assert_shared_state(
351 &indoc! {"
352 The quick brown
353 the_
354 ˇfox jumps over
355 _dog"}
356 .replace("_", " "), // Hack for trailing whitespace
357 )
358 .await;
359 cx.assert_shared_clipboard("lazy").await;
360 cx.set_shared_state(indoc! {"
361 The quick brown
362 fox juˇmps over
363 the lazy dog"})
364 .await;
365 cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
366 cx.assert_shared_state(indoc! {"
367 The quick brown
368 the laˇzy dog"})
369 .await;
370 // paste in visual line mode
371 cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
372 cx.assert_shared_state(indoc! {"
373 ˇfox jumps over
374 the lazy dog"})
375 .await;
376 cx.assert_shared_clipboard("The quick brown\n").await;
377 }
378
379 #[gpui::test]
380 async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
381 let mut cx = NeovimBackedTestContext::new(cx).await;
382 // copy in visual block mode
383 cx.set_shared_state(indoc! {"
384 The ˇquick brown
385 fox jumps over
386 the lazy dog"})
387 .await;
388 cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
389 .await;
390 cx.assert_shared_clipboard("q\nj\nl").await;
391 cx.simulate_shared_keystrokes(["p"]).await;
392 cx.assert_shared_state(indoc! {"
393 The qˇquick brown
394 fox jjumps over
395 the llazy dog"})
396 .await;
397 cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
398 .await;
399 cx.assert_shared_state(indoc! {"
400 The ˇq brown
401 fox jjjumps over
402 the lllazy dog"})
403 .await;
404 cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
405 .await;
406
407 cx.set_shared_state(indoc! {"
408 The ˇquick brown
409 fox jumps over
410 the lazy dog"})
411 .await;
412 cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
413 cx.assert_shared_clipboard("q\nj").await;
414 cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
415 .await;
416 cx.assert_shared_state(indoc! {"
417 The qˇqick brown
418 fox jjmps over
419 the lzy dog"})
420 .await;
421
422 cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
423 cx.assert_shared_state(indoc! {"
424 ˇq
425 j
426 fox jjmps over
427 the lzy dog"})
428 .await;
429 }
430
431 #[gpui::test]
432 async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
433 let mut cx = VimTestContext::new_typescript(cx).await;
434
435 cx.set_state(
436 indoc! {"
437 class A {ˇ
438 }
439 "},
440 Mode::Normal,
441 );
442 cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
443 cx.assert_state(
444 indoc! {"
445 class A {
446 a()ˇ{}
447 }
448 "},
449 Mode::Normal,
450 );
451 // cursor goes to the first non-blank character in the line;
452 cx.simulate_keystrokes(["y", "y", "p"]);
453 cx.assert_state(
454 indoc! {"
455 class A {
456 a(){}
457 ˇa(){}
458 }
459 "},
460 Mode::Normal,
461 );
462 // indentation is preserved when pasting
463 cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
464 cx.assert_state(
465 indoc! {"
466 ˇclass A {
467 a(){}
468 class A {
469 a(){}
470 }
471 "},
472 Mode::Normal,
473 );
474 }
475}