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