mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-19 08:08:39 +01:00
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user