1use editor::{ToOffset, movement};
2use gpui::{Action, Context, Window};
3use schemars::JsonSchema;
4use serde::Deserialize;
5
6use crate::{Vim, state::Mode};
7
8/// Pastes text from the specified register at the cursor position.
9#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
10#[action(namespace = vim)]
11#[serde(deny_unknown_fields)]
12pub struct HelixPaste {
13 #[serde(default)]
14 before: bool,
15}
16
17impl Vim {
18 pub fn helix_paste(
19 &mut self,
20 action: &HelixPaste,
21 window: &mut Window,
22 cx: &mut Context<Self>,
23 ) {
24 self.record_current_action(cx);
25 self.store_visual_marks(window, cx);
26 let count = Vim::take_count(cx).unwrap_or(1);
27 // TODO: vim paste calls take_forced_motion here, but I don't know what that does
28 // (none of the other helix_ methods call it)
29
30 self.update_editor(cx, |vim, editor, cx| {
31 editor.transact(window, cx, |editor, window, cx| {
32 editor.set_clip_at_line_ends(false, cx);
33
34 let selected_register = vim.selected_register.take();
35
36 let Some(register) = Vim::update_globals(cx, |globals, cx| {
37 globals.read_register(selected_register, Some(editor), cx)
38 })
39 .filter(|reg| !reg.text.is_empty()) else {
40 return;
41 };
42 let text = register.text;
43 let clipboard_selections = register.clipboard_selections;
44
45 let display_map = editor.display_snapshot(cx);
46 let current_selections = editor.selections.all_adjusted_display(&display_map);
47
48 // The clipboard can have multiple selections, and there can
49 // be multiple selections. Helix zips them together, so the first
50 // clipboard entry gets pasted at the first selection, the second
51 // entry gets pasted at the second selection, and so on. If there
52 // are more clipboard selections than selections, the extra ones
53 // don't get pasted anywhere. If there are more selections than
54 // clipboard selections, the last clipboard selection gets
55 // pasted at all remaining selections.
56
57 let mut edits = Vec::new();
58 let mut new_selections = Vec::new();
59 let mut start_offset = 0;
60
61 let mut replacement_texts: Vec<String> = Vec::new();
62
63 for ix in 0..current_selections.len() {
64 let to_insert = if let Some(clip_sel) =
65 clipboard_selections.as_ref().and_then(|s| s.get(ix))
66 {
67 let end_offset = start_offset + clip_sel.len;
68 let text = text[start_offset..end_offset].to_string();
69 start_offset = if clip_sel.is_entire_line {
70 end_offset
71 } else {
72 end_offset + 1
73 };
74 text
75 } else if let Some(last_text) = replacement_texts.last() {
76 // We have more current selections than clipboard selections: repeat the last one.
77 last_text.to_owned()
78 } else {
79 text.to_string()
80 };
81 replacement_texts.push(to_insert);
82 }
83
84 let line_mode = replacement_texts.iter().any(|text| text.ends_with('\n'));
85
86 for (to_insert, sel) in replacement_texts.into_iter().zip(current_selections) {
87 // Helix doesn't care about the head/tail of the selection.
88 // Pasting before means pasting before the whole selection.
89 let display_point = if line_mode {
90 if action.before {
91 movement::line_beginning(&display_map, sel.start, false)
92 } else {
93 if sel.start == sel.end {
94 movement::right(
95 &display_map,
96 movement::line_end(&display_map, sel.end, false),
97 )
98 } else {
99 sel.end
100 }
101 }
102 } else if action.before {
103 sel.start
104 } else if sel.start == sel.end {
105 // In Helix, a single-point cursor is "on top" of a
106 // character, and pasting after means after that character.
107 // At line end this means the next line. But on an empty
108 // line there is no character, so paste at the cursor.
109 let right = movement::right(&display_map, sel.end);
110 if right.row() != sel.end.row() && sel.end.column() == 0 {
111 sel.end
112 } else {
113 right
114 }
115 } else {
116 sel.end
117 };
118 let point = display_point.to_point(&display_map);
119 let anchor = if action.before {
120 display_map.buffer_snapshot().anchor_after(point)
121 } else {
122 display_map.buffer_snapshot().anchor_before(point)
123 };
124 edits.push((point..point, to_insert.repeat(count)));
125 new_selections.push((anchor, to_insert.len() * count));
126 }
127
128 editor.edit(edits, cx);
129
130 let snapshot = editor.buffer().read(cx).snapshot(cx);
131 editor.change_selections(Default::default(), window, cx, |s| {
132 s.select_ranges(new_selections.into_iter().map(|(anchor, len)| {
133 let offset = anchor.to_offset(&snapshot);
134 if action.before {
135 offset.saturating_sub_usize(len)..offset
136 } else {
137 offset..(offset + len)
138 }
139 }));
140 })
141 });
142 });
143
144 self.switch_mode(Mode::HelixNormal, true, window, cx);
145 }
146}
147
148#[cfg(test)]
149mod test {
150 use indoc::indoc;
151
152 use gpui::ClipboardItem;
153
154 use crate::{state::Mode, test::VimTestContext};
155
156 #[gpui::test]
157 async fn test_system_clipboard_paste(cx: &mut gpui::TestAppContext) {
158 let mut cx = VimTestContext::new(cx, true).await;
159 cx.enable_helix();
160 cx.set_state(
161 indoc! {"
162 The quiˇck brown
163 fox jumps over
164 the lazy dog."},
165 Mode::HelixNormal,
166 );
167
168 cx.write_to_clipboard(ClipboardItem::new_string("clipboard".to_string()));
169 cx.simulate_keystrokes("p");
170 cx.assert_state(
171 indoc! {"
172 The quic«clipboardˇ»k brown
173 fox jumps over
174 the lazy dog."},
175 Mode::HelixNormal,
176 );
177
178 // Multiple cursors with system clipboard (no metadata) pastes
179 // the same text at each cursor.
180 cx.set_state(
181 indoc! {"
182 ˇThe quick brown
183 fox ˇjumps over
184 the lazy dog."},
185 Mode::HelixNormal,
186 );
187 cx.write_to_clipboard(ClipboardItem::new_string("hi".to_string()));
188 cx.simulate_keystrokes("p");
189 cx.assert_state(
190 indoc! {"
191 T«hiˇ»he quick brown
192 fox j«hiˇ»umps over
193 the lazy dog."},
194 Mode::HelixNormal,
195 );
196
197 // Multiple cursors on empty lines should paste on those same lines.
198 cx.set_state("ˇ\nˇ\nˇ\nend", Mode::HelixNormal);
199 cx.write_to_clipboard(ClipboardItem::new_string("X".to_string()));
200 cx.simulate_keystrokes("p");
201 cx.assert_state("«Xˇ»\n«Xˇ»\n«Xˇ»\nend", Mode::HelixNormal);
202 }
203
204 #[gpui::test]
205 async fn test_paste(cx: &mut gpui::TestAppContext) {
206 let mut cx = VimTestContext::new(cx, true).await;
207 cx.enable_helix();
208 cx.set_state(
209 indoc! {"
210 The «quiˇ»ck brown
211 fox jumps over
212 the lazy dog."},
213 Mode::HelixNormal,
214 );
215
216 cx.simulate_keystrokes("y w p");
217
218 cx.assert_state(
219 indoc! {"
220 The quick «quiˇ»brown
221 fox jumps over
222 the lazy dog."},
223 Mode::HelixNormal,
224 );
225
226 // Pasting before the selection:
227 cx.set_state(
228 indoc! {"
229 The quick brown
230 fox «jumpsˇ» over
231 the lazy dog."},
232 Mode::HelixNormal,
233 );
234 cx.simulate_keystrokes("shift-p");
235 cx.assert_state(
236 indoc! {"
237 The quick brown
238 fox «quiˇ»jumps over
239 the lazy dog."},
240 Mode::HelixNormal,
241 );
242 }
243
244 #[gpui::test]
245 async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) {
246 let mut cx = VimTestContext::new(cx, true).await;
247 cx.enable_helix();
248 cx.set_state(
249 indoc! {"
250 The quiˇck brown
251 fox jumps over
252 the lazy dog."},
253 Mode::HelixNormal,
254 );
255
256 cx.simulate_keystrokes("y");
257
258 // Pasting before the selection:
259 cx.set_state(
260 indoc! {"
261 The quick brown
262 fox jumpsˇ over
263 the lazy dog."},
264 Mode::HelixNormal,
265 );
266 cx.simulate_keystrokes("shift-p");
267 cx.assert_state(
268 indoc! {"
269 The quick brown
270 fox jumps«cˇ» over
271 the lazy dog."},
272 Mode::HelixNormal,
273 );
274
275 // Pasting after the selection:
276 cx.set_state(
277 indoc! {"
278 The quick brown
279 fox jumpsˇ over
280 the lazy dog."},
281 Mode::HelixNormal,
282 );
283 cx.simulate_keystrokes("p");
284 cx.assert_state(
285 indoc! {"
286 The quick brown
287 fox jumps «cˇ»over
288 the lazy dog."},
289 Mode::HelixNormal,
290 );
291
292 // Pasting after the selection at the end of a line:
293 cx.set_state(
294 indoc! {"
295 The quick brown
296 fox jumps overˇ
297 the lazy dog."},
298 Mode::HelixNormal,
299 );
300 cx.simulate_keystrokes("p");
301 cx.assert_state(
302 indoc! {"
303 The quick brown
304 fox jumps over
305 «cˇ»the lazy dog."},
306 Mode::HelixNormal,
307 );
308 }
309
310 #[gpui::test]
311 async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) {
312 let mut cx = VimTestContext::new(cx, true).await;
313 cx.enable_helix();
314 // Select two blocks of text.
315 cx.set_state(
316 indoc! {"
317 The «quiˇ»ck brown
318 fox ju«mpsˇ» over
319 the lazy dog."},
320 Mode::HelixNormal,
321 );
322 cx.simulate_keystrokes("y");
323
324 // Only one cursor: only the first block gets pasted.
325 cx.set_state(
326 indoc! {"
327 ˇThe quick brown
328 fox jumps over
329 the lazy dog."},
330 Mode::HelixNormal,
331 );
332 cx.simulate_keystrokes("shift-p");
333 cx.assert_state(
334 indoc! {"
335 «quiˇ»The quick brown
336 fox jumps over
337 the lazy dog."},
338 Mode::HelixNormal,
339 );
340
341 // Two cursors: both get pasted.
342 cx.set_state(
343 indoc! {"
344 ˇThe ˇquick brown
345 fox jumps over
346 the lazy dog."},
347 Mode::HelixNormal,
348 );
349 cx.simulate_keystrokes("shift-p");
350 cx.assert_state(
351 indoc! {"
352 «quiˇ»The «mpsˇ»quick brown
353 fox jumps over
354 the lazy dog."},
355 Mode::HelixNormal,
356 );
357
358 // Three cursors: the second yanked block is duplicated.
359 cx.set_state(
360 indoc! {"
361 ˇThe ˇquick brown
362 fox jumpsˇ over
363 the lazy dog."},
364 Mode::HelixNormal,
365 );
366 cx.simulate_keystrokes("shift-p");
367 cx.assert_state(
368 indoc! {"
369 «quiˇ»The «mpsˇ»quick brown
370 fox jumps«mpsˇ» over
371 the lazy dog."},
372 Mode::HelixNormal,
373 );
374
375 // Again with three cursors. All three should be pasted twice.
376 cx.set_state(
377 indoc! {"
378 ˇThe ˇquick brown
379 fox jumpsˇ over
380 the lazy dog."},
381 Mode::HelixNormal,
382 );
383 cx.simulate_keystrokes("2 shift-p");
384 cx.assert_state(
385 indoc! {"
386 «quiquiˇ»The «mpsmpsˇ»quick brown
387 fox jumps«mpsmpsˇ» over
388 the lazy dog."},
389 Mode::HelixNormal,
390 );
391 }
392
393 #[gpui::test]
394 async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) {
395 let mut cx = VimTestContext::new(cx, true).await;
396 cx.enable_helix();
397 cx.set_state(
398 indoc! {"
399 The quick brow«n
400 ˇ»fox jumps over
401 the lazy dog."},
402 Mode::HelixNormal,
403 );
404
405 cx.simulate_keystrokes("y shift-p");
406
407 cx.assert_state(
408 indoc! {"
409 «n
410 ˇ»The quick brown
411 fox jumps over
412 the lazy dog."},
413 Mode::HelixNormal,
414 );
415
416 // In line mode, if we're in the middle of a line then pasting before pastes on
417 // the line before.
418 cx.set_state(
419 indoc! {"
420 The quick brown
421 fox jumpsˇ over
422 the lazy dog."},
423 Mode::HelixNormal,
424 );
425 cx.simulate_keystrokes("shift-p");
426 cx.assert_state(
427 indoc! {"
428 The quick brown
429 «n
430 ˇ»fox jumps over
431 the lazy dog."},
432 Mode::HelixNormal,
433 );
434
435 // In line mode, if we're in the middle of a line then pasting after pastes on
436 // the line after.
437 cx.set_state(
438 indoc! {"
439 The quick brown
440 fox jumpsˇ over
441 the lazy dog."},
442 Mode::HelixNormal,
443 );
444 cx.simulate_keystrokes("p");
445 cx.assert_state(
446 indoc! {"
447 The quick brown
448 fox jumps over
449 «n
450 ˇ»the lazy dog."},
451 Mode::HelixNormal,
452 );
453
454 // If we're currently at the end of a line, "the line after"
455 // means right after the cursor.
456 cx.set_state(
457 indoc! {"
458 The quick brown
459 fox jumps overˇ
460 the lazy dog."},
461 Mode::HelixNormal,
462 );
463 cx.simulate_keystrokes("p");
464 cx.assert_state(
465 indoc! {"
466 The quick brown
467 fox jumps over
468 «n
469 ˇ»the lazy dog."},
470 Mode::HelixNormal,
471 );
472
473 cx.set_state(
474 indoc! {"
475
476 The quick brown
477 fox jumps overˇ
478 the lazy dog."},
479 Mode::HelixNormal,
480 );
481 cx.simulate_keystrokes("x y up up p");
482 cx.assert_state(
483 indoc! {"
484
485 «fox jumps over
486 ˇ»The quick brown
487 fox jumps over
488 the lazy dog."},
489 Mode::HelixNormal,
490 );
491
492 cx.set_state(
493 indoc! {"
494 «The quick brown
495 fox jumps over
496 ˇ»the lazy dog."},
497 Mode::HelixNormal,
498 );
499 cx.simulate_keystrokes("y p p");
500 cx.assert_state(
501 indoc! {"
502 The quick brown
503 fox jumps over
504 The quick brown
505 fox jumps over
506 «The quick brown
507 fox jumps over
508 ˇ»the lazy dog."},
509 Mode::HelixNormal,
510 );
511 }
512}