paste.rs

  1use std::{borrow::Cow, cmp};
  2
  3use editor::{
  4    display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection,
  5    DisplayPoint,
  6};
  7use gpui::{impl_actions, AppContext, ViewContext};
  8use language::{Bias, SelectionGoal};
  9use serde::Deserialize;
 10use workspace::Workspace;
 11
 12use crate::{state::Mode, utils::copy_selections_content, Vim};
 13
 14#[derive(Clone, Deserialize, PartialEq)]
 15#[serde(rename_all = "camelCase")]
 16struct Paste {
 17    #[serde(default)]
 18    before: bool,
 19    #[serde(default)]
 20    preserve_clipboard: bool,
 21}
 22
 23impl_actions!(vim, [Paste]);
 24
 25pub(crate) fn init(cx: &mut AppContext) {
 26    cx.add_action(paste);
 27}
 28
 29fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
 30    Vim::update(cx, |vim, cx| {
 31        vim.update_active_editor(cx, |editor, cx| {
 32            editor.transact(cx, |editor, cx| {
 33                editor.set_clip_at_line_ends(false, cx);
 34
 35                let Some(item) = cx.read_from_clipboard() else {
 36                    return
 37                };
 38                let clipboard_text = Cow::Borrowed(item.text());
 39                if clipboard_text.is_empty() {
 40                    return;
 41                }
 42
 43                if !action.preserve_clipboard && vim.state().mode.is_visual() {
 44                    copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
 45                }
 46
 47                // if we are copying from multi-cursor (of visual block mode), we want
 48                // to
 49                let clipboard_selections =
 50                    item.metadata::<Vec<ClipboardSelection>>()
 51                        .filter(|clipboard_selections| {
 52                            clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
 53                        });
 54
 55                let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
 56
 57                // unlike zed, if you have a multi-cursor selection from vim block mode,
 58                // pasting it will paste it on subsequent lines, even if you don't yet
 59                // have a cursor there.
 60                let mut selections_to_process = Vec::new();
 61                let mut i = 0;
 62                while i < current_selections.len() {
 63                    selections_to_process
 64                        .push((current_selections[i].start..current_selections[i].end, true));
 65                    i += 1;
 66                }
 67                if let Some(clipboard_selections) = clipboard_selections.as_ref() {
 68                    let left = current_selections
 69                        .iter()
 70                        .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
 71                        .min()
 72                        .unwrap();
 73                    let mut row = current_selections.last().unwrap().end.row() + 1;
 74                    while i < clipboard_selections.len() {
 75                        let cursor =
 76                            display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
 77                        selections_to_process.push((cursor..cursor, false));
 78                        i += 1;
 79                        row += 1;
 80                    }
 81                }
 82
 83                let first_selection_indent_column =
 84                    clipboard_selections.as_ref().and_then(|zed_selections| {
 85                        zed_selections
 86                            .first()
 87                            .map(|selection| selection.first_line_indent)
 88                    });
 89                let before = action.before || vim.state().mode == Mode::VisualLine;
 90
 91                let mut edits = Vec::new();
 92                let mut new_selections = Vec::new();
 93                let mut original_indent_columns = Vec::new();
 94                let mut start_offset = 0;
 95
 96                for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
 97                    let (mut to_insert, original_indent_column) =
 98                        if let Some(clipboard_selections) = &clipboard_selections {
 99                            if let Some(clipboard_selection) = clipboard_selections.get(ix) {
100                                let end_offset = start_offset + clipboard_selection.len;
101                                let text = clipboard_text[start_offset..end_offset].to_string();
102                                start_offset = end_offset + 1;
103                                (text, Some(clipboard_selection.first_line_indent))
104                            } else {
105                                ("".to_string(), first_selection_indent_column)
106                            }
107                        } else {
108                            (clipboard_text.to_string(), first_selection_indent_column)
109                        };
110                    let line_mode = to_insert.ends_with("\n");
111                    let is_multiline = to_insert.contains("\n");
112
113                    if line_mode && !before {
114                        if selection.is_empty() {
115                            to_insert =
116                                "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
117                        } else {
118                            to_insert = "\n".to_owned() + &to_insert;
119                        }
120                    } else if !line_mode && vim.state().mode == Mode::VisualLine {
121                        to_insert = to_insert + "\n";
122                    }
123
124                    let display_range = if !selection.is_empty() {
125                        selection.start..selection.end
126                    } else if line_mode {
127                        let point = if before {
128                            movement::line_beginning(&display_map, selection.start, false)
129                        } else {
130                            movement::line_end(&display_map, selection.start, false)
131                        };
132                        point..point
133                    } else {
134                        let point = if before {
135                            selection.start
136                        } else {
137                            movement::saturating_right(&display_map, selection.start)
138                        };
139                        point..point
140                    };
141
142                    let point_range = display_range.start.to_point(&display_map)
143                        ..display_range.end.to_point(&display_map);
144                    let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
145                        display_map.buffer_snapshot.anchor_before(point_range.start)
146                    } else {
147                        display_map.buffer_snapshot.anchor_after(point_range.end)
148                    };
149
150                    if *preserve {
151                        new_selections.push((anchor, line_mode, is_multiline));
152                    }
153                    edits.push((point_range, to_insert));
154                    original_indent_columns.extend(original_indent_column);
155                }
156
157                editor.edit_with_block_indent(edits, original_indent_columns, cx);
158
159                // in line_mode vim will insert the new text on the next (or previous if before) line
160                // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
161                // otherwise vim will insert the next text at (or before) the current cursor position,
162                // the cursor will go to the last (or first, if is_multiline) inserted character.
163                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
164                    s.replace_cursors_with(|map| {
165                        let mut cursors = Vec::new();
166                        for (anchor, line_mode, is_multiline) in &new_selections {
167                            let mut cursor = anchor.to_display_point(map);
168                            if *line_mode {
169                                if !before {
170                                    cursor =
171                                        movement::down(map, cursor, SelectionGoal::None, false).0;
172                                }
173                                cursor = movement::indented_line_beginning(map, cursor, true);
174                            } else if !is_multiline {
175                                cursor = movement::saturating_left(map, cursor)
176                            }
177                            cursors.push(cursor);
178                            if vim.state().mode == Mode::VisualBlock {
179                                break;
180                            }
181                        }
182
183                        cursors
184                    });
185                })
186            });
187        });
188        vim.switch_mode(Mode::Normal, true, cx);
189    });
190}
191
192#[cfg(test)]
193mod test {
194    use crate::{
195        state::Mode,
196        test::{NeovimBackedTestContext, VimTestContext},
197    };
198    use indoc::indoc;
199
200    #[gpui::test]
201    async fn test_paste(cx: &mut gpui::TestAppContext) {
202        let mut cx = NeovimBackedTestContext::new(cx).await;
203
204        // single line
205        cx.set_shared_state(indoc! {"
206            The quick brown
207            fox ˇjumps over
208            the lazy dog"})
209            .await;
210        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
211        cx.assert_shared_clipboard("jumps o").await;
212        cx.set_shared_state(indoc! {"
213            The quick brown
214            fox jumps oveˇr
215            the lazy dog"})
216            .await;
217        cx.simulate_shared_keystroke("p").await;
218        cx.assert_shared_state(indoc! {"
219            The quick brown
220            fox jumps overjumps ˇo
221            the lazy dog"})
222            .await;
223
224        cx.set_shared_state(indoc! {"
225            The quick brown
226            fox jumps oveˇr
227            the lazy dog"})
228            .await;
229        cx.simulate_shared_keystroke("shift-p").await;
230        cx.assert_shared_state(indoc! {"
231            The quick brown
232            fox jumps ovejumps ˇor
233            the lazy dog"})
234            .await;
235
236        // line mode
237        cx.set_shared_state(indoc! {"
238            The quick brown
239            fox juˇmps over
240            the lazy dog"})
241            .await;
242        cx.simulate_shared_keystrokes(["d", "d"]).await;
243        cx.assert_shared_clipboard("fox jumps over\n").await;
244        cx.assert_shared_state(indoc! {"
245            The quick brown
246            the laˇzy dog"})
247            .await;
248        cx.simulate_shared_keystroke("p").await;
249        cx.assert_shared_state(indoc! {"
250            The quick brown
251            the lazy dog
252            ˇfox jumps over"})
253            .await;
254        cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
255        cx.assert_shared_state(indoc! {"
256            The quick brown
257            ˇfox jumps over
258            the lazy dog
259            fox jumps over"})
260            .await;
261
262        // multiline, cursor to first character of pasted text.
263        cx.set_shared_state(indoc! {"
264            The quick brown
265            fox jumps ˇover
266            the lazy dog"})
267            .await;
268        cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
269        cx.assert_shared_clipboard("over\nthe lazy do").await;
270
271        cx.simulate_shared_keystroke("p").await;
272        cx.assert_shared_state(indoc! {"
273            The quick brown
274            fox jumps oˇover
275            the lazy dover
276            the lazy dog"})
277            .await;
278        cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
279        cx.assert_shared_state(indoc! {"
280            The quick brown
281            fox jumps ˇover
282            the lazy doover
283            the lazy dog"})
284            .await;
285    }
286
287    #[gpui::test]
288    async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
289        let mut cx = NeovimBackedTestContext::new(cx).await;
290
291        // copy in visual mode
292        cx.set_shared_state(indoc! {"
293                The quick brown
294                fox jˇumps over
295                the lazy dog"})
296            .await;
297        cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
298        cx.assert_shared_state(indoc! {"
299                The quick brown
300                fox ˇjumps over
301                the lazy dog"})
302            .await;
303        // paste in visual mode
304        cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
305            .await;
306        cx.assert_shared_state(indoc! {"
307                The quick brown
308                fox jumps jumpˇs
309                the lazy dog"})
310            .await;
311        cx.assert_shared_clipboard("over").await;
312        // paste in visual line mode
313        cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
314            .await;
315        cx.assert_shared_state(indoc! {"
316            ˇover
317            fox jumps jumps
318            the lazy dog"})
319            .await;
320        cx.assert_shared_clipboard("over").await;
321        // paste in visual block mode
322        cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
323            .await;
324        cx.assert_shared_state(indoc! {"
325            oveˇrver
326            overox jumps jumps
327            overhe lazy dog"})
328            .await;
329
330        // copy in visual line mode
331        cx.set_shared_state(indoc! {"
332                The quick brown
333                fox juˇmps over
334                the lazy dog"})
335            .await;
336        cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
337        cx.assert_shared_state(indoc! {"
338                The quick brown
339                the laˇzy dog"})
340            .await;
341        // paste in visual mode
342        cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
343        cx.assert_shared_state(
344            &indoc! {"
345                The quick brown
346                the_
347                ˇfox jumps over
348                _dog"}
349            .replace("_", " "), // Hack for trailing whitespace
350        )
351        .await;
352        cx.assert_shared_clipboard("lazy").await;
353        cx.set_shared_state(indoc! {"
354            The quick brown
355            fox juˇmps over
356            the lazy dog"})
357            .await;
358        cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
359        cx.assert_shared_state(indoc! {"
360            The quick brown
361            the laˇzy dog"})
362            .await;
363        // paste in visual line mode
364        cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
365        cx.assert_shared_state(indoc! {"
366            ˇfox jumps over
367            the lazy dog"})
368            .await;
369        cx.assert_shared_clipboard("The quick brown\n").await;
370    }
371
372    #[gpui::test]
373    async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
374        let mut cx = NeovimBackedTestContext::new(cx).await;
375        // copy in visual block mode
376        cx.set_shared_state(indoc! {"
377            The ˇquick brown
378            fox jumps over
379            the lazy dog"})
380            .await;
381        cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
382            .await;
383        cx.assert_shared_clipboard("q\nj\nl").await;
384        cx.simulate_shared_keystrokes(["p"]).await;
385        cx.assert_shared_state(indoc! {"
386            The qˇquick brown
387            fox jjumps over
388            the llazy dog"})
389            .await;
390        cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
391            .await;
392        cx.assert_shared_state(indoc! {"
393            The ˇq brown
394            fox jjjumps over
395            the lllazy dog"})
396            .await;
397        cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
398            .await;
399
400        cx.set_shared_state(indoc! {"
401            The ˇquick brown
402            fox jumps over
403            the lazy dog"})
404            .await;
405        cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
406        cx.assert_shared_clipboard("q\nj").await;
407        cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
408            .await;
409        cx.assert_shared_state(indoc! {"
410            The qˇqick brown
411            fox jjmps over
412            the lzy dog"})
413            .await;
414
415        cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
416        cx.assert_shared_state(indoc! {"
417            ˇq
418            j
419            fox jjmps over
420            the lzy dog"})
421            .await;
422    }
423
424    #[gpui::test]
425    async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
426        let mut cx = VimTestContext::new_typescript(cx).await;
427
428        cx.set_state(
429            indoc! {"
430            class A {ˇ
431            }
432        "},
433            Mode::Normal,
434        );
435        cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
436        cx.assert_state(
437            indoc! {"
438            class A {
439                a()ˇ{}
440            }
441            "},
442            Mode::Normal,
443        );
444        // cursor goes to the first non-blank character in the line;
445        cx.simulate_keystrokes(["y", "y", "p"]);
446        cx.assert_state(
447            indoc! {"
448            class A {
449                a(){}
450                ˇa(){}
451            }
452            "},
453            Mode::Normal,
454        );
455        // indentation is preserved when pasting
456        cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
457        cx.assert_state(
458            indoc! {"
459                ˇclass A {
460                    a(){}
461                class A {
462                    a(){}
463                }
464                "},
465            Mode::Normal,
466        );
467    }
468}