Make paste resource respect paste location (#207635)

Fixes #207186
This commit is contained in:
Matt Bierner 2024-03-13 14:27:44 -07:00 committed by GitHub
parent 7c6e82d367
commit e5f5e91423
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 435 additions and 331 deletions

View file

@ -466,10 +466,20 @@
"description": "%markdown.server.log.desc%"
},
"markdown.editor.drop.enabled": {
"type": "boolean",
"default": true,
"type": "string",
"scope": "resource",
"markdownDescription": "%configuration.markdown.editor.drop.enabled%",
"scope": "resource"
"default": "smart",
"enum": [
"always",
"smart",
"never"
],
"markdownEnumDescriptions": [
"%configuration.markdown.editor.drop.enabled.always%",
"%configuration.markdown.editor.drop.enabled.smart%",
"%configuration.markdown.editor.drop.enabled.never%"
]
},
"markdown.editor.drop.copyIntoWorkspace": {
"type": "string",
@ -488,7 +498,17 @@
"type": "boolean",
"scope": "resource",
"markdownDescription": "%configuration.markdown.editor.filePaste.enabled%",
"default": true
"default": "smart",
"enum": [
"always",
"smart",
"never"
],
"markdownEnumDescriptions": [
"%configuration.markdown.editor.filePaste.enabled.always%",
"%configuration.markdown.editor.filePaste.enabled.smart%",
"%configuration.markdown.editor.filePaste.enabled.never%"
]
},
"markdown.editor.filePaste.copyIntoWorkspace": {
"type": "string",

View file

@ -38,8 +38,14 @@
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onDoubleHash": "Enable workspace header suggestions after typing `##` in a path, for example: `[link text](##`.",
"configuration.markdown.suggest.paths.includeWorkspaceHeaderCompletions.onSingleOrDoubleHash": "Enable workspace header suggestions after typing either `##` or `#` in a path, for example: `[link text](#` or `[link text](##`.",
"configuration.markdown.editor.drop.enabled": "Enable dropping files into a Markdown editor while holding Shift. Requires enabling `#editor.dropIntoEditor.enabled#`.",
"configuration.markdown.editor.drop.always": "Always insert Markdown links.",
"configuration.markdown.editor.drop.smart": "Smartly create Markdown links by default when not dropping into a code block or other special element. Use the drop widget to switch between pasting as plain text or as Markdown links.",
"configuration.markdown.editor.drop.never": "Never create Markdown links.",
"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.always": "Always insert Markdown links.",
"configuration.markdown.editor.filePaste.smart": "Smartly create Markdown links by default when not pasting into a code block or other special element. Use the paste widget to switch between pasting as plain text or as Markdown links.",
"configuration.markdown.editor.filePaste.never": "Never create Markdown links.",
"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.copyIntoWorkspace.mediaFiles": "Try to copy external image and video files into the workspace.",
"configuration.copyIntoWorkspace.never": "Do not copy external files into the workspace.",

View file

@ -58,7 +58,7 @@ function registerMarkdownLanguageFeatures(
// Language features
registerDiagnosticSupport(selector, commandManager),
registerFindFileReferenceSupport(commandManager, client),
registerResourceDropOrPasteSupport(selector),
registerResourceDropOrPasteSupport(selector, parser),
registerPasteUrlSupport(selector, parser),
registerUpdateLinksOnRename(client),
);

View file

@ -4,12 +4,20 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { IMdParser } from '../../markdownEngine';
import { coalesce } from '../../util/arrays';
import { getParentDocumentUri } from '../../util/document';
import { Mime, mediaMimes } from '../../util/mimes';
import { Schemes } from '../../util/schemes';
import { NewFilePathGenerator } from './newFilePathGenerator';
import { createInsertUriListEdit, createUriListSnippet, getSnippetLabel } from './shared';
import { DropOrPasteEdit, createInsertUriListEdit, createUriListSnippet, getSnippetLabel } from './shared';
import { InsertMarkdownLink, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste';
import { UriList } from '../../util/uriList';
enum CopyFilesSettings {
Never = 'never',
MediaFiles = 'mediaFiles',
}
/**
* Provides support for pasting or dropping resources into markdown documents.
@ -35,127 +43,146 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
vscode.DocumentPasteEditKind.Empty.append('markdown', 'image', 'attachment'),
];
constructor(
private readonly _parser: IMdParser,
) { }
public async provideDocumentDropEdits(
document: vscode.TextDocument,
position: vscode.Position,
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentDropEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);
if (!enabled) {
const edit = await this._createEdit(document, [new vscode.Range(position, position)], dataTransfer, {
insert: this._getEnabled(document, 'editor.drop.enabled'),
copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get<CopyFilesSettings>('editor.drop.copyIntoWorkspace', CopyFilesSettings.MediaFiles)
}, undefined, token);
if (!edit || token.isCancellationRequested) {
return;
}
const filesEdit = await this._getMediaFilesDropEdit(document, dataTransfer, token);
if (filesEdit) {
return filesEdit;
}
if (token.isCancellationRequested) {
return;
}
return this._createEditFromUriListData(document, [new vscode.Range(position, position)], dataTransfer, token);
const dropEdit = new vscode.DocumentDropEdit(edit.snippet);
dropEdit.title = edit.label;
dropEdit.kind = ResourcePasteOrDropProvider.kind;
dropEdit.additionalEdit = edit.additionalEdits;
dropEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];
return dropEdit;
}
public async provideDocumentPasteEdits(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
_context: vscode.DocumentPasteEditContext,
context: vscode.DocumentPasteEditContext,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit[] | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.filePaste.enabled', true);
if (!enabled) {
const edit = await this._createEdit(document, ranges, dataTransfer, {
insert: this._getEnabled(document, 'editor.paste.enabled'),
copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get<CopyFilesSettings>('editor.paste.copyIntoWorkspace', CopyFilesSettings.MediaFiles)
}, context, token);
if (!edit || token.isCancellationRequested) {
return;
}
const createEdit = await this._getMediaFilesPasteEdit(document, dataTransfer, token);
if (createEdit) {
return [createEdit];
const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, ResourcePasteOrDropProvider.kind);
pasteEdit.additionalEdit = edit.additionalEdits;
pasteEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];
return [pasteEdit];
}
private _getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink {
const setting = vscode.workspace.getConfiguration('markdown', document).get<boolean | InsertMarkdownLink>(settingName, true);
// Convert old boolean values to new enum setting
if (setting === false) {
return InsertMarkdownLink.Never;
} else if (setting === true) {
return InsertMarkdownLink.Smart;
} else {
return setting;
}
}
private async _createEdit(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
settings: {
insert: InsertMarkdownLink;
copyIntoWorkspace: CopyFilesSettings;
},
context: vscode.DocumentPasteEditContext | undefined,
token: vscode.CancellationToken,
): Promise<DropOrPasteEdit | undefined> {
if (settings.insert === InsertMarkdownLink.Never) {
return;
}
let edit = await this._createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token);
if (token.isCancellationRequested) {
return;
}
const edit = await this._createEditFromUriListData(document, ranges, dataTransfer, token);
return edit ? [edit] : undefined;
if (!edit) {
edit = await this._createEditFromUriListData(document, ranges, dataTransfer, context, token);
}
if (!edit || token.isCancellationRequested) {
return;
}
if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, settings.insert, ranges, token))) {
edit.yieldTo.push(vscode.DocumentPasteEditKind.Empty.append('uri'));
}
return edit;
}
private async _createEditFromUriListData(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
context: vscode.DocumentPasteEditContext | undefined,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit | undefined> {
const uriList = await dataTransfer.get(Mime.textUriList)?.asString();
if (!uriList || token.isCancellationRequested) {
): Promise<DropOrPasteEdit | undefined> {
const uriListData = await dataTransfer.get(Mime.textUriList)?.asString();
if (!uriListData || token.isCancellationRequested) {
return;
}
const pasteEdit = createInsertUriListEdit(document, ranges, uriList);
if (!pasteEdit) {
const uriList = UriList.from(uriListData);
if (!uriList.entries.length) {
return;
}
const uriEdit = new vscode.DocumentPasteEdit('', pasteEdit.label, ResourcePasteOrDropProvider.kind);
const edit = new vscode.WorkspaceEdit();
edit.set(document.uri, pasteEdit.edits);
uriEdit.additionalEdit = edit;
uriEdit.yieldTo = this._yieldTo;
return uriEdit;
}
// Disable ourselves if there's also a text entry with the same content as our list,
// unless we are explicitly requested.
if (uriList.entries.length === 1 && !context?.only?.contains(ResourcePasteOrDropProvider.kind)) {
const text = await dataTransfer.get(Mime.textPlain)?.asString();
if (token.isCancellationRequested) {
return;
}
private async _getMediaFilesPasteEdit(
document: vscode.TextDocument,
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit | undefined> {
if (getParentDocumentUri(document.uri).scheme === Schemes.untitled) {
return;
if (text && textMatchesUriList(text, uriList)) {
return;
}
}
const copyFilesIntoWorkspace = vscode.workspace.getConfiguration('markdown', document).get<'mediaFiles' | 'never'>('editor.filePaste.copyIntoWorkspace', 'mediaFiles');
if (copyFilesIntoWorkspace !== 'mediaFiles') {
return;
}
const edit = await this._createEditForMediaFiles(document, dataTransfer, token);
const edit = createInsertUriListEdit(document, ranges, uriList);
if (!edit) {
return;
}
const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, ResourcePasteOrDropProvider.kind);
pasteEdit.additionalEdit = edit.additionalEdits;
pasteEdit.yieldTo = this._yieldTo;
return pasteEdit;
}
const additionalEdits = new vscode.WorkspaceEdit();
additionalEdits.set(document.uri, edit.edits);
private async _getMediaFilesDropEdit(
document: vscode.TextDocument,
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentDropEdit | undefined> {
if (getParentDocumentUri(document.uri).scheme === Schemes.untitled) {
return;
}
const copyIntoWorkspace = vscode.workspace.getConfiguration('markdown', document).get<'mediaFiles' | 'never'>('editor.drop.copyIntoWorkspace', 'mediaFiles');
if (copyIntoWorkspace !== 'mediaFiles') {
return;
}
const edit = await this._createEditForMediaFiles(document, dataTransfer, token);
if (!edit) {
return;
}
const dropEdit = new vscode.DocumentDropEdit(edit.snippet);
dropEdit.title = edit.label;
dropEdit.additionalEdit = edit.additionalEdits;
dropEdit.yieldTo = this._yieldTo;
return dropEdit;
return {
label: edit.label,
snippet: new vscode.SnippetString(''),
additionalEdits,
yieldTo: []
};
}
/**
@ -166,8 +193,13 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
private async _createEditForMediaFiles(
document: vscode.TextDocument,
dataTransfer: vscode.DataTransfer,
copyIntoWorkspace: CopyFilesSettings,
token: vscode.CancellationToken,
): Promise<{ snippet: vscode.SnippetString; label: string; additionalEdits: vscode.WorkspaceEdit } | undefined> {
): Promise<DropOrPasteEdit | undefined> {
if (copyIntoWorkspace !== CopyFilesSettings.MediaFiles || getParentDocumentUri(document.uri).scheme === Schemes.untitled) {
return;
}
interface FileEntry {
readonly uri: vscode.Uri;
readonly newFile?: { readonly contents: vscode.DataTransferFile; readonly overwrite: boolean };
@ -202,36 +234,50 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
return;
}
const workspaceEdit = new vscode.WorkspaceEdit();
const snippet = createUriListSnippet(document.uri, fileEntries);
if (!snippet) {
return;
}
const additionalEdits = new vscode.WorkspaceEdit();
for (const entry of fileEntries) {
if (entry.newFile) {
workspaceEdit.createFile(entry.uri, {
additionalEdits.createFile(entry.uri, {
contents: entry.newFile.contents,
overwrite: entry.newFile.overwrite,
});
}
}
const snippet = createUriListSnippet(document.uri, fileEntries);
if (!snippet) {
return;
}
return {
snippet: snippet.snippet,
label: getSnippetLabel(snippet),
additionalEdits: workspaceEdit,
additionalEdits,
yieldTo: [],
};
}
}
export function registerResourceDropOrPasteSupport(selector: vscode.DocumentSelector): vscode.Disposable {
function textMatchesUriList(text: string, uriList: UriList): boolean {
if (text === uriList.entries[0].str) {
return true;
}
try {
const uri = vscode.Uri.parse(text);
return uriList.entries.some(entry => entry.uri.toString() === uri.toString());
} catch {
return false;
}
}
export function registerResourceDropOrPasteSupport(selector: vscode.DocumentSelector, parser: IMdParser): vscode.Disposable {
return vscode.Disposable.from(
vscode.languages.registerDocumentPasteEditProvider(selector, new ResourcePasteOrDropProvider(), {
vscode.languages.registerDocumentPasteEditProvider(selector, new ResourcePasteOrDropProvider(parser), {
providedPasteEditKinds: [ResourcePasteOrDropProvider.kind],
pasteMimeTypes: ResourcePasteOrDropProvider.mimeTypes,
}),
vscode.languages.registerDocumentDropEditProvider(selector, new ResourcePasteOrDropProvider(), {
vscode.languages.registerDocumentDropEditProvider(selector, new ResourcePasteOrDropProvider(parser), {
providedDropEditKinds: [ResourcePasteOrDropProvider.kind],
dropMimeTypes: ResourcePasteOrDropProvider.mimeTypes,
}),

View file

@ -5,22 +5,10 @@
import * as vscode from 'vscode';
import { IMdParser } from '../../markdownEngine';
import { ITextDocument } from '../../types/textDocument';
import { Mime } from '../../util/mimes';
import { Schemes } from '../../util/schemes';
import { createInsertUriListEdit } from './shared';
export enum PasteUrlAsMarkdownLink {
Always = 'always',
SmartWithSelection = 'smartWithSelection',
Smart = 'smart',
Never = 'never'
}
function getPasteUrlAsFormattedLinkSetting(document: vscode.TextDocument): PasteUrlAsMarkdownLink {
return vscode.workspace.getConfiguration('markdown', document)
.get<PasteUrlAsMarkdownLink>('editor.pasteUrlAsFormattedLink.enabled', PasteUrlAsMarkdownLink.SmartWithSelection);
}
import { InsertMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefault } from './smartDropOrPaste';
import { UriList } from '../../util/uriList';
/**
* Adds support for pasting text uris to create markdown links.
@ -44,8 +32,9 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {
_context: vscode.DocumentPasteEditContext,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit[] | undefined> {
const pasteUrlSetting = getPasteUrlAsFormattedLinkSetting(document);
if (pasteUrlSetting === PasteUrlAsMarkdownLink.Never) {
const pasteUrlSetting = vscode.workspace.getConfiguration('markdown', document)
.get<InsertMarkdownLink>('editor.pasteUrlAsFormattedLink.enabled', InsertMarkdownLink.SmartWithSelection);
if (pasteUrlSetting === InsertMarkdownLink.Never) {
return;
}
@ -60,7 +49,7 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {
return;
}
const edit = createInsertUriListEdit(document, ranges, uriText, { preserveAbsoluteUris: true });
const edit = createInsertUriListEdit(document, ranges, UriList.from(uriText), { preserveAbsoluteUris: true });
if (!edit) {
return;
}
@ -71,7 +60,10 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {
pasteEdit.additionalEdit = workspaceEdit;
if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, pasteUrlSetting, ranges, token))) {
pasteEdit.yieldTo = [vscode.DocumentPasteEditKind.Empty.append('text')];
pasteEdit.yieldTo = [
vscode.DocumentPasteEditKind.Empty.append('text'),
vscode.DocumentPasteEditKind.Empty.append('uri')
];
}
return [pasteEdit];
@ -84,168 +76,3 @@ export function registerPasteUrlSupport(selector: vscode.DocumentSelector, parse
pasteMimeTypes: PasteUrlEditProvider.pasteMimeTypes,
});
}
const smartPasteLineRegexes = [
{ regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link
{ regex: /\$\$[\s\S]*?\$\$/gm }, // In a fenced math block
{ regex: /`[^`]*`/g }, // In inline code
{ regex: /\$[^$]*\$/g }, // In inline math
{ regex: /<[^<>\s]*>/g }, // Autolink
{ regex: /^[ ]{0,3}\[\w+\]:\s.*$/g, isWholeLine: true }, // Block link definition (needed as tokens are not generated for these)
];
export async function shouldInsertMarkdownLinkByDefault(
parser: IMdParser,
document: ITextDocument,
pasteUrlSetting: PasteUrlAsMarkdownLink,
ranges: readonly vscode.Range[],
token: vscode.CancellationToken,
): Promise<boolean> {
switch (pasteUrlSetting) {
case PasteUrlAsMarkdownLink.Always: {
return true;
}
case PasteUrlAsMarkdownLink.Smart: {
return checkSmart();
}
case PasteUrlAsMarkdownLink.SmartWithSelection: {
// At least one range must not be empty
if (!ranges.some(range => document.getText(range).trim().length > 0)) {
return false;
}
// And all ranges must be smart
return checkSmart();
}
default: {
return false;
}
}
async function checkSmart(): Promise<boolean> {
return (await Promise.all(ranges.map(range => shouldSmartPasteForSelection(parser, document, range, token)))).every(x => x);
}
}
const textTokenTypes = new Set(['paragraph_open', 'inline', 'heading_open', 'ordered_list_open', 'bullet_list_open', 'list_item_open', 'blockquote_open']);
async function shouldSmartPasteForSelection(
parser: IMdParser,
document: ITextDocument,
selectedRange: vscode.Range,
token: vscode.CancellationToken,
): Promise<boolean> {
// Disable for multi-line selections
if (selectedRange.start.line !== selectedRange.end.line) {
return false;
}
const rangeText = document.getText(selectedRange);
// Disable when the selection is already a link
if (findValidUriInText(rangeText)) {
return false;
}
if (/\[.*\]\(.*\)/.test(rangeText) || /!\[.*\]\(.*\)/.test(rangeText)) {
return false;
}
// Check if selection is inside a special block level element using markdown engine
const tokens = await parser.tokenize(document);
if (token.isCancellationRequested) {
return false;
}
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (!token.map) {
continue;
}
if (token.map[0] <= selectedRange.start.line && token.map[1] > selectedRange.start.line) {
if (!textTokenTypes.has(token.type)) {
return false;
}
}
// Special case for html such as:
//
// <b>
// |
// </b>
//
// In this case pasting will cause the html block to be created even though the cursor is not currently inside a block
if (token.type === 'html_block' && token.map[1] === selectedRange.start.line) {
const nextToken = tokens.at(i + 1);
// The next token does not need to be a html_block, but it must be on the next line
if (nextToken?.map?.[0] === selectedRange.end.line + 1) {
return false;
}
}
}
// Run additional regex checks on the current line to check if we are inside an inline element
const line = document.getText(new vscode.Range(selectedRange.start.line, 0, selectedRange.start.line, Number.MAX_SAFE_INTEGER));
for (const regex of smartPasteLineRegexes) {
for (const match of line.matchAll(regex.regex)) {
if (match.index === undefined) {
continue;
}
if (regex.isWholeLine) {
return false;
}
if (selectedRange.start.character > match.index && selectedRange.start.character < match.index + match[0].length) {
return false;
}
}
}
return true;
}
const externalUriSchemes: ReadonlySet<string> = new Set([
Schemes.http,
Schemes.https,
Schemes.mailto,
Schemes.file,
]);
export function findValidUriInText(text: string): string | undefined {
const trimmedUrlList = text.trim();
if (
!/^\S+$/.test(trimmedUrlList) // Uri must consist of a single sequence of characters without spaces
|| !trimmedUrlList.includes(':') // And it must have colon somewhere for the scheme. We will verify the schema again later
) {
return;
}
let uri: vscode.Uri;
try {
uri = vscode.Uri.parse(trimmedUrlList);
} catch {
// Could not parse
return;
}
// `Uri.parse` is lenient and will return a `file:` uri even for non-uri text such as `abc`
// Make sure that the resolved scheme starts the original text
if (!trimmedUrlList.toLowerCase().startsWith(uri.scheme.toLowerCase() + ':')) {
return;
}
// Only enable for an allow list of schemes. Otherwise this can be accidentally activated for non-uri text
// such as `c:\abc` or `value:foo`
if (!externalUriSchemes.has(uri.scheme.toLowerCase())) {
return;
}
// Some part of the uri must not be empty
// This disables the feature for text such as `http:`
if (!uri.authority && uri.path.length < 2 && !uri.query && !uri.fragment) {
return;
}
return trimmedUrlList;
}

View file

@ -7,11 +7,10 @@ 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 { Schemes } from '../../util/schemes';
import { UriList } from '../../util/uriList';
import { resolveSnippet } from './snippets';
import { parseUriList } from '../../util/uriList';
enum MediaKind {
Image,
@ -68,24 +67,13 @@ export function getSnippetLabel(counter: { insertedAudioVideoCount: number; inse
export function createInsertUriListEdit(
document: ITextDocument,
ranges: readonly vscode.Range[],
urlList: string,
urlList: UriList,
options?: UriListSnippetOptions,
): { edits: vscode.SnippetTextEdit[]; label: string } | undefined {
if (!ranges.length) {
if (!ranges.length || !urlList.entries.length) {
return;
}
const entries = coalesce(parseUriList(urlList).map(line => {
try {
return { uri: vscode.Uri.parse(line), str: line };
} catch {
// Uri parse failure
return undefined;
}
}));
if (!entries.length) {
return;
}
const edits: vscode.SnippetTextEdit[] = [];
@ -94,14 +82,14 @@ export function createInsertUriListEdit(
let insertedAudioVideoCount = 0;
// Use 1 for all empty ranges but give non-empty range unique indices starting after 1
let placeHolderStartIndex = 1 + entries.length;
let placeHolderStartIndex = 1 + urlList.entries.length;
// Sort ranges by start position
const orderedRanges = [...ranges].sort((a, b) => a.start.compareTo(b.start));
const allRangesAreEmpty = orderedRanges.every(range => range.isEmpty);
for (const range of orderedRanges) {
const snippet = createUriListSnippet(document.uri, entries, {
const snippet = createUriListSnippet(document.uri, urlList.entries, {
placeholderText: range.isEmpty ? undefined : document.getText(range),
placeholderStartIndex: allRangesAreEmpty ? 1 : placeHolderStartIndex,
...options,
@ -114,7 +102,7 @@ export function createInsertUriListEdit(
insertedImageCount += snippet.insertedImageCount;
insertedAudioVideoCount += snippet.insertedAudioVideoCount;
placeHolderStartIndex += entries.length;
placeHolderStartIndex += urlList.entries.length;
edits.push(new vscode.SnippetTextEdit(range, snippet.snippet));
}
@ -273,3 +261,10 @@ function needsBracketLink(mdPath: string): boolean {
return nestingCount > 0;
}
export interface DropOrPasteEdit {
readonly snippet: vscode.SnippetString;
readonly label: string;
readonly additionalEdits: vscode.WorkspaceEdit;
readonly yieldTo: vscode.DocumentPasteEditKind[];
}

View file

@ -0,0 +1,188 @@
/*---------------------------------------------------------------------------------------------
* 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 { IMdParser } from '../../markdownEngine';
import { ITextDocument } from '../../types/textDocument';
import { Schemes } from '../../util/schemes';
const smartPasteLineRegexes = [
{ regex: /(\[[^\[\]]*](?:\([^\(\)]*\)|\[[^\[\]]*]))/g }, // In a Markdown link
{ regex: /\$\$[\s\S]*?\$\$/gm }, // In a fenced math block
{ regex: /`[^`]*`/g }, // In inline code
{ regex: /\$[^$]*\$/g }, // In inline math
{ regex: /<[^<>\s]*>/g }, // Autolink
{ regex: /^[ ]{0,3}\[\w+\]:\s.*$/g, isWholeLine: true }, // Block link definition (needed as tokens are not generated for these)
];
export async function shouldInsertMarkdownLinkByDefault(
parser: IMdParser,
document: ITextDocument,
pasteUrlSetting: InsertMarkdownLink,
ranges: readonly vscode.Range[],
token: vscode.CancellationToken
): Promise<boolean> {
switch (pasteUrlSetting) {
case InsertMarkdownLink.Always: {
return true;
}
case InsertMarkdownLink.Smart: {
return checkSmart();
}
case InsertMarkdownLink.SmartWithSelection: {
// At least one range must not be empty
if (!ranges.some(range => document.getText(range).trim().length > 0)) {
return false;
}
// And all ranges must be smart
return checkSmart();
}
default: {
return false;
}
}
async function checkSmart(): Promise<boolean> {
return (await Promise.all(ranges.map(range => shouldSmartPasteForSelection(parser, document, range, token)))).every(x => x);
}
}
const textTokenTypes = new Set([
'paragraph_open',
'inline',
'heading_open',
'ordered_list_open',
'bullet_list_open',
'list_item_open',
'blockquote_open',
]);
async function shouldSmartPasteForSelection(
parser: IMdParser,
document: ITextDocument,
selectedRange: vscode.Range,
token: vscode.CancellationToken
): Promise<boolean> {
// Disable for multi-line selections
if (selectedRange.start.line !== selectedRange.end.line) {
return false;
}
const rangeText = document.getText(selectedRange);
// Disable when the selection is already a link
if (findValidUriInText(rangeText)) {
return false;
}
if (/\[.*\]\(.*\)/.test(rangeText) || /!\[.*\]\(.*\)/.test(rangeText)) {
return false;
}
// Check if selection is inside a special block level element using markdown engine
const tokens = await parser.tokenize(document);
if (token.isCancellationRequested) {
return false;
}
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (!token.map) {
continue;
}
if (token.map[0] <= selectedRange.start.line && token.map[1] > selectedRange.start.line) {
if (!textTokenTypes.has(token.type)) {
return false;
}
}
// Special case for html such as:
//
// <b>
// |
// </b>
//
// In this case pasting will cause the html block to be created even though the cursor is not currently inside a block
if (token.type === 'html_block' && token.map[1] === selectedRange.start.line) {
const nextToken = tokens.at(i + 1);
// The next token does not need to be a html_block, but it must be on the next line
if (nextToken?.map?.[0] === selectedRange.end.line + 1) {
return false;
}
}
}
// Run additional regex checks on the current line to check if we are inside an inline element
const line = document.getText(new vscode.Range(selectedRange.start.line, 0, selectedRange.start.line, Number.MAX_SAFE_INTEGER));
for (const regex of smartPasteLineRegexes) {
for (const match of line.matchAll(regex.regex)) {
if (match.index === undefined) {
continue;
}
if (regex.isWholeLine) {
return false;
}
if (selectedRange.start.character > match.index && selectedRange.start.character < match.index + match[0].length) {
return false;
}
}
}
return true;
}
const externalUriSchemes: ReadonlySet<string> = new Set([
Schemes.http,
Schemes.https,
Schemes.mailto,
Schemes.file,
]);
export function findValidUriInText(text: string): string | undefined {
const trimmedUrlList = text.trim();
if (!/^\S+$/.test(trimmedUrlList) // Uri must consist of a single sequence of characters without spaces
|| !trimmedUrlList.includes(':') // And it must have colon somewhere for the scheme. We will verify the schema again later
) {
return;
}
let uri: vscode.Uri;
try {
uri = vscode.Uri.parse(trimmedUrlList);
} catch {
// Could not parse
return;
}
// `Uri.parse` is lenient and will return a `file:` uri even for non-uri text such as `abc`
// Make sure that the resolved scheme starts the original text
if (!trimmedUrlList.toLowerCase().startsWith(uri.scheme.toLowerCase() + ':')) {
return;
}
// Only enable for an allow list of schemes. Otherwise this can be accidentally activated for non-uri text
// such as `c:\abc` or `value:foo`
if (!externalUriSchemes.has(uri.scheme.toLowerCase())) {
return;
}
// Some part of the uri must not be empty
// This disables the feature for text such as `http:`
if (!uri.authority && uri.path.length < 2 && !uri.query && !uri.fragment) {
return;
}
return trimmedUrlList;
}
export enum InsertMarkdownLink {
Always = 'always',
SmartWithSelection = 'smartWithSelection',
Smart = 'smart',
Never = 'never'
}

View file

@ -6,10 +6,11 @@ import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { InMemoryDocument } from '../client/inMemoryDocument';
import { PasteUrlAsMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefault } from '../languageFeatures/copyFiles/pasteUrlProvider';
import { createInsertUriListEdit } from '../languageFeatures/copyFiles/shared';
import { createNewMarkdownEngine } from './engine';
import { InsertMarkdownLink, findValidUriInText, shouldInsertMarkdownLinkByDefault } from '../languageFeatures/copyFiles/smartDropOrPaste';
import { noopToken } from '../util/cancellation';
import { UriList } from '../util/uriList';
import { createNewMarkdownEngine } from './engine';
function makeTestDoc(contents: string) {
return new InMemoryDocument(vscode.Uri.file('test.md'), contents);
@ -21,7 +22,7 @@ suite('createEditAddingLinksForUriList', () => {
// createEditAddingLinksForUriList -> checkSmartPaste -> tryGetUriListSnippet -> createUriListSnippet -> createLinkSnippet
const result = createInsertUriListEdit(
new InMemoryDocument(vscode.Uri.file('test.md'), 'hello world!'), [new vscode.Range(0, 0, 0, 12)], 'https://www.microsoft.com/');
new InMemoryDocument(vscode.Uri.file('test.md'), 'hello world!'), [new vscode.Range(0, 0, 0, 12)], UriList.from('https://www.microsoft.com/'));
// need to check the actual result -> snippet value
assert.strictEqual(result?.label, 'Insert Markdown Link');
});
@ -110,27 +111,27 @@ suite('createEditAddingLinksForUriList', () => {
suite('createInsertUriListEdit', () => {
test('Should create snippet with < > when pasted link has an mismatched parentheses', () => {
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.mic(rosoft.com');
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.mic(rosoft.com'));
assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](<https://www.mic(rosoft.com>)');
});
test('Should create Markdown link snippet when pasteAsMarkdownLink is true', () => {
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.microsoft.com');
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.microsoft.com'));
assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.microsoft.com)');
});
test('Should use an unencoded URI string in Markdown link when passing in an external browser link', () => {
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.microsoft.com');
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.microsoft.com'));
assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.microsoft.com)');
});
test('Should not decode an encoded URI string when passing in an external browser link', () => {
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.microsoft.com/%20');
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.microsoft.com/%20'));
assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.microsoft.com/%20)');
});
test('Should not encode an unencoded URI string when passing in an external browser link', () => {
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], 'https://www.example.com/path?query=value&another=value#fragment');
const edit = createInsertUriListEdit(makeTestDoc(''), [new vscode.Range(0, 0, 0, 0)], UriList.from('https://www.example.com/path?query=value&another=value#fragment'));
assert.strictEqual(edit?.edits?.[0].snippet.value, '[${1:text}](https://www.example.com/path?query=value&another=value#fragment)');
});
});
@ -140,41 +141,41 @@ suite('createEditAddingLinksForUriList', () => {
test('Smart should be enabled for selected plain text', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('hello world'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 12)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('hello world'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 12)], noopToken),
true);
});
test('Smart should be enabled in headers', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('# title'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 2, 0, 2)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('# title'), InsertMarkdownLink.Smart, [new vscode.Range(0, 2, 0, 2)], noopToken),
true);
});
test('Smart should be enabled in lists', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('1. text'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('1. text'), InsertMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken),
true);
});
test('Smart should be enabled in blockquotes', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('> text'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('> text'), InsertMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken),
true);
});
test('Smart should be disabled in indented code blocks', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' code'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 4)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' code'), InsertMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 4)], noopToken),
false);
});
test('Smart should be disabled in fenced code blocks', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('```\r\n\r\n```'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('```\r\n\r\n```'), InsertMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken),
false);
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('~~~\r\n\r\n~~~'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('~~~\r\n\r\n~~~'), InsertMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken),
false);
});
@ -183,127 +184,127 @@ suite('createEditAddingLinksForUriList', () => {
const engine = createNewMarkdownEngine();
(await engine.getEngine(undefined)).use(katex);
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(engine, makeTestDoc('$$\r\n\r\n$$'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken),
await shouldInsertMarkdownLinkByDefault(engine, makeTestDoc('$$\r\n\r\n$$'), InsertMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 5)], noopToken),
false);
});
test('Smart should be disabled in link definitions', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: http://example.com'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 6)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: http://example.com'), InsertMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 6)], noopToken),
false);
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: '), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 7, 0, 7)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: '), InsertMarkdownLink.Smart, [new vscode.Range(0, 7, 0, 7)], noopToken),
false);
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: '), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[ref]: '), InsertMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken),
false);
});
test('Smart should be disabled in html blocks', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<p>\na\n</p>'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<p>\na\n</p>'), InsertMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken),
false);
});
test('Smart should be disabled in html blocks where paste creates the block', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<p>\n\n</p>'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<p>\n\n</p>'), InsertMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken),
false,
'Between two html tags should be treated as html block');
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<p>\n\ntext'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<p>\n\ntext'), InsertMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken),
false,
'Between opening html tag and text should be treated as html block');
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<p>\n\n\n</p>'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<p>\n\n\n</p>'), InsertMarkdownLink.Smart, [new vscode.Range(1, 0, 1, 0)], noopToken),
true,
'Extra new line after paste should not be treated as html block');
});
test('Smart should be disabled in Markdown links', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[a](bcdef)'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 6)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[a](bcdef)'), InsertMarkdownLink.Smart, [new vscode.Range(0, 4, 0, 6)], noopToken),
false);
});
test('Smart should be disabled in Markdown images', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('![a](bcdef)'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 10)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('![a](bcdef)'), InsertMarkdownLink.Smart, [new vscode.Range(0, 5, 0, 10)], noopToken),
false);
});
test('Smart should be disabled in inline code', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('``'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 1, 0, 1)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('``'), InsertMarkdownLink.Smart, [new vscode.Range(0, 1, 0, 1)], noopToken),
false,
'Should be disabled inside of inline code');
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('``'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('``'), InsertMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken),
true,
'Should be enabled when cursor is outside but next to inline code');
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('`a`'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('`a`'), InsertMarkdownLink.Smart, [new vscode.Range(0, 3, 0, 3)], noopToken),
true,
'Should be enabled when cursor is outside but next to inline code');
});
test('Smart should be enabled when pasting over inline code ', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('`xyz`'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 5)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('`xyz`'), InsertMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 5)], noopToken),
true);
});
test('Smart should be disabled in inline math', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('$$'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 1, 0, 1)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('$$'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 1, 0, 1)], noopToken),
false);
});
test('Smart should be enabled for empty selection', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('xyz'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('xyz'), InsertMarkdownLink.Smart, [new vscode.Range(0, 0, 0, 0)], noopToken),
true);
});
test('SmartWithSelection should disable for empty selection', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('xyz'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 0)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('xyz'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 0)], noopToken),
false);
});
test('Smart should disable for selected link', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('https://www.microsoft.com'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 25)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('https://www.microsoft.com'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 25)], noopToken),
false);
});
test('Smart should disable for selected link with trailing whitespace', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' https://www.microsoft.com '), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 30)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' https://www.microsoft.com '), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 30)], noopToken),
false);
});
test('Should evaluate pasteAsMarkdownLink as true for a link pasted in square brackets', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[abc]'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 1, 0, 4)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('[abc]'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 1, 0, 4)], noopToken),
true);
});
test('Should evaluate pasteAsMarkdownLink as false for selected whitespace and new lines', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' \r\n\r\n'), PasteUrlAsMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 7)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc(' \r\n\r\n'), InsertMarkdownLink.SmartWithSelection, [new vscode.Range(0, 0, 0, 7)], noopToken),
false);
});
test('Smart should be disabled inside of autolinks', async () => {
assert.strictEqual(
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<>'), PasteUrlAsMarkdownLink.Smart, [new vscode.Range(0, 1, 0, 1)], noopToken),
await shouldInsertMarkdownLinkByDefault(createNewMarkdownEngine(), makeTestDoc('<>'), InsertMarkdownLink.Smart, [new vscode.Range(0, 1, 0, 1)], noopToken),
false);
});
});

View file

@ -3,12 +3,33 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { coalesce } from './arrays';
import * as vscode from 'vscode';
function splitUriList(str: string): string[] {
return str.split('\r\n');
}
export function parseUriList(str: string): string[] {
function parseUriList(str: string): string[] {
return splitUriList(str)
.filter(value => !value.startsWith('#')) // Remove comments
.map(value => value.trim());
}
export class UriList {
static from(str: string): UriList {
return new UriList(coalesce(parseUriList(str).map(line => {
try {
return { uri: vscode.Uri.parse(line), str: line };
} catch {
// Uri parse failure
return undefined;
}
})));
}
private constructor(
public readonly entries: ReadonlyArray<{ readonly uri: vscode.Uri; readonly str: string }>
) { }
}