Skip to content

Commit 55e72d8

Browse files
committed
Add support for rendering jsdoc inline @link tags
Fixes microsoft#28624
1 parent 8fd777f commit 55e72d8

3 files changed

Lines changed: 66 additions & 11 deletions

File tree

extensions/typescript-language-features/src/features/hover.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as vscode from 'vscode';
77
import type * as Proto from '../protocol';
88
import { ITypeScriptServiceClient } from '../typescriptService';
9-
import { tagsMarkdownPreview } from '../utils/previewer';
9+
import { markdownDocumentation } from '../utils/previewer';
1010
import * as typeConverters from '../utils/typeConverters';
1111

1212

@@ -45,9 +45,7 @@ class TypeScriptHoverProvider implements vscode.HoverProvider {
4545
if (data.displayString) {
4646
parts.push({ language: 'typescript', value: data.displayString });
4747
}
48-
49-
const tags = tagsMarkdownPreview(data.tags);
50-
parts.push(data.documentation + (tags ? '\n\n' + tags : ''));
48+
parts.push(markdownDocumentation(data.documentation, data.tags));
5149
return parts;
5250
}
5351
}

extensions/typescript-language-features/src/test/previewer.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as assert from 'assert';
77
import 'mocha';
8-
import { tagsMarkdownPreview } from '../utils/previewer';
8+
import { tagsMarkdownPreview, markdownDocumentation } from '../utils/previewer';
99

1010
suite('typescript.previewer', () => {
1111
test('Should ignore hyphens after a param tag', async () => {
@@ -18,5 +18,41 @@ suite('typescript.previewer', () => {
1818
]),
1919
'*@param* `a` — b');
2020
});
21+
22+
test('Should parse url jsdoc @link', async () => {
23+
assert.strictEqual(
24+
markdownDocumentation('x {@link http://www.example.com/foo} y {@link https://api.jquery.com/bind/#bind-eventType-eventData-handler} z', []).value,
25+
'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');
26+
});
27+
28+
test('Should parse url jsdoc @link with text', async () => {
29+
assert.strictEqual(
30+
markdownDocumentation('x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z', []).value,
31+
'x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z');
32+
});
33+
34+
test('Should treat @linkcode jsdocs links as monospace', async () => {
35+
assert.strictEqual(
36+
markdownDocumentation('x {@linkcode http://www.example.com/foo} y {@linkplain http://www.example.com/bar} z', []).value,
37+
'x [`http://www.example.com/foo`](http://www.example.com/foo) y [http://www.example.com/bar](http://www.example.com/bar) z');
38+
});
39+
40+
test('Should parse url jsdoc @link in param tag', async () => {
41+
assert.strictEqual(
42+
tagsMarkdownPreview([
43+
{
44+
name: 'param',
45+
text: 'a x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z'
46+
}
47+
]),
48+
'*@param* `a` — x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z');
49+
});
50+
51+
test('Should ignore unclosed jsdocs @link', async () => {
52+
assert.strictEqual(
53+
markdownDocumentation('x {@link http://www.example.com/foo y {@link http://www.example.com/bar bar} z', []).value,
54+
'x {@link http://www.example.com/foo y [bar](http://www.example.com/bar) z');
55+
});
56+
2157
});
2258

extensions/typescript-language-features/src/utils/previewer.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66
import * as vscode from 'vscode';
77
import type * as Proto from '../protocol';
88

9+
function replaceLinks(text: string): string {
10+
return text
11+
// Http(s) links
12+
.replace(/\{@(link|linkplain|linkcode) (https?:\/\/[^ |}]+?)(?:[| ]([^{}\n]+?))?\}/gi, (_, tag: string, link: string, text?: string) => {
13+
switch (tag) {
14+
case 'linkcode':
15+
return `[\`${text ? text.trim() : link}\`](${link})`;
16+
17+
default:
18+
return `[${text ? text.trim() : link}](${link})`;
19+
}
20+
});
21+
}
22+
23+
function processInlineTags(text: string): string {
24+
return replaceLinks(text);
25+
}
26+
927
function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined {
1028
if (!tag.text) {
1129
return undefined;
@@ -41,7 +59,7 @@ function getTagBodyText(tag: Proto.JSDocTagInfo): string | undefined {
4159
return makeCodeblock(tag.text);
4260
}
4361

44-
return tag.text;
62+
return processInlineTags(tag.text);
4563
}
4664

4765
function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined {
@@ -58,7 +76,7 @@ function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined {
5876
if (!doc) {
5977
return label;
6078
}
61-
return label + (doc.match(/\r\n|\n/g) ? ' \n' + doc : ` — ${doc}`);
79+
return label + (doc.match(/\r\n|\n/g) ? ' \n' + processInlineTags(doc) : ` — ${processInlineTags(doc)}`);
6280
}
6381
}
6482

@@ -71,16 +89,19 @@ function getTagDocumentation(tag: Proto.JSDocTagInfo): string | undefined {
7189
return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`);
7290
}
7391

74-
export function plain(parts: Proto.SymbolDisplayPart[]): string {
75-
return parts.map(part => part.text).join('');
92+
export function plain(parts: Proto.SymbolDisplayPart[] | string): string {
93+
return processInlineTags(
94+
typeof parts === 'string'
95+
? parts
96+
: parts.map(part => part.text).join(''));
7697
}
7798

7899
export function tagsMarkdownPreview(tags: Proto.JSDocTagInfo[]): string {
79100
return tags.map(getTagDocumentation).join(' \n\n');
80101
}
81102

82103
export function markdownDocumentation(
83-
documentation: Proto.SymbolDisplayPart[],
104+
documentation: Proto.SymbolDisplayPart[] | string,
84105
tags: Proto.JSDocTagInfo[]
85106
): vscode.MarkdownString {
86107
const out = new vscode.MarkdownString();
@@ -90,7 +111,7 @@ export function markdownDocumentation(
90111

91112
export function addMarkdownDocumentation(
92113
out: vscode.MarkdownString,
93-
documentation: Proto.SymbolDisplayPart[] | undefined,
114+
documentation: Proto.SymbolDisplayPart[] | string | undefined,
94115
tags: Proto.JSDocTagInfo[] | undefined
95116
): vscode.MarkdownString {
96117
if (documentation) {

0 commit comments

Comments
 (0)