Skip to repository content1372 lines · 48.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:28:32.851Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
paste.rs
1use editor::{
2 DisplayPoint, MultiBufferOffset, RowExt, SelectionEffects, ToOffset,
3 display_map::ToDisplayPoint, movement,
4};
5use gpui::{Action, Context, Window};
6use language::{Bias, SelectionGoal};
7use schemars::JsonSchema;
8use serde::Deserialize;
9use settings::Settings;
10use std::cmp;
11use text::LineEnding;
12use vim_mode_setting::HelixModeSetting;
13
14use crate::{
15 Vim,
16 motion::{Motion, MotionKind},
17 object::Object,
18 state::{Mode, Register},
19};
20
21/// Pastes text from the specified register at the cursor position.
22#[derive(Clone, Default, Deserialize, JsonSchema, PartialEq, Action)]
23#[action(namespace = vim)]
24#[serde(deny_unknown_fields)]
25pub struct Paste {
26 #[serde(default)]
27 before: bool,
28 #[serde(default)]
29 pub(crate) preserve_clipboard: bool,
30}
31
32impl Vim {
33 pub fn paste(&mut self, action: &Paste, window: &mut Window, cx: &mut Context<Self>) {
34 self.record_current_action(cx);
35 self.store_visual_marks(window, cx);
36 let count = Vim::take_count(cx).unwrap_or(1);
37 Vim::take_forced_motion(cx);
38
39 self.update_editor(cx, |vim, editor, cx| {
40 if editor.read_only(cx) {
41 return;
42 }
43
44 let text_layout_details = editor.text_layout_details(window, cx);
45 editor.transact(window, cx, |editor, window, cx| {
46 editor.set_clip_at_line_ends(false, cx);
47
48 let selected_register = vim.selected_register.take();
49
50 let Some(Register {
51 text,
52 clipboard_selections,
53 }) = Vim::update_globals(cx, |globals, cx| {
54 globals.read_register(selected_register, Some(editor), cx)
55 })
56 .filter(|reg| !reg.text.is_empty())
57 else {
58 vim.set_status_label(
59 format!("Nothing in register {}", selected_register.unwrap_or('"')),
60 cx,
61 );
62 return;
63 };
64 let clipboard_selections = clipboard_selections
65 .filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine);
66
67 if !action.preserve_clipboard && vim.mode.is_visual() {
68 vim.copy_selections_content(editor, MotionKind::for_mode(vim.mode), window, cx);
69 }
70
71 let display_map = editor.display_snapshot(cx);
72 let current_selections = editor.selections.all_adjusted_display(&display_map);
73
74 // unlike zed, if you have a multi-cursor selection from vim block mode,
75 // pasting it will paste it on subsequent lines, even if you don't yet
76 // have a cursor there.
77 let mut selections_to_process = Vec::new();
78 let mut i = 0;
79 while i < current_selections.len() {
80 selections_to_process
81 .push((current_selections[i].start..current_selections[i].end, true));
82 i += 1;
83 }
84 if let Some(clipboard_selections) = clipboard_selections.as_ref() {
85 let left = current_selections
86 .iter()
87 .map(|selection| cmp::min(selection.start.column(), selection.end.column()))
88 .min()
89 .unwrap();
90 let mut row = current_selections.last().unwrap().end.row().next_row();
91 while i < clipboard_selections.len() {
92 let cursor =
93 display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
94 selections_to_process.push((cursor..cursor, false));
95 i += 1;
96 row.0 += 1;
97 }
98 }
99
100 let first_selection_indent_column =
101 clipboard_selections.as_ref().and_then(|zed_selections| {
102 zed_selections
103 .first()
104 .map(|selection| selection.first_line_indent)
105 });
106 let before = action.before || vim.mode == Mode::VisualLine;
107
108 let mut edits = Vec::new();
109 let mut new_selections = Vec::new();
110 let mut original_indent_columns = Vec::new();
111 let mut start_offset = 0;
112 let mut mark_start_adjustments = Vec::new();
113
114 for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
115 let (mut to_insert, original_indent_column) =
116 if let Some(clipboard_selections) = &clipboard_selections {
117 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
118 let end_offset = start_offset + clipboard_selection.len;
119 let text = text[start_offset..end_offset].to_string();
120 start_offset = if clipboard_selection.is_entire_line {
121 end_offset
122 } else {
123 end_offset + 1
124 };
125 (text, Some(clipboard_selection.first_line_indent))
126 } else {
127 ("".to_string(), first_selection_indent_column)
128 }
129 } else {
130 (text.to_string(), first_selection_indent_column)
131 };
132 LineEnding::normalize(&mut to_insert);
133 let line_mode = to_insert.ends_with('\n');
134 let is_multiline = to_insert.contains('\n');
135
136 if line_mode && !before {
137 if selection.is_empty() {
138 to_insert =
139 "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
140 mark_start_adjustments.push(1usize);
141 } else {
142 to_insert = "\n".to_owned() + &to_insert;
143 mark_start_adjustments.push(1usize);
144 }
145 } else if line_mode && vim.mode == Mode::VisualLine {
146 to_insert.pop();
147 mark_start_adjustments.push(0usize);
148 } else {
149 mark_start_adjustments.push(0usize);
150 }
151
152 let display_range = if !selection.is_empty() {
153 // If vim is in VISUAL LINE mode and the column for the
154 // selection's end point is 0, that means that the
155 // cursor is at the newline character (\n) at the end of
156 // the line. In this situation we'll want to move one
157 // position to the left, ensuring we don't join the last
158 // line of the selection with the line directly below.
159 let end_point =
160 if vim.mode == Mode::VisualLine && selection.end.column() == 0 {
161 movement::left(&display_map, selection.end)
162 } else {
163 selection.end
164 };
165
166 selection.start..end_point
167 } else if line_mode {
168 let point = if before {
169 movement::line_beginning(&display_map, selection.start, false)
170 } else {
171 movement::line_end(&display_map, selection.start, false)
172 };
173 point..point
174 } else {
175 let point = if before {
176 selection.start
177 } else {
178 movement::saturating_right(&display_map, selection.start)
179 };
180 point..point
181 };
182
183 let point_range = display_range.start.to_point(&display_map)
184 ..display_range.end.to_point(&display_map);
185 let anchor = if is_multiline || vim.mode == Mode::VisualLine {
186 display_map
187 .buffer_snapshot()
188 .anchor_before(point_range.start)
189 } else {
190 display_map.buffer_snapshot().anchor_after(point_range.end)
191 };
192
193 if *preserve {
194 new_selections.push((anchor, line_mode, is_multiline));
195 }
196 edits.push((point_range, to_insert.repeat(count)));
197 original_indent_columns.push(original_indent_column);
198 }
199
200 // Record anchors before applying edits to track pasted text start.
201 // anchor_before(start) stays before inserted text (for `[` mark).
202 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
203 let mark_start_anchors: Vec<_> = edits
204 .iter()
205 .map(|(range, _)| buffer_snapshot.anchor_before(range.start))
206 .collect();
207 let paste_text_last_character_offsets: Vec<_> = edits
208 .iter()
209 .map(|(_, text)| {
210 text.strip_suffix('\n')
211 .unwrap_or(text.as_str())
212 .char_indices()
213 .next_back()
214 .map_or(0, |(offset, _)| offset)
215 })
216 .collect();
217
218 let cursor_offset = editor
219 .selections
220 .last::<MultiBufferOffset>(&display_map)
221 .head();
222 if editor
223 .buffer()
224 .read(cx)
225 .snapshot(cx)
226 .language_settings_at(cursor_offset, cx)
227 .auto_indent_on_paste
228 {
229 editor.edit_with_block_indent(edits, original_indent_columns, cx);
230 } else {
231 editor.edit(edits, cx);
232 }
233
234 // Set `[` and `]` marks to the pasted text range.
235 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
236 let start_anchors: Vec<_> = mark_start_anchors
237 .iter()
238 .zip(mark_start_adjustments.iter())
239 .map(|(anchor, &adj)| {
240 let offset = anchor.to_offset(&buffer_snapshot) + adj;
241 buffer_snapshot.anchor_before(offset)
242 })
243 .collect();
244 let end_anchors: Vec<_> = mark_start_anchors
245 .iter()
246 .zip(paste_text_last_character_offsets.iter())
247 .map(|(anchor, &last_character_offset)| {
248 let start = anchor.to_offset(&buffer_snapshot);
249 let end = start + last_character_offset;
250 buffer_snapshot.anchor_after(end)
251 })
252 .collect();
253 vim.set_mark("[".to_string(), start_anchors, editor.buffer(), window, cx);
254 vim.set_mark("]".to_string(), end_anchors, editor.buffer(), window, cx);
255
256 // in line_mode vim will insert the new text on the next (or previous if before) line
257 // 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).
258 // otherwise vim will insert the next text at (or before) the current cursor position,
259 // the cursor will go to the last (or first, if is_multiline) inserted character.
260 editor.change_selections(Default::default(), window, cx, |s| {
261 s.replace_cursors_with(|map| {
262 let mut cursors = Vec::new();
263 for (anchor, line_mode, is_multiline) in &new_selections {
264 let mut cursor = anchor.to_display_point(map);
265 if *line_mode {
266 if !before {
267 cursor = movement::down(
268 map,
269 cursor,
270 SelectionGoal::None,
271 false,
272 &text_layout_details,
273 )
274 .0;
275 }
276 cursor = movement::indented_line_beginning(map, cursor, true, true);
277 } else if !is_multiline && !vim.temp_mode {
278 cursor = movement::saturating_left(map, cursor)
279 }
280 cursors.push(cursor);
281 if vim.mode == Mode::VisualBlock {
282 break;
283 }
284 }
285
286 cursors
287 });
288 })
289 });
290 });
291
292 if HelixModeSetting::get_global(cx).0 {
293 self.switch_mode(Mode::HelixNormal, true, window, cx);
294 } else {
295 self.switch_mode(Mode::Normal, true, window, cx);
296 }
297 }
298
299 pub fn replace_with_register_object(
300 &mut self,
301 object: Object,
302 around: bool,
303 window: &mut Window,
304 cx: &mut Context<Self>,
305 ) {
306 self.stop_recording(cx);
307 let selected_register = self.selected_register.take();
308 self.update_editor(cx, |vim, editor, cx| {
309 editor.transact(window, cx, |editor, window, cx| {
310 editor.set_clip_at_line_ends(false, cx);
311 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
312 s.move_with(&mut |map, selection| {
313 object.expand_selection(map, selection, around, None);
314 });
315 });
316
317 let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| {
318 globals.read_register(selected_register, Some(editor), cx)
319 })
320 .filter(|reg| !reg.text.is_empty()) else {
321 vim.set_status_label(
322 format!("Nothing in register {}", selected_register.unwrap_or('"')),
323 cx,
324 );
325 return;
326 };
327 editor.insert(&text, window, cx);
328 editor.set_clip_at_line_ends(true, cx);
329 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
330 s.move_with(&mut |map, selection| {
331 selection.start = map.clip_point(selection.start, Bias::Left);
332 selection.end = selection.start
333 })
334 })
335 });
336 });
337 }
338
339 pub fn replace_with_register_motion(
340 &mut self,
341 motion: Motion,
342 times: Option<usize>,
343 forced_motion: bool,
344 window: &mut Window,
345 cx: &mut Context<Self>,
346 ) {
347 self.stop_recording(cx);
348 let selected_register = self.selected_register.take();
349 self.update_editor(cx, |vim, editor, cx| {
350 let text_layout_details = editor.text_layout_details(window, cx);
351 editor.transact(window, cx, |editor, window, cx| {
352 editor.set_clip_at_line_ends(false, cx);
353 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
354 s.move_with(&mut |map, selection| {
355 motion.expand_selection(
356 map,
357 selection,
358 times,
359 &text_layout_details,
360 forced_motion,
361 );
362 });
363 });
364
365 let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| {
366 globals.read_register(selected_register, Some(editor), cx)
367 })
368 .filter(|reg| !reg.text.is_empty()) else {
369 vim.set_status_label(
370 format!("Nothing in register {}", selected_register.unwrap_or('"')),
371 cx,
372 );
373 return;
374 };
375 editor.insert(&text, window, cx);
376 editor.set_clip_at_line_ends(true, cx);
377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
378 s.move_with(&mut |map, selection| {
379 selection.start = map.clip_point(selection.start, Bias::Left);
380 selection.end = selection.start
381 })
382 })
383 });
384 });
385 }
386}
387
388#[cfg(test)]
389mod test {
390 use crate::{
391 state::{Mode, Register},
392 test::{NeovimBackedTestContext, VimTestContext},
393 };
394 use gpui::ClipboardItem;
395 use indoc::indoc;
396 use language::{LanguageName, language_settings::LanguageSettingsContent};
397 use settings::{SettingsStore, UseSystemClipboard};
398
399 #[gpui::test]
400 async fn test_paste(cx: &mut gpui::TestAppContext) {
401 let mut cx = NeovimBackedTestContext::new(cx).await;
402
403 // single line
404 cx.set_shared_state(indoc! {"
405 The quick brown
406 fox ˇjumps over
407 the lazy dog"})
408 .await;
409 cx.simulate_shared_keystrokes("v w y").await;
410 cx.shared_clipboard().await.assert_eq("jumps o");
411 cx.set_shared_state(indoc! {"
412 The quick brown
413 fox jumps oveˇr
414 the lazy dog"})
415 .await;
416 cx.simulate_shared_keystrokes("p").await;
417 cx.shared_state().await.assert_eq(indoc! {"
418 The quick brown
419 fox jumps overjumps ˇo
420 the lazy dog"});
421
422 cx.set_shared_state(indoc! {"
423 The quick brown
424 fox jumps oveˇr
425 the lazy dog"})
426 .await;
427 cx.simulate_shared_keystrokes("shift-p").await;
428 cx.shared_state().await.assert_eq(indoc! {"
429 The quick brown
430 fox jumps ovejumps ˇor
431 the lazy dog"});
432
433 // line mode
434 cx.set_shared_state(indoc! {"
435 The quick brown
436 fox juˇmps over
437 the lazy dog"})
438 .await;
439 cx.simulate_shared_keystrokes("d d").await;
440 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
441 cx.shared_state().await.assert_eq(indoc! {"
442 The quick brown
443 the laˇzy dog"});
444 cx.simulate_shared_keystrokes("p").await;
445 cx.shared_state().await.assert_eq(indoc! {"
446 The quick brown
447 the lazy dog
448 ˇfox jumps over"});
449 cx.simulate_shared_keystrokes("k shift-p").await;
450 cx.shared_state().await.assert_eq(indoc! {"
451 The quick brown
452 ˇfox jumps over
453 the lazy dog
454 fox jumps over"});
455
456 // multiline, cursor to first character of pasted text.
457 cx.set_shared_state(indoc! {"
458 The quick brown
459 fox jumps ˇover
460 the lazy dog"})
461 .await;
462 cx.simulate_shared_keystrokes("v j y").await;
463 cx.shared_clipboard().await.assert_eq("over\nthe lazy do");
464
465 cx.simulate_shared_keystrokes("p").await;
466 cx.shared_state().await.assert_eq(indoc! {"
467 The quick brown
468 fox jumps oˇover
469 the lazy dover
470 the lazy dog"});
471 cx.simulate_shared_keystrokes("u shift-p").await;
472 cx.shared_state().await.assert_eq(indoc! {"
473 The quick brown
474 fox jumps ˇover
475 the lazy doover
476 the lazy dog"});
477 }
478
479 #[gpui::test]
480 async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
481 let mut cx = VimTestContext::new(cx, true).await;
482
483 cx.update_global(|store: &mut SettingsStore, cx| {
484 store.update_user_settings(cx, |s| {
485 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
486 });
487 });
488
489 cx.set_state(
490 indoc! {"
491 The quick brown
492 fox jˇumps over
493 the lazy dog"},
494 Mode::Normal,
495 );
496 cx.simulate_keystrokes("v i w y");
497 cx.assert_state(
498 indoc! {"
499 The quick brown
500 fox ˇjumps over
501 the lazy dog"},
502 Mode::Normal,
503 );
504 cx.simulate_keystrokes("p");
505 cx.assert_state(
506 indoc! {"
507 The quick brown
508 fox jjumpˇsumps over
509 the lazy dog"},
510 Mode::Normal,
511 );
512 assert_eq!(cx.read_from_clipboard(), None);
513 }
514
515 #[gpui::test]
516 async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
517 let mut cx = VimTestContext::new(cx, true).await;
518
519 cx.update_global(|store: &mut SettingsStore, cx| {
520 store.update_user_settings(cx, |s| {
521 s.vim.get_or_insert_default().use_system_clipboard =
522 Some(UseSystemClipboard::OnYank)
523 });
524 });
525
526 // copy in visual mode
527 cx.set_state(
528 indoc! {"
529 The quick brown
530 fox jˇumps over
531 the lazy dog"},
532 Mode::Normal,
533 );
534 cx.simulate_keystrokes("v i w y");
535 cx.assert_state(
536 indoc! {"
537 The quick brown
538 fox ˇjumps over
539 the lazy dog"},
540 Mode::Normal,
541 );
542 cx.simulate_keystrokes("p");
543 cx.assert_state(
544 indoc! {"
545 The quick brown
546 fox jjumpˇsumps over
547 the lazy dog"},
548 Mode::Normal,
549 );
550 assert_eq!(
551 cx.read_from_clipboard().map(|item| item.text().unwrap()),
552 Some("jumps".into())
553 );
554 cx.simulate_keystrokes("d d p");
555 cx.assert_state(
556 indoc! {"
557 The quick brown
558 the lazy dog
559 ˇfox jjumpsumps over"},
560 Mode::Normal,
561 );
562 assert_eq!(
563 cx.read_from_clipboard().map(|item| item.text().unwrap()),
564 Some("jumps".into())
565 );
566 cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string()));
567 cx.simulate_keystrokes("shift-p");
568 cx.assert_state(
569 indoc! {"
570 The quick brown
571 the lazy dog
572 test-copˇyfox jjumpsumps over"},
573 Mode::Normal,
574 );
575 }
576
577 #[gpui::test]
578 async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
579 let mut cx = NeovimBackedTestContext::new(cx).await;
580
581 // copy in visual mode
582 cx.set_shared_state(indoc! {"
583 The quick brown
584 fox jˇumps over
585 the lazy dog"})
586 .await;
587 cx.simulate_shared_keystrokes("v i w y").await;
588 cx.shared_state().await.assert_eq(indoc! {"
589 The quick brown
590 fox ˇjumps over
591 the lazy dog"});
592 // paste in visual mode
593 cx.simulate_shared_keystrokes("w v i w p").await;
594 cx.shared_state().await.assert_eq(indoc! {"
595 The quick brown
596 fox jumps jumpˇs
597 the lazy dog"});
598 cx.shared_clipboard().await.assert_eq("over");
599 // paste in visual line mode
600 cx.simulate_shared_keystrokes("up shift-v shift-p").await;
601 cx.shared_state().await.assert_eq(indoc! {"
602 ˇover
603 fox jumps jumps
604 the lazy dog"});
605 cx.shared_clipboard().await.assert_eq("over");
606 // paste in visual block mode
607 cx.simulate_shared_keystrokes("ctrl-v down down p").await;
608 cx.shared_state().await.assert_eq(indoc! {"
609 oveˇrver
610 overox jumps jumps
611 overhe lazy dog"});
612
613 // copy in visual line mode
614 cx.set_shared_state(indoc! {"
615 The quick brown
616 fox juˇmps over
617 the lazy dog"})
618 .await;
619 cx.simulate_shared_keystrokes("shift-v d").await;
620 cx.shared_state().await.assert_eq(indoc! {"
621 The quick brown
622 the laˇzy dog"});
623 // paste in visual mode
624 cx.simulate_shared_keystrokes("v i w p").await;
625 cx.shared_state().await.assert_eq(indoc! {"
626 The quick brown
627 the•
628 ˇfox jumps over
629 dog"});
630 cx.shared_clipboard().await.assert_eq("lazy");
631 cx.set_shared_state(indoc! {"
632 The quick brown
633 fox juˇmps over
634 the lazy dog"})
635 .await;
636 cx.simulate_shared_keystrokes("shift-v d").await;
637 cx.shared_state().await.assert_eq(indoc! {"
638 The quick brown
639 the laˇzy dog"});
640 cx.shared_clipboard().await.assert_eq("fox jumps over\n");
641 // paste in visual line mode
642 cx.simulate_shared_keystrokes("k shift-v p").await;
643 cx.shared_state().await.assert_eq(indoc! {"
644 ˇfox jumps over
645 the lazy dog"});
646 cx.shared_clipboard().await.assert_eq("The quick brown\n");
647
648 // Copy line and paste in visual mode, with cursor on newline character.
649 cx.set_shared_state(indoc! {"
650 ˇThe quick brown
651 fox jumps over
652 the lazy dog"})
653 .await;
654 cx.simulate_shared_keystrokes("y y shift-v j $ p").await;
655 cx.shared_state().await.assert_eq(indoc! {"
656 ˇThe quick brown
657 the lazy dog"});
658 }
659
660 #[gpui::test]
661 async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
662 let mut cx = NeovimBackedTestContext::new(cx).await;
663 // copy in visual block mode
664 cx.set_shared_state(indoc! {"
665 The ˇquick brown
666 fox jumps over
667 the lazy dog"})
668 .await;
669 cx.simulate_shared_keystrokes("ctrl-v 2 j y").await;
670 cx.shared_clipboard().await.assert_eq("q\nj\nl");
671 cx.simulate_shared_keystrokes("p").await;
672 cx.shared_state().await.assert_eq(indoc! {"
673 The qˇquick brown
674 fox jjumps over
675 the llazy dog"});
676 cx.simulate_shared_keystrokes("v i w shift-p").await;
677 cx.shared_state().await.assert_eq(indoc! {"
678 The ˇq brown
679 fox jjjumps over
680 the lllazy dog"});
681 cx.simulate_shared_keystrokes("v i w shift-p").await;
682
683 cx.set_shared_state(indoc! {"
684 The ˇquick brown
685 fox jumps over
686 the lazy dog"})
687 .await;
688 cx.simulate_shared_keystrokes("ctrl-v j y").await;
689 cx.shared_clipboard().await.assert_eq("q\nj");
690 cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await;
691 cx.shared_state().await.assert_eq(indoc! {"
692 The qˇqick brown
693 fox jjmps over
694 the lzy dog"});
695
696 cx.simulate_shared_keystrokes("shift-v p").await;
697 cx.shared_state().await.assert_eq(indoc! {"
698 ˇq
699 j
700 fox jjmps over
701 the lzy dog"});
702 }
703
704 #[gpui::test]
705 async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
706 let mut cx = VimTestContext::new_typescript(cx).await;
707
708 cx.set_state(
709 indoc! {"
710 class A {ˇ
711 }
712 "},
713 Mode::Normal,
714 );
715 cx.simulate_keystrokes("o a ( ) { escape");
716 cx.assert_state(
717 indoc! {"
718 class A {
719 a()ˇ{}
720 }
721 "},
722 Mode::Normal,
723 );
724 // cursor goes to the first non-blank character in the line;
725 cx.simulate_keystrokes("y y p");
726 cx.assert_state(
727 indoc! {"
728 class A {
729 a(){}
730 ˇa(){}
731 }
732 "},
733 Mode::Normal,
734 );
735 // indentation is preserved when pasting
736 cx.simulate_keystrokes("u shift-v up y shift-p");
737 cx.assert_state(
738 indoc! {"
739 ˇclass A {
740 a(){}
741 class A {
742 a(){}
743 }
744 "},
745 Mode::Normal,
746 );
747 }
748
749 #[gpui::test]
750 async fn test_paste_auto_indent(cx: &mut gpui::TestAppContext) {
751 let mut cx = VimTestContext::new(cx, true).await;
752
753 cx.set_state(
754 indoc! {"
755 mod some_module {
756 ˇfn main() {
757 }
758 }
759 "},
760 Mode::Normal,
761 );
762 // default auto indentation
763 cx.simulate_keystrokes("y y p");
764 cx.assert_state(
765 indoc! {"
766 mod some_module {
767 fn main() {
768 ˇfn main() {
769 }
770 }
771 "},
772 Mode::Normal,
773 );
774 // back to previous state
775 cx.simulate_keystrokes("u u");
776 cx.assert_state(
777 indoc! {"
778 mod some_module {
779 ˇfn main() {
780 }
781 }
782 "},
783 Mode::Normal,
784 );
785 cx.update_global(|store: &mut SettingsStore, cx| {
786 store.update_user_settings(cx, |settings| {
787 settings.project.all_languages.languages.0.insert(
788 LanguageName::new_static("Rust").0.to_string(),
789 LanguageSettingsContent {
790 auto_indent_on_paste: Some(false),
791 ..Default::default()
792 },
793 );
794 });
795 });
796 // auto indentation turned off
797 cx.simulate_keystrokes("y y p");
798 cx.assert_state(
799 indoc! {"
800 mod some_module {
801 fn main() {
802 ˇfn main() {
803 }
804 }
805 "},
806 Mode::Normal,
807 );
808 }
809
810 #[gpui::test]
811 async fn test_paste_count(cx: &mut gpui::TestAppContext) {
812 let mut cx = NeovimBackedTestContext::new(cx).await;
813
814 cx.set_shared_state(indoc! {"
815 onˇe
816 two
817 three
818 "})
819 .await;
820 cx.simulate_shared_keystrokes("y y 3 p").await;
821 cx.shared_state().await.assert_eq(indoc! {"
822 one
823 ˇone
824 one
825 one
826 two
827 three
828 "});
829
830 cx.set_shared_state(indoc! {"
831 one
832 ˇtwo
833 three
834 "})
835 .await;
836 cx.simulate_shared_keystrokes("y $ $ 3 p").await;
837 cx.shared_state().await.assert_eq(indoc! {"
838 one
839 twotwotwotwˇo
840 three
841 "});
842 }
843
844 #[gpui::test]
845 async fn test_paste_system_clipboard_never(cx: &mut gpui::TestAppContext) {
846 let mut cx = VimTestContext::new(cx, true).await;
847
848 cx.update_global(|store: &mut SettingsStore, cx| {
849 store.update_user_settings(cx, |s| {
850 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
851 });
852 });
853
854 cx.set_state(
855 indoc! {"
856 ˇThe quick brown
857 fox jumps over
858 the lazy dog"},
859 Mode::Normal,
860 );
861
862 cx.write_to_clipboard(ClipboardItem::new_string("something else".to_string()));
863
864 cx.simulate_keystrokes("d d");
865 cx.assert_state(
866 indoc! {"
867 ˇfox jumps over
868 the lazy dog"},
869 Mode::Normal,
870 );
871
872 cx.simulate_keystrokes("shift-v p");
873 cx.assert_state(
874 indoc! {"
875 ˇThe quick brown
876 the lazy dog"},
877 Mode::Normal,
878 );
879
880 cx.simulate_keystrokes("shift-v");
881 cx.dispatch_action(editor::actions::Paste);
882 cx.assert_state(
883 indoc! {"
884 ˇsomething else
885 the lazy dog"},
886 Mode::Normal,
887 );
888 }
889
890 #[gpui::test]
891 async fn test_editor_paste_visual_preserves_system_clipboard(cx: &mut gpui::TestAppContext) {
892 let mut cx = VimTestContext::new(cx, true).await;
893
894 cx.set_state(
895 indoc! {"
896 The quick brown
897 fox ˇjumps over
898 the lazy dog"},
899 Mode::Normal,
900 );
901
902 // Put known content on the system clipboard
903 cx.write_to_clipboard(ClipboardItem::new_string("from clipboard".to_string()));
904
905 // Select "jumps" in visual mode, then editor::Paste (Cmd-V / Ctrl-V)
906 cx.simulate_keystrokes("v i w");
907 cx.dispatch_action(editor::actions::Paste);
908
909 // The selected text should be replaced with clipboard content
910 cx.assert_state(
911 indoc! {"
912 The quick brown
913 fox from clipboarˇd over
914 the lazy dog"},
915 Mode::Normal,
916 );
917
918 // System clipboard must still hold the original value, not "jumps"
919 assert_eq!(
920 cx.read_from_clipboard().map(|item| item.text().unwrap()),
921 Some("from clipboard".into()),
922 );
923 }
924
925 #[gpui::test]
926 async fn test_numbered_registers(cx: &mut gpui::TestAppContext) {
927 let mut cx = NeovimBackedTestContext::new(cx).await;
928
929 cx.update_global(|store: &mut SettingsStore, cx| {
930 store.update_user_settings(cx, |s| {
931 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
932 });
933 });
934
935 cx.set_shared_state(indoc! {"
936 The quick brown
937 fox jˇumps over
938 the lazy dog"})
939 .await;
940 cx.simulate_shared_keystrokes("y y \" 0 p").await;
941 cx.shared_register('0').await.assert_eq("fox jumps over\n");
942 cx.shared_register('"').await.assert_eq("fox jumps over\n");
943
944 cx.shared_state().await.assert_eq(indoc! {"
945 The quick brown
946 fox jumps over
947 ˇfox jumps over
948 the lazy dog"});
949 cx.simulate_shared_keystrokes("k k d d").await;
950 cx.shared_register('0').await.assert_eq("fox jumps over\n");
951 cx.shared_register('1').await.assert_eq("The quick brown\n");
952 cx.shared_register('"').await.assert_eq("The quick brown\n");
953
954 cx.simulate_shared_keystrokes("d d shift-g d d").await;
955 cx.shared_register('0').await.assert_eq("fox jumps over\n");
956 cx.shared_register('3').await.assert_eq("The quick brown\n");
957 cx.shared_register('2').await.assert_eq("fox jumps over\n");
958 cx.shared_register('1').await.assert_eq("the lazy dog\n");
959
960 cx.shared_state().await.assert_eq(indoc! {"
961 ˇfox jumps over"});
962
963 cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await;
964 cx.set_shared_state(indoc! {"
965 The quick brown
966 fox jumps over
967 ˇthe lazy dog"})
968 .await;
969 }
970
971 #[gpui::test]
972 async fn test_named_registers(cx: &mut gpui::TestAppContext) {
973 let mut cx = NeovimBackedTestContext::new(cx).await;
974
975 cx.update_global(|store: &mut SettingsStore, cx| {
976 store.update_user_settings(cx, |s| {
977 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
978 });
979 });
980
981 cx.set_shared_state(indoc! {"
982 The quick brown
983 fox jˇumps over
984 the lazy dog"})
985 .await;
986 cx.simulate_shared_keystrokes("\" a d a w").await;
987 cx.shared_register('a').await.assert_eq("jumps ");
988 cx.simulate_shared_keystrokes("\" shift-a d i w").await;
989 cx.shared_register('a').await.assert_eq("jumps over");
990 cx.shared_register('"').await.assert_eq("jumps over");
991 cx.simulate_shared_keystrokes("\" a p").await;
992 cx.shared_state().await.assert_eq(indoc! {"
993 The quick brown
994 fox jumps oveˇr
995 the lazy dog"});
996 cx.simulate_shared_keystrokes("\" a d a w").await;
997 cx.shared_register('a').await.assert_eq(" over");
998 }
999
1000 #[gpui::test]
1001 async fn test_special_registers(cx: &mut gpui::TestAppContext) {
1002 let mut cx = NeovimBackedTestContext::new(cx).await;
1003
1004 cx.update_global(|store: &mut SettingsStore, cx| {
1005 store.update_user_settings(cx, |s| {
1006 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
1007 });
1008 });
1009
1010 cx.set_shared_state(indoc! {"
1011 The quick brown
1012 fox jˇumps over
1013 the lazy dog"})
1014 .await;
1015 cx.simulate_shared_keystrokes("d i w").await;
1016 cx.shared_register('-').await.assert_eq("jumps");
1017 cx.simulate_shared_keystrokes("\" _ d d").await;
1018 cx.shared_register('_').await.assert_eq("");
1019
1020 cx.simulate_shared_keystrokes("shift-v \" _ y w").await;
1021 cx.shared_register('"').await.assert_eq("jumps");
1022
1023 cx.shared_state().await.assert_eq(indoc! {"
1024 The quick brown
1025 the ˇlazy dog"});
1026 cx.simulate_shared_keystrokes("\" \" d ^").await;
1027 cx.shared_register('0').await.assert_eq("the ");
1028 cx.shared_register('"').await.assert_eq("the ");
1029
1030 cx.simulate_shared_keystrokes("^ \" + d $").await;
1031 cx.shared_clipboard().await.assert_eq("lazy dog");
1032 cx.shared_register('"').await.assert_eq("lazy dog");
1033
1034 cx.simulate_shared_keystrokes("/ d o g enter").await;
1035 cx.shared_register('/').await.assert_eq("dog");
1036 cx.simulate_shared_keystrokes("\" / shift-p").await;
1037 cx.shared_state().await.assert_eq(indoc! {"
1038 The quick brown
1039 doˇg"});
1040
1041 // not testing nvim as it doesn't have a filename
1042 cx.simulate_keystrokes("\" % p");
1043 #[cfg(not(target_os = "windows"))]
1044 cx.assert_state(
1045 indoc! {"
1046 The quick brown
1047 dogdir/file.rˇs"},
1048 Mode::Normal,
1049 );
1050 #[cfg(target_os = "windows")]
1051 cx.assert_state(
1052 indoc! {"
1053 The quick brown
1054 dogdir\\file.rˇs"},
1055 Mode::Normal,
1056 );
1057 }
1058
1059 #[gpui::test]
1060 async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) {
1061 let mut cx = VimTestContext::new(cx, true).await;
1062
1063 cx.update_global(|store: &mut SettingsStore, cx| {
1064 store.update_user_settings(cx, |s| {
1065 s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
1066 });
1067 });
1068
1069 cx.set_state(
1070 indoc! {"
1071 ˇfish one
1072 fish two
1073 fish red
1074 fish blue
1075 "},
1076 Mode::Normal,
1077 );
1078 cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p");
1079 cx.assert_state(
1080 indoc! {"
1081 onˇefish•
1082 twˇofish•
1083 reˇdfish•
1084 bluˇefish•
1085 "},
1086 Mode::Normal,
1087 );
1088 }
1089
1090 #[gpui::test]
1091 async fn test_replace_with_register(cx: &mut gpui::TestAppContext) {
1092 let mut cx = VimTestContext::new(cx, true).await;
1093
1094 cx.set_state(
1095 indoc! {"
1096 ˇfish one
1097 two three
1098 "},
1099 Mode::Normal,
1100 );
1101 cx.simulate_keystrokes("y i w");
1102 cx.simulate_keystrokes("w");
1103 cx.simulate_keystrokes("g shift-r i w");
1104 cx.assert_state(
1105 indoc! {"
1106 fish fisˇh
1107 two three
1108 "},
1109 Mode::Normal,
1110 );
1111 cx.simulate_keystrokes("j b g shift-r e");
1112 cx.assert_state(
1113 indoc! {"
1114 fish fish
1115 two fisˇh
1116 "},
1117 Mode::Normal,
1118 );
1119 let clipboard: Register = cx.read_from_clipboard().unwrap().into();
1120 assert_eq!(clipboard.text, "fish");
1121
1122 cx.set_state(
1123 indoc! {"
1124 ˇfish one
1125 two three
1126 "},
1127 Mode::Normal,
1128 );
1129 cx.simulate_keystrokes("y i w");
1130 cx.simulate_keystrokes("w");
1131 cx.simulate_keystrokes("v i w g shift-r");
1132 cx.assert_state(
1133 indoc! {"
1134 fish fisˇh
1135 two three
1136 "},
1137 Mode::Normal,
1138 );
1139 cx.simulate_keystrokes("g shift-r r");
1140 cx.assert_state(
1141 indoc! {"
1142 fisˇh
1143 two three
1144 "},
1145 Mode::Normal,
1146 );
1147 cx.simulate_keystrokes("j w g shift-r $");
1148 cx.assert_state(
1149 indoc! {"
1150 fish
1151 two fisˇh
1152 "},
1153 Mode::Normal,
1154 );
1155 let clipboard: Register = cx.read_from_clipboard().unwrap().into();
1156 assert_eq!(clipboard.text, "fish");
1157 }
1158
1159 #[gpui::test]
1160 async fn test_replace_with_register_dot_repeat(cx: &mut gpui::TestAppContext) {
1161 let mut cx = VimTestContext::new(cx, true).await;
1162
1163 cx.set_state(
1164 indoc! {"
1165 ˇfish one
1166 two three
1167 "},
1168 Mode::Normal,
1169 );
1170 cx.simulate_keystrokes("y i w");
1171 cx.simulate_keystrokes("w");
1172 cx.simulate_keystrokes("g shift-r i w");
1173 cx.assert_state(
1174 indoc! {"
1175 fish fisˇh
1176 two three
1177 "},
1178 Mode::Normal,
1179 );
1180 cx.simulate_keystrokes("j .");
1181 cx.assert_state(
1182 indoc! {"
1183 fish fish
1184 two fisˇh
1185 "},
1186 Mode::Normal,
1187 );
1188 }
1189
1190 #[gpui::test]
1191 async fn test_paste_entire_line_from_editor_copy(cx: &mut gpui::TestAppContext) {
1192 let mut cx = VimTestContext::new(cx, true).await;
1193
1194 cx.set_state(
1195 indoc! {"
1196 ˇline one
1197 line two
1198 line three"},
1199 Mode::Normal,
1200 );
1201
1202 // Simulate what the editor's do_copy produces for two entire-line selections:
1203 // entire-line selections are NOT separated by an extra newline in the clipboard text.
1204 let clipboard_text = "line one\nline two\n".to_string();
1205 let clipboard_selections = vec![
1206 editor::ClipboardSelection {
1207 len: "line one\n".len(),
1208 is_entire_line: true,
1209 first_line_indent: 0,
1210 file_path: None,
1211 line_range: None,
1212 },
1213 editor::ClipboardSelection {
1214 len: "line two\n".len(),
1215 is_entire_line: true,
1216 first_line_indent: 0,
1217 file_path: None,
1218 line_range: None,
1219 },
1220 ];
1221 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1222 clipboard_text,
1223 clipboard_selections,
1224 ));
1225
1226 cx.simulate_keystrokes("p");
1227 cx.assert_state(
1228 indoc! {"
1229 line one
1230 ˇline one
1231 line two
1232 line two
1233 line three"},
1234 Mode::Normal,
1235 );
1236 }
1237
1238 #[gpui::test]
1239 async fn test_paste_marks(cx: &mut gpui::TestAppContext) {
1240 let mut cx = VimTestContext::new(cx, true).await;
1241
1242 // Yank a word, paste it elsewhere, then verify `[ and `] point to pasted text.
1243 cx.set_state(
1244 indoc! {"
1245 ˇhello world
1246 foo bar"},
1247 Mode::Normal,
1248 );
1249 cx.simulate_keystrokes("y i w");
1250 cx.simulate_keystrokes("j w");
1251 cx.simulate_keystrokes("p");
1252 cx.assert_state(
1253 indoc! {"
1254 hello world
1255 foo bhellˇoar"},
1256 Mode::Normal,
1257 );
1258 // `[ should go to start of pasted text
1259 cx.simulate_keystrokes("` [");
1260 cx.assert_state(
1261 indoc! {"
1262 hello world
1263 foo bˇhelloar"},
1264 Mode::Normal,
1265 );
1266 // `] should go to end of pasted text
1267 cx.simulate_keystrokes("` ]");
1268 cx.assert_state(
1269 indoc! {"
1270 hello world
1271 foo bhellˇoar"},
1272 Mode::Normal,
1273 );
1274
1275 // Line-mode paste: yank a line, paste below, verify marks.
1276 cx.set_state(
1277 indoc! {"
1278 ˇfirst line
1279 second line
1280 third line"},
1281 Mode::Normal,
1282 );
1283 cx.simulate_keystrokes("y y j p");
1284 cx.assert_state(
1285 indoc! {"
1286 first line
1287 second line
1288 ˇfirst line
1289 third line"},
1290 Mode::Normal,
1291 );
1292 cx.simulate_keystrokes("` [");
1293 cx.assert_state(
1294 indoc! {"
1295 first line
1296 second line
1297 ˇfirst line
1298 third line"},
1299 Mode::Normal,
1300 );
1301 cx.simulate_keystrokes("` ]");
1302 cx.assert_state(
1303 indoc! {"
1304 first line
1305 second line
1306 first linˇe
1307 third line"},
1308 Mode::Normal,
1309 );
1310 }
1311
1312 #[gpui::test]
1313 async fn test_paste_marks_unicode(cx: &mut gpui::TestAppContext) {
1314 let mut cx = VimTestContext::new(cx, true).await;
1315 cx.set_state("ˇxy", Mode::Normal);
1316 cx.write_to_clipboard(ClipboardItem::new_string("é".to_string()));
1317
1318 cx.simulate_keystrokes("p");
1319 cx.simulate_keystrokes("` ]");
1320
1321 cx.assert_state("xˇéy", Mode::Normal);
1322 }
1323
1324 #[gpui::test]
1325 async fn test_paste_marks_normalize_line_endings(cx: &mut gpui::TestAppContext) {
1326 let mut cx = VimTestContext::new(cx, true).await;
1327 cx.set_state("ˇxy", Mode::Normal);
1328 cx.write_to_clipboard(ClipboardItem::new_string("a\r\nb".to_string()));
1329
1330 cx.simulate_keystrokes("p");
1331 cx.simulate_keystrokes("` ]");
1332
1333 cx.assert_state("xa\nˇby", Mode::Normal);
1334 }
1335
1336 #[gpui::test]
1337 async fn test_paste_marks_read_only(cx: &mut gpui::TestAppContext) {
1338 let mut cx = VimTestContext::new(cx, true).await;
1339 cx.set_state("ˇx", Mode::Normal);
1340 cx.write_to_clipboard(ClipboardItem::new_string("long text".to_string()));
1341 cx.update_editor(|editor, _window, _cx| editor.set_read_only(true));
1342
1343 cx.simulate_keystrokes("p");
1344
1345 cx.assert_state("ˇx", Mode::Normal);
1346 }
1347
1348 #[gpui::test]
1349 async fn test_paste_marks_linewise_before(cx: &mut gpui::TestAppContext) {
1350 let mut cx = VimTestContext::new(cx, true).await;
1351 cx.set_state(
1352 indoc! {"
1353 ˇfirst line
1354 second line
1355 third line"},
1356 Mode::Normal,
1357 );
1358
1359 cx.simulate_keystrokes("y y j shift-p");
1360 cx.simulate_keystrokes("` [ v ` ]");
1361
1362 cx.assert_state(
1363 indoc! {"
1364 first line
1365 «first lineˇ»
1366 second line
1367 third line"},
1368 Mode::Visual,
1369 );
1370 }
1371}
1372