1use editor::{DisplayPoint, RowExt, SelectionEffects, display_map::ToDisplayPoint, movement};
2use gpui::{Action, Context, Window};
3use language::{Bias, SelectionGoal};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use settings::Settings;
7use std::cmp;
8use vim_mode_setting::HelixModeSetting;
9
10use crate::{
11 Vim,
12 motion::{Motion, MotionKind},
13 object::Object,
14 state::{Mode, Register},
15};
16
17/// Pastes text from the specified register at the cursor position.
18#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
19#[action(namespace = vim)]
20#[serde(deny_unknown_fields)]
21pub struct Paste {
22 #[serde(default)]
23 before: bool,
24 #[serde(default)]
25 preserve_clipboard: bool,
26}
27
28impl Vim {
29 pub fn paste(&mut self, action: &Paste, window: &mut Window, cx: &mut Context<Self>) {
30 self.record_current_action(cx);
31 self.store_visual_marks(window, cx);
32 let count = Vim::take_count(cx).unwrap_or(1);
33 Vim::take_forced_motion(cx);
34
35 self.update_editor(cx, |vim, editor, cx| {
36 let text_layout_details = editor.text_layout_details(window);
37 editor.transact(window, cx, |editor, window, cx| {
38 editor.set_clip_at_line_ends(false, cx);
39
40 let selected_register = vim.selected_register.take();
41
42 let Some(Register {
43 text,
44 clipboard_selections,
45 }) = Vim::update_globals(cx, |globals, cx| {
46 globals.read_register(selected_register, Some(editor), cx)
47 })
48 .filter(|reg| !reg.text.is_empty())
49 else {
50 return;
51 };
52 let clipboard_selections = clipboard_selections
53 .filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine);
54
55 if !action.preserve_clipboard && vim.mode.is_visual() {
56 vim.copy_selections_content(editor, MotionKind::for_mode(vim.mode), window, cx);
57 }
58
59 let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
60
61 // unlike zed, if you have a multi-cursor selection from vim block mode,
62 // pasting it will paste it on subsequent lines, even if you don't yet
63 // have a cursor there.
64 let mut selections_to_process = Vec::new();
65 let mut i = 0;
66 while i < current_selections.len() {
67 selections_to_process
68 .push((current_selections[i].start..current_selections[i].end, true));
69 i += 1;
70 }
71 if let Some(clipboard_selections) = clipboard_selections.as_ref() {
72 let left = current_selections
73 .iter()
74 .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
75 .min()
76 .unwrap();
77 let mut row = current_selections.last().unwrap().end.row().next_row();
78 while i < clipboard_selections.len() {
79 let cursor =
80 display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
81 selections_to_process.push((cursor..cursor, false));
82 i += 1;
83 row.0 += 1;
84 }
85 }
86
87 let first_selection_indent_column =
88 clipboard_selections.as_ref().and_then(|zed_selections| {
89 zed_selections
90 .first()
91 .map(|selection| selection.first_line_indent)
92 });
93 let before = action.before || vim.mode == Mode::VisualLine;
94
95 let mut edits = Vec::new();
96 let mut new_selections = Vec::new();
97 let mut original_indent_columns = Vec::new();
98 let mut start_offset = 0;
99
100 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
101 let (mut to_insert, original_indent_column) =
102 if let Some(clipboard_selections) = &clipboard_selections {
103 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
104 let end_offset = start_offset + clipboard_selection.len;
105 let text = text[start_offset..end_offset].to_string();
106 start_offset = end_offset + 1;
107 (text, Some(clipboard_selection.first_line_indent))
108 } else {
109 ("".to_string(), first_selection_indent_column)
110 }
111 } else {
112 (text.to_string(), first_selection_indent_column)
113 };
114 let line_mode = to_insert.ends_with('\n');
115 let is_multiline = to_insert.contains('\n');
116
117 if line_mode && !before {
118 if selection.is_empty() {
119 to_insert =
120 "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
121 } else {
122 to_insert = "\n".to_owned() + &to_insert;
123 }
124 } else if line_mode && vim.mode == Mode::VisualLine {
125 to_insert.pop();
126 }
127
128 let display_range = if !selection.is_empty() {
129 // If vim is in VISUAL LINE mode and the column for the
130 // selection's end point is 0, that means that the
131 // cursor is at the newline character (\n) at the end of
132 // the line. In this situation we'll want to move one
133 // position to the left, ensuring we don't join the last
134 // line of the selection with the line directly below.
135 let end_point =
136 if vim.mode == Mode::VisualLine && selection.end.column() == 0 {
137 movement::left(&display_map, selection.end)
138 } else {
139 selection.end
140 };
141
142 selection.start..end_point
143 } else if line_mode {
144 let point = if before {
145 movement::line_beginning(&display_map, selection.start, false)
146 } else {
147 movement::line_end(&display_map, selection.start, false)
148 };
149 point..point
150 } else {
151 let point = if before {
152 selection.start
153 } else {
154 movement::saturating_right(&display_map, selection.start)
155 };
156 point..point
157 };
158
159 let point_range = display_range.start.to_point(&display_map)
160 ..display_range.end.to_point(&display_map);
161 let anchor = if is_multiline || vim.mode == Mode::VisualLine {
162 display_map.buffer_snapshot.anchor_before(point_range.start)
163 } else {
164 display_map.buffer_snapshot.anchor_after(point_range.end)
165 };
166
167 if *preserve {
168 new_selections.push((anchor, line_mode, is_multiline));
169 }
170 edits.push((point_range, to_insert.repeat(count)));
171 original_indent_columns.push(original_indent_column);
172 }
173
174 let cursor_offset = editor.selections.last::<usize>(cx).head();
175 if editor
176 .buffer()
177 .read(cx)
178 .snapshot(cx)
179 .language_settings_at(cursor_offset, cx)
180 .auto_indent_on_paste
181 {
182 editor.edit_with_block_indent(edits, original_indent_columns, cx);
183 } else {
184 editor.edit(edits, cx);
185 }
186
187 // in line_mode vim will insert the new text on the next (or previous if before) line
188 // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
189 // otherwise vim will insert the next text at (or before) the current cursor position,
190 // the cursor will go to the last (or first, if is_multiline) inserted character.
191 editor.change_selections(Default::default(), window, cx, |s| {
192 s.replace_cursors_with(|map| {
193 let mut cursors = Vec::new();
194 for (anchor, line_mode, is_multiline) in &new_selections {
195 let mut cursor = anchor.to_display_point(map);
196 if *line_mode {
197 if !before {
198 cursor = movement::down(
199 map,
200 cursor,
201 SelectionGoal::None,
202 false,
203 &text_layout_details,
204 )
205 .0;
206 }
207 cursor = movement::indented_line_beginning(map, cursor, true, true);
208 } else if !is_multiline && !vim.temp_mode {
209 cursor = movement::saturating_left(map, cursor)
210 }
211 cursors.push(cursor);
212 if vim.mode == Mode::VisualBlock {
213 break;
214 }
215 }
216
217 cursors
218 });
219 })
220 });
221 });
222
223 if HelixModeSetting::get_global(cx).0 {
224 self.switch_mode(Mode::HelixNormal, true, window, cx);
225 } else {
226 self.switch_mode(Mode::Normal, true, window, cx);
227 }
228 }
229
230 pub fn replace_with_register_object(
231 &mut self,
232 object: Object,
233 around: bool,
234 window: &mut Window,
235 cx: &mut Context<Self>,
236 ) {
237 self.stop_recording(cx);
238 let selected_register = self.selected_register.take();
239 self.update_editor(cx, |_, editor, cx| {
240 editor.transact(window, cx, |editor, window, cx| {
241 editor.set_clip_at_line_ends(false, cx);
242 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
243 s.move_with(|map, selection| {
244 object.expand_selection(map, selection, around, None);
245 });
246 });
247
248 let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| {
249 globals.read_register(selected_register, Some(editor), cx)
250 })
251 .filter(|reg| !reg.text.is_empty()) else {
252 return;
253 };
254 editor.insert(&text, window, cx);
255 editor.set_clip_at_line_ends(true, cx);
256 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
257 s.move_with(|map, selection| {
258 selection.start = map.clip_point(selection.start, Bias::Left);
259 selection.end = selection.start
260 })
261 })
262 });
263 });
264 }
265
266 pub fn replace_with_register_motion(
267 &mut self,
268 motion: Motion,
269 times: Option<usize>,
270 forced_motion: bool,
271 window: &mut Window,
272 cx: &mut Context<Self>,
273 ) {
274 self.stop_recording(cx);
275 let selected_register = self.selected_register.take();
276 self.update_editor(cx, |_, editor, cx| {
277 let text_layout_details = editor.text_layout_details(window);
278 editor.transact(window, cx, |editor, window, cx| {
279 editor.set_clip_at_line_ends(false, cx);
280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
281 s.move_with(|map, selection| {
282 motion.expand_selection(
283 map,
284 selection,
285 times,
286 &text_layout_details,
287 forced_motion,
288 );
289 });
290 });
291
292 let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| {
293 globals.read_register(selected_register, Some(editor), cx)
294 })
295 .filter(|reg| !reg.text.is_empty()) else {
296 return;
297 };
298 editor.insert(&text, window, cx);
299 editor.set_clip_at_line_ends(true, cx);
300 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
301 s.move_with(|map, selection| {
302 selection.start = map.clip_point(selection.start, Bias::Left);
303 selection.end = selection.start
304 })
305 })
306 });
307 });
308 }
309}
310
311#[cfg(test)]
312mod test {
313 use crate::{
314 state::{Mode, Register},
315 test::{NeovimBackedTestContext, VimTestContext},
316 };
317 use gpui::ClipboardItem;
318 use indoc::indoc;
319 use language::{LanguageName, language_settings::LanguageSettingsContent};
320 use settings::{SettingsStore, UseSystemClipboard};
321
322 #[gpui::test]
323 async fn test_paste(cx: &mut gpui::TestAppContext) {
324 let mut cx = NeovimBackedTestContext::new(cx).await;
325
326 // single line
327 cx.set_shared_state(indoc! {"
328 The quick brown
329 fox ˇjumps over
330 the lazy dog"})
331 .await;
332 cx.simulate_shared_keystrokes("v w y").await;
333 cx.shared_clipboard().await.assert_eq("jumps o");
334 cx.set_shared_state(indoc! {"
335 The quick brown
336 fox jumps oveˇr
337 the lazy dog"})
338 .await;
339 cx.simulate_shared_keystrokes("p").await;
340 cx.shared_state().await.assert_eq(indoc! {"
341 The quick brown
342 fox jumps overjumps ˇo
343 the lazy dog"});
344
345 cx.set_shared_state(indoc! {"
346 The quick brown
347 fox jumps oveˇr
348 the lazy dog"})
349 .await;
350 cx.simulate_shared_keystrokes("shift-p").await;
351 cx.shared_state().await.assert_eq(indoc! {"
352 The quick brown
353 fox jumps ovejumps ˇor
354 the lazy dog"});
355
356 // line mode
357 cx.set_shared_state(indoc! {"
358 The quick brown
359 fox juˇmps over
360 the lazy dog"})
361 .await;
362 cx.simulate_shared_keystrokes("d d").await;
363 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
364 cx.shared_state().await.assert_eq(indoc! {"
365 The quick brown
366 the laˇzy dog"});
367 cx.simulate_shared_keystrokes("p").await;
368 cx.shared_state().await.assert_eq(indoc! {"
369 The quick brown
370 the lazy dog
371 ˇfox jumps over"});
372 cx.simulate_shared_keystrokes("k shift-p").await;
373 cx.shared_state().await.assert_eq(indoc! {"
374 The quick brown
375 ˇfox jumps over
376 the lazy dog
377 fox jumps over"});
378
379 // multiline, cursor to first character of pasted text.
380 cx.set_shared_state(indoc! {"
381 The quick brown
382 fox jumps ˇover
383 the lazy dog"})
384 .await;
385 cx.simulate_shared_keystrokes("v j y").await;
386 cx.shared_clipboard().await.assert_eq("over\nthe lazy do");
387
388 cx.simulate_shared_keystrokes("p").await;
389 cx.shared_state().await.assert_eq(indoc! {"
390 The quick brown
391 fox jumps oˇover
392 the lazy dover
393 the lazy dog"});
394 cx.simulate_shared_keystrokes("u shift-p").await;
395 cx.shared_state().await.assert_eq(indoc! {"
396 The quick brown
397 fox jumps ˇover
398 the lazy doover
399 the lazy dog"});
400 }
401
402 #[gpui::test]
403 async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
404 let mut cx = VimTestContext::new(cx, true).await;
405
406 cx.update_global(|store: &mut SettingsStore, cx| {
407 store.update_user_settings(cx, |s| {
408 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
409 });
410 });
411
412 cx.set_state(
413 indoc! {"
414 The quick brown
415 fox jˇumps over
416 the lazy dog"},
417 Mode::Normal,
418 );
419 cx.simulate_keystrokes("v i w y");
420 cx.assert_state(
421 indoc! {"
422 The quick brown
423 fox ˇjumps over
424 the lazy dog"},
425 Mode::Normal,
426 );
427 cx.simulate_keystrokes("p");
428 cx.assert_state(
429 indoc! {"
430 The quick brown
431 fox jjumpˇsumps over
432 the lazy dog"},
433 Mode::Normal,
434 );
435 assert_eq!(cx.read_from_clipboard(), None);
436 }
437
438 #[gpui::test]
439 async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
440 let mut cx = VimTestContext::new(cx, true).await;
441
442 cx.update_global(|store: &mut SettingsStore, cx| {
443 store.update_user_settings(cx, |s| {
444 s.vim.get_or_insert_default().use_system_clipboard =
445 Some(UseSystemClipboard::OnYank)
446 });
447 });
448
449 // copy in visual mode
450 cx.set_state(
451 indoc! {"
452 The quick brown
453 fox jˇumps over
454 the lazy dog"},
455 Mode::Normal,
456 );
457 cx.simulate_keystrokes("v i w y");
458 cx.assert_state(
459 indoc! {"
460 The quick brown
461 fox ˇjumps over
462 the lazy dog"},
463 Mode::Normal,
464 );
465 cx.simulate_keystrokes("p");
466 cx.assert_state(
467 indoc! {"
468 The quick brown
469 fox jjumpˇsumps over
470 the lazy dog"},
471 Mode::Normal,
472 );
473 assert_eq!(
474 cx.read_from_clipboard().map(|item| item.text().unwrap()),
475 Some("jumps".into())
476 );
477 cx.simulate_keystrokes("d d p");
478 cx.assert_state(
479 indoc! {"
480 The quick brown
481 the lazy dog
482 ˇfox jjumpsumps over"},
483 Mode::Normal,
484 );
485 assert_eq!(
486 cx.read_from_clipboard().map(|item| item.text().unwrap()),
487 Some("jumps".into())
488 );
489 cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string()));
490 cx.simulate_keystrokes("shift-p");
491 cx.assert_state(
492 indoc! {"
493 The quick brown
494 the lazy dog
495 test-copˇyfox jjumpsumps over"},
496 Mode::Normal,
497 );
498 }
499
500 #[gpui::test]
501 async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
502 let mut cx = NeovimBackedTestContext::new(cx).await;
503
504 // copy in visual mode
505 cx.set_shared_state(indoc! {"
506 The quick brown
507 fox jˇumps over
508 the lazy dog"})
509 .await;
510 cx.simulate_shared_keystrokes("v i w y").await;
511 cx.shared_state().await.assert_eq(indoc! {"
512 The quick brown
513 fox ˇjumps over
514 the lazy dog"});
515 // paste in visual mode
516 cx.simulate_shared_keystrokes("w v i w p").await;
517 cx.shared_state().await.assert_eq(indoc! {"
518 The quick brown
519 fox jumps jumpˇs
520 the lazy dog"});
521 cx.shared_clipboard().await.assert_eq("over");
522 // paste in visual line mode
523 cx.simulate_shared_keystrokes("up shift-v shift-p").await;
524 cx.shared_state().await.assert_eq(indoc! {"
525 ˇover
526 fox jumps jumps
527 the lazy dog"});
528 cx.shared_clipboard().await.assert_eq("over");
529 // paste in visual block mode
530 cx.simulate_shared_keystrokes("ctrl-v down down p").await;
531 cx.shared_state().await.assert_eq(indoc! {"
532 oveˇrver
533 overox jumps jumps
534 overhe lazy dog"});
535
536 // copy in visual line mode
537 cx.set_shared_state(indoc! {"
538 The quick brown
539 fox juˇmps over
540 the lazy dog"})
541 .await;
542 cx.simulate_shared_keystrokes("shift-v d").await;
543 cx.shared_state().await.assert_eq(indoc! {"
544 The quick brown
545 the laˇzy dog"});
546 // paste in visual mode
547 cx.simulate_shared_keystrokes("v i w p").await;
548 cx.shared_state().await.assert_eq(indoc! {"
549 The quick brown
550 the•
551 ˇfox jumps over
552 dog"});
553 cx.shared_clipboard().await.assert_eq("lazy");
554 cx.set_shared_state(indoc! {"
555 The quick brown
556 fox juˇmps over
557 the lazy dog"})
558 .await;
559 cx.simulate_shared_keystrokes("shift-v d").await;
560 cx.shared_state().await.assert_eq(indoc! {"
561 The quick brown
562 the laˇzy dog"});
563 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
564 // paste in visual line mode
565 cx.simulate_shared_keystrokes("k shift-v p").await;
566 cx.shared_state().await.assert_eq(indoc! {"
567 ˇfox jumps over
568 the lazy dog"});
569 cx.shared_clipboard().await.assert_eq("The quick brown\n");
570
571 // Copy line and paste in visual mode, with cursor on newline character.
572 cx.set_shared_state(indoc! {"
573 ˇThe quick brown
574 fox jumps over
575 the lazy dog"})
576 .await;
577 cx.simulate_shared_keystrokes("y y shift-v j $ p").await;
578 cx.shared_state().await.assert_eq(indoc! {"
579 ˇThe quick brown
580 the lazy dog"});
581 }
582
583 #[gpui::test]
584 async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
585 let mut cx = NeovimBackedTestContext::new(cx).await;
586 // copy in visual block mode
587 cx.set_shared_state(indoc! {"
588 The ˇquick brown
589 fox jumps over
590 the lazy dog"})
591 .await;
592 cx.simulate_shared_keystrokes("ctrl-v 2 j y").await;
593 cx.shared_clipboard().await.assert_eq("q\nj\nl");
594 cx.simulate_shared_keystrokes("p").await;
595 cx.shared_state().await.assert_eq(indoc! {"
596 The qˇquick brown
597 fox jjumps over
598 the llazy dog"});
599 cx.simulate_shared_keystrokes("v i w shift-p").await;
600 cx.shared_state().await.assert_eq(indoc! {"
601 The ˇq brown
602 fox jjjumps over
603 the lllazy dog"});
604 cx.simulate_shared_keystrokes("v i w shift-p").await;
605
606 cx.set_shared_state(indoc! {"
607 The ˇquick brown
608 fox jumps over
609 the lazy dog"})
610 .await;
611 cx.simulate_shared_keystrokes("ctrl-v j y").await;
612 cx.shared_clipboard().await.assert_eq("q\nj");
613 cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await;
614 cx.shared_state().await.assert_eq(indoc! {"
615 The qˇqick brown
616 fox jjmps over
617 the lzy dog"});
618
619 cx.simulate_shared_keystrokes("shift-v p").await;
620 cx.shared_state().await.assert_eq(indoc! {"
621 ˇq
622 j
623 fox jjmps over
624 the lzy dog"});
625 }
626
627 #[gpui::test]
628 async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
629 let mut cx = VimTestContext::new_typescript(cx).await;
630
631 cx.set_state(
632 indoc! {"
633 class A {ˇ
634 }
635 "},
636 Mode::Normal,
637 );
638 cx.simulate_keystrokes("o a ( ) { escape");
639 cx.assert_state(
640 indoc! {"
641 class A {
642 a()ˇ{}
643 }
644 "},
645 Mode::Normal,
646 );
647 // cursor goes to the first non-blank character in the line;
648 cx.simulate_keystrokes("y y p");
649 cx.assert_state(
650 indoc! {"
651 class A {
652 a(){}
653 ˇa(){}
654 }
655 "},
656 Mode::Normal,
657 );
658 // indentation is preserved when pasting
659 cx.simulate_keystrokes("u shift-v up y shift-p");
660 cx.assert_state(
661 indoc! {"
662 ˇclass A {
663 a(){}
664 class A {
665 a(){}
666 }
667 "},
668 Mode::Normal,
669 );
670 }
671
672 #[gpui::test]
673 async fn test_paste_auto_indent(cx: &mut gpui::TestAppContext) {
674 let mut cx = VimTestContext::new(cx, true).await;
675
676 cx.set_state(
677 indoc! {"
678 mod some_module {
679 ˇfn main() {
680 }
681 }
682 "},
683 Mode::Normal,
684 );
685 // default auto indentation
686 cx.simulate_keystrokes("y y p");
687 cx.assert_state(
688 indoc! {"
689 mod some_module {
690 fn main() {
691 ˇfn main() {
692 }
693 }
694 "},
695 Mode::Normal,
696 );
697 // back to previous state
698 cx.simulate_keystrokes("u u");
699 cx.assert_state(
700 indoc! {"
701 mod some_module {
702 ˇfn main() {
703 }
704 }
705 "},
706 Mode::Normal,
707 );
708 cx.update_global(|store: &mut SettingsStore, cx| {
709 store.update_user_settings(cx, |settings| {
710 settings.project.all_languages.languages.0.insert(
711 LanguageName::new("Rust").0,
712 LanguageSettingsContent {
713 auto_indent_on_paste: Some(false),
714 ..Default::default()
715 },
716 );
717 });
718 });
719 // auto indentation turned off
720 cx.simulate_keystrokes("y y p");
721 cx.assert_state(
722 indoc! {"
723 mod some_module {
724 fn main() {
725 ˇfn main() {
726 }
727 }
728 "},
729 Mode::Normal,
730 );
731 }
732
733 #[gpui::test]
734 async fn test_paste_count(cx: &mut gpui::TestAppContext) {
735 let mut cx = NeovimBackedTestContext::new(cx).await;
736
737 cx.set_shared_state(indoc! {"
738 onˇe
739 two
740 three
741 "})
742 .await;
743 cx.simulate_shared_keystrokes("y y 3 p").await;
744 cx.shared_state().await.assert_eq(indoc! {"
745 one
746 ˇone
747 one
748 one
749 two
750 three
751 "});
752
753 cx.set_shared_state(indoc! {"
754 one
755 ˇtwo
756 three
757 "})
758 .await;
759 cx.simulate_shared_keystrokes("y $ $ 3 p").await;
760 cx.shared_state().await.assert_eq(indoc! {"
761 one
762 twotwotwotwˇo
763 three
764 "});
765 }
766
767 #[gpui::test]
768 async fn test_numbered_registers(cx: &mut gpui::TestAppContext) {
769 let mut cx = NeovimBackedTestContext::new(cx).await;
770
771 cx.update_global(|store: &mut SettingsStore, cx| {
772 store.update_user_settings(cx, |s| {
773 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
774 });
775 });
776
777 cx.set_shared_state(indoc! {"
778 The quick brown
779 fox jˇumps over
780 the lazy dog"})
781 .await;
782 cx.simulate_shared_keystrokes("y y \" 0 p").await;
783 cx.shared_register('0').await.assert_eq("fox jumps over\n");
784 cx.shared_register('"').await.assert_eq("fox jumps over\n");
785
786 cx.shared_state().await.assert_eq(indoc! {"
787 The quick brown
788 fox jumps over
789 ˇfox jumps over
790 the lazy dog"});
791 cx.simulate_shared_keystrokes("k k d d").await;
792 cx.shared_register('0').await.assert_eq("fox jumps over\n");
793 cx.shared_register('1').await.assert_eq("The quick brown\n");
794 cx.shared_register('"').await.assert_eq("The quick brown\n");
795
796 cx.simulate_shared_keystrokes("d d shift-g d d").await;
797 cx.shared_register('0').await.assert_eq("fox jumps over\n");
798 cx.shared_register('3').await.assert_eq("The quick brown\n");
799 cx.shared_register('2').await.assert_eq("fox jumps over\n");
800 cx.shared_register('1').await.assert_eq("the lazy dog\n");
801
802 cx.shared_state().await.assert_eq(indoc! {"
803 ˇfox jumps over"});
804
805 cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await;
806 cx.set_shared_state(indoc! {"
807 The quick brown
808 fox jumps over
809 ˇthe lazy dog"})
810 .await;
811 }
812
813 #[gpui::test]
814 async fn test_named_registers(cx: &mut gpui::TestAppContext) {
815 let mut cx = NeovimBackedTestContext::new(cx).await;
816
817 cx.update_global(|store: &mut SettingsStore, cx| {
818 store.update_user_settings(cx, |s| {
819 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
820 });
821 });
822
823 cx.set_shared_state(indoc! {"
824 The quick brown
825 fox jˇumps over
826 the lazy dog"})
827 .await;
828 cx.simulate_shared_keystrokes("\" a d a w").await;
829 cx.shared_register('a').await.assert_eq("jumps ");
830 cx.simulate_shared_keystrokes("\" shift-a d i w").await;
831 cx.shared_register('a').await.assert_eq("jumps over");
832 cx.shared_register('"').await.assert_eq("jumps over");
833 cx.simulate_shared_keystrokes("\" a p").await;
834 cx.shared_state().await.assert_eq(indoc! {"
835 The quick brown
836 fox jumps oveˇr
837 the lazy dog"});
838 cx.simulate_shared_keystrokes("\" a d a w").await;
839 cx.shared_register('a').await.assert_eq(" over");
840 }
841
842 #[gpui::test]
843 async fn test_special_registers(cx: &mut gpui::TestAppContext) {
844 let mut cx = NeovimBackedTestContext::new(cx).await;
845
846 cx.update_global(|store: &mut SettingsStore, cx| {
847 store.update_user_settings(cx, |s| {
848 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
849 });
850 });
851
852 cx.set_shared_state(indoc! {"
853 The quick brown
854 fox jˇumps over
855 the lazy dog"})
856 .await;
857 cx.simulate_shared_keystrokes("d i w").await;
858 cx.shared_register('-').await.assert_eq("jumps");
859 cx.simulate_shared_keystrokes("\" _ d d").await;
860 cx.shared_register('_').await.assert_eq("");
861
862 cx.simulate_shared_keystrokes("shift-v \" _ y w").await;
863 cx.shared_register('"').await.assert_eq("jumps");
864
865 cx.shared_state().await.assert_eq(indoc! {"
866 The quick brown
867 the ˇlazy dog"});
868 cx.simulate_shared_keystrokes("\" \" d ^").await;
869 cx.shared_register('0').await.assert_eq("the ");
870 cx.shared_register('"').await.assert_eq("the ");
871
872 cx.simulate_shared_keystrokes("^ \" + d $").await;
873 cx.shared_clipboard().await.assert_eq("lazy dog");
874 cx.shared_register('"').await.assert_eq("lazy dog");
875
876 cx.simulate_shared_keystrokes("/ d o g enter").await;
877 cx.shared_register('/').await.assert_eq("dog");
878 cx.simulate_shared_keystrokes("\" / shift-p").await;
879 cx.shared_state().await.assert_eq(indoc! {"
880 The quick brown
881 doˇg"});
882
883 // not testing nvim as it doesn't have a filename
884 cx.simulate_keystrokes("\" % p");
885 #[cfg(not(target_os = "windows"))]
886 cx.assert_state(
887 indoc! {"
888 The quick brown
889 dogdir/file.rˇs"},
890 Mode::Normal,
891 );
892 #[cfg(target_os = "windows")]
893 cx.assert_state(
894 indoc! {"
895 The quick brown
896 dogdir\\file.rˇs"},
897 Mode::Normal,
898 );
899 }
900
901 #[gpui::test]
902 async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) {
903 let mut cx = VimTestContext::new(cx, true).await;
904
905 cx.update_global(|store: &mut SettingsStore, cx| {
906 store.update_user_settings(cx, |s| {
907 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
908 });
909 });
910
911 cx.set_state(
912 indoc! {"
913 ˇfish one
914 fish two
915 fish red
916 fish blue
917 "},
918 Mode::Normal,
919 );
920 cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p");
921 cx.assert_state(
922 indoc! {"
923 onˇefish•
924 twˇofish•
925 reˇdfish•
926 bluˇefish•
927 "},
928 Mode::Normal,
929 );
930 }
931
932 #[gpui::test]
933 async fn test_replace_with_register(cx: &mut gpui::TestAppContext) {
934 let mut cx = VimTestContext::new(cx, true).await;
935
936 cx.set_state(
937 indoc! {"
938 ˇfish one
939 two three
940 "},
941 Mode::Normal,
942 );
943 cx.simulate_keystrokes("y i w");
944 cx.simulate_keystrokes("w");
945 cx.simulate_keystrokes("g shift-r i w");
946 cx.assert_state(
947 indoc! {"
948 fish fisˇh
949 two three
950 "},
951 Mode::Normal,
952 );
953 cx.simulate_keystrokes("j b g shift-r e");
954 cx.assert_state(
955 indoc! {"
956 fish fish
957 two fisˇh
958 "},
959 Mode::Normal,
960 );
961 let clipboard: Register = cx.read_from_clipboard().unwrap().into();
962 assert_eq!(clipboard.text, "fish");
963
964 cx.set_state(
965 indoc! {"
966 ˇfish one
967 two three
968 "},
969 Mode::Normal,
970 );
971 cx.simulate_keystrokes("y i w");
972 cx.simulate_keystrokes("w");
973 cx.simulate_keystrokes("v i w g shift-r");
974 cx.assert_state(
975 indoc! {"
976 fish fisˇh
977 two three
978 "},
979 Mode::Normal,
980 );
981 cx.simulate_keystrokes("g shift-r r");
982 cx.assert_state(
983 indoc! {"
984 fisˇh
985 two three
986 "},
987 Mode::Normal,
988 );
989 cx.simulate_keystrokes("j w g shift-r $");
990 cx.assert_state(
991 indoc! {"
992 fish
993 two fisˇh
994 "},
995 Mode::Normal,
996 );
997 let clipboard: Register = cx.read_from_clipboard().unwrap().into();
998 assert_eq!(clipboard.text, "fish");
999 }
1000
1001 #[gpui::test]
1002 async fn test_replace_with_register_dot_repeat(cx: &mut gpui::TestAppContext) {
1003 let mut cx = VimTestContext::new(cx, true).await;
1004
1005 cx.set_state(
1006 indoc! {"
1007 ˇfish one
1008 two three
1009 "},
1010 Mode::Normal,
1011 );
1012 cx.simulate_keystrokes("y i w");
1013 cx.simulate_keystrokes("w");
1014 cx.simulate_keystrokes("g shift-r i w");
1015 cx.assert_state(
1016 indoc! {"
1017 fish fisˇh
1018 two three
1019 "},
1020 Mode::Normal,
1021 );
1022 cx.simulate_keystrokes("j .");
1023 cx.assert_state(
1024 indoc! {"
1025 fish fish
1026 two fisˇh
1027 "},
1028 Mode::Normal,
1029 );
1030 }
1031}