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