Skip to content

Commit 6626f5f

Browse files
MeghanKulkarniMeghan Kulkarni
andauthored
turning highlighted Markdown text to link to pasted URL (microsoft#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 <[email protected]>
1 parent 680cbcc commit 6626f5f

File tree

9 files changed

+142
-33
lines changed

9 files changed

+142
-33
lines changed

extensions/markdown-language-features/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,12 @@
498498
"%configuration.copyIntoWorkspace.never%"
499499
]
500500
},
501+
"markdown.editor.pasteUrlAsFormattedLink.enabled": {
502+
"type": "boolean",
503+
"scope": "resource",
504+
"markdownDescription": "%configuration.markdown.editor.pasteUrlAsFormattedLink.enabled%",
505+
"default": true
506+
},
501507
"markdown.validate.enabled": {
502508
"type": "boolean",
503509
"scope": "resource",

extensions/markdown-language-features/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"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",
4242
"configuration.markdown.editor.filePaste.enabled": "Enable pasting files into a Markdown editor to create Markdown links. Requires enabling `#editor.pasteAs.enabled#`.",
4343
"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.",
44+
"configuration.markdown.editor.pasteUrlAsFormattedLink.enabled": "Controls if a Markdown link is created when a URL is pasted into the Markdown editor.",
4445
"configuration.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.",
4546
"configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.",
4647
"configuration.markdown.validate.enabled.description": "Enable all error reporting in Markdown files.",

extensions/markdown-language-features/src/commands/insertResource.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: vscode
7676
await vscode.workspace.applyEdit(edit);
7777
}
7878

