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