Skip to repository content549 lines · 17.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:48:14.159Z 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::{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