Adding basic support for reference link completions

For #140602
This commit is contained in:
Matt Bierner
2022-01-12 18:37:18 -08:00
parent 973e50f544
commit 1f6c069a1a
3 changed files with 75 additions and 20 deletions

View File

@@ -102,10 +102,11 @@ export function stripAngleBrackets(link: string) {
return link.replace(angleBracketLinkRe, '$1');
}
const linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g;
const referenceLinkPattern = /(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]/g;
const definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)([^<]\S*|<[^>]+>)/gm;
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,
@@ -124,7 +125,7 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
document: vscode.TextDocument,
): vscode.DocumentLink[] {
const results: vscode.DocumentLink[] = [];
for (const match of text.matchAll(this.linkPattern)) {
for (const match of text.matchAll(linkPattern)) {
const matchImage = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index);
if (matchImage) {
results.push(matchImage);
@@ -143,8 +144,8 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
): vscode.DocumentLink[] {
const results: vscode.DocumentLink[] = [];
const definitions = this.getDefinitions(text, document);
for (const match of text.matchAll(this.referenceLinkPattern)) {
const definitions = LinkProvider.getDefinitions(text, document);
for (const match of text.matchAll(referenceLinkPattern)) {
let linkStart: vscode.Position;
let linkEnd: vscode.Position;
let reference = match[3];
@@ -188,9 +189,9 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
return results;
}
private getDefinitions(text: string, document: vscode.TextDocument) {
public static getDefinitions(text: string, document: vscode.TextDocument) {
const out = new Map<string, { link: string, linkRange: vscode.Range }>();
for (const match of text.matchAll(this.definitionPattern)) {
for (const match of text.matchAll(definitionPattern)) {
const pre = match[1];
const reference = match[2];
const link = match[3].trim();

View File

@@ -8,19 +8,23 @@ import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
import { resolveUriToMarkdownFile } from '../util/openDocumentLink';
import LinkProvider from './documentLinkProvider';
enum LinkKind {
Link, // [...](...)
ReferenceLink, // [...][...]
enum CompletionContextKind {
Link, // [...](|)
ReferenceLink, // [...][|]
LinkDefinition, // []: | // TODO: not implemented
}
interface CompletionContext {
readonly linkKind: LinkKind;
readonly kind: CompletionContextKind;
/**
* Text of the link before the current position
*
* For `[abc](xy|z)` this would be `xy`
* For `[abc](xy#z|abc)` this would be `xy#z`
*/
readonly linkPrefix: string;
@@ -28,7 +32,7 @@ interface CompletionContext {
readonly linkTextStartPosition: vscode.Position;
/**
* Info if the link looks like its for an anchor: `[](#header)`
* Info if the link looks like it is for an anchor: `[](#header)`
*/
readonly anchorInfo?: {
/** Text before the `#` */
@@ -59,14 +63,23 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
return [];
}
if (context.kind === CompletionContextKind.ReferenceLink) {
const completionRange = new vscode.Range(context.linkTextStartPosition, position);
return this.provideReferenceSuggestions(document, completionRange);
}
if (context.kind === CompletionContextKind.LinkDefinition) {
return [];
}
const items: vscode.CompletionItem[] = [];
const isAnchorInCurrentDoc = context.anchorInfo && context.anchorInfo.beforeAnchor.length === 0;
// Add anchor #links in current doc
if (context.linkPrefix.length === 0 || isAnchorInCurrentDoc) {
const range = new vscode.Range(context.linkTextStartPosition, position);
items.push(...(await this.provideHeaderSuggestions(document, range)));
const completionRange = new vscode.Range(context.linkTextStartPosition, position);
items.push(...(await this.provideHeaderSuggestions(document, completionRange)));
}
if (!isAnchorInCurrentDoc) {
@@ -98,6 +111,9 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
/// [...](...|
private readonly linkStartPattern = /\[([^\]]*?)\]\(\s*([^\s\(\)]*)$/;
/// [...][...|
private readonly referenceLinkStartPattern = /\[([^\]]*?)\]\[\s*([^\s\(\)]*)$/;
private getPathCompletionContext(document: vscode.TextDocument, position: vscode.Position): CompletionContext | undefined {
const prefixRange = new vscode.Range(position.with({ character: 0 }), position);
const linePrefix = document.getText(prefixRange);
@@ -112,7 +128,7 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
const anchorMatch = prefix.match(/^(.*)#([\w\d\-]*)$/);
return {
linkKind: LinkKind.Link,
kind: CompletionContextKind.Link,
linkPrefix: prefix,
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
anchorInfo: anchorMatch ? {
@@ -122,10 +138,35 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
};
}
const referenceLinkPrefixMatch = linePrefix.match(this.referenceLinkStartPattern);
if (referenceLinkPrefixMatch) {
const prefix = referenceLinkPrefixMatch[2];
return {
kind: CompletionContextKind.ReferenceLink,
linkPrefix: prefix,
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
};
}
return undefined;
}
private async provideHeaderSuggestions(document: vscode.TextDocument, range: vscode.Range,): Promise<vscode.CompletionItem[]> {
private provideReferenceSuggestions(document: vscode.TextDocument, completionRange: vscode.Range): vscode.CompletionItem[] {
const items: vscode.CompletionItem[] = [];
const definitions = LinkProvider.getDefinitions(document.getText(), document);
for (const def of definitions) {
items.push({
kind: vscode.CompletionItemKind.Reference,
label: def[0],
range: completionRange,
});
}
return items;
}
private async provideHeaderSuggestions(document: vscode.TextDocument, completionRange: vscode.Range): Promise<vscode.CompletionItem[]> {
const items: vscode.CompletionItem[] = [];
const tocProvider = new TableOfContentsProvider(this.engine, document);
@@ -134,7 +175,7 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
items.push({
kind: vscode.CompletionItemKind.Reference,
label: '#' + entry.slug.value,
range: range,
range: completionRange,
});
}

View File

@@ -27,7 +27,7 @@ function getCompletionsAtCursor(resource: vscode.Uri, fileContents: string) {
}
suite('markdown.PathCompletionProvider', () => {
suite('Markdown path completion provider', () => {
setup(async () => {
// These tests assume that the markdown completion provider is already registered
@@ -109,4 +109,17 @@ suite('markdown.PathCompletionProvider', () => {
assert.ok(completions.some(x => x.label === '#b'), 'Has #b header completion');
assert.ok(completions.some(x => x.label === '#header1'), 'Has #header1 header completion');
});
test('Should reference links for current file', async () => {
const completions = await getCompletionsAtCursor(workspaceFile('sub', 'new.md'), joinLines(
`[][${CURSOR}`,
``,
`[ref-1]: bla`,
`[ref-2]: bla`,
));
assert.strictEqual(completions.length, 2);
assert.ok(completions.some(x => x.label === 'ref-1'), 'Has ref-1 reference completion');
assert.ok(completions.some(x => x.label === 'ref-2'), 'Has ref-2 reference completion');
});
});