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