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,