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().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::<AllLanguageSettings>(cx, |settings| {
713 settings.languages.0.insert(
714 LanguageName::new("Rust"),
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::<VimSettings>(cx, |s| {
776 s.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::<VimSettings>(cx, |s| {
822 s.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::<VimSettings>(cx, |s| {
851 s.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::<VimSettings>(cx, |s| {
910 s.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}