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 UseSystemClipboard, VimSettings,
315 state::{Mode, Register},
316 test::{NeovimBackedTestContext, VimTestContext},
317 };
318 use gpui::ClipboardItem;
319 use indoc::indoc;
320 use language::{
321 LanguageName,
322 language_settings::{AllLanguageSettings, LanguageSettingsContent},
323 };
324 use settings::SettingsStore;
325
326 #[gpui::test]
327 async fn test_paste(cx: &mut gpui::TestAppContext) {
328 let mut cx = NeovimBackedTestContext::new(cx).await;
329
330 // single line
331 cx.set_shared_state(indoc! {"
332 The quick brown
333 fox ˇjumps over
334 the lazy dog"})
335 .await;
336 cx.simulate_shared_keystrokes("v w y").await;
337 cx.shared_clipboard().await.assert_eq("jumps o");
338 cx.set_shared_state(indoc! {"
339 The quick brown
340 fox jumps oveˇr
341 the lazy dog"})
342 .await;
343 cx.simulate_shared_keystrokes("p").await;
344 cx.shared_state().await.assert_eq(indoc! {"
345 The quick brown
346 fox jumps overjumps ˇo
347 the lazy dog"});
348
349 cx.set_shared_state(indoc! {"
350 The quick brown
351 fox jumps oveˇr
352 the lazy dog"})
353 .await;
354 cx.simulate_shared_keystrokes("shift-p").await;
355 cx.shared_state().await.assert_eq(indoc! {"
356 The quick brown
357 fox jumps ovejumps ˇor
358 the lazy dog"});
359
360 // line mode
361 cx.set_shared_state(indoc! {"
362 The quick brown
363 fox juˇmps over
364 the lazy dog"})
365 .await;
366 cx.simulate_shared_keystrokes("d d").await;
367 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
368 cx.shared_state().await.assert_eq(indoc! {"
369 The quick brown
370 the laˇzy dog"});
371 cx.simulate_shared_keystrokes("p").await;
372 cx.shared_state().await.assert_eq(indoc! {"
373 The quick brown
374 the lazy dog
375 ˇfox jumps over"});
376 cx.simulate_shared_keystrokes("k shift-p").await;
377 cx.shared_state().await.assert_eq(indoc! {"
378 The quick brown
379 ˇfox jumps over
380 the lazy dog
381 fox jumps over"});
382
383 // multiline, cursor to first character of pasted text.
384 cx.set_shared_state(indoc! {"
385 The quick brown
386 fox jumps ˇover
387 the lazy dog"})
388 .await;
389 cx.simulate_shared_keystrokes("v j y").await;
390 cx.shared_clipboard().await.assert_eq("over\nthe lazy do");
391
392 cx.simulate_shared_keystrokes("p").await;
393 cx.shared_state().await.assert_eq(indoc! {"
394 The quick brown
395 fox jumps oˇover
396 the lazy dover
397 the lazy dog"});
398 cx.simulate_shared_keystrokes("u shift-p").await;
399 cx.shared_state().await.assert_eq(indoc! {"
400 The quick brown
401 fox jumps ˇover
402 the lazy doover
403 the lazy dog"});
404 }
405
406 #[gpui::test]
407 async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
408 let mut cx = VimTestContext::new(cx, true).await;
409
410 cx.update_global(|store: &mut SettingsStore, cx| {
411 store.update_user_settings::<VimSettings>(cx, |s| {
412 s.use_system_clipboard = Some(UseSystemClipboard::Never)
413 });
414 });
415
416 cx.set_state(
417 indoc! {"
418 The quick brown
419 fox jˇumps over
420 the lazy dog"},
421 Mode::Normal,
422 );
423 cx.simulate_keystrokes("v i w y");
424 cx.assert_state(
425 indoc! {"
426 The quick brown
427 fox ˇjumps over
428 the lazy dog"},
429 Mode::Normal,
430 );
431 cx.simulate_keystrokes("p");
432 cx.assert_state(
433 indoc! {"
434 The quick brown
435 fox jjumpˇsumps over
436 the lazy dog"},
437 Mode::Normal,
438 );
439 assert_eq!(cx.read_from_clipboard(), None);
440 }
441
442 #[gpui::test]
443 async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
444 let mut cx = VimTestContext::new(cx, true).await;
445
446 cx.update_global(|store: &mut SettingsStore, cx| {
447 store.update_user_settings::<VimSettings>(cx, |s| {
448 s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
449 });
450 });
451
452 // copy in visual mode
453 cx.set_state(
454 indoc! {"
455 The quick brown
456 fox jˇumps over
457 the lazy dog"},
458 Mode::Normal,
459 );
460 cx.simulate_keystrokes("v i w y");
461 cx.assert_state(
462 indoc! {"
463 The quick brown
464 fox ˇjumps over
465 the lazy dog"},
466 Mode::Normal,
467 );
468 cx.simulate_keystrokes("p");
469 cx.assert_state(
470 indoc! {"
471 The quick brown
472 fox jjumpˇsumps over
473 the lazy dog"},
474 Mode::Normal,
475 );
476 assert_eq!(
477 cx.read_from_clipboard()
478 .map(|item| item.text().unwrap().to_string()),
479 Some("jumps".into())
480 );
481 cx.simulate_keystrokes("d d p");
482 cx.assert_state(
483 indoc! {"
484 The quick brown
485 the lazy dog
486 ˇfox jjumpsumps over"},
487 Mode::Normal,
488 );
489 assert_eq!(
490 cx.read_from_clipboard()
491 .map(|item| item.text().unwrap().to_string()),
492 Some("jumps".into())
493 );
494 cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string()));
495 cx.simulate_keystrokes("shift-p");
496 cx.assert_state(
497 indoc! {"
498 The quick brown
499 the lazy dog
500 test-copˇyfox jjumpsumps over"},
501 Mode::Normal,
502 );
503 }
504
505 #[gpui::test]
506 async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
507 let mut cx = NeovimBackedTestContext::new(cx).await;
508
509 // copy in visual mode
510 cx.set_shared_state(indoc! {"
511 The quick brown
512 fox jˇumps over
513 the lazy dog"})
514 .await;
515 cx.simulate_shared_keystrokes("v i w y").await;
516 cx.shared_state().await.assert_eq(indoc! {"
517 The quick brown
518 fox ˇjumps over
519 the lazy dog"});
520 // paste in visual mode
521 cx.simulate_shared_keystrokes("w v i w p").await;
522 cx.shared_state().await.assert_eq(indoc! {"
523 The quick brown
524 fox jumps jumpˇs
525 the lazy dog"});
526 cx.shared_clipboard().await.assert_eq("over");
527 // paste in visual line mode
528 cx.simulate_shared_keystrokes("up shift-v shift-p").await;
529 cx.shared_state().await.assert_eq(indoc! {"
530 ˇover
531 fox jumps jumps
532 the lazy dog"});
533 cx.shared_clipboard().await.assert_eq("over");
534 // paste in visual block mode
535 cx.simulate_shared_keystrokes("ctrl-v down down p").await;
536 cx.shared_state().await.assert_eq(indoc! {"
537 oveˇrver
538 overox jumps jumps
539 overhe lazy dog"});
540
541 // copy in visual line mode
542 cx.set_shared_state(indoc! {"
543 The quick brown
544 fox juˇmps over
545 the lazy dog"})
546 .await;
547 cx.simulate_shared_keystrokes("shift-v d").await;
548 cx.shared_state().await.assert_eq(indoc! {"
549 The quick brown
550 the laˇzy dog"});
551 // paste in visual mode
552 cx.simulate_shared_keystrokes("v i w p").await;
553 cx.shared_state().await.assert_eq(indoc! {"
554 The quick brown
555 the•
556 ˇfox jumps over
557 dog"});
558 cx.shared_clipboard().await.assert_eq("lazy");
559 cx.set_shared_state(indoc! {"
560 The quick brown
561 fox juˇmps over
562 the lazy dog"})
563 .await;
564 cx.simulate_shared_keystrokes("shift-v d").await;
565 cx.shared_state().await.assert_eq(indoc! {"
566 The quick brown
567 the laˇzy dog"});
568 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
569 // paste in visual line mode
570 cx.simulate_shared_keystrokes("k shift-v p").await;
571 cx.shared_state().await.assert_eq(indoc! {"
572 ˇfox jumps over
573 the lazy dog"});
574 cx.shared_clipboard().await.assert_eq("The quick brown\n");
575
576 // Copy line and paste in visual mode, with cursor on newline character.
577 cx.set_shared_state(indoc! {"
578 ˇThe quick brown
579 fox jumps over
580 the lazy dog"})
581 .await;
582 cx.simulate_shared_keystrokes("y y shift-v j $ p").await;
583 cx.shared_state().await.assert_eq(indoc! {"
584 ˇThe quick brown
585 the lazy dog"});
586 }
587
588 #[gpui::test]
589 async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
590 let mut cx = NeovimBackedTestContext::new(cx).await;
591 // copy in visual block mode
592 cx.set_shared_state(indoc! {"
593 The ˇquick brown
594 fox jumps over
595 the lazy dog"})
596 .await;
597 cx.simulate_shared_keystrokes("ctrl-v 2 j y").await;
598 cx.shared_clipboard().await.assert_eq("q\nj\nl");
599 cx.simulate_shared_keystrokes("p").await;
600 cx.shared_state().await.assert_eq(indoc! {"
601 The qˇquick brown
602 fox jjumps over
603 the llazy dog"});
604 cx.simulate_shared_keystrokes("v i w shift-p").await;
605 cx.shared_state().await.assert_eq(indoc! {"
606 The ˇq brown
607 fox jjjumps over
608 the lllazy dog"});
609 cx.simulate_shared_keystrokes("v i w shift-p").await;
610
611 cx.set_shared_state(indoc! {"
612 The ˇquick brown
613 fox jumps over
614 the lazy dog"})
615 .await;
616 cx.simulate_shared_keystrokes("ctrl-v j y").await;
617 cx.shared_clipboard().await.assert_eq("q\nj");
618 cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await;
619 cx.shared_state().await.assert_eq(indoc! {"
620 The qˇqick brown
621 fox jjmps over
622 the lzy dog"});
623
624 cx.simulate_shared_keystrokes("shift-v p").await;
625 cx.shared_state().await.assert_eq(indoc! {"
626 ˇq
627 j
628 fox jjmps over
629 the lzy dog"});
630 }
631
632 #[gpui::test]
633 async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
634 let mut cx = VimTestContext::new_typescript(cx).await;
635
636 cx.set_state(
637 indoc! {"
638 class A {ˇ
639 }
640 "},
641 Mode::Normal,
642 );
643 cx.simulate_keystrokes("o a ( ) { escape");
644 cx.assert_state(
645 indoc! {"
646 class A {
647 a()ˇ{}
648 }
649 "},
650 Mode::Normal,
651 );
652 // cursor goes to the first non-blank character in the line;
653 cx.simulate_keystrokes("y y p");
654 cx.assert_state(
655 indoc! {"
656 class A {
657 a(){}
658 ˇa(){}
659 }
660 "},
661 Mode::Normal,
662 );
663 // indentation is preserved when pasting
664 cx.simulate_keystrokes("u shift-v up y shift-p");
665 cx.assert_state(
666 indoc! {"
667 ˇclass A {
668 a(){}
669 class A {
670 a(){}
671 }
672 "},
673 Mode::Normal,
674 );
675 }
676
677 #[gpui::test]
678 async fn test_paste_auto_indent(cx: &mut gpui::TestAppContext) {
679 let mut cx = VimTestContext::new(cx, true).await;
680
681 cx.set_state(
682 indoc! {"
683 mod some_module {
684 ˇfn main() {
685 }
686 }
687 "},
688 Mode::Normal,
689 );
690 // default auto indentation
691 cx.simulate_keystrokes("y y p");
692 cx.assert_state(
693 indoc! {"
694 mod some_module {
695 fn main() {
696 ˇfn main() {
697 }
698 }
699 "},
700 Mode::Normal,
701 );
702 // back to previous state
703 cx.simulate_keystrokes("u u");
704 cx.assert_state(
705 indoc! {"
706 mod some_module {
707 ˇfn main() {
708 }
709 }
710 "},
711 Mode::Normal,
712 );
713 cx.update_global(|store: &mut SettingsStore, cx| {
714 store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
715 settings.languages.0.insert(
716 LanguageName::new("Rust"),
717 LanguageSettingsContent {
718 auto_indent_on_paste: Some(false),
719 ..Default::default()
720 },
721 );
722 });
723 });
724 // auto indentation turned off
725 cx.simulate_keystrokes("y y p");
726 cx.assert_state(
727 indoc! {"
728 mod some_module {
729 fn main() {
730 ˇfn main() {
731 }
732 }
733 "},
734 Mode::Normal,
735 );
736 }
737
738 #[gpui::test]
739 async fn test_paste_count(cx: &mut gpui::TestAppContext) {
740 let mut cx = NeovimBackedTestContext::new(cx).await;
741
742 cx.set_shared_state(indoc! {"
743 onˇe
744 two
745 three
746 "})
747 .await;
748 cx.simulate_shared_keystrokes("y y 3 p").await;
749 cx.shared_state().await.assert_eq(indoc! {"
750 one
751 ˇone
752 one
753 one
754 two
755 three
756 "});
757
758 cx.set_shared_state(indoc! {"
759 one
760 ˇtwo
761 three
762 "})
763 .await;
764 cx.simulate_shared_keystrokes("y $ $ 3 p").await;
765 cx.shared_state().await.assert_eq(indoc! {"
766 one
767 twotwotwotwˇo
768 three
769 "});
770 }
771
772 #[gpui::test]
773 async fn test_numbered_registers(cx: &mut gpui::TestAppContext) {
774 let mut cx = NeovimBackedTestContext::new(cx).await;
775
776 cx.update_global(|store: &mut SettingsStore, cx| {
777 store.update_user_settings::<VimSettings>(cx, |s| {
778 s.use_system_clipboard = Some(UseSystemClipboard::Never)
779 });
780 });
781
782 cx.set_shared_state(indoc! {"
783 The quick brown
784 fox jˇumps over
785 the lazy dog"})
786 .await;
787 cx.simulate_shared_keystrokes("y y \" 0 p").await;
788 cx.shared_register('0').await.assert_eq("fox jumps over\n");
789 cx.shared_register('"').await.assert_eq("fox jumps over\n");
790
791 cx.shared_state().await.assert_eq(indoc! {"
792 The quick brown
793 fox jumps over
794 ˇfox jumps over
795 the lazy dog"});
796 cx.simulate_shared_keystrokes("k k d d").await;
797 cx.shared_register('0').await.assert_eq("fox jumps over\n");
798 cx.shared_register('1').await.assert_eq("The quick brown\n");
799 cx.shared_register('"').await.assert_eq("The quick brown\n");
800
801 cx.simulate_shared_keystrokes("d d shift-g d d").await;
802 cx.shared_register('0').await.assert_eq("fox jumps over\n");
803 cx.shared_register('3').await.assert_eq("The quick brown\n");
804 cx.shared_register('2').await.assert_eq("fox jumps over\n");
805 cx.shared_register('1').await.assert_eq("the lazy dog\n");
806
807 cx.shared_state().await.assert_eq(indoc! {"
808 ˇfox jumps over"});
809
810 cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await;
811 cx.set_shared_state(indoc! {"
812 The quick brown
813 fox jumps over
814 ˇthe lazy dog"})
815 .await;
816 }
817
818 #[gpui::test]
819 async fn test_named_registers(cx: &mut gpui::TestAppContext) {
820 let mut cx = NeovimBackedTestContext::new(cx).await;
821
822 cx.update_global(|store: &mut SettingsStore, cx| {
823 store.update_user_settings::<VimSettings>(cx, |s| {
824 s.use_system_clipboard = Some(UseSystemClipboard::Never)
825 });
826 });
827
828 cx.set_shared_state(indoc! {"
829 The quick brown
830 fox jˇumps over
831 the lazy dog"})
832 .await;
833 cx.simulate_shared_keystrokes("\" a d a w").await;
834 cx.shared_register('a').await.assert_eq("jumps ");
835 cx.simulate_shared_keystrokes("\" shift-a d i w").await;
836 cx.shared_register('a').await.assert_eq("jumps over");
837 cx.shared_register('"').await.assert_eq("jumps over");
838 cx.simulate_shared_keystrokes("\" a p").await;
839 cx.shared_state().await.assert_eq(indoc! {"
840 The quick brown
841 fox jumps oveˇr
842 the lazy dog"});
843 cx.simulate_shared_keystrokes("\" a d a w").await;
844 cx.shared_register('a').await.assert_eq(" over");
845 }
846
847 #[gpui::test]
848 async fn test_special_registers(cx: &mut gpui::TestAppContext) {
849 let mut cx = NeovimBackedTestContext::new(cx).await;
850
851 cx.update_global(|store: &mut SettingsStore, cx| {
852 store.update_user_settings::<VimSettings>(cx, |s| {
853 s.use_system_clipboard = Some(UseSystemClipboard::Never)
854 });
855 });
856
857 cx.set_shared_state(indoc! {"
858 The quick brown
859 fox jˇumps over
860 the lazy dog"})
861 .await;
862 cx.simulate_shared_keystrokes("d i w").await;
863 cx.shared_register('-').await.assert_eq("jumps");
864 cx.simulate_shared_keystrokes("\" _ d d").await;
865 cx.shared_register('_').await.assert_eq("");
866
867 cx.simulate_shared_keystrokes("shift-v \" _ y w").await;
868 cx.shared_register('"').await.assert_eq("jumps");
869
870 cx.shared_state().await.assert_eq(indoc! {"
871 The quick brown
872 the ˇlazy dog"});
873 cx.simulate_shared_keystrokes("\" \" d ^").await;
874 cx.shared_register('0').await.assert_eq("the ");
875 cx.shared_register('"').await.assert_eq("the ");
876
877 cx.simulate_shared_keystrokes("^ \" + d $").await;
878 cx.shared_clipboard().await.assert_eq("lazy dog");
879 cx.shared_register('"').await.assert_eq("lazy dog");
880
881 cx.simulate_shared_keystrokes("/ d o g enter").await;
882 cx.shared_register('/').await.assert_eq("dog");
883 cx.simulate_shared_keystrokes("\" / shift-p").await;
884 cx.shared_state().await.assert_eq(indoc! {"
885 The quick brown
886 doˇg"});
887
888 // not testing nvim as it doesn't have a filename
889 cx.simulate_keystrokes("\" % p");
890 #[cfg(not(target_os = "windows"))]
891 cx.assert_state(
892 indoc! {"
893 The quick brown
894 dogdir/file.rˇs"},
895 Mode::Normal,
896 );
897 #[cfg(target_os = "windows")]
898 cx.assert_state(
899 indoc! {"
900 The quick brown
901 dogdir\\file.rˇs"},
902 Mode::Normal,
903 );
904 }
905
906 #[gpui::test]
907 async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) {
908 let mut cx = VimTestContext::new(cx, true).await;
909
910 cx.update_global(|store: &mut SettingsStore, cx| {
911 store.update_user_settings::<VimSettings>(cx, |s| {
912 s.use_system_clipboard = Some(UseSystemClipboard::Never)
913 });
914 });
915
916 cx.set_state(
917 indoc! {"
918 ˇfish one
919 fish two
920 fish red
921 fish blue
922 "},
923 Mode::Normal,
924 );
925 cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p");
926 cx.assert_state(
927 indoc! {"
928 onˇefish•
929 twˇofish•
930 reˇdfish•
931 bluˇefish•
932 "},
933 Mode::Normal,
934 );
935 }
936
937 #[gpui::test]
938 async fn test_replace_with_register(cx: &mut gpui::TestAppContext) {
939 let mut cx = VimTestContext::new(cx, true).await;
940
941 cx.set_state(
942 indoc! {"
943 ˇfish one
944 two three
945 "},
946 Mode::Normal,
947 );
948 cx.simulate_keystrokes("y i w");
949 cx.simulate_keystrokes("w");
950 cx.simulate_keystrokes("g shift-r i w");
951 cx.assert_state(
952 indoc! {"
953 fish fisˇh
954 two three
955 "},
956 Mode::Normal,
957 );
958 cx.simulate_keystrokes("j b g shift-r e");
959 cx.assert_state(
960 indoc! {"
961 fish fish
962 two fisˇh
963 "},
964 Mode::Normal,
965 );
966 let clipboard: Register = cx.read_from_clipboard().unwrap().into();
967 assert_eq!(clipboard.text, "fish");
968
969 cx.set_state(
970 indoc! {"
971 ˇfish one
972 two three
973 "},
974 Mode::Normal,
975 );
976 cx.simulate_keystrokes("y i w");
977 cx.simulate_keystrokes("w");
978 cx.simulate_keystrokes("v i w g shift-r");
979 cx.assert_state(
980 indoc! {"
981 fish fisˇh
982 two three
983 "},
984 Mode::Normal,
985 );
986 cx.simulate_keystrokes("g shift-r r");
987 cx.assert_state(
988 indoc! {"
989 fisˇh
990 two three
991 "},
992 Mode::Normal,
993 );
994 cx.simulate_keystrokes("j w g shift-r $");
995 cx.assert_state(
996 indoc! {"
997 fish
998 two fisˇh
999 "},
1000 Mode::Normal,
1001 );
1002 let clipboard: Register = cx.read_from_clipboard().unwrap().into();
1003 assert_eq!(clipboard.text, "fish");
1004 }
1005
1006 #[gpui::test]
1007 async fn test_replace_with_register_dot_repeat(cx: &mut gpui::TestAppContext) {
1008 let mut cx = VimTestContext::new(cx, true).await;
1009
1010 cx.set_state(
1011 indoc! {"
1012 ˇfish one
1013 two three
1014 "},
1015 Mode::Normal,
1016 );
1017 cx.simulate_keystrokes("y i w");
1018 cx.simulate_keystrokes("w");
1019 cx.simulate_keystrokes("g shift-r i w");
1020 cx.assert_state(
1021 indoc! {"
1022 fish fisˇh
1023 two three
1024 "},
1025 Mode::Normal,
1026 );
1027 cx.simulate_keystrokes("j .");
1028 cx.assert_state(
1029 indoc! {"
1030 fish fish
1031 two fisˇh
1032 "},
1033 Mode::Normal,
1034 );
1035 }
1036}