Initial work on md references provider

#146277
This commit is contained in:
Matt Bierner
2022-03-29 17:14:39 -07:00
parent 9b6435af76
commit 338ae07ccb
10 changed files with 357 additions and 131 deletions

View File

@@ -9,22 +9,38 @@ import * as uri from 'vscode-uri';
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
import { MarkdownEngine } from '../markdownEngine';
import { getUriForLinkWithKnownExternalScheme, isOfScheme, Schemes } from '../util/schemes';
import { SkinnyTextDocument } from '../workspaceContents';
const localize = nls.loadMessageBundle();
function parseLink(
document: vscode.TextDocument,
link: string,
): { uri: vscode.Uri; tooltip?: string } | undefined {
export interface ExternalLinkTarget {
readonly kind: 'external';
readonly uri: vscode.Uri;
}
export interface InternalLinkTarget {
readonly kind: 'internal';
readonly fromResource: vscode.Uri;
readonly path: vscode.Uri;
readonly fragment: string;
}
export type LinkTarget = ExternalLinkTarget | InternalLinkTarget;
function parseLink(
document: SkinnyTextDocument,
link: string,
): LinkTarget | undefined {
const cleanLink = stripAngleBrackets(link);
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(cleanLink);
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 { kind: 'external', uri: vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }) };
}
return { uri: externalSchemeUri };
return { kind: 'external', uri: externalSchemeUri };
}
// Assume it must be an relative or absolute file path
@@ -58,36 +74,42 @@ function parseLink(
resourceUri = resourceUri.with({ fragment: tempUri.fragment });
return {
uri: OpenDocumentLinkCommand.createCommandUri(document.uri, resourceUri, tempUri.fragment),
tooltip: localize('documentLink.tooltip', 'Follow link')
kind: 'internal',
fromResource: document.uri,
path: resourceUri,
fragment: tempUri.fragment,
};
}
function getWorkspaceFolder(document: vscode.TextDocument) {
function getWorkspaceFolder(document: SkinnyTextDocument) {
return vscode.workspace.getWorkspaceFolder(document.uri)?.uri
|| vscode.workspace.workspaceFolders?.[0]?.uri;
}
export interface LinkData {
readonly target: LinkTarget;
readonly sourceRange: vscode.Range;
}
function extractDocumentLink(
document: vscode.TextDocument,
document: SkinnyTextDocument,
pre: number,
link: string,
matchIndex: number | undefined
): vscode.DocumentLink | undefined {
): LinkData | 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) {
const linkTarget = parseLink(document, link);
if (!linkTarget) {
return undefined;
}
const documentLink = new vscode.DocumentLink(
new vscode.Range(linkStart, linkEnd),
linkData.uri);
documentLink.tooltip = linkData.tooltip;
return documentLink;
} catch (e) {
return {
target: linkTarget,
sourceRange: new vscode.Range(linkStart, linkEnd)
};
} catch {
return undefined;
}
}
@@ -132,7 +154,7 @@ interface CodeInDocument {
readonly inline: readonly vscode.Range[];
}
async function findCode(document: vscode.TextDocument, engine: MarkdownEngine): Promise<CodeInDocument> {
async function findCode(document: SkinnyTextDocument, engine: MarkdownEngine): Promise<CodeInDocument> {
const tokens = await engine.parse(document);
const multiline = tokens.filter(t => (t.type === 'code_block' || t.type === 'fence') && !!t.map).map(t => t.map) as [number, number][];
@@ -145,9 +167,21 @@ async function findCode(document: vscode.TextDocument, engine: MarkdownEngine):
return { multiline, inline };
}
function isLinkInsideCode(code: CodeInDocument, link: vscode.DocumentLink) {
return code.multiline.some(interval => link.range.start.line >= interval[0] && link.range.start.line < interval[1]) ||
code.inline.some(position => position.intersection(link.range));
function isLinkInsideCode(code: CodeInDocument, link: LinkData) {
return code.multiline.some(interval => link.sourceRange.start.line >= interval[0] && link.sourceRange.start.line < interval[1]) ||
code.inline.some(position => position.intersection(link.sourceRange));
}
function createDocumentLink(sourceRange: vscode.Range, target: LinkTarget) {
if (target.kind === 'external') {
return new vscode.DocumentLink(sourceRange, target.uri);
} else {
const uri = OpenDocumentLinkCommand.createCommandUri(target.fromResource, target.path, target.fragment);
const documentLink = new vscode.DocumentLink(sourceRange, uri);
documentLink.tooltip = localize('documentLink.tooltip', 'Follow link');
return documentLink;
}
}
export class MdLinkProvider implements vscode.DocumentLinkProvider {
@@ -160,23 +194,24 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
_token: vscode.CancellationToken
): Promise<vscode.DocumentLink[]> {
const text = document.getText();
const inlineLinks = await this.getInlineLinks(text, document);
return [
...(await this.getInlineLinks(text, document)),
...inlineLinks.map(data => createDocumentLink(data.sourceRange, data.target)),
...this.getReferenceLinks(text, document)
];
}
private async getInlineLinks(text: string, document: vscode.TextDocument): Promise<vscode.DocumentLink[]> {
const results: vscode.DocumentLink[] = [];
public async getInlineLinks(text: string, document: SkinnyTextDocument): Promise<LinkData[]> {
const results: LinkData[] = [];
const codeInDocument = await findCode(document, this.engine);
for (const match of text.matchAll(linkPattern)) {
const matchImage = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index);
if (matchImage && !isLinkInsideCode(codeInDocument, matchImage)) {
results.push(matchImage);
const matchImageData = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index);
if (matchImageData && !isLinkInsideCode(codeInDocument, matchImageData)) {
results.push(matchImageData);
}
const matchLink = extractDocumentLink(document, match[1].length, match[5], match.index);
if (matchLink && !isLinkInsideCode(codeInDocument, matchLink)) {
results.push(matchLink);
const matchLinkData = extractDocumentLink(document, match[1].length, match[5], match.index);
if (matchLinkData && !isLinkInsideCode(codeInDocument, matchLinkData)) {
results.push(matchLinkData);
}
}
return results;
@@ -216,9 +251,9 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
for (const definition of definitions.values()) {
try {
const linkData = parseLink(document, definition.link);
if (linkData) {
yield new vscode.DocumentLink(definition.linkRange, linkData.uri);
const target = parseLink(document, definition.link);
if (target) {
yield createDocumentLink(definition.linkRange, target);
}
} catch (e) {
// noop

View File

@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* 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 { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { Disposable } from '../util/dispose';
import { MdWorkspaceContents } from '../workspaceContents';
import { InternalLinkTarget, LinkData, MdLinkProvider } from './documentLinkProvider';
import { MdWorkspaceCache } from './workspaceCache';
export class MdReferencesProvider extends Disposable implements vscode.ReferenceProvider {
private readonly _linkCache: MdWorkspaceCache<Promise<LinkData[]>>;
public constructor(
linkProvider: MdLinkProvider,
workspaceContents: MdWorkspaceContents,
private readonly engine: MarkdownEngine,
) {
super();
this._linkCache = this._register(new MdWorkspaceCache(workspaceContents, doc => linkProvider.getInlineLinks(doc.getText(), doc)));
}
async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise<vscode.Location[] | undefined> {
const toc = await TableOfContents.create(this.engine, document);
if (token.isCancellationRequested) {
return undefined;
}
const header = toc.entries.find(entry => entry.line === position.line);
if (!header) {
return undefined;
}
const links = await Promise.all(await this._linkCache.getAll());
return links
.flat()
.filter(link => {
return link.target.kind === 'internal'
&& link.target.path.fsPath === document.uri.fsPath
&& link.target.fragment === header.slug.value;
})
.map(link => {
const target = link.target as InternalLinkTarget;
return new vscode.Location(target.fromResource, link.sourceRange);
});
}
}

View File

@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* 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 { Disposable } from '../util/dispose';
import { Lazy, lazy } from '../util/lazy';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
/**
* Cache of information for markdown files in the workspace.
*/
export class MdWorkspaceCache<T> extends Disposable {
private readonly _cache = new Map<string, Lazy<T>>();
private _hasPopulatedCache = false;
public constructor(
private readonly workspaceContents: MdWorkspaceContents,
private readonly getValue: (document: SkinnyTextDocument) => T,
) {
super();
}
public async getAll(): Promise<T[]> {
if (!this._hasPopulatedCache) {
await this.populateSymbolCache();
this._hasPopulatedCache = true;
this.workspaceContents.onDidChangeMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
this.workspaceContents.onDidCreateMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
this.workspaceContents.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this, this._disposables);
}
return Array.from(this._cache.values(), x => x.value);
}
private async populateSymbolCache(): Promise<void> {
const markdownDocumentUris = await this.workspaceContents.getAllMarkdownDocuments();
for (const document of markdownDocumentUris) {
this._cache.set(document.uri.toString(), this.update(document));
}
}
private update(document: SkinnyTextDocument): Lazy<T> {
return lazy(() => this.getValue(document));
}
private onDidChangeDocument(document: SkinnyTextDocument) {
this._cache.set(document.uri.toString(), this.update(document));
}
private onDidDeleteDocument(resource: vscode.Uri) {
this._cache.delete(resource.toString());
}
}

View File

@@ -5,10 +5,9 @@
import * as vscode from 'vscode';
import { Disposable } from '../util/dispose';
import { Lazy, lazy } from '../util/lazy';
import { SkinnyTextDocument, MdWorkspaceContents } from '../workspaceContents';
import { MdWorkspaceContents } from '../workspaceContents';
import { MdDocumentSymbolProvider } from './documentSymbolProvider';
import { MdWorkspaceCache } from './workspaceCache';
export class MdWorkspaceSymbolProvider extends Disposable implements vscode.WorkspaceSymbolProvider {
@@ -29,51 +28,3 @@ export class MdWorkspaceSymbolProvider extends Disposable implements vscode.Work
return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1);
}
}
/**
* Cache of information for markdown files in the workspace.
*/
class MdWorkspaceCache<T> extends Disposable {
private readonly _cache = new Map<string, Lazy<T>>();
private _hasPopulatedCache = false;
public constructor(
private readonly workspaceContents: MdWorkspaceContents,
private readonly getValue: (document: SkinnyTextDocument) => T,
) {
super();
}
public async getAll(): Promise<T[]> {
if (!this._hasPopulatedCache) {
await this.populateSymbolCache();
this._hasPopulatedCache = true;
this.workspaceContents.onDidChangeMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
this.workspaceContents.onDidCreateMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
this.workspaceContents.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this, this._disposables);
}
return Array.from(this._cache.values(), x => x.value);
}
private async populateSymbolCache(): Promise<void> {
const markdownDocumentUris = await this.workspaceContents.getAllMarkdownDocuments();
for (const document of markdownDocumentUris) {
this._cache.set(document.uri.toString(), this.update(document));
}
}
private update(document: SkinnyTextDocument): Lazy<T> {
return lazy(() => this.getValue(document));
}
private onDidChangeDocument(document: SkinnyTextDocument) {
this._cache.set(document.uri.toString(), this.update(document));
}
private onDidDeleteDocument(resource: vscode.Uri) {
this._cache.delete(resource.toString());
}
}