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.
This commit is contained in:
Tom Planche authored and GitHub committed 2026-06-11 02:56:56 +00:00
1 parent 325ff16742
commit 053ea47e5a
2 files changed
+53 -6

No files matched your search

+23 -6
View File
@@ -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::<MultiBufferOffset>(&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<MultiBufferOffset>,
to_insert: &'a str,
url: Option<url::Url>,
to_insert_is_url: bool,
) -> (Range<MultiBufferOffset>, 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::<String>();
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())
}
+30
View File
@@ -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,