Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:34:28.673Z 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

clipboard.rs

764 lines · 28.8 KB · rust
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!("![$1]({filename})$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
Served at tenant.openagents/omega Member data and write actions are omitted.