Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up and fix markdown url pasting #198706

Merged
merged 1 commit into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@
"type": "string",
"scope": "resource",
"markdownDescription": "%configuration.markdown.editor.pasteUrlAsFormattedLink.enabled%",
"default":"never",
"default": "never",
"enum": [
"always",
"smart",
Expand Down Expand Up @@ -734,6 +734,7 @@
"morphdom": "^2.6.1",
"picomatch": "^2.3.1",
"vscode-languageclient": "^8.0.2",
"vscode-languageserver-textdocument": "^1.0.11",
"vscode-uri": "^3.0.3"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { TextDocument } from 'vscode-languageserver-textdocument';
import * as vscode from 'vscode';
import { ITextDocument } from '../types/textDocument';

export class InMemoryDocument implements ITextDocument {

private readonly _doc: TextDocument;

public readonly uri: vscode.Uri;
public readonly version: number;

constructor(
public readonly uri: vscode.Uri,
private readonly _contents: string,
public readonly version = 0,
) { }
uri: vscode.Uri,
contents: string,
version: number = 0,
) {
this.uri = uri;
this.version = version;
this._doc = TextDocument.create(this.uri.toString(), 'markdown', 0, contents);
}

getText(): string {
return this._contents;
getText(range?: vscode.Range): string {
return this._doc.getText(range);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as vscode from 'vscode';
import { Mime } from '../../util/mimes';
import { createEditAddingLinksForUriList, getPasteUrlAsFormattedLinkSetting, PasteUrlAsFormattedLink, validateLink } from './shared';
import { createEditAddingLinksForUriList, findValidUriInText, getPasteUrlAsFormattedLinkSetting, PasteUrlAsFormattedLink } from './shared';

class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {

Expand All @@ -28,11 +28,16 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {

const item = dataTransfer.get(Mime.textPlain);
const urlList = await item?.asString();
if (token.isCancellationRequested || !urlList || !validateLink(urlList).isValid) {
if (token.isCancellationRequested || !urlList) {
return;
}

const pasteEdit = createEditAddingLinksForUriList(document, ranges, validateLink(urlList).cleanedUrlList, true, pasteUrlSetting === PasteUrlAsFormattedLink.Smart);
const uriText = findValidUriInText(urlList);
if (!uriText) {
return;
}

const pasteEdit = createEditAddingLinksForUriList(document, ranges, uriText, true, pasteUrlSetting === PasteUrlAsFormattedLink.Smart);
if (!pasteEdit) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as path from 'path';
import * as vscode from 'vscode';
import * as URI from 'vscode-uri';
import { ITextDocument } from '../../types/textDocument';
import { coalesce } from '../../util/arrays';
import { getDocumentDir } from '../../util/document';
import { mediaMimes } from '../../util/mimes';
Expand All @@ -18,7 +19,7 @@ enum MediaKind {
Audio,
}

export const externalUriSchemes = [
const externalUriSchemes = [
'http',
'https',
'mailto',
Expand Down Expand Up @@ -50,21 +51,6 @@ export const mediaFileExtensions = new Map<string, MediaKind>([
['wav', MediaKind.Audio],
]);

const smartPasteRegexes = [
{ regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link
{ regex: /^```[\s\S]*?```$/gm }, // In a backtick fenced code block
{ regex: /^~~~[\s\S]*?~~~$/gm }, // In a tildefenced code block
{ regex: /^\$\$[\s\S]*?\$\$$/gm }, // In a fenced math block
{ regex: /`[^`]*`/g }, // In inline code
{ regex: /\$[^$]*\$/g }, // In inline math
];

export interface SkinnyTextDocument {
offsetAt(position: vscode.Position): number;
getText(range?: vscode.Range): string;
readonly uri: vscode.Uri;
}

export enum PasteUrlAsFormattedLink {
Always = 'always',
Smart = 'smart',
Expand All @@ -76,30 +62,25 @@ export function getPasteUrlAsFormattedLinkSetting(document: vscode.TextDocument)
}

export function createEditAddingLinksForUriList(
document: SkinnyTextDocument,
document: ITextDocument,
ranges: readonly vscode.Range[],
urlList: string,
isExternalLink: boolean,
useSmartPaste: boolean
useSmartPaste: boolean,
): { additionalEdits: vscode.WorkspaceEdit; label: string; markdownLink: boolean } | undefined {

if (ranges.length === 0) {
if (!ranges.length) {
return;
}

const edits: vscode.SnippetTextEdit[] = [];
let placeHolderValue: number = ranges.length;
let label: string = '';
let pasteAsMarkdownLink: boolean = true;
let markdownLink: boolean = true;

for (const range of ranges) {
const selectedRange: vscode.Range = new vscode.Range(
new vscode.Position(range.start.line, document.offsetAt(range.start)),
new vscode.Position(range.end.line, document.offsetAt(range.end))
);

if (useSmartPaste) {
pasteAsMarkdownLink = checkSmartPaste(document, selectedRange, range);
pasteAsMarkdownLink = shouldSmartPaste(document, range);
markdownLink = pasteAsMarkdownLink; // FIX: this will only match the last range
}

Expand All @@ -120,13 +101,47 @@ export function createEditAddingLinksForUriList(
return { additionalEdits, label, markdownLink };
}

export function checkSmartPaste(document: SkinnyTextDocument, selectedRange: vscode.Range, range: vscode.Range): boolean {
if (selectedRange.isEmpty || /^[\s\n]*$/.test(document.getText(range)) || validateLink(document.getText(range)).isValid) {
export function findValidUriInText(text: string): string | undefined {
const trimmedUrlList = text.trim();

// Uri must consist of a single sequence of characters without spaces
if (!/^\S+$/.test(trimmedUrlList)) {
return;
}

let uri: vscode.Uri;
try {
uri = vscode.Uri.parse(trimmedUrlList);
} catch {
// Could not parse
return;
}

if (!externalUriSchemes.includes(uri.scheme.toLowerCase()) || uri.authority.length <= 1) {
return;
}

return trimmedUrlList;
}

const smartPasteRegexes = [
{ regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link
{ regex: /^```[\s\S]*?```$/gm }, // In a backtick fenced code block
{ regex: /^~~~[\s\S]*?~~~$/gm }, // In a tildefenced code block
{ regex: /^\$\$[\s\S]*?\$\$$/gm }, // In a fenced math block
{ regex: /`[^`]*`/g }, // In inline code
{ regex: /\$[^$]*\$/g }, // In inline math
];

export function shouldSmartPaste(document: ITextDocument, selectedRange: vscode.Range): boolean {
if (selectedRange.isEmpty || /^[\s\n]*$/.test(document.getText(selectedRange)) || findValidUriInText(document.getText(selectedRange))) {
return false;
}
if (/\[.*\]\(.*\)/.test(document.getText(range)) || /!\[.*\]\(.*\)/.test(document.getText(range))) {

if (/\[.*\]\(.*\)/.test(document.getText(selectedRange)) || /!\[.*\]\(.*\)/.test(document.getText(selectedRange))) {
return false;
}

for (const regex of smartPasteRegexes) {
const matches = [...document.getText().matchAll(regex.regex)];
for (const match of matches) {
Expand All @@ -138,29 +153,14 @@ export function checkSmartPaste(document: SkinnyTextDocument, selectedRange: vsc
}
}
}
return true;
}

export function validateLink(urlList: string): { isValid: boolean; cleanedUrlList: string } {
let isValid = false;
let uri = undefined;
const trimmedUrlList = urlList?.trim(); //remove leading and trailing whitespace and new lines
try {
uri = vscode.Uri.parse(trimmedUrlList);
} catch (error) {
return { isValid: false, cleanedUrlList: urlList };
}
const splitUrlList = trimmedUrlList.split(' ').filter(item => item !== ''); //split on spaces and remove empty strings
if (uri) {
isValid = splitUrlList.length === 1 && !splitUrlList[0].includes('\n') && externalUriSchemes.includes(vscode.Uri.parse(splitUrlList[0]).scheme) && !!vscode.Uri.parse(splitUrlList[0]).authority;
}
return { isValid, cleanedUrlList: splitUrlList[0] };
return true;
}

export function tryGetUriListSnippet(document: SkinnyTextDocument, urlList: String, title = '', placeHolderValue = 0, pasteAsMarkdownLink = true, isExternalLink = false): { snippet: vscode.SnippetString; label: string } | undefined {
const entries = coalesce(urlList.split(/\r?\n/g).map(resource => {
export function tryGetUriListSnippet(document: ITextDocument, urlList: String, title = '', placeHolderValue = 0, pasteAsMarkdownLink = true, isExternalLink = false): { snippet: vscode.SnippetString; label: string } | undefined {
const entries = coalesce(urlList.split(/\r?\n/g).map(line => {
try {
return { uri: vscode.Uri.parse(resource), str: resource };
return { uri: vscode.Uri.parse(line), str: line };
} catch {
// Uri parse failure
return undefined;
Expand Down Expand Up @@ -197,7 +197,7 @@ export function appendToLinkSnippet(
}

export function createUriListSnippet(
document: SkinnyTextDocument,
document: ITextDocument,
uris: ReadonlyArray<{
readonly uri: vscode.Uri;
readonly str?: string;
Expand Down Expand Up @@ -412,4 +412,3 @@ function needsBracketLink(mdPath: string) {

return nestingCount > 0;
}