paste.rs

  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(register) = Vim::update_globals(cx, |globals, cx| {
 37                    globals.read_register(selected_register, Some(editor), cx)
 38                })
 39                .filter(|reg| !reg.text.is_empty()) else {
 40                    return;
 41                };
 42                let text = register.text;
 43                let clipboard_selections = register.clipboard_selections;
 44
 45                let display_map = editor.display_snapshot(cx);
 46                let current_selections = editor.selections.all_adjusted_display(&display_map);
 47
 48                // The clipboard can have multiple selections, and there can
 49                // be multiple selections. Helix zips them together, so the first
 50                // clipboard entry gets pasted at the first selection, the second
 51                // entry gets pasted at the second selection, and so on. If there
 52                // are more clipboard selections than selections, the extra ones
 53                // don't get pasted anywhere. If there are more selections than
 54                // clipboard selections, the last clipboard selection gets
 55                // pasted at all remaining selections.
 56
 57                let mut edits = Vec::new();
 58                let mut new_selections = Vec::new();
 59                let mut start_offset = 0;
 60
 61                let mut replacement_texts: Vec<String> = Vec::new();
 62
 63                for ix in 0..current_selections.len() {
 64                    let to_insert = if let Some(clip_sel) =
 65                        clipboard_selections.as_ref().and_then(|s| s.get(ix))
 66                    {
 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                        // In Helix, a single-point cursor is "on top" of a
106                        // character, and pasting after means after that character.
107                        // At line end this means the next line. But on an empty
108                        // line there is no character, so paste at the cursor.
109                        let right = movement::right(&display_map, sel.end);
110                        if right.row() != sel.end.row() && sel.end.column() == 0 {
111                            sel.end
112                        } else {
113                            right
114                        }
115                    } else {
116                        sel.end
117                    };
118                    let point = display_point.to_point(&display_map);
119                    let anchor = if action.before {
120                        display_map.buffer_snapshot().anchor_after(point)
121                    } else {
122                        display_map.buffer_snapshot().anchor_before(point)
123                    };
124                    edits.push((point..point, to_insert.repeat(count)));
125                    new_selections.push((anchor, to_insert.len() * count));
126                }
127
128                editor.edit(edits, cx);
129
130                let snapshot = editor.buffer().read(cx).snapshot(cx);
131                editor.change_selections(Default::default(), window, cx, |s| {
132                    s.select_ranges(new_selections.into_iter().map(|(anchor, len)| {
133                        let offset = anchor.to_offset(&snapshot);
134                        if action.before {
135                            offset.saturating_sub_usize(len)..offset
136                        } else {
137                            offset..(offset + len)
138                        }
139                    }));
140                })
141            });
142        });
143
144        self.switch_mode(Mode::HelixNormal, true, window, cx);
145    }
146}
147
148#[cfg(test)]
149mod test {
150    use indoc::indoc;
151
152    use gpui::ClipboardItem;
153
154    use crate::{state::Mode, test::VimTestContext};
155
156    #[gpui::test]
157    async fn test_system_clipboard_paste(cx: &mut gpui::TestAppContext) {
158        let mut cx = VimTestContext::new(cx, true).await;
159        cx.enable_helix();
160        cx.set_state(
161            indoc! {"
162            The quiˇck brown
163            fox jumps over
164            the lazy dog."},
165            Mode::HelixNormal,
166        );
167
168        cx.write_to_clipboard(ClipboardItem::new_string("clipboard".to_string()));
169        cx.simulate_keystrokes("p");
170        cx.assert_state(
171            indoc! {"
172            The quic«clipboardˇ»k brown
173            fox jumps over
174            the lazy dog."},
175            Mode::HelixNormal,
176        );
177
178        // Multiple cursors with system clipboard (no metadata) pastes
179        // the same text at each cursor.
180        cx.set_state(
181            indoc! {"
182            ˇThe quick brown
183            fox ˇjumps over
184            the lazy dog."},
185            Mode::HelixNormal,
186        );
187        cx.write_to_clipboard(ClipboardItem::new_string("hi".to_string()));
188        cx.simulate_keystrokes("p");
189        cx.assert_state(
190            indoc! {"
191            T«hiˇ»he quick brown
192            fox j«hiˇ»umps over
193            the lazy dog."},
194            Mode::HelixNormal,
195        );
196
197        // Multiple cursors on empty lines should paste on those same lines.
198        cx.set_state("ˇ\nˇ\nˇ\nend", Mode::HelixNormal);
199        cx.write_to_clipboard(ClipboardItem::new_string("X".to_string()));
200        cx.simulate_keystrokes("p");
201        cx.assert_state("«Xˇ»\n«Xˇ»\n«Xˇ»\nend", Mode::HelixNormal);
202    }
203
204    #[gpui::test]
205    async fn test_paste(cx: &mut gpui::TestAppContext) {
206        let mut cx = VimTestContext::new(cx, true).await;
207        cx.enable_helix();
208        cx.set_state(
209            indoc! {"
210            The «quiˇ»ck brown
211            fox jumps over
212            the lazy dog."},
213            Mode::HelixNormal,
214        );
215
216        cx.simulate_keystrokes("y w p");
217
218        cx.assert_state(
219            indoc! {"
220            The quick «quiˇ»brown
221            fox jumps over
222            the lazy dog."},
223            Mode::HelixNormal,
224        );
225
226        // Pasting before the selection:
227        cx.set_state(
228            indoc! {"
229            The quick brown
230            fox «jumpsˇ» over
231            the lazy dog."},
232            Mode::HelixNormal,
233        );
234        cx.simulate_keystrokes("shift-p");
235        cx.assert_state(
236            indoc! {"
237            The quick brown
238            fox «quiˇ»jumps over
239            the lazy dog."},
240            Mode::HelixNormal,
241        );
242    }
243
244    #[gpui::test]
245    async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) {
246        let mut cx = VimTestContext::new(cx, true).await;
247        cx.enable_helix();
248        cx.set_state(
249            indoc! {"
250            The quiˇck brown
251            fox jumps over
252            the lazy dog."},
253            Mode::HelixNormal,
254        );
255
256        cx.simulate_keystrokes("y");
257
258        // Pasting before the selection:
259        cx.set_state(
260            indoc! {"
261            The quick brown
262            fox jumpsˇ over
263            the lazy dog."},
264            Mode::HelixNormal,
265        );
266        cx.simulate_keystrokes("shift-p");
267        cx.assert_state(
268            indoc! {"
269            The quick brown
270            fox jumps«cˇ» over
271            the lazy dog."},
272            Mode::HelixNormal,
273        );
274
275        // Pasting after the selection:
276        cx.set_state(
277            indoc! {"
278            The quick brown
279            fox jumpsˇ over
280            the lazy dog."},
281            Mode::HelixNormal,
282        );
283        cx.simulate_keystrokes("p");
284        cx.assert_state(
285            indoc! {"
286            The quick brown
287            fox jumps «cˇ»over
288            the lazy dog."},
289            Mode::HelixNormal,
290        );
291
292        // Pasting after the selection at the end of a line:
293        cx.set_state(
294            indoc! {"
295            The quick brown
296            fox jumps overˇ
297            the lazy dog."},
298            Mode::HelixNormal,
299        );
300        cx.simulate_keystrokes("p");
301        cx.assert_state(
302            indoc! {"
303            The quick brown
304            fox jumps over
305            «cˇ»the lazy dog."},
306            Mode::HelixNormal,
307        );
308    }
309
310    #[gpui::test]
311    async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) {
312        let mut cx = VimTestContext::new(cx, true).await;
313        cx.enable_helix();
314        // Select two blocks of text.
315        cx.set_state(
316            indoc! {"
317            The «quiˇ»ck brown
318            fox ju«mpsˇ» over
319            the lazy dog."},
320            Mode::HelixNormal,
321        );
322        cx.simulate_keystrokes("y");
323
324        // Only one cursor: only the first block gets pasted.
325        cx.set_state(
326            indoc! {"
327            ˇThe quick brown
328            fox jumps over
329            the lazy dog."},
330            Mode::HelixNormal,
331        );
332        cx.simulate_keystrokes("shift-p");
333        cx.assert_state(
334            indoc! {"
335            «quiˇ»The quick brown
336            fox jumps over
337            the lazy dog."},
338            Mode::HelixNormal,
339        );
340
341        // Two cursors: both get pasted.
342        cx.set_state(
343            indoc! {"
344            ˇThe ˇquick brown
345            fox jumps over
346            the lazy dog."},
347            Mode::HelixNormal,
348        );
349        cx.simulate_keystrokes("shift-p");
350        cx.assert_state(
351            indoc! {"
352            «quiˇ»The «mpsˇ»quick brown
353            fox jumps over
354            the lazy dog."},
355            Mode::HelixNormal,
356        );
357
358        // Three cursors: the second yanked block is duplicated.
359        cx.set_state(
360            indoc! {"
361            ˇThe ˇquick brown
362            fox jumpsˇ over
363            the lazy dog."},
364            Mode::HelixNormal,
365        );
366        cx.simulate_keystrokes("shift-p");
367        cx.assert_state(
368            indoc! {"
369            «quiˇ»The «mpsˇ»quick brown
370            fox jumps«mpsˇ» over
371            the lazy dog."},
372            Mode::HelixNormal,
373        );
374
375        // Again with three cursors. All three should be pasted twice.
376        cx.set_state(
377            indoc! {"
378            ˇThe ˇquick brown
379            fox jumpsˇ over
380            the lazy dog."},
381            Mode::HelixNormal,
382        );
383        cx.simulate_keystrokes("2 shift-p");
384        cx.assert_state(
385            indoc! {"
386            «quiquiˇ»The «mpsmpsˇ»quick brown
387            fox jumps«mpsmpsˇ» over
388            the lazy dog."},
389            Mode::HelixNormal,
390        );
391    }
392
393    #[gpui::test]
394    async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) {
395        let mut cx = VimTestContext::new(cx, true).await;
396        cx.enable_helix();
397        cx.set_state(
398            indoc! {"
399            The quick brow«n
400            ˇ»fox jumps over
401            the lazy dog."},
402            Mode::HelixNormal,
403        );
404
405        cx.simulate_keystrokes("y shift-p");
406
407        cx.assert_state(
408            indoc! {"
409            «n
410            ˇ»The quick brown
411            fox jumps over
412            the lazy dog."},
413            Mode::HelixNormal,
414        );
415
416        // In line mode, if we're in the middle of a line then pasting before pastes on
417        // the line before.
418        cx.set_state(
419            indoc! {"
420            The quick brown
421            fox jumpsˇ over
422            the lazy dog."},
423            Mode::HelixNormal,
424        );
425        cx.simulate_keystrokes("shift-p");
426        cx.assert_state(
427            indoc! {"
428            The quick brown
429            «n
430            ˇ»fox jumps over
431            the lazy dog."},
432            Mode::HelixNormal,
433        );
434
435        // In line mode, if we're in the middle of a line then pasting after pastes on
436        // the line after.
437        cx.set_state(
438            indoc! {"
439            The quick brown
440            fox jumpsˇ over
441            the lazy dog."},
442            Mode::HelixNormal,
443        );
444        cx.simulate_keystrokes("p");
445        cx.assert_state(
446            indoc! {"
447            The quick brown
448            fox jumps over
449            «n
450            ˇ»the lazy dog."},
451            Mode::HelixNormal,
452        );
453
454        // If we're currently at the end of a line, "the line after"
455        // means right after the cursor.
456        cx.set_state(
457            indoc! {"
458            The quick brown
459            fox jumps overˇ
460            the lazy dog."},
461            Mode::HelixNormal,
462        );
463        cx.simulate_keystrokes("p");
464        cx.assert_state(
465            indoc! {"
466            The quick brown
467            fox jumps over
468            «n
469            ˇ»the lazy dog."},
470            Mode::HelixNormal,
471        );
472
473        cx.set_state(
474            indoc! {"
475
476            The quick brown
477            fox jumps overˇ
478            the lazy dog."},
479            Mode::HelixNormal,
480        );
481        cx.simulate_keystrokes("x y up up p");
482        cx.assert_state(
483            indoc! {"
484
485            «fox jumps over
486            ˇ»The quick brown
487            fox jumps over
488            the lazy dog."},
489            Mode::HelixNormal,
490        );
491
492        cx.set_state(
493            indoc! {"
494            «The quick brown
495            fox jumps over
496            ˇ»the lazy dog."},
497            Mode::HelixNormal,
498        );
499        cx.simulate_keystrokes("y p p");
500        cx.assert_state(
501            indoc! {"
502            The quick brown
503            fox jumps over
504            The quick brown
505            fox jumps over
506            «The quick brown
507            fox jumps over
508            ˇ»the lazy dog."},
509            Mode::HelixNormal,
510        );
511    }
512}