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