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