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}