paste.rs

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