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