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