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