Skip to content

Commit ef3f91d

Browse files
ivanwonderatscott
authored andcommitted
fix: support to show the tag info in the jsDoc (#1904)
Fixes #1902 (cherry picked from commit 9442292)
1 parent 619b5ec commit ef3f91d

File tree

6 files changed

+520
-11
lines changed

6 files changed

+520
-11
lines changed

client/src/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ export class AngularLanguageClient implements vscode.Disposable {
5959
// Don't let our output console pop open
6060
revealOutputChannelOn: lsp.RevealOutputChannelOn.Never,
6161
outputChannel: this.outputChannel,
62+
markdown: {
63+
isTrusted: true,
64+
},
6265
middleware: {
6366
provideCodeActions: async (
6467
document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext,

client/src/commands.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88

99
import * as vscode from 'vscode';
1010

11-
import {ServerOptions} from '../../common/initialize';
11+
import {OpenJsDocLinkCommand_Args, OpenJsDocLinkCommandId, ServerOptions} from '../../common/initialize';
1212

1313
import {AngularLanguageClient} from './client';
1414
import {ANGULAR_SCHEME, TcbContentProvider} from './providers';
1515

1616
/**
1717
* Represent a vscode command with an ID and an impl function `execute`.
1818
*/
19-
type Command = {
19+
type Command<T = any> = {
2020
id: string,
2121
isTextEditorCommand: false,
22-
execute(): Promise<unknown>,
22+
execute(_: T): Promise<unknown>,
2323
}|{
2424
id: string,
2525
isTextEditorCommand: true,
@@ -169,6 +169,28 @@ function goToTemplateForComponent(ngClient: AngularLanguageClient): Command {
169169
};
170170
}
171171

172+
/**
173+
* Proxy command for opening links in jsdoc comments.
174+
*
175+
* This is needed to avoid incorrectly rewriting uris.
176+
*/
177+
function openJsDocLinkCommand(): Command<OpenJsDocLinkCommand_Args> {
178+
return {
179+
id: OpenJsDocLinkCommandId,
180+
isTextEditorCommand: false,
181+
async execute(args) {
182+
return await vscode.commands.executeCommand(
183+
'vscode.open', vscode.Uri.parse(args.file), <vscode.TextDocumentShowOptions>{
184+
selection: new vscode.Range(
185+
new vscode.Position(
186+
args.position?.start.line ?? 0, args.position?.start.character ?? 0),
187+
new vscode.Position(
188+
args.position?.end.line ?? 0, args.position?.end.character ?? 0)),
189+
});
190+
},
191+
};
192+
}
193+
172194
/**
173195
* Register all supported vscode commands for the Angular extension.
174196
* @param client language client
@@ -182,6 +204,7 @@ export function registerCommands(
182204
getTemplateTcb(client, context),
183205
goToComponentWithTemplateFile(client),
184206
goToTemplateForComponent(client),
207+
openJsDocLinkCommand(),
185208
];
186209

187210
for (const command of commands) {

common/initialize.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,13 @@
99
export interface ServerOptions {
1010
logFile?: string;
1111
}
12+
13+
export interface OpenJsDocLinkCommand_Args {
14+
readonly file: string;
15+
readonly position?: {
16+
start: {character: number; line: number;},
17+
end: {character: number; line: number;},
18+
};
19+
}
20+
21+
export const OpenJsDocLinkCommandId = 'angular.openJsDocLink';

server/src/session.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {readNgCompletionData, tsCompletionEntryToLspCompletionItem} from './comp
2121
import {tsDiagnosticToLspDiagnostic} from './diagnostic';
2222
import {getHTMLVirtualContent} from './embedded_support';
2323
import {ServerHost} from './server_host';
24+
import {documentationToMarkdown} from './text_render';
2425
import {filePathToUri, getMappedDefinitionInfo, isConfiguredProject, isDebugMode, lspPositionToTsPosition, lspRangeToTsPositions, MruTracker, tsDisplayPartsToText, tsFileTextChangesToLspWorkspaceEdit, tsTextSpanToLspRange, uriToFilePath} from './utils';
2526

2627
export interface SessionOptions {
@@ -1097,7 +1098,7 @@ export class Session {
10971098
if (!info) {
10981099
return null;
10991100
}
1100-
const {kind, kindModifiers, textSpan, displayParts, documentation} = info;
1101+
const {kind, kindModifiers, textSpan, displayParts, documentation, tags} = info;
11011102
let desc = kindModifiers ? kindModifiers + ' ' : '';
11021103
if (displayParts && displayParts.length > 0) {
11031104
// displayParts does not contain info about kindModifiers
@@ -1110,11 +1111,9 @@ export class Session {
11101111
language: 'typescript',
11111112
value: desc,
11121113
}];
1113-
if (documentation) {
1114-
for (const d of documentation) {
1115-
contents.push(d.text);
1116-
}
1117-
}
1114+
const mds = documentationToMarkdown(
1115+
documentation, tags, (fileName: string) => this.getLSAndScriptInfo(fileName)?.scriptInfo);
1116+
contents.push(mds.join('\n'));
11181117
return {
11191118
contents,
11201119
range: tsTextSpanToLspRange(scriptInfo, textSpan),
@@ -1177,7 +1176,7 @@ export class Session {
11771176
return item;
11781177
}
11791178

1180-
const {kind, kindModifiers, displayParts, documentation} = details;
1179+
const {kind, kindModifiers, displayParts, documentation, tags} = details;
11811180
let desc = kindModifiers ? kindModifiers + ' ' : '';
11821181
if (displayParts && displayParts.length > 0) {
11831182
// displayParts does not contain info about kindModifiers
@@ -1187,7 +1186,12 @@ export class Session {
11871186
desc += kind;
11881187
}
11891188
item.detail = desc;
1190-
item.documentation = documentation?.map(d => d.text).join('');
1189+
item.documentation = {
1190+
kind: lsp.MarkupKind.Markdown,
1191+
value: documentationToMarkdown(
1192+
documentation, tags, (fileName) => this.getLSAndScriptInfo(fileName)?.scriptInfo)
1193+
.join('\n'),
1194+
};
11911195
return item;
11921196
}
11931197

server/src/tests/text_render_spec.ts

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as tss from 'typescript/lib/tsserverlibrary';
10+
11+
import {asPlainTextWithLinks, documentationToMarkdown, tagsToMarkdown} from '../text_render';
12+
13+
14+
describe('typescript.previewer', () => {
15+
it('Should ignore hyphens after a param tag', () => {
16+
expect(
17+
tagsToMarkdown([{name: 'param', text: [{kind: 'text', text: 'a - b'}]}], () => undefined))
18+
.toBe('*@param* `a` — b');
19+
});
20+
21+
it('Should parse url jsdoc @link', () => {
22+
expect(documentationToMarkdown(
23+
[
24+
{'text': 'x ', 'kind': 'text'}, {'text': '{@link ', 'kind': 'link'},
25+
{'text': 'http://www.example.com/foo', 'kind': 'linkText'},
26+
{'text': '}', 'kind': 'link'}, {'text': ' y ', 'kind': 'text'},
27+
{'text': '{@link ', 'kind': 'link'}, {
28+
'text': 'https://api.jquery.com/bind/#bind-eventType-eventData-handler',
29+
'kind': 'linkText'
30+
},
31+
{'text': '}', 'kind': 'link'}, {'text': ' z', 'kind': 'text'}
32+
],
33+
[], () => undefined))
34+
.toEqual([
35+
'x http://www.example.com/foo y https://api.jquery.com/bind/#bind-eventType-eventData-handler z'
36+
]);
37+
});
38+
39+
it('Should parse url jsdoc @link with text', () => {
40+
expect(documentationToMarkdown(
41+
[
42+
{'text': 'x ', 'kind': 'text'}, {'text': '{@link ', 'kind': 'link'},
43+
{'text': 'http://www.example.com/foo abc xyz', 'kind': 'linkText'},
44+
{'text': '}', 'kind': 'link'}, {'text': ' y ', 'kind': 'text'},
45+
{'text': '{@link ', 'kind': 'link'},
46+
{'text': 'http://www.example.com/bar|b a z', 'kind': 'linkText'},
47+
{'text': '}', 'kind': 'link'}, {'text': ' z', 'kind': 'text'}
48+
],
49+
[], () => undefined))
50+
.toEqual(
51+
['x [abc xyz](http://www.example.com/foo) y [a z](http://www.example.com/bar|b) z']);
52+
});
53+
54+
it('Should treat @linkcode jsdocs links as monospace', () => {
55+
expect(documentationToMarkdown(
56+
[
57+
{'text': 'x ', 'kind': 'text'}, {'text': '{@linkcode ', 'kind': 'link'},
58+
{'text': 'http://www.example.com/foo', 'kind': 'linkText'},
59+
{'text': '}', 'kind': 'link'}, {'text': ' y ', 'kind': 'text'},
60+
{'text': '{@linkplain ', 'kind': 'link'},
61+
{'text': 'http://www.example.com/bar', 'kind': 'linkText'},
62+
{'text': '}', 'kind': 'link'}, {'text': ' z', 'kind': 'text'}
63+
],
64+
[], () => undefined))
65+
.toEqual(['x http://www.example.com/foo y http://www.example.com/bar z']);
66+
});
67+
68+
it('Should parse url jsdoc @link in param tag', () => {
69+
expect(
70+
tagsToMarkdown(
71+
[{
72+
name: 'param',
73+
text: [{
74+
kind: 'text',
75+
text:
76+
'a x {@link http://www.example.com/foo abc xyz} y {@link http://www.example.com/bar|b a z} z'
77+
}]
78+
79+
}],
80+
() => undefined))
81+
.toBe(
82+
'*@param* `a` — x [abc xyz](http://www.example.com/foo) y [b a z](http://www.example.com/bar) z');
83+
});
84+
85+
it('Should ignore unclosed jsdocs @link', () => {
86+
expect(documentationToMarkdown(
87+
[
88+
{'text': 'x ', 'kind': 'text'}, {'text': '{@link ', 'kind': 'link'}, {
89+
'text': 'http://www.example.com/foo y {@link http://www.example.com/bar bar',
90+
'kind': 'linkText'
91+
},
92+
{'text': '}', 'kind': 'link'}, {'text': ' z', 'kind': 'text'}
93+
],
94+
[], () => undefined))
95+
.toEqual(['x [y {@link http://www.example.com/bar bar](http://www.example.com/foo) z']);
96+
});
97+
98+
it('Should support non-ascii characters in parameter name (#90108)', () => {
99+
expect(
100+
tagsToMarkdown(
101+
[
102+
{name: 'param', text: [{kind: 'text', text: 'parámetroConDiacríticos this will not'}]}
103+
],
104+
() => undefined))
105+
.toBe('*@param* `parámetroConDiacríticos` — this will not');
106+
});
107+
108+
it('Should render @example blocks as code', () => {
109+
expect(tagsToMarkdown(
110+
[{name: 'example', text: [{kind: 'text', text: 'code();'}]}], () => undefined))
111+
.toBe('*@example* \n```\ncode();\n```');
112+
});
113+
114+
it('Should not render @example blocks as code as if they contain a codeblock', () => {
115+
expect(tagsToMarkdown(
116+
[{name: 'example', text: [{kind: 'text', text: 'Not code\n```\ncode();\n```'}]}],
117+
() => undefined))
118+
.toBe('*@example* \nNot code\n```\ncode();\n```');
119+
});
120+
121+
it('Should render @example blocks as code if they contain a <caption>', () => {
122+
expect(tagsToMarkdown(
123+
[{
124+
name: 'example',
125+
text: [{
126+
kind: 'text',
127+
text: '<caption>Not code</caption>\ncode();',
128+
}]
129+
}],
130+
() => undefined))
131+
.toBe('*@example* \nNot code\n```\ncode();\n```');
132+
});
133+
134+
it('Should not render @example blocks as code if they contain a <caption> and a codeblock',
135+
() => {
136+
expect(tagsToMarkdown(
137+
[{
138+
name: 'example',
139+
text: [{kind: 'text', text: '<caption>Not code</caption>\n```\ncode();\n```'}]
140+
}],
141+
() => undefined))
142+
.toBe('*@example* \nNot code\n```\ncode();\n```');
143+
});
144+
145+
it('Should not render @link inside of @example #187768', () => {
146+
expect(tagsToMarkdown(
147+
[{
148+
'name': 'example',
149+
'text': [
150+
{'text': '1 + 1 ', 'kind': 'text'}, {'text': '{@link ', 'kind': 'link'},
151+
{'text': 'foo', 'kind': 'linkName'}, {'text': '}', 'kind': 'link'}
152+
]
153+
}],
154+
() => undefined))
155+
.toBe('*@example* \n```\n1 + 1 {@link foo}\n```');
156+
});
157+
158+
it('Should render @linkcode symbol name as code', () => {
159+
expect(asPlainTextWithLinks(
160+
[
161+
{'text': 'a ', 'kind': 'text'}, {'text': '{@linkcode ', 'kind': 'link'}, {
162+
'text': 'dog',
163+
'kind': 'linkName',
164+
'target': {
165+
'fileName': '/path/file.ts',
166+
'start': {'line': 7, 'offset': 5},
167+
'end': {'line': 7, 'offset': 13}
168+
}
169+
} as tss.SymbolDisplayPart,
170+
{'text': '}', 'kind': 'link'}, {'text': ' b', 'kind': 'text'}
171+
],
172+
() => undefined))
173+
.toBe(
174+
'a [`dog`](command:angular.openJsDocLink?%7B%22file%22%3A%22%2Fpath%2Ffile.ts%22%7D) b');
175+
});
176+
177+
it('Should render @linkcode text as code', () => {
178+
expect(asPlainTextWithLinks(
179+
[
180+
{'text': 'a ', 'kind': 'text'}, {'text': '{@linkcode ', 'kind': 'link'}, {
181+
'text': 'dog',
182+
'kind': 'linkName',
183+
'target': {
184+
'fileName': '/path/file.ts',
185+
'start': {'line': 7, 'offset': 5},
186+
'end': {'line': 7, 'offset': 13}
187+
}
188+
} as tss.SymbolDisplayPart,
189+
{'text': 'husky', 'kind': 'linkText'}, {'text': '}', 'kind': 'link'},
190+
{'text': ' b', 'kind': 'text'}
191+
],
192+
() => undefined))
193+
.toBe(
194+
'a [`husky`](command:angular.openJsDocLink?%7B%22file%22%3A%22%2Fpath%2Ffile.ts%22%7D) b');
195+
});
196+
});

0 commit comments

Comments
 (0)