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 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}