paste.rs

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