/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { OpenDocumentLinkCommand } from '../commands/openDocumentLink'; import { getUriForLinkWithKnownExternalScheme, isOfScheme, Schemes } from '../util/links'; const localize = nls.loadMessageBundle(); function parseLink( document: vscode.TextDocument, link: string, ): { uri: vscode.Uri, tooltip?: string } | undefined { const externalSchemeUri = getUriForLinkWithKnownExternalScheme(link); if (externalSchemeUri) { // Normalize VS Code links to target currently running version if (isOfScheme(Schemes.vscode, link) || isOfScheme(Schemes['vscode-insiders'], link)) { return { uri: vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }) }; } return { uri: externalSchemeUri }; } // Assume it must be an relative or absolute file path // Use a fake scheme to avoid parse warnings const tempUri = vscode.Uri.parse(`vscode-resource:${link}`); let resourceUri: vscode.Uri | undefined; if (!tempUri.path) { resourceUri = document.uri; } else if (tempUri.path[0] === '/') { const root = getWorkspaceFolder(document); if (root) { resourceUri = vscode.Uri.joinPath(root, tempUri.path); } } else { if (document.uri.scheme === Schemes.untitled) { const root = getWorkspaceFolder(document); if (root) { resourceUri = vscode.Uri.joinPath(root, tempUri.path); } } else { const base = document.uri.with({ path: path.dirname(document.uri.fsPath) }); resourceUri = vscode.Uri.joinPath(base, tempUri.path); } } if (!resourceUri) { return undefined; } resourceUri = resourceUri.with({ fragment: tempUri.fragment }); return { uri: OpenDocumentLinkCommand.createCommandUri(document.uri, resourceUri, tempUri.fragment), tooltip: localize('documentLink.tooltip', 'Follow link') }; } function getWorkspaceFolder(document: vscode.TextDocument) { return vscode.workspace.getWorkspaceFolder(document.uri)?.uri || vscode.workspace.workspaceFolders?.[0]?.uri; } function matchAll( pattern: RegExp, text: string ): Array { const out: RegExpMatchArray[] = []; pattern.lastIndex = 0; let match: RegExpMatchArray | null; while ((match = pattern.exec(text))) { out.push(match); } return out; } function extractDocumentLink( document: vscode.TextDocument, pre: number, link: string, matchIndex: number | undefined ): vscode.DocumentLink | undefined { const offset = (matchIndex || 0) + pre; const linkStart = document.positionAt(offset); const linkEnd = document.positionAt(offset + link.length); try { const linkData = parseLink(document, link); if (!linkData) { return undefined; } const documentLink = new vscode.DocumentLink( new vscode.Range(linkStart, linkEnd), linkData.uri); documentLink.tooltip = linkData.tooltip; return documentLink; } catch (e) { return undefined; } } export default class LinkProvider implements vscode.DocumentLinkProvider { private readonly linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\(\S*?\))+)\s*(".*?")?\)/g; private readonly referenceLinkPattern = /(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]/g; private readonly definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)(\S+)/gm; public provideDocumentLinks( document: vscode.TextDocument, _token: vscode.CancellationToken ): vscode.DocumentLink[] { const text = document.getText(); return [ ...this.providerInlineLinks(text, document), ...this.provideReferenceLinks(text, document) ]; } private providerInlineLinks( text: string, document: vscode.TextDocument, ): vscode.DocumentLink[] { const results: vscode.DocumentLink[] = []; for (const match of matchAll(this.linkPattern, text)) { const matchImage = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index); if (matchImage) { results.push(matchImage); } const matchLink = extractDocumentLink(document, match[1].length, match[5], match.index); if (matchLink) { results.push(matchLink); } } return results; } private provideReferenceLinks( text: string, document: vscode.TextDocument, ): vscode.DocumentLink[] { const results: vscode.DocumentLink[] = []; const definitions = this.getDefinitions(text, document); for (const match of matchAll(this.referenceLinkPattern, text)) { let linkStart: vscode.Position; let linkEnd: vscode.Position; let reference = match[3]; if (reference) { // [text][ref] const pre = match[1]; const offset = (match.index || 0) + pre.length; linkStart = document.positionAt(offset); linkEnd = document.positionAt(offset + reference.length); } else if (match[2]) { // [ref][] reference = match[2]; const offset = (match.index || 0) + 1; linkStart = document.positionAt(offset); linkEnd = document.positionAt(offset + match[2].length); } else { continue; } try { const link = definitions.get(reference); if (link) { results.push(new vscode.DocumentLink( new vscode.Range(linkStart, linkEnd), vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([link.linkRange.start.line, link.linkRange.start.character]))}`))); } } catch (e) { // noop } } for (const definition of definitions.values()) { try { const linkData = parseLink(document, definition.link); if (linkData) { results.push(new vscode.DocumentLink(definition.linkRange, linkData.uri)); } } catch (e) { // noop } } return results; } private getDefinitions(text: string, document: vscode.TextDocument) { const out = new Map(); for (const match of matchAll(this.definitionPattern, text)) { const pre = match[1]; const reference = match[2]; const link = match[3].trim(); const offset = (match.index || 0) + pre.length; const linkStart = document.positionAt(offset); const linkEnd = document.positionAt(offset + link.length); out.set(reference, { link: link, linkRange: new vscode.Range(linkStart, linkEnd) }); } return out; } }