Skip to content

Commit ec14807

Browse files
authored
feat: add support for signature help (#1277)
This commit adds support for signature help to the LS extension, by translating from the TS `getSignatureHelpItems` API into LSP.
1 parent 9e08bc2 commit ec14807

File tree

6 files changed

+132
-8
lines changed

6 files changed

+132
-8
lines changed

client/src/client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ export class AngularLanguageClient implements vscode.Disposable {
9595
await Promise.all([angularResultsPromise, htmlProviderResultsPromise]);
9696
return angularResults ?? htmlProviderResults?.[0];
9797
},
98+
provideSignatureHelp: async (
99+
document: vscode.TextDocument, position: vscode.Position,
100+
context: vscode.SignatureHelpContext, token: vscode.CancellationToken,
101+
next: lsp.ProvideSignatureHelpSignature) => {
102+
if (await this.isInAngularProject(document) &&
103+
isInsideInlineTemplateRegion(document, position)) {
104+
return next(document, position, context, token);
105+
}
106+
},
98107
provideCompletionItem: async (
99108
document: vscode.TextDocument, position: vscode.Position,
100109
context: vscode.CompletionContext, token: vscode.CancellationToken,

integration/lsp/ivy_spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,61 @@ describe('Angular Ivy language server', () => {
155155
});
156156
});
157157

