Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:34:10.918Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

paste.rs

1372 lines · 48.4 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.