Include invalid references as targets for find all references

For #146277
This commit is contained in:
Matt Bierner 2022-03-30 14:33:01 -07:00
parent 17783a09e5
commit d441406b1e
No known key found for this signature in database
GPG key ID: 099C331567E11888
3 changed files with 80 additions and 57 deletions

View file

@ -8,6 +8,7 @@ import * as nls from 'vscode-nls';
import * as uri from 'vscode-uri';
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
import { MarkdownEngine } from '../markdownEngine';
import { coalesce } from '../util/arrays';
import { getUriForLinkWithKnownExternalScheme, isOfScheme, Schemes } from '../util/schemes';
import { SkinnyTextDocument } from '../workspaceContents';
@ -34,7 +35,6 @@ export interface ReferenceLinkTarget {
readonly fromResource: vscode.Uri;
readonly ref: string;
readonly position: vscode.Position;
}
export interface DefinitionLinkTarget {
@ -192,7 +192,7 @@ function isLinkInsideCode(code: CodeInDocument, link: LinkData) {
code.inline.some(position => position.intersection(link.sourceRange));
}
function createDocumentLink(sourceRange: vscode.Range, target: LinkTarget): vscode.DocumentLink {
function createDocumentLink(sourceRange: vscode.Range, target: LinkTarget, definitionSet: LinkDefinitionSet): vscode.DocumentLink | undefined {
switch (target.kind) {
case 'external': {
return new vscode.DocumentLink(sourceRange, target.uri);
@ -204,25 +204,38 @@ function createDocumentLink(sourceRange: vscode.Range, target: LinkTarget): vsco
return documentLink;
}
case 'reference': {
return new vscode.DocumentLink(
sourceRange,
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([target.position.line, target.position.character]))}`));
const def = definitionSet.lookup(target.ref);
if (def) {
return new vscode.DocumentLink(
sourceRange,
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([def.sourceRange.start.line, def.sourceRange.start.character]))}`));
} else {
return undefined;
}
}
case 'definition':
return createDocumentLink(sourceRange, target.target);
return createDocumentLink(sourceRange, target.target, definitionSet);
}
}
export class MdLinkProvider implements vscode.DocumentLinkProvider {
constructor(
private readonly engine: MarkdownEngine
) { }
public async provideDocumentLinks(
document: SkinnyTextDocument,
_token: vscode.CancellationToken
token: vscode.CancellationToken
): Promise<vscode.DocumentLink[]> {
return (await this.getAllLinks(document)).map(data => createDocumentLink(data.sourceRange, data.target));
const allLinks = await this.getAllLinks(document);
if (token.isCancellationRequested) {
return [];
}
const definitionSet = new LinkDefinitionSet(allLinks);
return coalesce(allLinks
.map(data => createDocumentLink(data.sourceRange, data.target, definitionSet)));
}
public async getAllLinks(document: SkinnyTextDocument): Promise<LinkData[]> {
@ -253,7 +266,6 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
public *getReferenceLinks(document: SkinnyTextDocument): Iterable<LinkData> {
const text = document.getText();
const definitions = this.getDefinitions(document);
for (const match of text.matchAll(referenceLinkPattern)) {
let linkStart: vscode.Position;
let linkEnd: vscode.Position;
@ -272,46 +284,19 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
continue;
}
const link = definitions.get(reference);
if (link) {
yield {
sourceRange: new vscode.Range(linkStart, linkEnd),
target: {
kind: 'reference',
fromResource: document.uri,
ref: reference,
position: link.linkRange.start
}
};
}
yield {
sourceRange: new vscode.Range(linkStart, linkEnd),
target: {
kind: 'reference',
fromResource: document.uri,
ref: reference,
}
};
}
}
public *getDefinitionLinks(document: SkinnyTextDocument): Iterable<LinkData> {
const definitions = this.getDefinitions(document);
for (const [ref, definition] of definitions) {
try {
const target = parseLink(document, definition.link);
if (target) {
yield {
sourceRange: definition.linkRange,
target: {
fromResource: document.uri,
ref,
kind: 'definition',
target
}
};
}
} catch (e) {
// noop
}
}
}
public getDefinitions(document: SkinnyTextDocument): Map<string, { readonly link: string; readonly linkRange: vscode.Range }> {
const text = document.getText();
const out = new Map<string, { link: string; linkRange: vscode.Range }>();
for (const match of text.matchAll(definitionPattern)) {
const pre = match[1];
const reference = match[2];
@ -321,19 +306,50 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
if (angleBracketLinkRe.test(link)) {
const linkStart = document.positionAt(offset + 1);
const linkEnd = document.positionAt(offset + link.length - 1);
out.set(reference, {
link: link.substring(1, link.length - 1),
linkRange: new vscode.Range(linkStart, linkEnd)
});
const target = parseLink(document, link.substring(1, link.length - 1));
if (target) {
yield {
sourceRange: new vscode.Range(linkStart, linkEnd),
target: {
kind: 'definition',
fromResource: document.uri,
ref: reference,
target
}
};
}
} else {
const linkStart = document.positionAt(offset);
const linkEnd = document.positionAt(offset + link.length);
out.set(reference, {
link: link,
linkRange: new vscode.Range(linkStart, linkEnd)
});
const target = parseLink(document, link);
if (target) {
yield {
sourceRange: new vscode.Range(linkStart, linkEnd),
target: {
kind: 'definition',
fromResource: document.uri,
ref: reference,
target,
}
};
}
}
}
return out;
}
}
export class LinkDefinitionSet {
private readonly _map = new Map<string, LinkData>();
constructor(links: Iterable<LinkData>) {
for (const link of links) {
if (link.target.kind === 'definition') {
this._map.set(link.target.ref, link);
}
}
}
public lookup(ref: string): LinkData | undefined {
return this._map.get(ref);
}
}

View file

@ -9,7 +9,7 @@ import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { resolveUriToMarkdownFile } from '../util/openDocumentLink';
import { SkinnyTextDocument } from '../workspaceContents';
import { MdLinkProvider } from './documentLinkProvider';
import { DefinitionLinkTarget, MdLinkProvider } from './documentLinkProvider';
enum CompletionContextKind {
/** `[...](|)` */
@ -236,11 +236,11 @@ export class MdPathCompletionProvider implements vscode.CompletionItemProvider {
const insertionRange = new vscode.Range(context.linkTextStartPosition, position);
const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length }));
const definitions = this.linkProvider.getDefinitions(document);
const definitions = this.linkProvider.getDefinitionLinks(document);
for (const def of definitions) {
yield {
kind: vscode.CompletionItemKind.Reference,
label: def[0],
label: (def.target as DefinitionLinkTarget).ref,
range: {
inserting: insertionRange,
replacing: replacementRange,

View file

@ -15,4 +15,11 @@ export function equals<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, itemEq
}
return true;
}
}
/**
* @returns New array with all falsy values removed. The original array IS NOT modified.
*/
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}