paste.rs

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