mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-21 00:59:03 +01:00
making the markdown link paste feature smart (#188119)
* making markdown link pasting feature smarter * Update settings description Co-authored-by: Joyce Er <joyceerhl@gmail.com> * made checkPaste more concise * won't paste md link in fenced code or math * updated the smart md link pasting * link validation and naming changes * resolving comments and tests * resolving comments & writing tests --------- Co-authored-by: Joyce Er <joyceerhl@gmail.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Schemes } from '../../util/schemes';
|
||||
import { createEditForMediaFiles, getMarkdownLink, mediaMimes } from './shared';
|
||||
import { createEditForMediaFiles, createEditAddingLinksForUriList, mediaMimes } from './shared';
|
||||
|
||||
class PasteEditProvider implements vscode.DocumentPasteEditProvider {
|
||||
|
||||
@@ -32,7 +32,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
|
||||
if (!urlList) {
|
||||
return;
|
||||
}
|
||||
const pasteEdit = await getMarkdownLink(document, ranges, urlList, token);
|
||||
const pasteEdit = await createEditAddingLinksForUriList(document, ranges, urlList, token, false);
|
||||
if (!pasteEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { getMarkdownLink } from './shared';
|
||||
import { externalUriSchemes, createEditAddingLinksForUriList } from './shared';
|
||||
class PasteLinkEditProvider implements vscode.DocumentPasteEditProvider {
|
||||
|
||||
readonly id = 'insertMarkdownLink';
|
||||
@@ -19,20 +19,22 @@ class PasteLinkEditProvider implements vscode.DocumentPasteEditProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if dataTransfer contains a URL
|
||||
const item = dataTransfer.get('text/plain');
|
||||
try {
|
||||
new URL(await item?.value);
|
||||
} catch (error) {
|
||||
const urlList = await item?.asString();
|
||||
|
||||
if (urlList === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateLink(urlList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uriEdit = new vscode.DocumentPasteEdit('', this.id, '');
|
||||
const urlList = await item?.asString();
|
||||
if (!urlList) {
|
||||
return undefined;
|
||||
}
|
||||
const pasteEdit = await getMarkdownLink(document, ranges, urlList, token);
|
||||
const pasteEdit = await createEditAddingLinksForUriList(document, ranges, urlList, token, true);
|
||||
if (!pasteEdit) {
|
||||
return;
|
||||
}
|
||||
@@ -43,6 +45,14 @@ class PasteLinkEditProvider implements vscode.DocumentPasteEditProvider {
|
||||
}
|
||||
}
|
||||
|
||||
export function validateLink(urlList: string): boolean {
|
||||
const url = urlList?.split(/\s+/);
|
||||
if (url.length > 1 || !externalUriSchemes.includes(vscode.Uri.parse(url[0]).scheme)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function registerLinkPasteSupport(selector: vscode.DocumentSelector,) {
|
||||
return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteLinkEditProvider(), {
|
||||
pasteMimeTypes: [
|
||||
|
||||
@@ -17,9 +17,11 @@ enum MediaKind {
|
||||
Audio,
|
||||
}
|
||||
|
||||
const externalUriSchemes = [
|
||||
export const externalUriSchemes = [
|
||||
'http',
|
||||
'https',
|
||||
'mailto',
|
||||
'ftp',
|
||||
];
|
||||
|
||||
export const mediaFileExtensions = new Map<string, MediaKind>([
|
||||
@@ -61,30 +63,53 @@ 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> {
|
||||
const smartPasteRegexes = [
|
||||
{ regex: /\[.*\]\(.*\)/g, is_markdown_link: true }, // Is a Markdown Link
|
||||
{ regex: /!\[.*\]\(.*\)/g, is_markdown_link: true }, // Is a Markdown Image Link
|
||||
{ regex: /\[([^\]]*)\]\(([^)]*)\)/g, is_markdown_link: false }, // In a Markdown link
|
||||
{ regex: /^```[\s\S]*?```$/gm, is_markdown_link: false }, // In a fenced code block
|
||||
{ regex: /^\$\$[\s\S]*?\$\$$/gm, is_markdown_link: false }, // In a fenced math block
|
||||
{ regex: /`[^`]*`/g, is_markdown_link: false }, // In inline code
|
||||
{ regex: /\$[^$]*\$/g, is_markdown_link: false }, // In inline math
|
||||
];
|
||||
export interface SmartPaste {
|
||||
|
||||
/**
|
||||
* `true` if the link is not being pasted within a markdown link, code, or math.
|
||||
*/
|
||||
pasteAsMarkdownLink: boolean;
|
||||
|
||||
/**
|
||||
* `true` if the link is being pasted over a markdown link.
|
||||
*/
|
||||
updateTitle: boolean;
|
||||
|
||||
}
|
||||
|
||||
export async function createEditAddingLinksForUriList(document: vscode.TextDocument, ranges: readonly vscode.Range[], urlList: string, token: vscode.CancellationToken, isExternalLink: boolean): Promise<{ additionalEdits: vscode.WorkspaceEdit; label: string } | undefined> {
|
||||
if (ranges.length === 0) {
|
||||
return;
|
||||
}
|
||||
const enabled = vscode.workspace.getConfiguration('markdown', document).get<'always' | 'smart' | 'never'>('editor.pasteUrlAsFormattedLink.enabled', 'always');
|
||||
|
||||
const edits: vscode.SnippetTextEdit[] = [];
|
||||
let placeHolderValue: number = ranges.length;
|
||||
let label: string = '';
|
||||
let smartPaste: boolean = false;
|
||||
let smartPaste = { pasteAsMarkdownLink: true, updateTitle: false };
|
||||
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
|
||||
let title = document.getText(ranges[i]);
|
||||
if (enabled === 'smart') {
|
||||
const inMarkdownLink = checkPaste(document, ranges, /\[([^\]]*)\]\(([^)]*)\)/g, i);
|
||||
const inFencedCode = checkPaste(document, ranges, /^```[\s\S]*?```$/gm, i);
|
||||
const inFencedMath = checkPaste(document, ranges, /^\$\$[\s\S]*?\$\$$/gm, i);
|
||||
smartPaste = (inMarkdownLink || inFencedCode || inFencedMath);
|
||||
smartPaste = checkSmartPaste(document.getText(), document.offsetAt(ranges[i].start), document.offsetAt(ranges[i].end));
|
||||
title = smartPaste.updateTitle ? '' : document.getText(ranges[i]);
|
||||
}
|
||||
|
||||
const snippet = await tryGetUriListSnippet(document, urlList, token, document.getText(ranges[i]), placeHolderValue, smartPaste);
|
||||
const snippet = await tryGetUriListSnippet(document, urlList, token, title, placeHolderValue, smartPaste.pasteAsMarkdownLink, isExternalLink);
|
||||
if (!snippet) {
|
||||
return;
|
||||
}
|
||||
|
||||
smartPaste = false;
|
||||
smartPaste.pasteAsMarkdownLink = true;
|
||||
placeHolderValue--;
|
||||
edits.push(new vscode.SnippetTextEdit(ranges[i], snippet.snippet));
|
||||
label = snippet.label;
|
||||
@@ -96,20 +121,25 @@ export async function getMarkdownLink(document: vscode.TextDocument, ranges: rea
|
||||
return { additionalEdits, label };
|
||||
}
|
||||
|
||||
function checkPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], regex: RegExp, index: number): boolean {
|
||||
const rangeStartOffset = document.offsetAt(ranges[index].start);
|
||||
const rangeEndOffset = document.offsetAt(ranges[index].end);
|
||||
const matches = [...document.getText().matchAll(regex)];
|
||||
for (const match of matches) {
|
||||
if (match.index !== undefined && rangeStartOffset > match.index && rangeEndOffset < match.index + match[0].length) {
|
||||
return true;
|
||||
export function checkSmartPaste(documentText: string, start: number, end: number): SmartPaste {
|
||||
const SmartPaste: SmartPaste = { pasteAsMarkdownLink: true, updateTitle: false };
|
||||
for (const regex of smartPasteRegexes) {
|
||||
const matches = [...documentText.matchAll(regex.regex)];
|
||||
for (const match of matches) {
|
||||
if (match.index !== undefined) {
|
||||
const useDefaultPaste = start > match.index && end < match.index + match[0].length;
|
||||
SmartPaste.pasteAsMarkdownLink = !useDefaultPaste;
|
||||
SmartPaste.updateTitle = regex.is_markdown_link && start === match.index && end === match.index + match[0].length;
|
||||
if (!SmartPaste.pasteAsMarkdownLink || SmartPaste.updateTitle) {
|
||||
return SmartPaste;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return SmartPaste;
|
||||
}
|
||||
|
||||
export async function tryGetUriListSnippet(document: vscode.TextDocument, urlList: String, token: vscode.CancellationToken, title = '', placeHolderValue = 0, smartPaste = false): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> {
|
||||
export async function tryGetUriListSnippet(document: vscode.TextDocument, urlList: String, token: vscode.CancellationToken, title = '', placeHolderValue = 0, pasteAsMarkdownLink = true, isExternalLink = false): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> {
|
||||
if (token.isCancellationRequested) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -123,7 +153,7 @@ export async function tryGetUriListSnippet(document: vscode.TextDocument, urlLis
|
||||
}
|
||||
}
|
||||
|
||||
return createUriListSnippet(document, uris, title, placeHolderValue, smartPaste);
|
||||
return createUriListSnippet(document, uris, title, placeHolderValue, pasteAsMarkdownLink, isExternalLink);
|
||||
}
|
||||
|
||||
interface UriListSnippetOptions {
|
||||
@@ -141,28 +171,48 @@ interface UriListSnippetOptions {
|
||||
readonly separator?: string;
|
||||
}
|
||||
|
||||
export function createLinkSnippet(
|
||||
pasteAsMarkdownLink: boolean,
|
||||
mdPath: string,
|
||||
title: string,
|
||||
uri: vscode.Uri,
|
||||
placeholderValue: number,
|
||||
isExternalLink: boolean,
|
||||
): vscode.SnippetString {
|
||||
const uriString = uri.toString(true);
|
||||
const snippet = new vscode.SnippetString();
|
||||
if (pasteAsMarkdownLink) {
|
||||
snippet.appendText('[');
|
||||
snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue);
|
||||
snippet.appendText(isExternalLink ? `](${uriString})` : `](${escapeMarkdownLinkPath(mdPath)})`);
|
||||
} else {
|
||||
snippet.appendText(isExternalLink ? uriString : escapeMarkdownLinkPath(mdPath));
|
||||
}
|
||||
return snippet;
|
||||
}
|
||||
|
||||
export function createUriListSnippet(
|
||||
document: vscode.TextDocument,
|
||||
uris: readonly vscode.Uri[],
|
||||
title = '',
|
||||
placeholderValue = 0,
|
||||
smartPaste = false,
|
||||
pasteAsMarkdownLink = true,
|
||||
isExternalLink = false,
|
||||
options?: UriListSnippetOptions,
|
||||
): { snippet: vscode.SnippetString; label: string } | undefined {
|
||||
if (!uris.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dir = getDocumentDir(document);
|
||||
|
||||
const snippet = new vscode.SnippetString();
|
||||
const documentDir = getDocumentDir(document);
|
||||
|
||||
let snippet = new vscode.SnippetString();
|
||||
let insertedLinkCount = 0;
|
||||
let insertedImageCount = 0;
|
||||
let insertedAudioVideoCount = 0;
|
||||
|
||||
uris.forEach((uri, i) => {
|
||||
const mdPath = getMdPath(dir, uri);
|
||||
const mdPath = getMdPath(documentDir, uri);
|
||||
|
||||
const ext = URI.Utils.extname(uri).toLowerCase().replace('.', '');
|
||||
const insertAsMedia = typeof options?.insertAsMedia === 'undefined' ? mediaFileExtensions.has(ext) : !!options.insertAsMedia;
|
||||
@@ -179,33 +229,22 @@ export function createUriListSnippet(
|
||||
snippet.appendText(`<audio src="${escapeHtmlAttribute(mdPath)}" controls title="`);
|
||||
snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue);
|
||||
snippet.appendText('"></audio>');
|
||||
} else {
|
||||
} else if (insertAsMedia) {
|
||||
if (insertAsMedia) {
|
||||
insertedImageCount++;
|
||||
snippet.appendText('})`);
|
||||
} else {
|
||||
insertedLinkCount++;
|
||||
if (smartPaste) {
|
||||
if (externalUriSchemes.includes(uri.scheme)) {
|
||||
snippet.appendText(uri.toString(true));
|
||||
} else {
|
||||
snippet.appendText(escapeMarkdownLinkPath(mdPath));
|
||||
}
|
||||
if (pasteAsMarkdownLink) {
|
||||
snippet.appendText('})`);
|
||||
} else {
|
||||
snippet.appendText('[');
|
||||
snippet.appendPlaceholder(escapeBrackets(title) || 'Title', placeholderValue);
|
||||
if (externalUriSchemes.includes(uri.scheme)) {
|
||||
const uriString = uri.toString(true);
|
||||
snippet.appendText(`](${uriString})`);
|
||||
} else {
|
||||
snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`);
|
||||
}
|
||||
snippet.appendText(escapeMarkdownLinkPath(mdPath));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
insertedLinkCount++;
|
||||
snippet = createLinkSnippet(pasteAsMarkdownLink, mdPath, title, uri, placeholderValue, isExternalLink);
|
||||
}
|
||||
|
||||
if (i < uris.length - 1 && uris.length > 1) {
|
||||
|
||||
Reference in New Issue
Block a user