From 6626f5f07a8e6f69997b8e0b0aafd2cb58b86c99 Mon Sep 17 00:00:00 2001 From: Meghan Kulkarni Date: Mon, 26 Jun 2023 17:25:52 -0700 Subject: [PATCH] turning highlighted Markdown text to link to pasted URL (#185924) * turning highlighted Mardown text to link to pasted URL * resolved comments * resolved more comments * preserved behavior of existing file pasting logic --------- Co-authored-by: Meghan Kulkarni --- .../markdown-language-features/package.json | 6 ++ .../package.nls.json | 1 + .../src/commands/insertResource.ts | 4 +- .../src/extension.shared.ts | 2 + .../languageFeatures/copyFiles/copyPaste.ts | 16 +++-- .../copyFiles/copyPasteLinks.ts | 53 ++++++++++++++++ .../copyFiles/dropIntoEditor.ts | 6 +- .../src/languageFeatures/copyFiles/shared.ts | 61 ++++++++++++++----- .../dropOrPasteInto/browser/postEditWidget.ts | 26 +++++--- 9 files changed, 142 insertions(+), 33 deletions(-) create mode 100644 extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPasteLinks.ts diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index bcac9df9e23bd..1f4fcbc6034d9 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -498,6 +498,12 @@ "%configuration.copyIntoWorkspace.never%" ] }, + "markdown.editor.pasteUrlAsFormattedLink.enabled": { + "type": "boolean", + "scope": "resource", + "markdownDescription": "%configuration.markdown.editor.pasteUrlAsFormattedLink.enabled%", + "default": true + }, "markdown.validate.enabled": { "type": "boolean", "scope": "resource", diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 9f7d5f4bce623..c97971764ea9d 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -41,6 +41,7 @@ "configuration.markdown.editor.drop.copyIntoWorkspace": "Controls if files outside of the workspace that are dropped into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied dropped files should be created", "configuration.markdown.editor.filePaste.enabled": "Enable pasting files into a Markdown editor to create Markdown links. Requires enabling `#editor.pasteAs.enabled#`.", "configuration.markdown.editor.filePaste.copyIntoWorkspace": "Controls if files outside of the workspace that are pasted into a Markdown editor should be copied into the workspace.\n\nUse `#markdown.copyFiles.destination#` to configure where copied files should be created.", + "configuration.markdown.editor.pasteUrlAsFormattedLink.enabled": "Controls if a Markdown link is created when a URL is pasted into the Markdown editor.", "configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.", "configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.", "configuration.markdown.validate.enabled.description": "Enable all error reporting in Markdown files.", diff --git a/extensions/markdown-language-features/src/commands/insertResource.ts b/extensions/markdown-language-features/src/commands/insertResource.ts index f5d0cf8fe139b..2e7b97c3048fe 100644 --- a/extensions/markdown-language-features/src/commands/insertResource.ts +++ b/extensions/markdown-language-features/src/commands/insertResource.ts @@ -76,10 +76,10 @@ async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: vscode await vscode.workspace.applyEdit(edit); } -function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean) { +function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean, title = '', placeholderValue = 0) { const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => { const selectionText = activeEditor.document.getText(selection); - const snippet = createUriListSnippet(activeEditor.document, selectedFiles, { + const snippet = createUriListSnippet(activeEditor.document, selectedFiles, title, placeholderValue, { insertAsMedia, placeholderText: selectionText, placeholderStartIndex: (i + 1) * selectedFiles.length, diff --git a/extensions/markdown-language-features/src/extension.shared.ts b/extensions/markdown-language-features/src/extension.shared.ts index ce709783d0bed..79242573136a5 100644 --- a/extensions/markdown-language-features/src/extension.shared.ts +++ b/extensions/markdown-language-features/src/extension.shared.ts @@ -8,6 +8,7 @@ import { MdLanguageClient } from './client/client'; import { CommandManager } from './commandManager'; import { registerMarkdownCommands } from './commands/index'; import { registerPasteSupport } from './languageFeatures/copyFiles/copyPaste'; +import { registerLinkPasteSupport } from './languageFeatures/copyFiles/copyPasteLinks'; import { registerDiagnosticSupport } from './languageFeatures/diagnostics'; import { registerDropIntoEditorSupport } from './languageFeatures/copyFiles/dropIntoEditor'; import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences'; @@ -59,6 +60,7 @@ function registerMarkdownLanguageFeatures( registerDropIntoEditorSupport(selector), registerFindFileReferenceSupport(commandManager, client), registerPasteSupport(selector), + registerLinkPasteSupport(selector), registerUpdateLinksOnRename(client), ); } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts index ed6d99bd97312..9d1bc00310605 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { Schemes } from '../../util/schemes'; -import { createEditForMediaFiles, mediaMimes, tryGetUriListSnippet } from './shared'; +import { createEditForMediaFiles, getMarkdownLink, mediaMimes } from './shared'; class PasteEditProvider implements vscode.DocumentPasteEditProvider { @@ -13,7 +13,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider { async provideDocumentPasteEdits( document: vscode.TextDocument, - _ranges: readonly vscode.Range[], + ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken, ): Promise { @@ -27,12 +27,18 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider { return createEdit; } - const snippet = await tryGetUriListSnippet(document, dataTransfer, token); - if (!snippet) { + const label = vscode.l10n.t('Insert Markdown Media'); + const uriEdit = new vscode.DocumentPasteEdit('', this._id, label); + const urlList = await dataTransfer.get('text/uri-list')?.asString(); + if (!urlList) { + return; + } + const pasteEdit = await getMarkdownLink(document, ranges, urlList, token); + if (!pasteEdit) { return; } - const uriEdit = new vscode.DocumentPasteEdit(snippet.snippet, this._id, snippet.label); + uriEdit.additionalEdit = pasteEdit.additionalEdits; uriEdit.priority = this._getPriority(dataTransfer); return uriEdit; } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPasteLinks.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPasteLinks.ts new file mode 100644 index 0000000000000..d6946ff6fbedc --- /dev/null +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPasteLinks.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { getMarkdownLink } from './shared'; + +class PasteLinkEditProvider implements vscode.DocumentPasteEditProvider { + + private readonly _id = 'insertMarkdownLink'; + async provideDocumentPasteEdits( + document: vscode.TextDocument, + ranges: readonly vscode.Range[], + dataTransfer: vscode.DataTransfer, + token: vscode.CancellationToken, + ): Promise { + const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.pasteUrlAsFormattedLink.enabled', true); + if (!enabled) { + return; + } + + // Check if dataTransfer contains a URL + const item = dataTransfer.get('text/plain'); + try { + new URL(await item?.value); + } catch (error) { + return; + } + + const label = vscode.l10n.t('Insert Markdown Link'); + const uriEdit = new vscode.DocumentPasteEdit('', this._id, label); + const urlList = await item?.asString(); + if (!urlList) { + return undefined; + } + const pasteEdit = await getMarkdownLink(document, ranges, urlList, token); + if (!pasteEdit) { + return; + } + + uriEdit.additionalEdit = pasteEdit.additionalEdits; + return uriEdit; + } +} + +export function registerLinkPasteSupport(selector: vscode.DocumentSelector,) { + return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteLinkEditProvider(), { + pasteMimeTypes: [ + 'text/plain', + ] + }); +} diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropIntoEditor.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropIntoEditor.ts index 4eec8a93dec2f..95c8455e307ad 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropIntoEditor.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/dropIntoEditor.ts @@ -30,7 +30,11 @@ class MarkdownImageDropProvider implements vscode.DocumentDropEditProvider { } private async _getUriListEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { - const snippet = await tryGetUriListSnippet(document, dataTransfer, token); + const urlList = await dataTransfer.get('text/uri-list')?.asString(); + if (!urlList) { + return undefined; + } + const snippet = await tryGetUriListSnippet(document, urlList, token); if (!snippet) { return undefined; } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts index 92d455b51aaf4..402ed0727980f 100644 --- a/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts +++ b/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts @@ -56,10 +56,32 @@ export const mediaMimes = new Set([ 'audio/x-wav', ]); +export async function getMarkdownLink(document: vscode.TextDocument, ranges: readonly vscode.Range[], urlList: string, token: vscode.CancellationToken): Promise<{ additionalEdits: vscode.WorkspaceEdit; label: string } | undefined> { + if (ranges.length === 0) { + return; + } + + const edits: vscode.SnippetTextEdit[] = []; + let placeHolderValue: number = ranges.length; + let label: string = ''; + for (let i = 0; i < ranges.length; i++) { + const snippet = await tryGetUriListSnippet(document, urlList, token, document.getText(ranges[i]), placeHolderValue); + if (!snippet) { + return; + } + placeHolderValue--; + edits.push(new vscode.SnippetTextEdit(ranges[i], snippet.snippet)); + label = snippet.label; + } -export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> { - const urlList = await dataTransfer.get('text/uri-list')?.asString(); - if (!urlList || token.isCancellationRequested) { + const additionalEdits = new vscode.WorkspaceEdit(); + additionalEdits.set(document.uri, edits); + + return { additionalEdits, label }; +} + +export async function tryGetUriListSnippet(document: vscode.TextDocument, urlList: String, token: vscode.CancellationToken, title = '', placeHolderValue = 0): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> { + if (token.isCancellationRequested) { return undefined; } @@ -72,7 +94,7 @@ export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTr } } - return createUriListSnippet(document, uris); + return createUriListSnippet(document, uris, title, placeHolderValue); } interface UriListSnippetOptions { @@ -90,11 +112,12 @@ interface UriListSnippetOptions { readonly separator?: string; } - export function createUriListSnippet( document: vscode.TextDocument, uris: readonly vscode.Uri[], - options?: UriListSnippetOptions + title = '', + placeholderValue = 0, + options?: UriListSnippetOptions, ): { snippet: vscode.SnippetString; label: string } | undefined { if (!uris.length) { return; @@ -119,27 +142,27 @@ export function createUriListSnippet( if (insertAsVideo) { insertedAudioVideoCount++; snippet.appendText(`'); } else if (insertAsAudio) { insertedAudioVideoCount++; snippet.appendText(`'); } else { if (insertAsMedia) { insertedImageCount++; + snippet.appendText('!['); + const placeholderText = options?.placeholderText ? (escapeBrackets(title) || 'Alt text') : 'label'; + const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : (placeholderValue === 0 ? undefined : placeholderValue); + snippet.appendPlaceholder(placeholderText, placeholderIndex); + snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`); } else { insertedLinkCount++; + snippet.appendText('['); + snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue); + snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`); } - - snippet.appendText(insertAsMedia ? '![' : '['); - - const placeholderText = options?.placeholderText ?? (insertAsMedia ? 'Alt text' : 'label'); - const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : undefined; - snippet.appendPlaceholder(placeholderText, placeholderIndex); - - snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`); } if (i < uris.length - 1 && uris.length > 1) { @@ -267,6 +290,12 @@ function escapeMarkdownLinkPath(mdPath: string): string { return encodeURI(mdPath); } +function escapeBrackets(value: string): string { + value = value.replace(/[\[\]]/g, '\\$&'); + // value = value.replace(/\r\n\r\n/g, '\n\n'); + return value; +} + function needsBracketLink(mdPath: string) { // Links with whitespace or control characters must be enclosed in brackets if (mdPath.startsWith('<') || /\s|[\u007F\u0000-\u001f]/.test(mdPath)) { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts index 329d40cb4cc79..af54bdbf846cb 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts @@ -166,16 +166,24 @@ export class PostEditWidgetManager extends Disposable { return; } + let insertTextEdit: ResourceTextEdit[] = []; + if (typeof edit.insertText === 'string' ? edit.insertText === '' : edit.insertText.snippet === '') { + insertTextEdit = []; + } else { + insertTextEdit = ranges.map(range => new ResourceTextEdit(model.uri, + typeof edit.insertText === 'string' + ? { range, text: edit.insertText, insertAsSnippet: false } + : { range, text: edit.insertText.snippet, insertAsSnippet: true } + )); + } + + const allEdits = [ + ...insertTextEdit, + ...(edit.additionalEdit?.edits ?? []) + ]; + const combinedWorkspaceEdit: WorkspaceEdit = { - edits: [ - ...ranges.map(range => - new ResourceTextEdit(model.uri, - typeof edit.insertText === 'string' - ? { range, text: edit.insertText, insertAsSnippet: false } - : { range, text: edit.insertText.snippet, insertAsSnippet: true } - )), - ...(edit.additionalEdit?.edits ?? []) - ] + edits: allEdits }; // Use a decoration to track edits around the trigger range