diff --git a/extensions/typescript-language-features/src/features/hover.ts b/extensions/typescript-language-features/src/features/hover.ts index ecc331d08b7..2ff92d6e308 100644 --- a/extensions/typescript-language-features/src/features/hover.ts +++ b/extensions/typescript-language-features/src/features/hover.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; -import { tagsMarkdownPreview } from '../utils/previewer'; +import { markdownDocumentation } from '../utils/previewer'; import * as typeConverters from '../utils/typeConverters'; @@ -45,9 +45,7 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { if (data.displayString) { parts.push({ language: 'typescript', value: data.displayString }); } - - const tags = tagsMarkdownPreview(data.tags); - parts.push(data.documentation + (tags ? '\n\n' + tags : '')); + parts.push(markdownDocumentation(data.documentation, data.tags)); return parts; } } diff --git a/extensions/typescript-language-features/src/test/previewer.test.ts b/extensions/typescript-language-features/src/test/previewer.test.ts index 011187b2af4..abcdace9f60 100644 --- a/extensions/typescript-language-features/src/test/previewer.test.ts +++ b/extensions/typescript-language-features/src/test/previewer.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { tagsMarkdownPreview } from '../utils/previewer'; +import { tagsMarkdownPreview, markdownDocumentation } from '../utils/previewer'; suite('typescript.previewer', () => { test('Should ignore hyphens after a param tag', async () => { @@ -18,5 +18,41 @@ suite('typescript.previewer', () => { ]), '*@param* `a` — b'); }); + + test('Should parse url jsdoc @link', async () => { + assert.strictEqual( + markdownDocumentation('x {@link http://www.example.com/foo} y {@link https://api.jquery.com/bind/#bind-eventType-eventData-handler} z', []).value, + 'x [http://www.example.com/foo](http://www.example.com/foo) y [https://api.jquery.com/bind/#bind-eventType-eventData-handler](https://api.jquery.com/bind/#bind-eventType-eventData-handler) z'); + }); + + test('Should parse url jsdoc @link with text', async () => { + assert.strictEqual( + markdownDocumentation('x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z', []).value, + 'x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z'); + }); + + test('Should treat @linkcode jsdocs links as monospace', async () => { + assert.strictEqual( + markdownDocumentation('x {@linkcode http://www.example.com/foo} y {@linkplain http://www.example.com/bar} z', []).value, + 'x [`http://www.example.com/foo`](http://www.example.com/foo) y [http://www.example.com/bar](http://www.example.com/bar) z'); + }); + + test('Should parse url jsdoc @link in param tag', async () => { + assert.strictEqual( + tagsMarkdownPreview([ + { + name: 'param', + text: 'a x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z' + } + ]), + '*@param* `a` — x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z'); + }); + + test('Should ignore unclosed jsdocs @link', async () => { + assert.strictEqual( + markdownDocumentation('x {@link http://www.example.com/foo y {@link http://www.example.com/bar bar} z', []).value, + 'x {@link http://www.example.com/foo y [bar](http://www.example.com/bar) z'); + }); + }); diff --git a/extensions/typescript-language-features/src/utils/previewer.ts b/extensions/typescript-language-features/src/utils/previewer.ts index e93fe83094e..f29d03189ff 100644 --- a/extensions/typescript-language-features/src/utils/previewer.ts +++ b/extensions/typescript-language-features/src/utils/previewer.ts @@ -6,6 +6,24 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; +function replaceLinks(text: string): string { + return text + // Http(s) links + .replace(/\{@(link|linkplain|linkcode) (https?:\/\/[^ |}]+?)(?:[| ]([^{}\n]+?))?\}/gi, (_, tag: string, link: string, text?: string) => { + switch (tag) { + case 'linkcode': + return `[\`${text ? text.trim() : link}\`](${link})`; + + default: + return `[${text ? text.trim() : link}](${link})`; + } + }); +} + +function processInlineTags(text: string): string { + return replaceLinks(text); +} + function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined { if (!tag.text) { return undefined; @@ -41,7 +59,7 @@ function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined { return makeCodeblock(tag.text); } - return tag.text; + return processInlineTags(tag.text); } function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { @@ -58,7 +76,7 @@ function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { if (!doc) { return label; } - return label + (doc.match(/\r\n|\n/g) ? ' \n' + doc : ` — ${doc}`); + return label + (doc.match(/\r\n|\n/g) ? ' \n' + processInlineTags(doc) : ` — ${processInlineTags(doc)}`); } } @@ -71,8 +89,11 @@ function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined { return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`); } -export function plain(parts: Proto.SymbolDisplayPart[]): string { - return parts.map(part => part.text).join(''); +export function plain(parts: Proto.SymbolDisplayPart[] | string): string { + return processInlineTags( + typeof parts === 'string' + ? parts + : parts.map(part => part.text).join('')); } export function tagsMarkdownPreview(tags: Proto.JSDocTagInfo[]): string { @@ -80,7 +101,7 @@ export function tagsMarkdownPreview(tags: Proto.JSDocTagInfo[]): string { } export function markdownDocumentation( - documentation: Proto.SymbolDisplayPart[], + documentation: Proto.SymbolDisplayPart[] | string, tags: Proto.JSDocTagInfo[] ): vscode.MarkdownString { const out = new vscode.MarkdownString(); @@ -90,7 +111,7 @@ export function markdownDocumentation( export function addMarkdownDocumentation( out: vscode.MarkdownString, - documentation: Proto.SymbolDisplayPart[] | undefined, + documentation: Proto.SymbolDisplayPart[] | string | undefined, tags: Proto.JSDocTagInfo[] | undefined ): vscode.MarkdownString { if (documentation) {