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.shared_clipboard().await.assert_eq("jumps o");
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_keystrokes("p").await;
257        cx.shared_state().await.assert_eq(indoc! {"
258            The quick brown
259            fox jumps overjumps ˇo
260            the lazy dog"});
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_keystrokes("shift-p").await;
268        cx.shared_state().await.assert_eq(indoc! {"
269            The quick brown
270            fox jumps ovejumps ˇor
271            the lazy dog"});
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.shared_clipboard().await.assert_eq("fox jumps over\n");
281        cx.shared_state().await.assert_eq(indoc! {"
282            The quick brown
283            the laˇzy dog"});
284        cx.simulate_shared_keystrokes("p").await;
285        cx.shared_state().await.assert_eq(indoc! {"
286            The quick brown
287            the lazy dog
288            ˇfox jumps over"});
289        cx.simulate_shared_keystrokes("k shift-p").await;
290        cx.shared_state().await.assert_eq(indoc! {"
291            The quick brown
292            ˇfox jumps over
293            the lazy dog
294            fox jumps over"});
295
296        // multiline, cursor to first character of pasted text.
297        cx.set_shared_state(indoc! {"
298            The quick brown
299            fox jumps ˇover
300            the lazy dog"})
301            .await;
302        cx.simulate_shared_keystrokes("v j y").await;
303        cx.shared_clipboard().await.assert_eq("over\nthe lazy do");
304
305        cx.simulate_shared_keystrokes("p").await;
306        cx.shared_state().await.assert_eq(indoc! {"
307            The quick brown
308            fox jumps oˇover
309            the lazy dover
310            the lazy dog"});
311        cx.simulate_shared_keystrokes("u shift-p").await;
312        cx.shared_state().await.assert_eq(indoc! {"
313            The quick brown
314            fox jumps ˇover
315            the lazy doover
316            the lazy dog"});
317    }
318
319    #[gpui::test]
320    async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
321        let mut cx = VimTestContext::new(cx, true).await;
322
323        cx.update_global(|store: &mut SettingsStore, cx| {
324            store.update_user_settings::<VimSettings>(cx, |s| {
325                s.use_system_clipboard = Some(UseSystemClipboard::Never)
326            });
327        });
328
329        cx.set_state(
330            indoc! {"
331                The quick brown
332                fox jˇumps over
333                the lazy dog"},
334            Mode::Normal,
335        );
336        cx.simulate_keystrokes("v i w y");
337        cx.assert_state(
338            indoc! {"
339                The quick brown
340                fox ˇjumps over
341                the lazy dog"},
342            Mode::Normal,
343        );
344        cx.simulate_keystrokes("p");
345        cx.assert_state(
346            indoc! {"
347                The quick brown
348                fox jjumpˇsumps over
349                the lazy dog"},
350            Mode::Normal,
351        );
352        assert_eq!(cx.read_from_clipboard(), None);
353    }
354
355    #[gpui::test]
356    async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
357        let mut cx = VimTestContext::new(cx, true).await;
358
359        cx.update_global(|store: &mut SettingsStore, cx| {
360            store.update_user_settings::<VimSettings>(cx, |s| {
361                s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
362            });
363        });
364
365        // copy in visual mode
366        cx.set_state(
367            indoc! {"
368                The quick brown
369                fox jˇumps over
370                the lazy dog"},
371            Mode::Normal,
372        );
373        cx.simulate_keystrokes("v i w y");
374        cx.assert_state(
375            indoc! {"
376                The quick brown
377                fox ˇjumps over
378                the lazy dog"},
379            Mode::Normal,
380        );
381        cx.simulate_keystrokes("p");
382        cx.assert_state(
383            indoc! {"
384                The quick brown
385                fox jjumpˇsumps over
386                the lazy dog"},
387            Mode::Normal,
388        );
389        assert_eq!(
390            cx.read_from_clipboard().map(|item| item.text().clone()),
391            Some("jumps".into())
392        );
393        cx.simulate_keystrokes("d d p");
394        cx.assert_state(
395            indoc! {"
396                The quick brown
397                the lazy dog
398                ˇfox jjumpsumps over"},
399            Mode::Normal,
400        );
401        assert_eq!(
402            cx.read_from_clipboard().map(|item| item.text().clone()),
403            Some("jumps".into())
404        );
405        cx.write_to_clipboard(ClipboardItem::new("test-copy".to_string()));
406        cx.simulate_keystrokes("shift-p");
407        cx.assert_state(
408            indoc! {"
409                The quick brown
410                the lazy dog
411                test-copˇyfox jjumpsumps over"},
412            Mode::Normal,
413        );
414    }
415
416    #[gpui::test]
417    async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
418        let mut cx = NeovimBackedTestContext::new(cx).await;
419
420        // copy in visual mode
421        cx.set_shared_state(indoc! {"
422                The quick brown
423                fox jˇumps over
424                the lazy dog"})
425            .await;
426        cx.simulate_shared_keystrokes("v i w y").await;
427        cx.shared_state().await.assert_eq(indoc! {"
428                The quick brown
429                fox ˇjumps over
430                the lazy dog"});
431        // paste in visual mode
432        cx.simulate_shared_keystrokes("w v i w p").await;
433        cx.shared_state().await.assert_eq(indoc! {"
434                The quick brown
435                fox jumps jumpˇs
436                the lazy dog"});
437        cx.shared_clipboard().await.assert_eq("over");
438        // paste in visual line mode
439        cx.simulate_shared_keystrokes("up shift-v shift-p").await;
440        cx.shared_state().await.assert_eq(indoc! {"
441            ˇover
442            fox jumps jumps
443            the lazy dog"});
444        cx.shared_clipboard().await.assert_eq("over");
445        // paste in visual block mode
446        cx.simulate_shared_keystrokes("ctrl-v down down p").await;
447        cx.shared_state().await.assert_eq(indoc! {"
448            oveˇrver
449            overox jumps jumps
450            overhe lazy dog"});
451
452        // copy in visual line mode
453        cx.set_shared_state(indoc! {"
454                The quick brown
455                fox juˇmps over
456                the lazy dog"})
457            .await;
458        cx.simulate_shared_keystrokes("shift-v d").await;
459        cx.shared_state().await.assert_eq(indoc! {"
460                The quick brown
461                the laˇzy dog"});
462        // paste in visual mode
463        cx.simulate_shared_keystrokes("v i w p").await;
464        cx.shared_state().await.assert_eq(&indoc! {"
465                The quick brown
466                the•
467                ˇfox jumps over
468                 dog"});
469        cx.shared_clipboard().await.assert_eq("lazy");
470        cx.set_shared_state(indoc! {"
471            The quick brown
472            fox juˇmps over
473            the lazy dog"})
474            .await;
475        cx.simulate_shared_keystrokes("shift-v d").await;
476        cx.shared_state().await.assert_eq(indoc! {"
477            The quick brown
478            the laˇzy dog"});
479        // paste in visual line mode
480        cx.simulate_shared_keystrokes("k shift-v p").await;
481        cx.shared_state().await.assert_eq(indoc! {"
482            ˇfox jumps over
483            the lazy dog"});
484        cx.shared_clipboard().await.assert_eq("The quick brown\n");
485    }
486
487    #[gpui::test]
488    async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
489        let mut cx = NeovimBackedTestContext::new(cx).await;
490        // copy in visual block mode
491        cx.set_shared_state(indoc! {"
492            The ˇquick brown
493            fox jumps over
494            the lazy dog"})
495            .await;
496        cx.simulate_shared_keystrokes("ctrl-v 2 j y").await;
497        cx.shared_clipboard().await.assert_eq("q\nj\nl");
498        cx.simulate_shared_keystrokes("p").await;
499        cx.shared_state().await.assert_eq(indoc! {"
500            The qˇquick brown
501            fox jjumps over
502            the llazy dog"});
503        cx.simulate_shared_keystrokes("v i w shift-p").await;
504        cx.shared_state().await.assert_eq(indoc! {"
505            The ˇq brown
506            fox jjjumps over
507            the lllazy dog"});
508        cx.simulate_shared_keystrokes("v i w shift-p").await;
509
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 j y").await;
516        cx.shared_clipboard().await.assert_eq("q\nj");
517        cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await;
518        cx.shared_state().await.assert_eq(indoc! {"
519            The qˇqick brown
520            fox jjmps over
521            the lzy dog"});
522
523        cx.simulate_shared_keystrokes("shift-v p").await;
524        cx.shared_state().await.assert_eq(indoc! {"
525            ˇq
526            j
527            fox jjmps over
528            the lzy dog"});
529    }
530
531    #[gpui::test]
532    async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
533        let mut cx = VimTestContext::new_typescript(cx).await;
534
535        cx.set_state(
536            indoc! {"
537            class A {ˇ
538            }
539        "},
540            Mode::Normal,
541        );
542        cx.simulate_keystrokes("o a ( ) { escape");
543        cx.assert_state(
544            indoc! {"
545            class A {
546                a()ˇ{}
547            }
548            "},
549            Mode::Normal,
550        );
551        // cursor goes to the first non-blank character in the line;
552        cx.simulate_keystrokes("y y p");
553        cx.assert_state(
554            indoc! {"
555            class A {
556                a(){}
557                ˇa(){}
558            }
559            "},
560            Mode::Normal,
561        );
562        // indentation is preserved when pasting
563        cx.simulate_keystrokes("u shift-v up y shift-p");
564        cx.assert_state(
565            indoc! {"
566                ˇclass A {
567                    a(){}
568                class A {
569                    a(){}
570                }
571                "},
572            Mode::Normal,
573        );
574    }
575
576    #[gpui::test]
577    async fn test_paste_count(cx: &mut gpui::TestAppContext) {
578        let mut cx = NeovimBackedTestContext::new(cx).await;
579
580        cx.set_shared_state(indoc! {"
581            onˇe
582            two
583            three
584        "})
585            .await;
586        cx.simulate_shared_keystrokes("y y 3 p").await;
587        cx.shared_state().await.assert_eq(indoc! {"
588            one
589            ˇone
590            one
591            one
592            two
593            three
594        "});
595
596        cx.set_shared_state(indoc! {"
597            one
598            ˇtwo
599            three
600        "})
601            .await;
602        cx.simulate_shared_keystrokes("y $ $ 3 p").await;
603        cx.shared_state().await.assert_eq(indoc! {"
604            one
605            twotwotwotwˇo
606            three
607        "});
608    }
609}