Skip to repository content764 lines · 28.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:34:48.717Z 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
clipboard.rs
1use super::*;
2use util::rel_path::RelPath;
3
4#[derive(Serialize, Deserialize, Clone, Debug)]
5pub struct ClipboardSelection {
6 /// The number of bytes in this selection.
7 pub len: usize,
8 /// Whether this was a full-line selection.
9 pub is_entire_line: bool,
10 /// The indentation of the first line when this content was originally copied.
11 pub first_line_indent: u32,
12 #[serde(default)]
13 pub file_path: Option<PathBuf>,
14 #[serde(default)]
15 pub line_range: Option<RangeInclusive<u32>>,
16}
17
18impl ClipboardSelection {
19 pub fn for_buffer(
20 len: usize,
21 is_entire_line: bool,
22 range: Range<Point>,
23 buffer: &MultiBufferSnapshot,
24 project: Option<&Entity<Project>>,
25 cx: &App,
26 ) -> Self {
27 let first_line_indent = buffer
28 .indent_size_for_line(MultiBufferRow(range.start.row))
29 .len;
30
31 let file_path = util::maybe!({
32 let project = project?.read(cx);
33 let file = buffer.file_at(range.start)?;
34 let project_path = ProjectPath {
35 worktree_id: file.worktree_id(cx),
36 path: file.path().clone(),
37 };
38 project.absolute_path(&project_path, cx)
39 });
40
41 let line_range = if file_path.is_some() {
42 buffer
43 .range_to_buffer_range(range)
44 .map(|(_, buffer_range)| buffer_range.start.row..=buffer_range.end.row)
45 } else {
46 None
47 };
48
49 Self {
50 len,
51 is_entire_line,
52 first_line_indent,
53 file_path,
54 line_range,
55 }
56 }
57}
58
59impl Editor {
60 pub fn do_paste(
61 &mut self,
62 text: &String,
63 clipboard_selections: Option<Vec<ClipboardSelection>>,
64 handle_entire_lines: bool,
65 window: &mut Window,
66 cx: &mut Context<Self>,
67 ) {
68 if self.read_only(cx) {
69 return;
70 }
71
72 self.finalize_last_transaction(cx);
73
74 let clipboard_text = Cow::Borrowed(text.as_str());
75
76 self.transact(window, cx, |this, window, cx| {
77 let had_active_edit_prediction = this.has_active_edit_prediction();
78 let display_map = this.display_snapshot(cx);
79 let old_selections = this.selections.all::<MultiBufferOffset>(&display_map);
80 let cursor_offset = this
81 .selections
82 .last::<MultiBufferOffset>(&display_map)
83 .head();
84
85 if let Some(mut clipboard_selections) = clipboard_selections {
86 let all_selections_were_entire_line =
87 clipboard_selections.iter().all(|s| s.is_entire_line);
88 let first_selection_indent_column =
89 clipboard_selections.first().map(|s| s.first_line_indent);
90 if clipboard_selections.len() != old_selections.len() {
91 clipboard_selections.drain(..);
92 }
93 let mut auto_indent_on_paste = true;
94
95 this.buffer.update(cx, |buffer, cx| {
96 let snapshot = buffer.read(cx);
97 auto_indent_on_paste = snapshot
98 .language_settings_at(cursor_offset, cx)
99 .auto_indent_on_paste;
100
101 let mut start_offset = 0;
102 let mut edits = Vec::new();
103 let mut original_indent_columns = Vec::new();
104 for (ix, selection) in old_selections.iter().enumerate() {
105 let to_insert;
106 let entire_line;
107 let original_indent_column;
108 if let Some(clipboard_selection) = clipboard_selections.get(ix) {
109 let end_offset = start_offset + clipboard_selection.len;
110 to_insert = &clipboard_text[start_offset..end_offset];
111 entire_line = clipboard_selection.is_entire_line;
112 start_offset = if entire_line {
113 end_offset
114 } else {
115 end_offset + 1
116 };
117 original_indent_column = Some(clipboard_selection.first_line_indent);
118 } else {
119 to_insert = &*clipboard_text;
120 entire_line = all_selections_were_entire_line;
121 original_indent_column = first_selection_indent_column
122 }
123
124 let (range, to_insert) =
125 if selection.is_empty() && handle_entire_lines && entire_line {
126 // If the corresponding selection was empty when this slice of the
127 // clipboard text was written, then the entire line containing the
128 // selection was copied. If this selection is also currently empty,
129 // then paste the line before the current line of the buffer.
130 let column = selection.start.to_point(&snapshot).column as usize;
131 let line_start = selection.start - column;
132 (line_start..line_start, Cow::Borrowed(to_insert))
133 } else {
134 let language = snapshot.language_at(selection.head());
135 let range = selection.range();
136 if let Some(language) = language
137 && language.name() == "Markdown"
138 {
139 edit_for_markdown_paste(
140 &snapshot,
141 range,
142 to_insert,
143 is_standalone_url(to_insert),
144 )
145 } else {
146 (range, Cow::Borrowed(to_insert))
147 }
148 };
149
150 edits.push((range, to_insert));
151 original_indent_columns.push(original_indent_column);
152 }
153 drop(snapshot);
154
155 buffer.edit(
156 edits,
157 if auto_indent_on_paste {
158 Some(AutoindentMode::Block {
159 original_indent_columns,
160 })
161 } else {
162 None
163 },
164 cx,
165 );
166 });
167
168 let selections = this
169 .selections
170 .all::<MultiBufferOffset>(&this.display_snapshot(cx));
171 this.change_selections(Default::default(), window, cx, |s| s.select(selections));
172 } else {
173 let clipboard_is_url = is_standalone_url(&clipboard_text);
174
175 let auto_indent_mode = if !clipboard_text.is_empty() {
176 Some(AutoindentMode::Block {
177 original_indent_columns: Vec::new(),
178 })
179 } else {
180 None
181 };
182
183 let selection_anchors = this.buffer.update(cx, |buffer, cx| {
184 let snapshot = buffer.snapshot(cx);
185
186 let anchors = old_selections
187 .iter()
188 .map(|s| {
189 let anchor = snapshot.anchor_after(s.head());
190 s.map(|_| anchor)
191 })
192 .collect::<Vec<_>>();
193
194 let mut edits = Vec::new();
195
196 // When pasting text without metadata (e.g. copied from an
197 // external editor using multiple cursors) and the number of
198 // lines matches the number of selections, distribute one
199 // line per cursor instead of pasting the whole text at each.
200 let lines: Vec<&str> = clipboard_text.split('\n').collect();
201 let distribute_lines =
202 old_selections.len() > 1 && lines.len() == old_selections.len();
203
204 for (ix, selection) in old_selections.iter().enumerate() {
205 let language = snapshot.language_at(selection.head());
206 let range = selection.range();
207
208 let text_for_cursor: &str = if distribute_lines {
209 lines[ix]
210 } else {
211 &clipboard_text
212 };
213
214 let (edit_range, edit_text) = if let Some(language) = language
215 && language.name() == "Markdown"
216 {
217 edit_for_markdown_paste(
218 &snapshot,
219 range,
220 text_for_cursor,
221 clipboard_is_url,
222 )
223 } else {
224 (range, Cow::Borrowed(text_for_cursor))
225 };
226
227 edits.push((edit_range, edit_text));
228 }
229
230 drop(snapshot);
231 buffer.edit(edits, auto_indent_mode, cx);
232
233 anchors
234 });
235
236 this.change_selections(Default::default(), window, cx, |s| {
237 s.select_anchors(selection_anchors);
238 });
239 }
240
241 // 🤔 | .. | show_in_menu |
242 // | .. | true true
243 // | had_edit_prediction | false true
244
245 let trigger_in_words =
246 this.show_edit_predictions_in_menu() || !had_active_edit_prediction;
247
248 this.trigger_completion_on_input(text, trigger_in_words, window, cx);
249 });
250 }
251
252 pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
253 if let Some(item) = cx.read_from_clipboard() {
254 self.paste_item(&item, window, cx);
255 }
256 }
257
258 pub fn paste_item(
259 &mut self,
260 item: &ClipboardItem,
261 window: &mut Window,
262 cx: &mut Context<Self>,
263 ) {
264 if self.read_only(cx) {
265 return;
266 }
267
268 let clipboard_image = item.entries().iter().find_map(|entry| match entry {
269 ClipboardEntry::Image(image) if !image.bytes.is_empty() => Some(image),
270 _ => None,
271 });
272
273 if let Some(image) = clipboard_image {
274 let is_markdown = {
275 let display_map = self.display_snapshot(cx);
276 let selections = self.selections.all::<MultiBufferOffset>(&display_map);
277 selections
278 .first()
279 .and_then(|s| display_map.buffer_snapshot().language_at(s.head()))
280 .map(|lang| lang.name() == "Markdown")
281 .unwrap_or(false)
282 };
283
284 if is_markdown {
285 let handled = maybe!({
286 let buffer = self.buffer().read(cx).as_singleton()?;
287 let file = buffer.read(cx).file()?;
288 let worktree_id = file.worktree_id(cx);
289 let dir_rel_path = file.path().parent()?.into_arc();
290 let worktree = self
291 .project
292 .as_ref()?
293 .read(cx)
294 .worktree_for_id(worktree_id, cx)?;
295
296 let extension = image.format.extension();
297 let snapshot = worktree.read(cx).snapshot();
298 let (filename, file_path) =
299 unused_image_path(&dir_rel_path, extension, |path| {
300 snapshot.entry_for_path(path).is_some()
301 })?;
302
303 let create_task = worktree.update(cx, |worktree, cx| {
304 worktree.create_entry(file_path, false, Some(image.bytes.clone()), cx)
305 });
306
307 cx.spawn_in(window, async move |editor, cx| {
308 create_task.await?;
309 editor.update_in(cx, |editor, window, cx| {
310 editor.insert_image_snippet(&filename, window, cx)
311 })
312 })
313 .detach_and_log_err(cx);
314
315 Some(())
316 });
317 if handled.is_some() {
318 // stop clipboard handling when the snippet is inserted
319 return;
320 }
321 }
322 }
323
324 let clipboard_string = item.entries().iter().find_map(|entry| match entry {
325 ClipboardEntry::String(s) => Some(s),
326 _ => None,
327 });
328 match clipboard_string {
329 Some(clipboard_string) => self.do_paste(
330 clipboard_string.text(),
331 clipboard_string.metadata_json::<Vec<ClipboardSelection>>(),
332 true,
333 window,
334 cx,
335 ),
336 _ => self.do_paste(&item.text().unwrap_or_default(), None, true, window, cx),
337 }
338 }
339
340 fn insert_image_snippet(
341 &mut self,
342 filename: &str,
343 window: &mut Window,
344 cx: &mut Context<Self>,
345 ) {
346 let Some(snippet) = Snippet::parse(&format!("$0")).log_err() else {
347 return;
348 };
349 let display_map = self.display_snapshot(cx);
350 let insertion_ranges: Vec<Range<MultiBufferOffset>> = self
351 .selections
352 .all::<MultiBufferOffset>(&display_map)
353 .into_iter()
354 .map(|selection| selection.start..selection.end)
355 .collect();
356 self.insert_snippet(&insertion_ranges, snippet, window, cx)
357 .log_err();
358 }
359
360 pub(super) fn cut_common(
361 &mut self,
362 cut_no_selection_line: bool,
363 window: &mut Window,
364 cx: &mut Context<Self>,
365 ) -> ClipboardItem {
366 let mut text = String::new();
367 let buffer = self.buffer.read(cx).snapshot(cx);
368 let mut selections = self.selections.all::<Point>(&self.display_snapshot(cx));
369 let mut clipboard_selections = Vec::with_capacity(selections.len());
370 {
371 let max_point = buffer.max_point();
372 let mut is_first = true;
373 let mut prev_selection_was_entire_line = false;
374 for selection in &mut selections {
375 let is_entire_line =
376 (selection.is_empty() && cut_no_selection_line) || self.selections.line_mode();
377 if is_entire_line {
378 selection.start = Point::new(selection.start.row, 0);
379 if !selection.is_empty() && selection.end.column == 0 {
380 selection.end = cmp::min(max_point, selection.end);
381 } else {
382 selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
383 }
384 selection.goal = SelectionGoal::None;
385 }
386 if is_first {
387 is_first = false;
388 } else if !prev_selection_was_entire_line {
389 text += "\n";
390 }
391 prev_selection_was_entire_line = is_entire_line;
392 let mut len = 0;
393 for chunk in buffer.text_for_range(selection.start..selection.end) {
394 text.push_str(chunk);
395 len += chunk.len();
396 }
397
398 clipboard_selections.push(ClipboardSelection::for_buffer(
399 len,
400 is_entire_line,
401 selection.range(),
402 &buffer,
403 self.project.as_ref(),
404 cx,
405 ));
406 }
407 }
408
409 self.transact(window, cx, |this, window, cx| {
410 this.change_selections(Default::default(), window, cx, |s| {
411 s.select(selections);
412 });
413 this.insert("", window, cx);
414 });
415 ClipboardItem::new_string_with_json_metadata(text, clipboard_selections)
416 }
417
418 pub(super) fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
419 if self.read_only(cx) {
420 return;
421 }
422 let item = self.cut_common(true, window, cx);
423 cx.write_to_clipboard(item);
424 }
425
426 pub(super) fn kill_ring_cut(
427 &mut self,
428 _: &KillRingCut,
429 window: &mut Window,
430 cx: &mut Context<Self>,
431 ) {
432 if self.read_only(cx) {
433 return;
434 }
435 let selection_count = self.selections.count();
436 let first_selection = self.selections.first_anchor();
437 let snapshot = self.buffer.read(cx).snapshot(cx);
438 let first_selection_is_empty = first_selection.start == first_selection.end;
439 let selection_start_point = first_selection.start.to_point(&snapshot);
440 let selection_start_row = selection_start_point.row;
441 let selection_start_column = selection_start_point.column;
442 let Some((_, text_anchor)) = self
443 .buffer
444 .read(cx)
445 .text_anchor_for_position(first_selection.start, cx)
446 else {
447 return;
448 };
449 let buffer_id = text_anchor.buffer_id;
450
451 self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
452 s.move_with(&mut |snapshot, sel| {
453 if sel.is_empty() {
454 sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row()));
455 }
456 if sel.is_empty() {
457 sel.end = DisplayPoint::new(sel.end.row() + 1_u32, 0);
458 }
459 });
460 });
461 let item = self.cut_common(false, window, cx);
462
463 let Some(item_text) = item.text() else {
464 return;
465 };
466
467 let entry_metadata = item.entries().first().and_then(|entry| match entry {
468 ClipboardEntry::String(entry) => entry.metadata_json::<Vec<ClipboardSelection>>(),
469 _ => None,
470 });
471
472 let can_append = selection_count == 1 && first_selection_is_empty;
473 let item = if can_append
474 && let Some(previous_ring) = cx.try_global::<KillRing>()
475 && previous_ring.can_append
476 && previous_ring.buffer_id == buffer_id
477 && previous_ring.row == selection_start_row
478 && previous_ring.column == selection_start_column
479 {
480 let mut entries = previous_ring
481 .metadata
482 .as_ref()
483 .map_or_else(Vec::new, Clone::clone);
484 if let Some(metadata) = entry_metadata.as_ref() {
485 entries.extend_from_slice(metadata);
486 }
487
488 let mut text = previous_ring.text.clone();
489 text.push_str(&item_text);
490 let text_len = text.len();
491
492 KillRing {
493 text,
494 metadata: kill_ring_metadata_for_text(entries, text_len),
495 row: previous_ring.row,
496 column: previous_ring.column,
497 buffer_id: previous_ring.buffer_id,
498 can_append,
499 }
500 } else {
501 KillRing {
502 text: item_text,
503 metadata: entry_metadata,
504 row: selection_start_row,
505 column: selection_start_column,
506 buffer_id,
507 can_append,
508 }
509 };
510
511 cx.set_global(item)
512 }
513
514 pub(super) fn kill_ring_yank(
515 &mut self,
516 _: &KillRingYank,
517 window: &mut Window,
518 cx: &mut Context<Self>,
519 ) {
520 if !cx.has_global::<KillRing>() {
521 return;
522 }
523
524 let (text, metadata) = cx.update_global::<KillRing, _>(|kill_ring, _| {
525 kill_ring.can_append = false;
526 (kill_ring.text.clone(), kill_ring.metadata.clone())
527 });
528
529 self.do_paste(&text, metadata, false, window, cx);
530 }
531
532 pub(super) fn copy_and_trim(
533 &mut self,
534 _: &CopyAndTrim,
535 _: &mut Window,
536 cx: &mut Context<Self>,
537 ) {
538 self.do_copy(true, cx);
539 }
540
541 pub(super) fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
542 self.do_copy(false, cx);
543 }
544
545 pub(super) fn diff_clipboard_with_selection(
546 &mut self,
547 _: &DiffClipboardWithSelection,
548 window: &mut Window,
549 cx: &mut Context<Self>,
550 ) {
551 let selections = self
552 .selections
553 .all::<MultiBufferOffset>(&self.display_snapshot(cx));
554
555 if selections.is_empty() {
556 log::warn!("There should always be at least one selection in Omega. This is a bug.");
557 return;
558 };
559
560 let clipboard_text = cx.read_from_clipboard().and_then(|item| {
561 item.entries().iter().find_map(|entry| match entry {
562 ClipboardEntry::String(text) => Some(text.text().to_string()),
563 _ => None,
564 })
565 });
566
567 let Some(clipboard_text) = clipboard_text else {
568 log::warn!("Clipboard doesn't contain text.");
569 return;
570 };
571
572 window.dispatch_action(
573 Box::new(DiffClipboardWithSelectionData {
574 clipboard_text,
575 editor: cx.entity(),
576 }),
577 cx,
578 );
579 }
580
581 fn do_copy(&self, strip_leading_indents: bool, cx: &mut Context<Self>) {
582 let selections = self.selections.all::<Point>(&self.display_snapshot(cx));
583 let buffer = self.buffer.read(cx).read(cx);
584 let mut text = String::new();
585 let mut clipboard_selections = Vec::with_capacity(selections.len());
586
587 let max_point = buffer.max_point();
588 let mut is_first = true;
589 let mut prev_selection_was_entire_line = false;
590 for selection in &selections {
591 let mut start = selection.start;
592 let mut end = selection.end;
593 let is_entire_line = selection.is_empty() || self.selections.line_mode();
594 let mut add_trailing_newline = false;
595 if is_entire_line {
596 start = Point::new(start.row, 0);
597 let next_line_start = Point::new(end.row + 1, 0);
598 if next_line_start <= max_point {
599 end = next_line_start;
600 } else {
601 // We're on the last line without a trailing newline.
602 // Copy to the end of the line and add a newline afterwards.
603 end = Point::new(end.row, buffer.line_len(MultiBufferRow(end.row)));
604 add_trailing_newline = true;
605 }
606 }
607
608 let mut trimmed_selections = Vec::new();
609 if strip_leading_indents && end.row.saturating_sub(start.row) > 0 {
610 let row = MultiBufferRow(start.row);
611 let first_indent = buffer.indent_size_for_line(row);
612 if first_indent.len == 0 || start.column > first_indent.len {
613 trimmed_selections.push(start..end);
614 } else {
615 trimmed_selections.push(
616 Point::new(row.0, first_indent.len)
617 ..Point::new(row.0, buffer.line_len(row)),
618 );
619 for row in start.row + 1..=end.row {
620 let mut line_len = buffer.line_len(MultiBufferRow(row));
621 if row == end.row {
622 line_len = end.column;
623 }
624 if line_len == 0 {
625 trimmed_selections.push(Point::new(row, 0)..Point::new(row, line_len));
626 continue;
627 }
628 let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row));
629 if row_indent_size.len >= first_indent.len {
630 trimmed_selections
631 .push(Point::new(row, first_indent.len)..Point::new(row, line_len));
632 } else {
633 trimmed_selections.clear();
634 trimmed_selections.push(start..end);
635 break;
636 }
637 }
638 }
639 } else {
640 trimmed_selections.push(start..end);
641 }
642
643 let is_multiline_trim = trimmed_selections.len() > 1;
644 let mut selection_len: usize = 0;
645
646 for trimmed_range in trimmed_selections {
647 if is_first {
648 is_first = false;
649 } else if is_multiline_trim || !prev_selection_was_entire_line {
650 text.push('\n');
651 if is_multiline_trim {
652 selection_len += 1;
653 }
654 }
655 for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) {
656 text.push_str(chunk);
657 selection_len += chunk.len();
658 }
659 if add_trailing_newline {
660 text.push('\n');
661 selection_len += 1;
662 }
663 }
664 prev_selection_was_entire_line = is_entire_line && !is_multiline_trim;
665
666 clipboard_selections.push(ClipboardSelection::for_buffer(
667 selection_len,
668 is_entire_line,
669 start..end,
670 &buffer,
671 self.project.as_ref(),
672 cx,
673 ));
674 }
675
676 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
677 text,
678 clipboard_selections,
679 ));
680 }
681}
682
683struct KillRing {
684 text: String,
685 metadata: Option<Vec<ClipboardSelection>>,
686 row: u32,
687 column: u32,
688 buffer_id: BufferId,
689 can_append: bool,
690}
691impl Global for KillRing {}
692
693fn kill_ring_metadata_for_text(
694 mut metadata: Vec<ClipboardSelection>,
695 text_len: usize,
696) -> Option<Vec<ClipboardSelection>> {
697 match metadata.len() {
698 0 => None,
699 1 => Some(metadata),
700 _ => {
701 let first_selection = metadata.remove(0);
702 Some(vec![ClipboardSelection {
703 len: text_len,
704 is_entire_line: false,
705 first_line_indent: first_selection.first_line_indent,
706 file_path: None,
707 line_range: None,
708 }])
709 }
710 }
711}
712
713fn edit_for_markdown_paste<'a>(
714 buffer: &MultiBufferSnapshot,
715 range: Range<MultiBufferOffset>,
716 to_insert: &'a str,
717 to_insert_is_url: bool,
718) -> (Range<MultiBufferOffset>, Cow<'a, str>) {
719 if !to_insert_is_url {
720 return (range, Cow::Borrowed(to_insert));
721 };
722
723 let old_text = buffer.text_for_range(range.clone()).collect::<String>();
724
725 let new_text = if range.is_empty() || is_standalone_url(&old_text) {
726 Cow::Borrowed(to_insert)
727 } else {
728 Cow::Owned(format!("[{old_text}]({to_insert})"))
729 };
730 (range, new_text)
731}
732
733/// Returns a filename of the form `image.{extension}` (or `image_{N}.{extension}`
734/// if taken) that does not collide with an existing entry in `dir_rel_path`,
735/// along with the full path of the candidate file.
736fn unused_image_path(
737 dir_rel_path: &RelPath,
738 extension: &str,
739 exists: impl Fn(&RelPath) -> bool,
740) -> Option<(String, Arc<RelPath>)> {
741 let mut filename = format!("image.{extension}");
742 let mut counter = 1u32;
743 loop {
744 let candidate = dir_rel_path.join(RelPath::from_unix_str(&filename).ok()?);
745 if !exists(&candidate) {
746 return Some((filename, candidate.into()));
747 }
748 filename = format!("image_{counter}.{extension}");
749 counter += 1;
750 }
751}
752
753/// Whether `text` consists solely of a single URL, as opposed to merely
754/// starting with a scheme-like prefix (e.g. a commit message like
755/// `editor: Fix ...`, which `url::Url::parse` would accept).
756fn is_standalone_url(text: &str) -> bool {
757 let mut finder = linkify::LinkFinder::new();
758 finder.kinds(&[linkify::LinkKind::Url]);
759 finder
760 .links(text)
761 .next()
762 .is_some_and(|link| link.start() == 0 && link.end() == text.len())
763}
764