79-
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean) {
79+
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean, title = '', placeholderValue = 0) {
8080
const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => {
8181
const selectionText = activeEditor.document.getText(selection);
82-
const snippet = createUriListSnippet(activeEditor.document, selectedFiles, {
82+
const snippet = createUriListSnippet(activeEditor.document, selectedFiles, title, placeholderValue, {
8383
insertAsMedia,
8484
placeholderText: selectionText,
8585
placeholderStartIndex: (i + 1) * selectedFiles.length,

extensions/markdown-language-features/src/extension.shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { MdLanguageClient } from './client/client';
88
import { CommandManager } from './commandManager';
99
import { registerMarkdownCommands } from './commands/index';
1010
import { registerPasteSupport } from './languageFeatures/copyFiles/copyPaste';
11+
import { registerLinkPasteSupport } from './languageFeatures/copyFiles/copyPasteLinks';
1112
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
1213
import { registerDropIntoEditorSupport } from './languageFeatures/copyFiles/dropIntoEditor';
1314
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
@@ -59,6 +60,7 @@ function registerMarkdownLanguageFeatures(
5960
registerDropIntoEditorSupport(selector),
6061
registerFindFileReferenceSupport(commandManager, client),
6162
registerPasteSupport(selector),
63+
registerLinkPasteSupport(selector),
6264
registerUpdateLinksOnRename(client),
6365
);
6466
}

extensions/markdown-language-features/src/languageFeatures/copyFiles/copyPaste.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55

66
import * as vscode from 'vscode';
77
import { Schemes } from '../../util/schemes';
8-
import { createEditForMediaFiles, mediaMimes, tryGetUriListSnippet } from './shared';
8+
import { createEditForMediaFiles, getMarkdownLink, mediaMimes } from './shared';
99

1010
class PasteEditProvider implements vscode.DocumentPasteEditProvider {
1111

1212
private readonly _id = 'insertLink';
1313

1414
async provideDocumentPasteEdits(
1515
document: vscode.TextDocument,
16-
_ranges: readonly vscode.Range[],
16+
ranges: readonly vscode.Range[],
1717
dataTransfer: vscode.DataTransfer,
1818
token: vscode.CancellationToken,
1919
): Promise<vscode.DocumentPasteEdit | undefined> {
@@ -27,12 +27,18 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
2727
return createEdit;
2828
}
2929

30-
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
31-
if (!snippet) {
30+
const label = vscode.l10n.t('Insert Markdown Media');
31+
const uriEdit = new vscode.DocumentPasteEdit('', this._id, label);
32+
const urlList = await dataTransfer.get('text/uri-list')?.asString();
33+
if (!urlList) {
34+
return;
35+
}
36+
const pasteEdit = await getMarkdownLink(document, ranges, urlList, token);
37+
if (!pasteEdit) {
3238
return;
3339
}
3440

35-
const uriEdit = new vscode.DocumentPasteEdit(snippet.snippet, this._id, snippet.label);
41+
uriEdit.additionalEdit = pasteEdit.additionalEdits;
3642
uriEdit.priority = this._getPriority(dataTransfer);
3743
return uriEdit;
3844
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import { getMarkdownLink } from './shared';
8+
9+
class PasteLinkEditProvider implements vscode.DocumentPasteEditProvider {
10+
11+
private readonly _id = 'insertMarkdownLink';
12+
async provideDocumentPasteEdits(
13+
document: vscode.TextDocument,
14+
ranges: readonly vscode.Range[],
15+
dataTransfer: vscode.DataTransfer,
16+
token: vscode.CancellationToken,
17+
): Promise<vscode.DocumentPasteEdit | undefined> {
18+
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.pasteUrlAsFormattedLink.enabled', true);
19+
if (!enabled) {
20+
return;
21+
}
22+
23+
// Check if dataTransfer contains a URL
24+
const item = dataTransfer.get('text/plain');
25+
try {
26+
new URL(await item?.value);
27+
} catch (error) {
28+
return;
29+
}
30+
31+
const label = vscode.l10n.t('Insert Markdown Link');
32+
const uriEdit = new vscode.DocumentPasteEdit('', this._id, label);
33+
const urlList = await item?.asString();
34+
if (!urlList) {
35+
return undefined;
36+
}
37+
const pasteEdit = await getMarkdownLink(document, ranges, urlList, token);
38+
if (!pasteEdit) {
39+
return;
40+
}
41+
42+
uriEdit.additionalEdit = pasteEdit.additionalEdits;
43+
return uriEdit;
44+
}
45+
}
46+
47+
export function registerLinkPasteSupport(selector: vscode.DocumentSelector,) {
48+
return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteLinkEditProvider(), {
49+
pasteMimeTypes: [
50+
'text/plain',
51+
]
52+
});
53+
}

extensions/markdown-language-features/src/languageFeatures/copyFiles/dropIntoEditor.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ class MarkdownImageDropProvider implements vscode.DocumentDropEditProvider {
3030
}
3131

3232
private async _getUriListEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
33-
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
33+
const urlList = await dataTransfer.get('text/uri-list')?.asString();
34+
if (!urlList) {
35+
return undefined;
36+
}
37+
const snippet = await tryGetUriListSnippet(document, urlList, token);
3438
if (!snippet) {
3539
return undefined;
3640
}

extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,32 @@ export const mediaMimes = new Set([
5656
'audio/x-wav',
5757
]);
5858

59+
export async function getMarkdownLink(document: vscode.TextDocument, ranges: readonly vscode.Range[], urlList: string, token: vscode.CancellationToken): Promise<{ additionalEdits: vscode.WorkspaceEdit; label: string } | undefined> {
60+
if (ranges.length === 0) {
61+
return;
62+
}
63+
64+
const edits: vscode.SnippetTextEdit[] = [];
65+
let placeHolderValue: number = ranges.length;
66+
let label: string = '';
67+
for (let i = 0; i < ranges.length; i++) {
68+
const snippet = await tryGetUriListSnippet(document, urlList, token, document.getText(ranges[i]), placeHolderValue);
69+
if (!snippet) {
70+
return;
71+
}
72+
placeHolderValue--;
73+
edits.push(new vscode.SnippetTextEdit(ranges[i], snippet.snippet));
74+
label = snippet.label;
75+
}
5976

60-
export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> {
61-
const urlList = await dataTransfer.get('text/uri-list')?.asString();
62-
if (!urlList || token.isCancellationRequested) {
77+
const additionalEdits = new vscode.WorkspaceEdit();
78+
additionalEdits.set(document.uri, edits);
79+
80+
return { additionalEdits, label };
81+
}
82+
83+
export async function tryGetUriListSnippet(document: vscode.TextDocument, urlList: String, token: vscode.CancellationToken, title = '', placeHolderValue = 0): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> {
84+
if (token.isCancellationRequested) {
6385
return undefined;
6486
}
6587

@@ -72,7 +94,7 @@ export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTr
7294
}
7395
}
7496

75-
return createUriListSnippet(document, uris);
97+
return createUriListSnippet(document, uris, title, placeHolderValue);
7698
}
7799

78100
interface UriListSnippetOptions {
@@ -90,11 +112,12 @@ interface UriListSnippetOptions {
90112
readonly separator?: string;
91113
}
92114

93-
94115
export function createUriListSnippet(
95116
document: vscode.TextDocument,
96117
uris: readonly vscode.Uri[],
97-
options?: UriListSnippetOptions
118+
title = '',
119+
placeholderValue = 0,
120+
options?: UriListSnippetOptions,
98121
): { snippet: vscode.SnippetString; label: string } | undefined {
99122
if (!uris.length) {
100123
return;
@@ -119,27 +142,27 @@ export function createUriListSnippet(
119142
if (insertAsVideo) {
120143
insertedAudioVideoCount++;
121144
snippet.appendText(`<video src="${escapeHtmlAttribute(mdPath)}" controls title="`);
122-
snippet.appendPlaceholder('Title');
145+
snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue);
123146
snippet.appendText('"></video>');
124147
} else if (insertAsAudio) {
125148
insertedAudioVideoCount++;
126149
snippet.appendText(`<audio src="${escapeHtmlAttribute(mdPath)}" controls title="`);
127-
snippet.appendPlaceholder('Title');
150+
snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue);
128151
snippet.appendText('"></audio>');
129152
} else {
130153
if (insertAsMedia) {
131154
insertedImageCount++;
155+
snippet.appendText('![');
156+
const placeholderText = options?.placeholderText ? (escapeBrackets(title) || 'Alt text') : 'label';
157+
const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : (placeholderValue === 0 ? undefined : placeholderValue);
158+
snippet.appendPlaceholder(placeholderText, placeholderIndex);
159+
snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`);
132160
} else {
133161
insertedLinkCount++;
162+
snippet.appendText('[');
163+
snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue);
164+
snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`);
134165
}
135-
136-
snippet.appendText(insertAsMedia ? '![' : '[');
137-
138-
const placeholderText = options?.placeholderText ?? (insertAsMedia ? 'Alt text' : 'label');
139-
const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : undefined;
140-
snippet.appendPlaceholder(placeholderText, placeholderIndex);
141-
142-
snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`);
143166
}
144167

145168
if (i < uris.length - 1 && uris.length > 1) {
@@ -267,6 +290,12 @@ function escapeMarkdownLinkPath(mdPath: string): string {
267290
return encodeURI(mdPath);
268291
}
269292

293+
function escapeBrackets(value: string): string {
294+
value = value.replace(/[\[\]]/g, '\\$&');
295+
// value = value.replace(/\r\n\r\n/g, '\n\n');
296+
return value;
297+
}
298+
270299
function needsBracketLink(mdPath: string) {
271300
// Links with whitespace or control characters must be enclosed in brackets
272301
if (mdPath.startsWith('<') || /\s|[\u007F\u0000-\u001f]/.test(mdPath)) {

src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,24 @@ export class PostEditWidgetManager extends Disposable {
166166
return;
167167
}
168168

169+
let insertTextEdit: ResourceTextEdit[] = [];
170+
if (typeof edit.insertText === 'string' ? edit.insertText === '' : edit.insertText.snippet === '') {
171+
insertTextEdit = [];
172+
} else {
173+
insertTextEdit = ranges.map(range => new ResourceTextEdit(model.uri,
174+
typeof edit.insertText === 'string'
175+
? { range, text: edit.insertText, insertAsSnippet: false }
176+
: { range, text: edit.insertText.snippet, insertAsSnippet: true }
177+
));
178+
}
179+
180+
const allEdits = [
181+
...insertTextEdit,
182+
...(edit.additionalEdit?.edits ?? [])
183+
];
184+
169185
const combinedWorkspaceEdit: WorkspaceEdit = {
170-
edits: [
171-
...ranges.map(range =>
172-
new ResourceTextEdit(model.uri,
173-
typeof edit.insertText === 'string'
174-
? { range, text: edit.insertText, insertAsSnippet: false }
175-
: { range, text: edit.insertText.snippet, insertAsSnippet: true }
176-
)),
177-
...(edit.additionalEdit?.edits ?? [])
178-
]
186+
edits: allEdits
179187
};
180188

181189
// Use a decoration to track edits around the trigger range

0 commit comments

Comments
 (0)