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