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((text, clipboard_selections)) = Vim::update_globals(cx, |globals, cx| {
37 globals.read_register(selected_register, Some(editor), cx)
38 })
39 .and_then(|reg| {
40 (!reg.text.is_empty())
41 .then_some(reg.text)
42 .zip(reg.clipboard_selections)
43 }) else {
44 return;
45 };
46
47 let display_map = editor.display_snapshot(cx);
48 let current_selections = editor.selections.all_adjusted_display(&display_map);
49
50 // The clipboard can have multiple selections, and there can
51 // be multiple selections. Helix zips them together, so the first
52 // clipboard entry gets pasted at the first selection, the second
53 // entry gets pasted at the second selection, and so on. If there
54 // are more clipboard selections than selections, the extra ones
55 // don't get pasted anywhere. If there are more selections than
56 // clipboard selections, the last clipboard selection gets
57 // pasted at all remaining selections.
58
59 let mut edits = Vec::new();
60 let mut new_selections = Vec::new();
61 let mut start_offset = 0;
62
63 let mut replacement_texts: Vec<String> = Vec::new();
64
65 for ix in 0..current_selections.len() {
66 let to_insert = if let Some(clip_sel) = clipboard_selections.get(ix) {
67 let end_offset = start_offset + clip_sel.len;
68 let text = text[start_offset..end_offset].to_string();
69 start_offset = end_offset + 1;
70 text
71 } else if let Some(last_text) = replacement_texts.last() {
72 // We have more current selections than clipboard selections: repeat the last one.
73 last_text.to_owned()
74 } else {
75 text.to_string()
76 };
77 replacement_texts.push(to_insert);
78 }
79
80 let line_mode = replacement_texts.iter().any(|text| text.ends_with('\n'));
81
82 for (to_insert, sel) in replacement_texts.into_iter().zip(current_selections) {
83 // Helix doesn't care about the head/tail of the selection.
84 // Pasting before means pasting before the whole selection.
85 let display_point = if line_mode {
86 if action.before {
87 movement::line_beginning(&display_map, sel.start, false)
88 } else {
89 if sel.start == sel.end {
90 movement::right(
91 &display_map,
92 movement::line_end(&display_map, sel.end, false),
93 )
94 } else {
95 sel.end
96 }
97 }
98 } else if action.before {
99 sel.start
100 } else if sel.start == sel.end {
101 // Helix and Zed differ in how they understand
102 // single-point cursors. In Helix, a single-point cursor
103 // is "on top" of some character, and pasting after that
104 // cursor means that the pasted content should go after
105 // that character. (If the cursor is at the end of a
106 // line, the pasted content goes on the next line.)
107 movement::right(&display_map, sel.end)
108 } else {
109 sel.end
110 };
111 let point = display_point.to_point(&display_map);
112 let anchor = if action.before {
113 display_map.buffer_snapshot().anchor_after(point)
114 } else {
115 display_map.buffer_snapshot().anchor_before(point)
116 };
117 edits.push((point..point, to_insert.repeat(count)));
118 new_selections.push((anchor, to_insert.len() * count));
119 }
120
121 editor.edit(edits, cx);
122
123 editor.change_selections(Default::default(), window, cx, |s| {
124 let snapshot = s.buffer().clone();
125 s.select_ranges(new_selections.into_iter().map(|(anchor, len)| {
126 let offset = anchor.to_offset(&snapshot);
127 if action.before {
128 offset.saturating_sub(len)..offset
129 } else {
130 offset..(offset + len)
131 }
132 }));
133 })
134 });
135 });
136
137 self.switch_mode(Mode::HelixNormal, true, window, cx);
138 }
139}
140
141#[cfg(test)]
142mod test {
143 use indoc::indoc;
144
145 use crate::{state::Mode, test::VimTestContext};
146
147 #[gpui::test]
148 async fn test_paste(cx: &mut gpui::TestAppContext) {
149 let mut cx = VimTestContext::new(cx, true).await;
150 cx.enable_helix();
151 cx.set_state(
152 indoc! {"
153 The «quiˇ»ck brown
154 fox jumps over
155 the lazy dog."},
156 Mode::HelixNormal,
157 );
158
159 cx.simulate_keystrokes("y w p");
160
161 cx.assert_state(
162 indoc! {"
163 The quick «quiˇ»brown
164 fox jumps over
165 the lazy dog."},
166 Mode::HelixNormal,
167 );
168
169 // Pasting before the selection:
170 cx.set_state(
171 indoc! {"
172 The quick brown
173 fox «jumpsˇ» over
174 the lazy dog."},
175 Mode::HelixNormal,
176 );
177 cx.simulate_keystrokes("shift-p");
178 cx.assert_state(
179 indoc! {"
180 The quick brown
181 fox «quiˇ»jumps over
182 the lazy dog."},
183 Mode::HelixNormal,
184 );
185 }
186
187 #[gpui::test]
188 async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) {
189 let mut cx = VimTestContext::new(cx, true).await;
190 cx.enable_helix();
191 cx.set_state(
192 indoc! {"
193 The quiˇck brown
194 fox jumps over
195 the lazy dog."},
196 Mode::HelixNormal,
197 );
198
199 cx.simulate_keystrokes("y");
200
201 // Pasting before the selection:
202 cx.set_state(
203 indoc! {"
204 The quick brown
205 fox jumpsˇ over
206 the lazy dog."},
207 Mode::HelixNormal,
208 );
209 cx.simulate_keystrokes("shift-p");
210 cx.assert_state(
211 indoc! {"
212 The quick brown
213 fox jumps«cˇ» over
214 the lazy dog."},
215 Mode::HelixNormal,
216 );
217
218 // Pasting after the selection:
219 cx.set_state(
220 indoc! {"
221 The quick brown
222 fox jumpsˇ over
223 the lazy dog."},
224 Mode::HelixNormal,
225 );
226 cx.simulate_keystrokes("p");
227 cx.assert_state(
228 indoc! {"
229 The quick brown
230 fox jumps «cˇ»over
231 the lazy dog."},
232 Mode::HelixNormal,
233 );
234
235 // Pasting after the selection at the end of a line:
236 cx.set_state(
237 indoc! {"
238 The quick brown
239 fox jumps overˇ
240 the lazy dog."},
241 Mode::HelixNormal,
242 );
243 cx.simulate_keystrokes("p");
244 cx.assert_state(
245 indoc! {"
246 The quick brown
247 fox jumps over
248 «cˇ»the lazy dog."},
249 Mode::HelixNormal,
250 );
251 }
252
253 #[gpui::test]
254 async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) {
255 let mut cx = VimTestContext::new(cx, true).await;
256 cx.enable_helix();
257 // Select two blocks of text.
258 cx.set_state(
259 indoc! {"
260 The «quiˇ»ck brown
261 fox ju«mpsˇ» over
262 the lazy dog."},
263 Mode::HelixNormal,
264 );
265 cx.simulate_keystrokes("y");
266
267 // Only one cursor: only the first block gets pasted.
268 cx.set_state(
269 indoc! {"
270 ˇThe quick brown
271 fox jumps over
272 the lazy dog."},
273 Mode::HelixNormal,
274 );
275 cx.simulate_keystrokes("shift-p");
276 cx.assert_state(
277 indoc! {"
278 «quiˇ»The quick brown
279 fox jumps over
280 the lazy dog."},
281 Mode::HelixNormal,
282 );
283
284 // Two cursors: both get pasted.
285 cx.set_state(
286 indoc! {"
287 ˇThe ˇquick brown
288 fox jumps over
289 the lazy dog."},
290 Mode::HelixNormal,
291 );
292 cx.simulate_keystrokes("shift-p");
293 cx.assert_state(
294 indoc! {"
295 «quiˇ»The «mpsˇ»quick brown
296 fox jumps over
297 the lazy dog."},
298 Mode::HelixNormal,
299 );
300
301 // Three cursors: the second yanked block is duplicated.
302 cx.set_state(
303 indoc! {"
304 ˇThe ˇquick brown
305 fox jumpsˇ over
306 the lazy dog."},
307 Mode::HelixNormal,
308 );
309 cx.simulate_keystrokes("shift-p");
310 cx.assert_state(
311 indoc! {"
312 «quiˇ»The «mpsˇ»quick brown
313 fox jumps«mpsˇ» over
314 the lazy dog."},
315 Mode::HelixNormal,
316 );
317
318 // Again with three cursors. All three should be pasted twice.
319 cx.set_state(
320 indoc! {"
321 ˇThe ˇquick brown
322 fox jumpsˇ over
323 the lazy dog."},
324 Mode::HelixNormal,
325 );
326 cx.simulate_keystrokes("2 shift-p");
327 cx.assert_state(
328 indoc! {"
329 «quiquiˇ»The «mpsmpsˇ»quick brown
330 fox jumps«mpsmpsˇ» over
331 the lazy dog."},
332 Mode::HelixNormal,
333 );
334 }
335
336 #[gpui::test]
337 async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) {
338 let mut cx = VimTestContext::new(cx, true).await;
339 cx.enable_helix();
340 cx.set_state(
341 indoc! {"
342 The quick brow«n
343 ˇ»fox jumps over
344 the lazy dog."},
345 Mode::HelixNormal,
346 );
347
348 cx.simulate_keystrokes("y shift-p");
349
350 cx.assert_state(
351 indoc! {"
352 «n
353 ˇ»The quick brown
354 fox jumps over
355 the lazy dog."},
356 Mode::HelixNormal,
357 );
358
359 // In line mode, if we're in the middle of a line then pasting before pastes on
360 // the line before.
361 cx.set_state(
362 indoc! {"
363 The quick brown
364 fox jumpsˇ over
365 the lazy dog."},
366 Mode::HelixNormal,
367 );
368 cx.simulate_keystrokes("shift-p");
369 cx.assert_state(
370 indoc! {"
371 The quick brown
372 «n
373 ˇ»fox jumps over
374 the lazy dog."},
375 Mode::HelixNormal,
376 );
377
378 // In line mode, if we're in the middle of a line then pasting after pastes on
379 // the line after.
380 cx.set_state(
381 indoc! {"
382 The quick brown
383 fox jumpsˇ over
384 the lazy dog."},
385 Mode::HelixNormal,
386 );
387 cx.simulate_keystrokes("p");
388 cx.assert_state(
389 indoc! {"
390 The quick brown
391 fox jumps over
392 «n
393 ˇ»the lazy dog."},
394 Mode::HelixNormal,
395 );
396
397 // If we're currently at the end of a line, "the line after"
398 // means right after the cursor.
399 cx.set_state(
400 indoc! {"
401 The quick brown
402 fox jumps overˇ
403 the lazy dog."},
404 Mode::HelixNormal,
405 );
406 cx.simulate_keystrokes("p");
407 cx.assert_state(
408 indoc! {"
409 The quick brown
410 fox jumps over
411 «n
412 ˇ»the lazy dog."},
413 Mode::HelixNormal,
414 );
415
416 cx.set_state(
417 indoc! {"
418
419 The quick brown
420 fox jumps overˇ
421 the lazy dog."},
422 Mode::HelixNormal,
423 );
424 cx.simulate_keystrokes("x y up up p");
425 cx.assert_state(
426 indoc! {"
427
428 «fox jumps over
429 ˇ»The quick brown
430 fox jumps over
431 the lazy dog."},
432 Mode::HelixNormal,
433 );
434
435 cx.set_state(
436 indoc! {"
437 «The quick brown
438 fox jumps over
439 ˇ»the lazy dog."},
440 Mode::HelixNormal,
441 );
442 cx.simulate_keystrokes("y p p");
443 cx.assert_state(
444 indoc! {"
445 The quick brown
446 fox jumps over
447 The quick brown
448 fox jumps over
449 «The quick brown
450 fox jumps over
451 ˇ»the lazy dog."},
452 Mode::HelixNormal,
453 );
454 }
455}