158+
describe('signature help', () => {
159+
it('should show signature help for an empty call', async () => {
160+
client.sendNotification(lsp.DidOpenTextDocumentNotification.type, {
161+
textDocument: {
162+
uri: `file://${FOO_TEMPLATE}`,
163+
languageId: 'html',
164+
version: 1,
165+
text: `{{ title.toString() }}`,
166+
},
167+
});
168+
const languageServiceEnabled = await waitForNgcc(client);
169+
expect(languageServiceEnabled).toBeTrue();
170+
const response = (await client.sendRequest(lsp.SignatureHelpRequest.type, {
171+
textDocument: {
172+
uri: `file://${FOO_TEMPLATE}`,
173+
},
174+
position: {line: 0, character: 18},
175+
}))!;
176+
expect(response).not.toBeNull();
177+
expect(response.signatures.length).toEqual(1);
178+
expect(response.signatures[0].label).toEqual('toString(): string');
179+
});
180+
181+
it('should show signature help with multiple arguments', async () => {
182+
client.sendNotification(lsp.DidOpenTextDocumentNotification.type, {
183+
textDocument: {
184+
uri: `file://${FOO_TEMPLATE}`,
185+
languageId: 'html',
186+
version: 1,
187+
text: `{{ title.substr(0, ) }}`,
188+
},
189+
});
190+
const languageServiceEnabled = await waitForNgcc(client);
191+
expect(languageServiceEnabled).toBeTrue();
192+
const response = (await client.sendRequest(lsp.SignatureHelpRequest.type, {
193+
textDocument: {
194+
uri: `file://${FOO_TEMPLATE}`,
195+
},
196+
position: {line: 0, character: 19},
197+
}))!;
198+
expect(response).not.toBeNull();
199+
expect(response.signatures.length).toEqual(1);
200+
expect(response.signatures[0].label).toEqual('substr(from: number, length?: number): string');
201+
expect(response.signatures[0].parameters).not.toBeUndefined();
202+
expect(response.activeParameter).toBe(1);
203+
204+
const label = response.signatures[0].label;
205+
const paramLabels = response.signatures[0].parameters!.map(param => {
206+
const [start, end] = param.label as [number, number];
207+
return label.substring(start, end);
208+
});
209+
expect(paramLabels).toEqual(['from: number', 'length?: number']);
210+
});
211+
});
212+
158213
describe('project reload', () => {
159214
const dummy = `${PROJECT_PATH}/node_modules/__foo__`;
160215

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
"test:syntaxes": "yarn compile:syntaxes-test && yarn build:syntaxes && jasmine dist/syntaxes/test/driver.js"
168168
},
169169
"dependencies": {
170-
"@angular/language-service": "12.0.0-next.8",
170+
"@angular/language-service": "12.0.0-next.9",
171171
"typescript": "4.2.4",
172172
"vscode-jsonrpc": "6.0.0",
173173
"vscode-languageclient": "7.0.0",

server/src/session.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {readNgCompletionData, tsCompletionEntryToLspCompletionItem} from './comp
2121
import {tsDiagnosticToLspDiagnostic} from './diagnostic';
2222
import {resolveAndRunNgcc} from './ngcc';
2323
import {ServerHost} from './server_host';
24-
import {filePathToUri, isConfiguredProject, isDebugMode, lspPositionToTsPosition, lspRangeToTsPositions, MruTracker, tsTextSpanToLspRange, uriToFilePath} from './utils';
24+
import {filePathToUri, isConfiguredProject, isDebugMode, lspPositionToTsPosition, lspRangeToTsPositions, MruTracker, tsDisplayPartsToText, tsTextSpanToLspRange, uriToFilePath} from './utils';
2525
import {resolve, Version} from './version_provider';
2626

2727
export interface SessionOptions {
@@ -168,6 +168,7 @@ export class Session {
168168
conn.onRequest(IsInAngularProject, p => this.isInAngularProject(p));
169169
conn.onCodeLens(p => this.onCodeLens(p));
170170
conn.onCodeLensResolve(p => this.onCodeLensResolve(p));
171+
conn.onSignatureHelp(p => this.onSignatureHelp(p));
171172
}
172173

173174
private isInAngularProject(params: IsInAngularProjectParams): boolean {
@@ -232,6 +233,57 @@ export class Session {
232233
return results;
233234
}
234235

236+
private onSignatureHelp(params: lsp.SignatureHelpParams): lsp.SignatureHelp|undefined {
237+
const lsInfo = this.getLSAndScriptInfo(params.textDocument);
238+
if (lsInfo === undefined) {
239+
return undefined;
240+
}
241+
242+
const {languageService, scriptInfo} = lsInfo;
243+
const offset = lspPositionToTsPosition(scriptInfo, params.position);
244+
245+
const help = languageService.getSignatureHelpItems(scriptInfo.fileName, offset, undefined);
246+
if (help === undefined) {
247+
return undefined;
248+
}
249+
250+
return {
251+
activeParameter: help.argumentCount > 0 ? help.argumentIndex : null,
252+
activeSignature: help.selectedItemIndex,
253+
signatures: help.items.map((item: ts.SignatureHelpItem): lsp.SignatureInformation => {
254+
// For each signature, build up a 'label' which represents the full signature text, as well
255+
// as a parameter list where each parameter label is a span within the signature label.
256+
let label = tsDisplayPartsToText(item.prefixDisplayParts);
257+
const parameters: lsp.ParameterInformation[] = [];
258+
let first = true;
259+
for (const param of item.parameters) {
260+
if (!first) {
261+
label += tsDisplayPartsToText(item.separatorDisplayParts);
262+
}
263+
first = false;
264+
265+
// Add the parameter to the label, keeping track of its start and end positions.
266+
const start = label.length;
267+
label += tsDisplayPartsToText(param.displayParts);
268+
const end = label.length;
269+
270+
// The parameter itself uses a range within the signature label as its own label.
271+
parameters.push({
272+
label: [start, end],
273+
documentation: tsDisplayPartsToText(param.documentation),
274+
});
275+
}
276+
277+
label += tsDisplayPartsToText(item.suffixDisplayParts);
278+
return {
279+
label,
280+
documentation: tsDisplayPartsToText(item.documentation),
281+
parameters,
282+
};
283+
}),
284+
};
285+
}
286+
235287
private onCodeLens(params: lsp.CodeLensParams): lsp.CodeLens[]|undefined {
236288
if (!params.textDocument.uri.endsWith('.html')) {
237289
return undefined;
@@ -526,6 +578,11 @@ export class Session {
526578
} :
527579
false,
528580
hoverProvider: true,
581+
signatureHelpProvider: this.ivy ? {
582+
triggerCharacters: ['(', ','],
583+
retriggerCharacters: [','],
584+
} :
585+
undefined,
529586
workspace: {
530587
workspaceFolders: {supported: true},
531588
},

server/src/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,8 @@ export class MruTracker {
109109
// we reverse the result.
110110
return [...this.set].reverse();
111111
}
112-
}
112+
}
113+
114+
export function tsDisplayPartsToText(parts: ts.SymbolDisplayPart[]): string {
115+
return parts.map(dp => dp.text).join('');
116+
}

yarn.lock

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
"@angular/dev-infra-private@https://github.com/angular/dev-infra-private-builds.git#a2a81f02228a86bcd2714accd16a52eac31714f5":
2121
version "0.0.0"
22-
uid a2a81f02228a86bcd2714accd16a52eac31714f5
2322
resolved "https://github.com/angular/dev-infra-private-builds.git#a2a81f02228a86bcd2714accd16a52eac31714f5"
2423
dependencies:
2524
"@angular/benchpress" "0.2.1"
@@ -52,10 +51,10 @@
5251
yaml "^1.10.0"
5352
yargs "^16.2.0"
5453

55-
"@angular/[email protected].8":
56-
version "12.0.0-next.8"
57-
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-12.0.0-next.8.tgz#3b589cec2b63e0e4512acd5ff47283fc0558254b"
58-
integrity sha512-IN5LB26LonNr6aafm9lEHJwkeruLUegFTWO4L10gUzUtyBQ7FGX+GqrX6VF1ePUeUeYkjhXbE++2Ya0jFsSBEg==
54+
"@angular/[email protected].9":
55+
version "12.0.0-next.9"
56+
resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-12.0.0-next.9.tgz#7fa62e9c4c78841fac7b53fd10672f31cd19161b"
57+
integrity sha512-LOUwEF2v5zG+9+hcgt2Lb3GcfPAojHsqV47sH5P0GssErgfRqFVqK5ZnHt4Kj0SmAEUXJ4e/1ZjGpP35CWFq/A==
5958

6059
"@babel/code-frame@^7.0.0":
6160
version "7.12.13"

0 commit comments

Comments
 (0)