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