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:
1 parent
325ff16742
commit
053ea47e5a
2 files changed
+53
-6
No files matched your search
@@ -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())
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user