diff --git a/extensions/markdown-language-features/src/features/pathCompletions.ts b/extensions/markdown-language-features/src/features/pathCompletions.ts index 3ff2dd945c9..001a69a5bfc 100644 --- a/extensions/markdown-language-features/src/features/pathCompletions.ts +++ b/extensions/markdown-language-features/src/features/pathCompletions.ts @@ -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 { const insertionRange = new vscode.Range(context.linkTextStartPosition, position); const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length })); diff --git a/extensions/markdown-language-features/src/test/pathCompletion.test.ts b/extensions/markdown-language-features/src/test/pathCompletion.test.ts index 6f7365116ab..529ef06fd7d 100644 --- a/extensions/markdown-language-features/src/test/pathCompletion.test.ts +++ b/extensions/markdown-language-features/src/test/pathCompletion.test.ts @@ -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'); + }); });