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