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