paste.rs

  1use std::cmp;
  2
  3use editor::{
  4    display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint,
  5};
  6use gpui::{impl_actions, AppContext, ViewContext};
  7use language::{Bias, SelectionGoal};
  8use serde::Deserialize;
  9use settings::Settings;
 10use workspace::Workspace;
 11
 12use crate::{state::Mode, utils::copy_selections_content, UseSystemClipboard, Vim, VimSettings};
 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 register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 26    workspace.register_action(paste);
 27}
 28
 29fn system_clipboard_is_newer(vim: &Vim, cx: &mut AppContext) -> bool {
 30    cx.read_from_clipboard().is_some_and(|item| {
 31        if let Some(last_state) = vim.workspace_state.registers.get(".system.") {
 32            last_state != item.text()
 33        } else {
 34            true
 35        }
 36    })
 37}
 38
 39fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
 40    Vim::update(cx, |vim, cx| {
 41        vim.record_current_action(cx);
 42        vim.update_active_editor(cx, |vim, editor, cx| {
 43            let text_layout_details = editor.text_layout_details(cx);
 44            editor.transact(cx, |editor, cx| {
 45                editor.set_clip_at_line_ends(false, cx);
 46
 47                let (clipboard_text, clipboard_selections): (String, Option<_>) =
 48                    if VimSettings::get_global(cx).use_system_clipboard == UseSystemClipboard::Never
 49                        || VimSettings::get_global(cx).use_system_clipboard
 50                            == UseSystemClipboard::OnYank
 51                            && !system_clipboard_is_newer(vim, cx)
 52                    {
 53                        (
 54                            vim.workspace_state
 55                                .registers
 56                                .get("\"")
 57                                .cloned()
 58                                .unwrap_or_else(|| "".to_string()),
 59                            None,
 60                        )
 61                    } else {
 62                        if let Some(item) = cx.read_from_clipboard() {
 63                            let clipboard_selections = item
 64                                .metadata::<Vec<ClipboardSelection>>()
 65                                .filter(|clipboard_selections| {
 66                                    clipboard_selections.len() > 1
 67                                        && vim.state().mode != Mode::VisualLine
 68                                });
 69                            (item.text().clone(), clipboard_selections)
 70                        } else {
 71                            ("".into(), None)
 72                        }
 73                    };
 74
 75                if clipboard_text.is_empty() {
 76                    return;
 77                }
 78
 79                if !action.preserve_clipboard && vim.state().mode.is_visual() {
 80                    copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx);
 81                }
 82
 83                let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
 84
 85                // unlike zed, if you have a multi-cursor selection from vim block mode,
 86                // pasting it will paste it on subsequent lines, even if you don't yet
 87                // have a cursor there.
 88                let mut selections_to_process = Vec::new();
 89                let mut i = 0;
 90                while i < current_selections.len() {
 91                    selections_to_process
 92                        .push((current_selections[i].start..current_selections[i].end, true));
 93                    i += 1;
 94                }
 95                if let Some(clipboard_selections) = clipboard_selections.as_ref() {
 96                    let left = current_selections
 97                        .iter()
 98                        .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
 99                        .min()
100                        .unwrap();
101                    let mut row = current_selections.last().unwrap().end.row() + 1;
102                    while i < clipboard_selections.len() {
103                        let cursor =
104                            display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
105                        selections_to_process.push((cursor..cursor, false));
106                        i += 1;
107                        row += 1;
108                    }
109                }
110
111                let first_selection_indent_column =
112                    clipboard_selections.as_ref().and_then(|zed_selections| {
113                        zed_selections
114                            .first()
115                            .map(|selection| selection.first_line_indent)
116                    });
117                let before = action.before || vim.state().mode == Mode::VisualLine;
118
119                let mut edits = Vec::new();
120                let mut new_selections = Vec::new();
121                let mut original_indent_columns = Vec::new();
122                let mut start_offset = 0;
123
124                for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
125                    let (mut to_insert, original_indent_column) =
126                        if let Some(clipboard_selections) = &clipboard_selections {
127                            if let Some(clipboard_selection) = clipboard_selections.get(ix) {
128                                let end_offset = start_offset + clipboard_selection.len;
129                                let text = clipboard_text[start_offset..end_offset].to_string();
130                                start_offset = end_offset + 1;
131                                (text, Some(clipboard_selection.first_line_indent))
132                            } else {
133                                ("".to_string(), first_selection_indent_column)
134                            }
135                        } else {
136                            (clipboard_text.to_string(), first_selection_indent_column)
137                        };
138                    let line_mode = to_insert.ends_with('\n');
139                    let is_multiline = to_insert.contains('\n');
140
141                    if line_mode && !before {
142                        if selection.is_empty() {
143                            to_insert =
144                                "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
145                        } else {
146                            to_insert = "\n".to_owned() + &to_insert;
147                        }
148                    } else if !line_mode && vim.state().mode == Mode::VisualLine {
149                        to_insert = to_insert + "\n";
150                    }
151
152                    let display_range = if !selection.is_empty() {
153                        selection.start..selection.end
154                    } else if line_mode {
155                        let point = if before {
156                            movement::line_beginning(&display_map, selection.start, false)
157                        } else {
158                            movement::line_end(&display_map, selection.start, false)
159                        };
160                        point..point
161                    } else {
162                        let point = if before {
163                            selection.start
164                        } else {
165                            movement::saturating_right(&display_map, selection.start)
166                        };
167                        point..point
168                    };
169
170                    let point_range = display_range.start.to_point(&display_map)
171                        ..display_range.end.to_point(&display_map);
172                    let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
173                        display_map.buffer_snapshot.anchor_before(point_range.start)
174                    } else {
175                        display_map.buffer_snapshot.anchor_after(point_range.end)
176                    };
177
178                    if *preserve {
179                        new_selections.push((anchor, line_mode, is_multiline));
180                    }
181                    edits.push((point_range, to_insert));
182                    original_indent_columns.extend(original_indent_column);
183                }
184
185                editor.edit_with_block_indent(edits, original_indent_columns, cx);
186
187                // in line_mode vim will insert the new text on the next (or previous if before) line
188                // 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).
189                // otherwise vim will insert the next text at (or before) the current cursor position,
190                // the cursor will go to the last (or first, if is_multiline) inserted character.
191                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
192                    s.replace_cursors_with(|map| {
193                        let mut cursors = Vec::new();
194                        for (anchor, line_mode, is_multiline) in &new_selections {
195                            let mut cursor = anchor.to_display_point(map);
196                            if *line_mode {
197                                if !before {
198                                    cursor = movement::down(
199                                        map,
200                                        cursor,
201                                        SelectionGoal::None,
202                                        false,
203                                        &text_layout_details,
204                                    )
205                                    .0;
206                                }
207                                cursor = movement::indented_line_beginning(map, cursor, true);
208                            } else if !is_multiline {
209                                cursor = movement::saturating_left(map, cursor)
210                            }
211                            cursors.push(cursor);
212                            if vim.state().mode == Mode::VisualBlock {
213                                break;
214                            }
215                        }
216
217                        cursors
218                    });
219                })
220            });
221        });
222        vim.switch_mode(Mode::Normal, true, cx);
223    });
224}
225
226#[cfg(test)]
227mod test {
228    use crate::{
229        state::Mode,
230        test::{NeovimBackedTestContext, VimTestContext},
231        UseSystemClipboard, VimSettings,
232    };
233    use gpui::ClipboardItem;
234    use indoc::indoc;
235    use settings::SettingsStore;
236
237    #[gpui::test]
238    async fn test_paste(cx: &mut gpui::TestAppContext) {
239        let mut cx = NeovimBackedTestContext::new(cx).await;
240
241        // single line
242        cx.set_shared_state(indoc! {"
243            The quick brown
244            fox ˇjumps over
245            the lazy dog"})
246            .await;
247        cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
248        cx.assert_shared_clipboard("jumps o").await;
249        cx.set_shared_state(indoc! {"
250            The quick brown
251            fox jumps oveˇr
252            the lazy dog"})
253            .await;
254        cx.simulate_shared_keystroke("p").await;
255        cx.assert_shared_state(indoc! {"
256            The quick brown
257            fox jumps overjumps ˇo
258            the lazy dog"})
259            .await;
260
261        cx.set_shared_state(indoc! {"
262            The quick brown
263            fox jumps oveˇr
264            the lazy dog"})
265            .await;
266        cx.simulate_shared_keystroke("shift-p").await;
267        cx.assert_shared_state(indoc! {"
268            The quick brown
269            fox jumps ovejumps ˇor
270            the lazy dog"})
271            .await;
272
273        // line mode
274        cx.set_shared_state(indoc! {"
275            The quick brown
276            fox juˇmps over
277            the lazy dog"})
278            .await;
279        cx.simulate_shared_keystrokes(["d", "d"]).await;
280        cx.assert_shared_clipboard("fox jumps over\n").await;
281        cx.assert_shared_state(indoc! {"
282            The quick brown
283            the laˇzy dog"})
284            .await;
285        cx.simulate_shared_keystroke("p").await;
286        cx.assert_shared_state(indoc! {"
287            The quick brown
288            the lazy dog
289            ˇfox jumps over"})
290            .await;
291        cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
292        cx.assert_shared_state(indoc! {"
293            The quick brown
294            ˇfox jumps over
295            the lazy dog
296            fox jumps over"})
297            .await;
298
299        // multiline, cursor to first character of pasted text.
300        cx.set_shared_state(indoc! {"
301            The quick brown
302            fox jumps ˇover
303            the lazy dog"})
304            .await;
305        cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
306        cx.assert_shared_clipboard("over\nthe lazy do").await;
307
308        cx.simulate_shared_keystroke("p").await;
309        cx.assert_shared_state(indoc! {"
310            The quick brown
311            fox jumps oˇover
312            the lazy dover
313            the lazy dog"})
314            .await;
315        cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
316        cx.assert_shared_state(indoc! {"
317            The quick brown
318            fox jumps ˇover
319            the lazy doover
320            the lazy dog"})
321            .await;
322    }
323
324    #[gpui::test]
325    async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
326        let mut cx = VimTestContext::new(cx, true).await;
327
328        cx.update_global(|store: &mut SettingsStore, cx| {
329            store.update_user_settings::<VimSettings>(cx, |s| {
330                s.use_system_clipboard = Some(UseSystemClipboard::Never)
331            });
332        });
333
334        cx.set_state(
335            indoc! {"
336                The quick brown
337                fox jˇumps over
338                the lazy dog"},
339            Mode::Normal,
340        );
341        cx.simulate_keystrokes(["v", "i", "w", "y"]);
342        cx.assert_state(
343            indoc! {"
344                The quick brown
345                fox ˇjumps over
346                the lazy dog"},
347            Mode::Normal,
348        );
349        cx.simulate_keystroke("p");
350        cx.assert_state(
351            indoc! {"
352                The quick brown
353                fox jjumpˇsumps over
354                the lazy dog"},
355            Mode::Normal,
356        );
357        assert_eq!(cx.read_from_clipboard(), None);
358    }
359
360    #[gpui::test]
361    async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
362        let mut cx = VimTestContext::new(cx, true).await;
363
364        cx.update_global(|store: &mut SettingsStore, cx| {
365            store.update_user_settings::<VimSettings>(cx, |s| {
366                s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
367            });
368        });
369
370        // copy in visual mode
371        cx.set_state(
372            indoc! {"
373                The quick brown
374                fox jˇumps over
375                the lazy dog"},
376            Mode::Normal,
377        );
378        cx.simulate_keystrokes(["v", "i", "w", "y"]);
379        cx.assert_state(
380            indoc! {"
381                The quick brown
382                fox ˇjumps over
383                the lazy dog"},
384            Mode::Normal,
385        );
386        cx.simulate_keystroke("p");
387        cx.assert_state(
388            indoc! {"
389                The quick brown
390                fox jjumpˇsumps over
391                the lazy dog"},
392            Mode::Normal,
393        );
394        assert_eq!(
395            cx.read_from_clipboard().map(|item| item.text().clone()),
396            Some("jumps".into())
397        );
398        cx.simulate_keystrokes(["d", "d", "p"]);
399        cx.assert_state(
400            indoc! {"
401                The quick brown
402                the lazy dog
403                ˇfox jjumpsumps over"},
404            Mode::Normal,
405        );
406        assert_eq!(
407            cx.read_from_clipboard().map(|item| item.text().clone()),
408            Some("jumps".into())
409        );
410        cx.write_to_clipboard(ClipboardItem::new("test-copy".to_string()));
411        cx.simulate_keystroke("shift-p");
412        cx.assert_state(
413            indoc! {"
414                The quick brown
415                the lazy dog
416                test-copˇyfox jjumpsumps over"},
417            Mode::Normal,
418        );
419    }
420
421    #[gpui::test]
422    async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
423        let mut cx = NeovimBackedTestContext::new(cx).await;
424
425        // copy in visual mode
426        cx.set_shared_state(indoc! {"
427                The quick brown
428                fox jˇumps over
429                the lazy dog"})
430            .await;
431        cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
432        cx.assert_shared_state(indoc! {"
433                The quick brown
434                fox ˇjumps over
435                the lazy dog"})
436            .await;
437        // paste in visual mode
438        cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
439            .await;
440        cx.assert_shared_state(indoc! {"
441                The quick brown
442                fox jumps jumpˇs
443                the lazy dog"})
444            .await;
445        cx.assert_shared_clipboard("over").await;
446        // paste in visual line mode
447        cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
448            .await;
449        cx.assert_shared_state(indoc! {"
450            ˇover
451            fox jumps jumps
452            the lazy dog"})
453            .await;
454        cx.assert_shared_clipboard("over").await;
455        // paste in visual block mode
456        cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
457            .await;
458        cx.assert_shared_state(indoc! {"
459            oveˇrver
460            overox jumps jumps
461            overhe lazy dog"})
462            .await;
463
464        // copy in visual line mode
465        cx.set_shared_state(indoc! {"
466                The quick brown
467                fox juˇmps over
468                the lazy dog"})
469            .await;
470        cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
471        cx.assert_shared_state(indoc! {"
472                The quick brown
473                the laˇzy dog"})
474            .await;
475        // paste in visual mode
476        cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
477        cx.assert_shared_state(
478            &indoc! {"
479                The quick brown
480                the_
481                ˇfox jumps over
482                _dog"}
483            .replace('_', " "), // Hack for trailing whitespace
484        )
485        .await;
486        cx.assert_shared_clipboard("lazy").await;
487        cx.set_shared_state(indoc! {"
488            The quick brown
489            fox juˇmps over
490            the lazy dog"})
491            .await;
492        cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
493        cx.assert_shared_state(indoc! {"
494            The quick brown
495            the laˇzy dog"})
496            .await;
497        // paste in visual line mode
498        cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
499        cx.assert_shared_state(indoc! {"
500            ˇfox jumps over
501            the lazy dog"})
502            .await;
503        cx.assert_shared_clipboard("The quick brown\n").await;
504    }
505
506    #[gpui::test]
507    async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
508        let mut cx = NeovimBackedTestContext::new(cx).await;
509        // copy in visual block mode
510        cx.set_shared_state(indoc! {"
511            The ˇquick brown
512            fox jumps over
513            the lazy dog"})
514            .await;
515        cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
516            .await;
517        cx.assert_shared_clipboard("q\nj\nl").await;
518        cx.simulate_shared_keystrokes(["p"]).await;
519        cx.assert_shared_state(indoc! {"
520            The qˇquick brown
521            fox jjumps over
522            the llazy dog"})
523            .await;
524        cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
525            .await;
526        cx.assert_shared_state(indoc! {"
527            The ˇq brown
528            fox jjjumps over
529            the lllazy dog"})
530            .await;
531        cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
532            .await;
533
534        cx.set_shared_state(indoc! {"
535            The ˇquick brown
536            fox jumps over
537            the lazy dog"})
538            .await;
539        cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
540        cx.assert_shared_clipboard("q\nj").await;
541        cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
542            .await;
543        cx.assert_shared_state(indoc! {"
544            The qˇqick brown
545            fox jjmps over
546            the lzy dog"})
547            .await;
548
549        cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
550        cx.assert_shared_state(indoc! {"
551            ˇq
552            j
553            fox jjmps over
554            the lzy dog"})
555            .await;
556    }
557
558    #[gpui::test]
559    async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
560        let mut cx = VimTestContext::new_typescript(cx).await;
561
562        cx.set_state(
563            indoc! {"
564            class A {ˇ
565            }
566        "},
567            Mode::Normal,
568        );
569        cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
570        cx.assert_state(
571            indoc! {"
572            class A {
573                a()ˇ{}
574            }
575            "},
576            Mode::Normal,
577        );
578        // cursor goes to the first non-blank character in the line;
579        cx.simulate_keystrokes(["y", "y", "p"]);
580        cx.assert_state(
581            indoc! {"
582            class A {
583                a(){}
584                ˇa(){}
585            }
586            "},
587            Mode::Normal,
588        );
589        // indentation is preserved when pasting
590        cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
591        cx.assert_state(
592            indoc! {"
593                ˇclass A {
594                    a(){}
595                class A {
596                    a(){}
597                }
598                "},
599            Mode::Normal,
600        );
601    }
602}