Skip to content

Commit 2dfb6e8

Browse files
authored
(feat) typescript signature help (#653)
* typescript signature help * test * credit * config * tweak
1 parent 09dd498 commit 2dfb6e8

File tree

10 files changed

+347
-11
lines changed

10 files changed

+347
-11
lines changed

packages/language-server/src/ls-config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const defaultLSConfig: LSConfig = {
1414
documentSymbols: { enable: true },
1515
codeActions: { enable: true },
1616
rename: { enable: true },
17-
selectionRange: { enable: true }
17+
selectionRange: { enable: true },
18+
signatureHelp: { enable: true }
1819
},
1920
css: {
2021
enable: true,
@@ -87,6 +88,9 @@ export interface LSTypescriptConfig {
8788
selectionRange: {
8889
enable: boolean;
8990
};
91+
signatureHelp: {
92+
enable: boolean;
93+
}
9094
}
9195

9296
export interface LSCSSConfig {

packages/language-server/src/plugins/PluginHost.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import {
2020
FormattingOptions,
2121
ReferenceContext,
2222
Location,
23-
SelectionRange
23+
SelectionRange,
24+
SignatureHelp,
25+
SignatureHelpContext
2426
} from 'vscode-languageserver';
2527
import { LSConfig, LSConfigManager } from '../ls-config';
2628
import { DocumentManager } from '../lib/documents';
@@ -347,6 +349,23 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
347349
);
348350
}
349351

352+
async getSignatureHelp(
353+
textDocument: TextDocumentIdentifier,
354+
position: Position,
355+
context: SignatureHelpContext | undefined
356+
): Promise<SignatureHelp | null> {
357+
const document = this.getDocument(textDocument.uri);
358+
if (!document) {
359+
throw new Error('Cannot call methods on an unopened document');
360+
}
361+
362+
return await this.execute<any>(
363+
'getSignatureHelp',
364+
[document, position, context],
365+
ExecuteMode.FirstNonNull
366+
);
367+
}
368+
350369
/**
351370
* The selection range supports multiple cursors,
352371
* each position should return its own selection range tree like `Array.map`.

packages/language-server/src/plugins/interfaces.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CompletionContext, FileChangeType } from 'vscode-languageserver';
1+
import { CompletionContext, FileChangeType, SignatureHelpContext } from 'vscode-languageserver';
22
import {
33
CodeAction,
44
CodeActionContext,
@@ -19,7 +19,8 @@ import {
1919
TextDocumentIdentifier,
2020
TextEdit,
2121
WorkspaceEdit,
22-
SelectionRange
22+
SelectionRange,
23+
SignatureHelp
2324
} from 'vscode-languageserver-types';
2425
import { Document } from '../lib/documents';
2526

@@ -128,6 +129,14 @@ export interface FindReferencesProvider {
128129
): Promise<Location[] | null>;
129130
}
130131

132+
export interface SignatureHelpProvider {
133+
getSignatureHelp(
134+
document: Document,
135+
position: Position,
136+
context: SignatureHelpContext | undefined
137+
): Resolvable<SignatureHelp | null>
138+
}
139+
131140
export interface SelectionRangeProvider {
132141
getSelectionRange(document: Document, position: Position): Resolvable<SelectionRange | null>;
133142
}
@@ -152,7 +161,8 @@ type ProviderBase = DiagnosticsProvider &
152161
UpdateImportsProvider &
153162
CodeActionsProvider &
154163
FindReferencesProvider &
155-
RenameProvider;
164+
RenameProvider &
165+
SignatureHelpProvider;
156166

157167
export type LSProvider = ProviderBase & BackwardsCompatibleDefinitionsProvider;
158168

packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
SymbolInformation,
1616
WorkspaceEdit,
1717
CompletionList,
18-
SelectionRange
18+
SelectionRange,
19+
SignatureHelp,
20+
SignatureHelpContext
1921
} from 'vscode-languageserver';
2022
import {
2123
Document,
@@ -39,6 +41,7 @@ import {
3941
OnWatchFileChanges,
4042
RenameProvider,
4143
SelectionRangeProvider,
44+
SignatureHelpProvider,
4245
UpdateImportsProvider,
4346
OnWatchFileChangesPara
4447
} from '../interfaces';
@@ -57,6 +60,7 @@ import { convertToLocationRange, getScriptKindFromFileName, symbolKindFromString
5760
import { getDirectiveCommentCompletions } from './features/getDirectiveCommentCompletions';
5861
import { FindReferencesProviderImpl } from './features/FindReferencesProvider';
5962
import { SelectionRangeProviderImpl } from './features/SelectionRangeProvider';
63+
import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider';
6064
import { SnapshotManager } from './SnapshotManager';
6165

6266
export class TypeScriptPlugin
@@ -70,6 +74,7 @@ export class TypeScriptPlugin
7074
RenameProvider,
7175
FindReferencesProvider,
7276
SelectionRangeProvider,
77+
SignatureHelpProvider,
7378
OnWatchFileChanges,
7479
CompletionsProvider<CompletionEntryWithIdentifer> {
7580
private readonly configManager: LSConfigManager;
@@ -82,6 +87,7 @@ export class TypeScriptPlugin
8287
private readonly hoverProvider: HoverProviderImpl;
8388
private readonly findReferencesProvider: FindReferencesProviderImpl;
8489
private readonly selectionRangeProvider: SelectionRangeProviderImpl;
90+
private readonly signatureHelpProvider: SignatureHelpProviderImpl;
8591

8692
constructor(
8793
docManager: DocumentManager,
@@ -101,6 +107,7 @@ export class TypeScriptPlugin
101107
this.hoverProvider = new HoverProviderImpl(this.lsAndTsDocResolver);
102108
this.findReferencesProvider = new FindReferencesProviderImpl(this.lsAndTsDocResolver);
103109
this.selectionRangeProvider = new SelectionRangeProviderImpl(this.lsAndTsDocResolver);
110+
this.signatureHelpProvider = new SignatureHelpProviderImpl(this.lsAndTsDocResolver);
104111
}
105112

106113
async getDiagnostics(document: Document): Promise<Diagnostic[]> {
@@ -380,6 +387,16 @@ export class TypeScriptPlugin
380387
return this.selectionRangeProvider.getSelectionRange(document, position);
381388
}
382389

390+
async getSignatureHelp(
391+
document: Document, position: Position, context: SignatureHelpContext | undefined
392+
): Promise<SignatureHelp | null> {
393+
if (!this.featureEnabled('signatureHelp')) {
394+
return null;
395+
}
396+
397+
return this.signatureHelpProvider.getSignatureHelp(document, position, context);
398+
}
399+
383400
private getLSAndTSDoc(document: Document) {
384401
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
385402
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import ts from 'typescript';
2+
import {
3+
Position,
4+
SignatureHelpContext,
5+
SignatureHelp,
6+
SignatureHelpTriggerKind,
7+
SignatureInformation,
8+
ParameterInformation,
9+
MarkupKind
10+
} from 'vscode-languageserver';
11+
import { SignatureHelpProvider } from '../..';
12+
import { Document } from '../../../lib/documents';
13+
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
14+
import { getMarkdownDocumentation } from '../previewer';
15+
16+
export class SignatureHelpProviderImpl implements SignatureHelpProvider {
17+
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) { }
18+
19+
private static readonly triggerCharacters = ['(', ',', '<'];
20+
private static readonly retriggerCharacters = [')'];
21+
22+
async getSignatureHelp(
23+
document: Document,
24+
position: Position,
25+
context: SignatureHelpContext | undefined
26+
): Promise<SignatureHelp | null> {
27+
const { lang, tsDoc } = this.lsAndTsDocResolver.getLSAndTSDoc(document);
28+
const fragment = await tsDoc.getFragment();
29+
30+
const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
31+
const triggerReason = this.toTsTriggerReason(context);
32+
const info = lang.getSignatureHelpItems(
33+
tsDoc.filePath,
34+
offset,
35+
triggerReason ? { triggerReason } : undefined
36+
);
37+
if (
38+
!info ||
39+
info.items.some((signature) => this.isInSvelte2tsxGeneratedFunction(signature))
40+
) {
41+
return null;
42+
}
43+
44+
const signatures = info.items
45+
.map(this.toSignatureHelpInformation);
46+
47+
return {
48+
signatures,
49+
activeSignature: info.selectedItemIndex,
50+
activeParameter: info.argumentIndex
51+
};
52+
}
53+
54+
private isReTrigger(
55+
isRetrigger: boolean,
56+
triggerCharacter: string
57+
): triggerCharacter is ts.SignatureHelpRetriggerCharacter {
58+
return (
59+
isRetrigger &&
60+
(this.isTriggerCharacter(triggerCharacter) ||
61+
SignatureHelpProviderImpl.retriggerCharacters.includes(triggerCharacter))
62+
);
63+
}
64+
65+
private isTriggerCharacter(
66+
triggerCharacter: string
67+
): triggerCharacter is ts.SignatureHelpTriggerCharacter {
68+
return SignatureHelpProviderImpl.triggerCharacters.includes(triggerCharacter);
69+
}
70+
71+
/**
72+
* adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L103
73+
*/
74+
private toTsTriggerReason(
75+
context: SignatureHelpContext | undefined
76+
): ts.SignatureHelpTriggerReason {
77+
switch (context?.triggerKind) {
78+
case SignatureHelpTriggerKind.TriggerCharacter:
79+
if (context.triggerCharacter) {
80+
if (this.isReTrigger(context.isRetrigger, context.triggerCharacter)) {
81+
return { kind: 'retrigger', triggerCharacter: context.triggerCharacter };
82+
}
83+
if (this.isTriggerCharacter(context.triggerCharacter)) {
84+
return {
85+
kind: 'characterTyped',
86+
triggerCharacter: context.triggerCharacter
87+
};
88+
}
89+
}
90+
return { kind: 'invoked' };
91+
case SignatureHelpTriggerKind.ContentChange:
92+
return context.isRetrigger ? { kind: 'retrigger' } : { kind: 'invoked' };
93+
94+
case SignatureHelpTriggerKind.Invoked:
95+
default:
96+
return { kind: 'invoked' };
97+
}
98+
}
99+
100+
/**
101+
* adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L73
102+
*/
103+
private toSignatureHelpInformation(item: ts.SignatureHelpItem): SignatureInformation {
104+
const [prefixLabel, separatorLabel, suffixLabel] = [
105+
item.prefixDisplayParts,
106+
item.separatorDisplayParts,
107+
item.suffixDisplayParts
108+
].map(ts.displayPartsToString);
109+
110+
let textIndex = prefixLabel.length;
111+
let signatureLabel = '';
112+
const parameters: ParameterInformation[] = [];
113+
const lastIndex = item.parameters.length - 1;
114+
115+
item.parameters.forEach((parameter, index) => {
116+
const label = ts.displayPartsToString(parameter.displayParts);
117+
118+
const startIndex = textIndex;
119+
const endIndex = textIndex + label.length;
120+
const doc = ts.displayPartsToString(parameter.documentation);
121+
122+
signatureLabel += label;
123+
parameters.push(ParameterInformation.create([startIndex, endIndex], doc));
124+
125+
if (index < lastIndex) {
126+
textIndex = endIndex + separatorLabel.length;
127+
signatureLabel += separatorLabel;
128+
}
129+
});
130+
const signatureDocumentation = getMarkdownDocumentation(
131+
item.documentation,
132+
item.tags.filter((tag) => tag.name !== 'param')
133+
);
134+
135+
return {
136+
label: prefixLabel + signatureLabel + suffixLabel,
137+
documentation: signatureDocumentation ? {
138+
value: signatureDocumentation,
139+
kind: MarkupKind.Markdown
140+
} : undefined,
141+
parameters
142+
};
143+
}
144+
145+
private isInSvelte2tsxGeneratedFunction(
146+
signatureHelpItem: ts.SignatureHelpItem
147+
) {
148+
return signatureHelpItem.prefixDisplayParts
149+
.some((part) => part.text.includes('__sveltets'));
150+
}
151+
}

packages/language-server/src/server.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,11 @@ export function startServer(options?: LSOptions) {
190190
? { prepareProvider: true }
191191
: true,
192192
referencesProvider: true,
193-
selectionRangeProvider: true
193+
selectionRangeProvider: true,
194+
signatureHelpProvider: {
195+
triggerCharacters: ['(', ',', '<'],
196+
retriggerCharacters: [')']
197+
}
194198
}
195199
};
196200
});
@@ -263,6 +267,10 @@ export function startServer(options?: LSOptions) {
263267
return pluginHost.resolveCompletion(data, completionItem);
264268
});
265269

270+
connection.onSignatureHelp((evt) =>
271+
pluginHost.getSignatureHelp(evt.textDocument, evt.position, evt.context)
272+
);
273+
266274
connection.onSelectionRanges((evt) =>
267275
pluginHost.getSelectionRanges(evt.textDocument, evt.positions)
268276
);

0 commit comments

Comments
 (0)