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:
Meghan Kulkarni
2023-07-20 15:16:40 -07:00
committed by GitHub
parent aa7bc8579a
commit 2ba9f17b3f
6 changed files with 244 additions and 62 deletions

View File

@@ -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;
}

View File

@@ -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: [

View File

@@ -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('![');
const placeholderText = escapeBrackets(title) || options?.placeholderText || 'Alt text';
const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : (placeholderValue === 0 ? undefined : placeholderValue);
snippet.appendPlaceholder(placeholderText, placeholderIndex);
snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`);
} else {
insertedLinkCount++;
if (smartPaste) {
if (externalUriSchemes.includes(uri.scheme)) {
snippet.appendText(uri.toString(true));
} else {
snippet.appendText(escapeMarkdownLinkPath(mdPath));
}
if (pasteAsMarkdownLink) {
snippet.appendText('![');
const placeholderText = escapeBrackets(title) || options?.placeholderText || 'Alt text';
const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : (placeholderValue === 0 ? undefined : placeholderValue);
snippet.appendPlaceholder(placeholderText, placeholderIndex);
snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`);
} 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) {