Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:48:14.159Z 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

549 lines · 17.8 KB · rust
1use editor::{ToOffset, movement};
2use gpui::{Action, Context, Window};
3use schemars::JsonSchema;
4use serde::Deserialize;
5use text::LineEnding;
6
7use crate::{Vim, state::Mode};
8
9/// Pastes text from the specified register at the cursor position.
10#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
11#[action(namespace = vim)]
12#[serde(deny_unknown_fields)]
13pub struct HelixPaste {
14    #[serde(default)]
15    before: bool,
16}
17
18impl Vim {
19    pub fn helix_paste(
20        &mut self,
21        action: &HelixPaste,
22        window: &mut Window,
23        cx: &mut Context<Self>,
24    ) {
25        self.record_current_action(cx);
26        self.store_visual_marks(window, cx);
27        let count = Vim::take_count(cx).unwrap_or(1);
28        // TODO: vim paste calls take_forced_motion here, but I don't know what that does
29        // (none of the other helix_ methods call it)
30
31        self.update_editor(cx, |vim, editor, cx| {
32            if editor.read_only(cx) {
33                return;
34            }
35
36            editor.transact(window, cx, |editor, window, cx| {
37                editor.set_clip_at_line_ends(false, cx);
38
39                let selected_register = vim.selected_register.take();
40
41                let Some(register) = Vim::update_globals(cx, |globals, cx| {
42                    globals.read_register(selected_register, Some(editor), cx)
43                })
44                .filter(|reg| !reg.text.is_empty()) else {
45                    return;
46                };
47                let text = register.text;
48                let clipboard_selections = register.clipboard_selections;
49
50                let display_map = editor.display_snapshot(cx);
51                let current_selections = editor.selections.all_adjusted_display(&display_map);
52
53                // The clipboard can have multiple selections, and there can
54                // be multiple selections. Helix zips them together, so the first
55                // clipboard entry gets pasted at the first selection, the second
56                // entry gets pasted at the second selection, and so on. If there
57                // are more clipboard selections than selections, the extra ones
58                // don't get pasted anywhere. If there are more selections than
59                // clipboard selections, the last clipboard selection gets
60                // pasted at all remaining selections.
61
62                let mut edits = Vec::new();
63                let mut new_selections = Vec::new();
64                let mut start_offset = 0;
65
66                let mut replacement_texts: Vec<String> = Vec::new();
67
68                for ix in 0..current_selections.len() {
69                    let to_insert = if let Some(clip_sel) =
70                        clipboard_selections.as_ref().and_then(|s| s.get(ix))
71                    {
72                        let end_offset = start_offset + clip_sel.len;
73                        let text = text[start_offset..end_offset].to_string();
74                        start_offset = if clip_sel.is_entire_line {
75                            end_offset
76                        } else {
77                            end_offset + 1
78                        };
79                        text
80                    } else if let Some(last_text) = replacement_texts.last() {
81                        // We have more current selections than clipboard selections: repeat the last one.
82                        last_text.to_owned()
83                    } else {
84                        text.to_string()
85                    };
86                    replacement_texts.push(to_insert);
87                }
88
89                let line_mode = replacement_texts.iter().any(|text| text.ends_with('\n'));
90
91                for (to_insert, sel) in replacement_texts.into_iter().zip(current_selections) {
92                    // Helix doesn't care about the head/tail of the selection.
93                    // Pasting before means pasting before the whole selection.
94                    let display_point = if line_mode {
95                        if action.before {
96                            movement::line_beginning(&display_map, sel.start, false)
97                        } else {
98                            if sel.start == sel.end {
99                                movement::right(
100                                    &display_map,
101                                    movement::line_end(&display_map, sel.end, false),
102                                )
103                            } else {
104                                sel.end
105                            }
106                        }
107                    } else if action.before {
108                        sel.start
109                    } else if sel.start == sel.end {
110                        // In Helix, a single-point cursor is "on top" of a
111                        // character, and pasting after means after that character.
112                        // At line end this means the next line. But on an empty
113                        // line there is no character, so paste at the cursor.
114                        let right = movement::right(&display_map, sel.end);
115                        if right.row() != sel.end.row() && sel.end.column() == 0 {
116                            sel.end
117                        } else {
118                            right
119                        }
120                    } else {
121                        sel.end
122                    };
123                    let point = display_point.to_point(&display_map);
124                    let anchor = if action.before {
125                        display_map.buffer_snapshot().anchor_after(point)
126                    } else {
127                        display_map.buffer_snapshot().anchor_before(point)
128                    };
129                    let mut to_insert = to_insert.repeat(count);
130                    // Buffer edits normalize line endings, so measure the normalized text.
131                    // Otherwise CRLF paste text can produce a selection range past the inserted text.
132                    // which can cause panics (selection offset greater than snapshot len) or invalid
133                    // selections
134                    LineEnding::normalize(&mut to_insert);
135                    new_selections.push((anchor, to_insert.len()));
136                    edits.push((point..point, to_insert));
137                }
138
139                editor.edit(edits, cx);
140
141                let snapshot = editor.buffer().read(cx).snapshot(cx);
142                editor.change_selections(Default::default(), window, cx, |s| {
143                    s.select_ranges(new_selections.into_iter().map(|(anchor, len)| {
144                        let offset = anchor.to_offset(&snapshot);
145                        if action.before {
146                            offset.saturating_sub_usize(len)..offset
147                        } else {
148                            offset..(offset + len)
149                        }
150                    }));
151                })
152            });
153        });
154
155        self.switch_mode(Mode::HelixNormal, true, window, cx);
156    }
157}
158
159#[cfg(test)]
160mod test {
161    use indoc::indoc;
162
163    use gpui::ClipboardItem;
164
165    use crate::{state::Mode, test::VimTestContext};
166
167    #[gpui::test]
168    async fn test_system_clipboard_paste(cx: &mut gpui::TestAppContext) {
169        let mut cx = VimTestContext::new(cx, true).await;
170        cx.enable_helix();
171        cx.set_state(
172            indoc! {"
173            The quiˇck brown
174            fox jumps over
175            the lazy dog."},
176            Mode::HelixNormal,
177        );
178
179        cx.write_to_clipboard(ClipboardItem::new_string("clipboard".to_string()));
180        cx.simulate_keystrokes("p");
181        cx.assert_state(
182            indoc! {"
183            The quic«clipboardˇ»k brown
184            fox jumps over
185            the lazy dog."},
186            Mode::HelixNormal,
187        );
188
189        // Multiple cursors with system clipboard (no metadata) pastes
190        // the same text at each cursor.
191        cx.set_state(
192            indoc! {"
193            ˇThe quick brown
194            fox ˇjumps over
195            the lazy dog."},
196            Mode::HelixNormal,
197        );
198        cx.write_to_clipboard(ClipboardItem::new_string("hi".to_string()));
199        cx.simulate_keystrokes("p");
200        cx.assert_state(
201            indoc! {"
202            T«hiˇ»he quick brown
203            fox j«hiˇ»umps over
204            the lazy dog."},
205            Mode::HelixNormal,
206        );
207
208        // Multiple cursors on empty lines should paste on those same lines.
209        cx.set_state(\nˇ\nˇ\nend", Mode::HelixNormal);
210        cx.write_to_clipboard(ClipboardItem::new_string("X".to_string()));
211        cx.simulate_keystrokes("p");
212        cx.assert_state("«Xˇ»\n«Xˇ»\n«Xˇ»\nend", Mode::HelixNormal);
213    }
214
215    #[gpui::test]
216    async fn test_system_clipboard_crlf_paste_at_end_of_buffer(cx: &mut gpui::TestAppContext) {
217        let mut cx = VimTestContext::new(cx, true).await;
218        cx.enable_helix();
219        cx.set_state("ˇ", Mode::HelixNormal);
220
221        cx.write_to_clipboard(ClipboardItem::new_string("a\r\nb".to_string()));
222        cx.simulate_keystrokes("p");
223
224        cx.assert_state("«a\nbˇ»", Mode::HelixNormal);
225    }
226
227    #[gpui::test]
228    async fn test_read_only_paste(cx: &mut gpui::TestAppContext) {
229        let mut cx = VimTestContext::new(cx, true).await;
230        cx.enable_helix();
231        cx.set_state("aˇb", Mode::HelixNormal);
232        cx.write_to_clipboard(ClipboardItem::new_string("clipboard".to_string()));
233        cx.update_editor(|editor, _window, _cx| editor.set_read_only(true));
234
235        cx.simulate_keystrokes("p");
236
237        cx.assert_state("aˇb", Mode::HelixNormal);
238    }
239
240    #[gpui::test]
241    async fn test_paste(cx: &mut gpui::TestAppContext) {
242        let mut cx = VimTestContext::new(cx, true).await;
243        cx.enable_helix();
244        cx.set_state(
245            indoc! {"
246            The «quiˇ»ck brown
247            fox jumps over
248            the lazy dog."},
249            Mode::HelixNormal,
250        );
251
252        cx.simulate_keystrokes("y w p");
253
254        cx.assert_state(
255            indoc! {"
256            The quick «quiˇ»brown
257            fox jumps over
258            the lazy dog."},
259            Mode::HelixNormal,
260        );
261
262        // Pasting before the selection:
263        cx.set_state(
264            indoc! {"
265            The quick brown
266            fox «jumpsˇ» over
267            the lazy dog."},
268            Mode::HelixNormal,
269        );
270        cx.simulate_keystrokes("shift-p");
271        cx.assert_state(
272            indoc! {"
273            The quick brown
274            fox «quiˇ»jumps over
275            the lazy dog."},
276            Mode::HelixNormal,
277        );
278    }
279
280    #[gpui::test]
281    async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) {
282        let mut cx = VimTestContext::new(cx, true).await;
283        cx.enable_helix();
284        cx.set_state(
285            indoc! {"
286            The quiˇck brown
287            fox jumps over
288            the lazy dog."},
289            Mode::HelixNormal,
290        );
291
292        cx.simulate_keystrokes("y");
293
294        // Pasting before the selection:
295        cx.set_state(
296            indoc! {"
297            The quick brown
298            fox jumpsˇ over
299            the lazy dog."},
300            Mode::HelixNormal,
301        );
302        cx.simulate_keystrokes("shift-p");
303        cx.assert_state(
304            indoc! {"
305            The quick brown
306            fox jumps«cˇ» over
307            the lazy dog."},
308            Mode::HelixNormal,
309        );
310
311        // Pasting after the selection:
312        cx.set_state(
313            indoc! {"
314            The quick brown
315            fox jumpsˇ over
316            the lazy dog."},
317            Mode::HelixNormal,
318        );
319        cx.simulate_keystrokes("p");
320        cx.assert_state(
321            indoc! {"
322            The quick brown
323            fox jumps «cˇ»over
324            the lazy dog."},
325            Mode::HelixNormal,
326        );
327
328        // Pasting after the selection at the end of a line:
329        cx.set_state(
330            indoc! {"
331            The quick brown
332            fox jumps overˇ
333            the lazy dog."},
334            Mode::HelixNormal,
335        );
336        cx.simulate_keystrokes("p");
337        cx.assert_state(
338            indoc! {"
339            The quick brown
340            fox jumps over
341            «cˇ»the lazy dog."},
342            Mode::HelixNormal,
343        );
344    }
345
346    #[gpui::test]
347    async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) {
348        let mut cx = VimTestContext::new(cx, true).await;
349        cx.enable_helix();
350        // Select two blocks of text.
351        cx.set_state(
352            indoc! {"
353            The «quiˇ»ck brown
354            fox ju«mpsˇ» over
355            the lazy dog."},
356            Mode::HelixNormal,
357        );
358        cx.simulate_keystrokes("y");
359
360        // Only one cursor: only the first block gets pasted.
361        cx.set_state(
362            indoc! {"
363            ˇThe quick brown
364            fox jumps over
365            the lazy dog."},
366            Mode::HelixNormal,
367        );
368        cx.simulate_keystrokes("shift-p");
369        cx.assert_state(
370            indoc! {"
371            «quiˇ»The quick brown
372            fox jumps over
373            the lazy dog."},
374            Mode::HelixNormal,
375        );
376
377        // Two cursors: both get pasted.
378        cx.set_state(
379            indoc! {"
380            ˇThe ˇquick brown
381            fox jumps over
382            the lazy dog."},
383            Mode::HelixNormal,
384        );
385        cx.simulate_keystrokes("shift-p");
386        cx.assert_state(
387            indoc! {"
388            «quiˇ»The «mpsˇ»quick brown
389            fox jumps over
390            the lazy dog."},
391            Mode::HelixNormal,
392        );
393
394        // Three cursors: the second yanked block is duplicated.
395        cx.set_state(
396            indoc! {"
397            ˇThe ˇquick brown
398            fox jumpsˇ over
399            the lazy dog."},
400            Mode::HelixNormal,
401        );
402        cx.simulate_keystrokes("shift-p");
403        cx.assert_state(
404            indoc! {"
405            «quiˇ»The «mpsˇ»quick brown
406            fox jumps«mpsˇ» over
407            the lazy dog."},
408            Mode::HelixNormal,
409        );
410
411        // Again with three cursors. All three should be pasted twice.
412        cx.set_state(
413            indoc! {"
414            ˇThe ˇquick brown
415            fox jumpsˇ over
416            the lazy dog."},
417            Mode::HelixNormal,
418        );
419        cx.simulate_keystrokes("2 shift-p");
420        cx.assert_state(
421            indoc! {"
422            «quiquiˇ»The «mpsmpsˇ»quick brown
423            fox jumps«mpsmpsˇ» over
424            the lazy dog."},
425            Mode::HelixNormal,
426        );
427    }
428
429    #[gpui::test]
430    async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) {
431        let mut cx = VimTestContext::new(cx, true).await;
432        cx.enable_helix();
433        cx.set_state(
434            indoc! {"
435            The quick brow«n
436            ˇ»fox jumps over
437            the lazy dog."},
438            Mode::HelixNormal,
439        );
440
441        cx.simulate_keystrokes("y shift-p");
442
443        cx.assert_state(
444            indoc! {"
445            «n
446            ˇ»The quick brown
447            fox jumps over
448            the lazy dog."},
449            Mode::HelixNormal,
450        );
451
452        // In line mode, if we're in the middle of a line then pasting before pastes on
453        // the line before.
454        cx.set_state(
455            indoc! {"
456            The quick brown
457            fox jumpsˇ over
458            the lazy dog."},
459            Mode::HelixNormal,
460        );
461        cx.simulate_keystrokes("shift-p");
462        cx.assert_state(
463            indoc! {"
464            The quick brown
465            «n
466            ˇ»fox jumps over
467            the lazy dog."},
468            Mode::HelixNormal,
469        );
470
471        // In line mode, if we're in the middle of a line then pasting after pastes on
472        // the line after.
473        cx.set_state(
474            indoc! {"
475            The quick brown
476            fox jumpsˇ over
477            the lazy dog."},
478            Mode::HelixNormal,
479        );
480        cx.simulate_keystrokes("p");
481        cx.assert_state(
482            indoc! {"
483            The quick brown
484            fox jumps over
485            «n
486            ˇ»the lazy dog."},
487            Mode::HelixNormal,
488        );
489
490        // If we're currently at the end of a line, "the line after"
491        // means right after the cursor.
492        cx.set_state(
493            indoc! {"
494            The quick brown
495            fox jumps overˇ
496            the lazy dog."},
497            Mode::HelixNormal,
498        );
499        cx.simulate_keystrokes("p");
500        cx.assert_state(
501            indoc! {"
502            The quick brown
503            fox jumps over
504            «n
505            ˇ»the lazy dog."},
506            Mode::HelixNormal,
507        );
508
509        cx.set_state(
510            indoc! {"
511
512            The quick brown
513            fox jumps overˇ
514            the lazy dog."},
515            Mode::HelixNormal,
516        );
517        cx.simulate_keystrokes("x y up up p");
518        cx.assert_state(
519            indoc! {"
520
521            «fox jumps over
522            ˇ»The quick brown
523            fox jumps over
524            the lazy dog."},
525            Mode::HelixNormal,
526        );
527
528        cx.set_state(
529            indoc! {"
530            «The quick brown
531            fox jumps over
532            ˇ»the lazy dog."},
533            Mode::HelixNormal,
534        );
535        cx.simulate_keystrokes("y p p");
536        cx.assert_state(
537            indoc! {"
538            The quick brown
539            fox jumps over
540            The quick brown
541            fox jumps over
542            «The quick brown
543            fox jumps over
544            ˇ»the lazy dog."},
545            Mode::HelixNormal,
546        );
547    }
548}
549
Served at tenant.openagents/omega Member data and write actions are omitted.