From 053ea47e5ac15c330dd62b669ca41b8a64ff3f02 Mon Sep 17 00:00:00 2001 From: Tom Planche Date: Thu, 11 Jun 2026 04:56:56 +0200 Subject: [PATCH] editor: Don't treat scheme-prefixed text as a URL when pasting in Markdown (#59071) Pasting text that merely starts with a scheme-like prefix (for example a commit message like `editor: Fix crash in project panel`) over a selection in a Markdown buffer would wrap the selection in a Markdown link, because `url::Url::parse` accepts `editor:` as a valid URL scheme. Paste now only treats clipboard text as a URL when the whole text is a single standalone URL, using `linkify` (already a workspace dependency) instead of `url::Url::parse`. Added a regression test covering the scheme-like prefix case. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #59070 Release Notes: - Fixed pasting text starting with a scheme-like prefix (such as `editor: ...`) over a selection in a Markdown buffer incorrectly creating a Markdown link. --- crates/editor/src/clipboard.rs | 29 +++++++++++++++++++++++------ crates/editor/src/editor_tests.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/clipboard.rs b/crates/editor/src/clipboard.rs index d1380a732c..2d3afdac12 100644 --- a/crates/editor/src/clipboard.rs +++ b/crates/editor/src/clipboard.rs @@ -139,7 +139,7 @@ impl Editor { &snapshot, range, to_insert, - url::Url::parse(to_insert).ok(), + is_standalone_url(to_insert), ) } else { (range, Cow::Borrowed(to_insert)) @@ -169,7 +169,7 @@ impl Editor { .all::(&this.display_snapshot(cx)); this.change_selections(Default::default(), window, cx, |s| s.select(selections)); } else { - let url = url::Url::parse(&clipboard_text).ok(); + let clipboard_is_url = is_standalone_url(&clipboard_text); let auto_indent_mode = if !clipboard_text.is_empty() { Some(AutoindentMode::Block { @@ -213,7 +213,12 @@ impl Editor { let (edit_range, edit_text) = if let Some(language) = language && language.name() == "Markdown" { - edit_for_markdown_paste(&snapshot, range, text_for_cursor, url.clone()) + edit_for_markdown_paste( + &snapshot, + range, + text_for_cursor, + clipboard_is_url, + ) } else { (range, Cow::Borrowed(text_for_cursor)) }; @@ -538,18 +543,30 @@ fn edit_for_markdown_paste<'a>( buffer: &MultiBufferSnapshot, range: Range, to_insert: &'a str, - url: Option, + to_insert_is_url: bool, ) -> (Range, Cow<'a, str>) { - if url.is_none() { + if !to_insert_is_url { return (range, Cow::Borrowed(to_insert)); }; let old_text = buffer.text_for_range(range.clone()).collect::(); - let new_text = if range.is_empty() || url::Url::parse(&old_text).is_ok() { + let new_text = if range.is_empty() || is_standalone_url(&old_text) { Cow::Borrowed(to_insert) } else { Cow::Owned(format!("[{old_text}]({to_insert})")) }; (range, new_text) } + +/// Whether `text` consists solely of a single URL, as opposed to merely +/// starting with a scheme-like prefix (e.g. a commit message like +/// `editor: Fix ...`, which `url::Url::parse` would accept). +fn is_standalone_url(text: &str) -> bool { + let mut finder = linkify::LinkFinder::new(); + finder.kinds(&[linkify::LinkKind::Url]); + finder + .links(text) + .next() + .is_some_and(|link| link.start() == 0 && link.end() == text.len()) +} diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b476b9bcda..fdcf906d4d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -33268,6 +33268,36 @@ async fn test_paste_plain_text_from_other_app_replaces_selection_without_creatin cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ")); } +#[gpui::test] +async fn test_paste_text_with_scheme_like_prefix_replaces_selection_without_creating_markdown_link( + cx: &mut gpui::TestAppContext, +) { + init_test(cx, |_| {}); + + // `url::Url::parse` accepts this as a URL with the scheme `editor`, but it + // should not be treated as one when pasting. + let text = "editor: Fix double-click bracket selection for large spans"; + + let markdown_language = Arc::new(Language::new( + LanguageConfig { + name: "Markdown".into(), + ..LanguageConfig::default() + }, + None, + )); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx)); + cx.set_state("«(feat on git-ui-add-info-exclude-to-context-menus) Fmtˇ»"); + + cx.update_editor(|editor, window, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(text.to_string())); + editor.paste(&Paste, window, cx); + }); + + cx.assert_editor_state(&format!("{text}ˇ")); +} + #[gpui::test] async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language( cx: &mut gpui::TestAppContext,