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