Enable completions in markdown defintions

Fixes #140602
This commit is contained in:
Matt Bierner
2022-01-14 15:15:35 -08:00
parent 12902bc7a1
commit a3e350e5b3
2 changed files with 78 additions and 29 deletions

View File

@@ -18,6 +18,21 @@ enum CompletionContextKind {
LinkDefinition, // []: | // TODO: not implemented
}
interface AnchorContext {
/**
* Link text before the `#`.
*
* For `[text](xy#z|abc)` this is `xy`.
*/
readonly beforeAnchor: string;
/**
* Text of the anchor before the current position.
*
* For `[text](xy#z|abc)` this is `z`.
*/
readonly anchorPrefix: string;
}
interface CompletionContext {
readonly kind: CompletionContextKind;
@@ -45,21 +60,7 @@ interface CompletionContext {
/**
* Info if the link looks like it is for an anchor: `[](#header)`
*/
readonly anchorInfo?: {
/**
* Link text before the `#`.
*
* For `[text](xy#z|abc)` this is `xy`.
*/
readonly beforeAnchor: string;
/**
* Text of the anchor before the current position.
*
* For `[text](xy#z|abc)` this is `z`.
*/
readonly anchorPrefix: string;
}
readonly anchorInfo?: AnchorContext
}
export class PathCompletionProvider implements vscode.CompletionItemProvider {
@@ -87,10 +88,7 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
return Array.from(this.provideReferenceSuggestions(document, position, context));
}
case CompletionContextKind.LinkDefinition: {
return [];
}
case CompletionContextKind.LinkDefinition:
case CompletionContextKind.Link: {
const items: vscode.CompletionItem[] = [];
@@ -140,6 +138,9 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
/// [...][...|
private readonly referenceLinkStartPattern = /\[([^\]]*?)\]\[\s*([^\s\(\)]*)$/;
/// [id]: |
private readonly definitionPattern = /^\s*\[[\w\-]+\]:\s*([^\s]*)$/m;
private getPathCompletionContext(document: vscode.TextDocument, position: vscode.Position): CompletionContext | undefined {
const line = document.lineAt(position.line).text;
@@ -149,25 +150,34 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
const linkPrefixMatch = linePrefixText.match(this.linkStartPattern);
if (linkPrefixMatch) {
const prefix = linkPrefixMatch[2];
if (/^\s*[\w\d\-]+:/.test(prefix)) { // Check if this looks like a 'http:' style uri
if (this.refLooksLikeUrl(prefix)) {
return undefined;
}
const anchorMatch = prefix.match(/^(.*)#([\w\d\-]*)$/);
const suffix = lineSuffixText.match(/^[^\)\s]*/);
return {
kind: CompletionContextKind.Link,
linkPrefix: prefix,
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
linkSuffix: suffix ? suffix[0] : '',
anchorInfo: this.getAnchorContext(prefix),
};
}
anchorInfo: anchorMatch ? {
beforeAnchor: anchorMatch[1],
anchorPrefix: anchorMatch[2],
} : undefined,
const definitionLinkPrefixMatch = linePrefixText.match(this.definitionPattern);
if (definitionLinkPrefixMatch) {
const prefix = definitionLinkPrefixMatch[1];
if (this.refLooksLikeUrl(prefix)) {
return undefined;
}
const suffix = lineSuffixText.match(/^[^\s]*/);
return {
kind: CompletionContextKind.LinkDefinition,
linkPrefix: prefix,
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
linkSuffix: suffix ? suffix[0] : '',
anchorInfo: this.getAnchorContext(prefix),
};
}
@@ -179,7 +189,6 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
kind: CompletionContextKind.ReferenceLink,
linkPrefix: prefix,
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
linkSuffix: suffix ? suffix[0] : '',
};
}
@@ -187,6 +196,24 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
return undefined;
}
/**
* Check if {@param ref} looks like a 'http:' style url.
*/
private refLooksLikeUrl(prefix: string): boolean {
return /^\s*[\w\d\-]+:/.test(prefix);
}
private getAnchorContext(prefix: string): AnchorContext | undefined {
const anchorMatch = prefix.match(/^(.*)#([\w\d\-]*)$/);
if (!anchorMatch) {
return undefined;
}
return {
beforeAnchor: anchorMatch[1],
anchorPrefix: anchorMatch[2],
};
}
private *provideReferenceSuggestions(document: vscode.TextDocument, position: vscode.Position, context: CompletionContext): Iterable<vscode.CompletionItem> {
const insertionRange = new vscode.Range(context.linkTextStartPosition, position);
const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length }));

View File

@@ -121,4 +121,26 @@ suite('Markdown path completion provider', () => {
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');
});
test('Should complete headers in link definitions', async () => {
const completions = await getCompletionsAtCursor(workspaceFile('sub', 'new.md'), joinLines(
`# a B c`,
`# x y Z`,
`[ref-1]: ${CURSOR}`,
));
assert.ok(completions.some(x => x.label === '#a-b-c'), 'Has #a-b-c header completion');
assert.ok(completions.some(x => x.label === '#x-y-z'), 'Has #x-y-z header completion');
});
test('Should complete relative paths in link definitions', async () => {
const completions = await getCompletionsAtCursor(workspaceFile('new.md'), joinLines(
`# a B c`,
`[ref-1]: ${CURSOR}`,
));
assert.ok(completions.some(x => x.label === 'a.md'), 'Has a.md file completion');
assert.ok(completions.some(x => x.label === 'b.md'), 'Has b.md file completion');
assert.ok(completions.some(x => x.label === 'sub/'), 'Has sub folder completion');
});
});