Skip to content

Commit d33b1c3

Browse files
authored
Initial work on signature help context (microsoft#58135)
* Initial work on signature help context Fixes microsoft#54972 Adds `SignatureHelpContext`. This tells providers why signature help was requested TODO: - [ ] Better understand semantics of retrigger. Should `retrigger` be an flag instead of a `triggerReason`? - [ ] Fix skipped test - [ ] Add more tests for trigger reasons / trigger characters * Fix unit test * Make sure we retrigger sig help if it is already showing * Add test for dismiss and re-invoke * Extract some constants * Extract createMockEditor
1 parent 4f2ffa0 commit d33b1c3

15 files changed

Lines changed: 372 additions & 91 deletions

File tree

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider {
2020
public async provideSignatureHelp(
2121
document: vscode.TextDocument,
2222
position: vscode.Position,
23-
token: vscode.CancellationToken
23+
token: vscode.CancellationToken,
24+
context?: vscode.SignatureHelpContext,
2425
): Promise<vscode.SignatureHelp | undefined> {
2526
const filepath = this.client.toPath(document.uri);
2627
if (!filepath) {
2728
return undefined;
2829
}
29-
const args: Proto.SignatureHelpRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
30+
const args: Proto.SignatureHelpRequestArgs = {
31+
...typeConverters.Position.toFileLocationRequestArgs(filepath, position),
32+
triggerReason: toTsTriggerReason(context!)
33+
};
3034

3135
let info: Proto.SignatureHelpItems;
3236
try {
@@ -71,6 +75,23 @@ class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider {
7175
}
7276
}
7377

78+
function toTsTriggerReason(context: vscode.SignatureHelpContext): Proto.SignatureHelpTriggerReason {
79+
switch (context.triggerReason) {
80+
case vscode.SignatureHelpTriggerReason.Retrigger:
81+
return { kind: 'retrigger' };
82+
83+
case vscode.SignatureHelpTriggerReason.TriggerCharacter:
84+
if (context.triggerCharacter) {
85+
return { kind: 'characterTyped', triggerCharacter: context.triggerCharacter as any };
86+
} else {
87+
return { kind: 'invoked' };
88+
}
89+
90+
case vscode.SignatureHelpTriggerReason.Invoke:
91+
default:
92+
return { kind: 'invoked' };
93+
}
94+
}
7495
export function register(
7596
selector: vscode.DocumentSelector,
7697
client: ITypeScriptServiceClient,

src/vs/editor/common/modes.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,18 @@ export interface SignatureHelp {
447447
*/
448448
activeParameter: number;
449449
}
450+
451+
export enum SignatureHelpTriggerReason {
452+
Invoke = 1,
453+
TriggerCharacter = 2,
454+
Retrigger = 3,
455+
}
456+
457+
export interface SignatureHelpContext {
458+
triggerReason: SignatureHelpTriggerReason;
459+
triggerCharacter?: string;
460+
}
461+
450462
/**
451463
* The signature help provider interface defines the contract between extensions and
452464
* the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature.
@@ -458,7 +470,7 @@ export interface SignatureHelpProvider {
458470
/**
459471
* Provide help for the signature at the given position and document.
460472
*/
461-
provideSignatureHelp(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<SignatureHelp>;
473+
provideSignatureHelp(model: model.ITextModel, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult<SignatureHelp>;
462474
}
463475

464476
/**

src/vs/editor/contrib/parameterHints/parameterHints.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
1616
import { ParameterHintsWidget } from './parameterHintsWidget';
1717
import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp';
1818
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
19+
import * as modes from 'vs/editor/common/modes';
1920

2021
class ParameterHintsController implements IEditorContribution {
2122

@@ -49,8 +50,8 @@ class ParameterHintsController implements IEditorContribution {
4950
this.widget.next();
5051
}
5152

52-
trigger(): void {
53-
this.widget.trigger();
53+
trigger(context: modes.SignatureHelpContext): void {
54+
this.widget.trigger(context);
5455
}
5556

5657
dispose(): void {
@@ -77,7 +78,7 @@ export class TriggerParameterHintsAction extends EditorAction {
7778
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
7879
let controller = ParameterHintsController.get(editor);
7980
if (controller) {
80-
controller.trigger();
81+
controller.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke });
8182
}
8283
}
8384
}

src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as nls from 'vs/nls';
1010
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
1111
import * as dom from 'vs/base/browser/dom';
1212
import * as aria from 'vs/base/browser/ui/aria/aria';
13-
import { SignatureHelp, SignatureInformation, SignatureHelpProviderRegistry } from 'vs/editor/common/modes';
13+
import * as modes from 'vs/editor/common/modes';
1414
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
1515
import { RunOnceScheduler, createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
1616
import { onUnexpectedError } from 'vs/base/common/errors';
@@ -31,12 +31,12 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
3131
const $ = dom.$;
3232

3333
export interface IHintEvent {
34-
hints: SignatureHelp;
34+
hints: modes.SignatureHelp;
3535
}
3636

3737
export class ParameterHintsModel extends Disposable {
3838

39-
static DELAY = 120; // ms
39+
private static readonly DEFAULT_DELAY = 120; // ms
4040

4141
private readonly _onHint = this._register(new Emitter<IHintEvent>());
4242
public readonly onHint: Event<IHintEvent> = this._onHint.event;
@@ -47,34 +47,42 @@ export class ParameterHintsModel extends Disposable {
4747
private editor: ICodeEditor;
4848
private enabled: boolean;
4949
private triggerCharactersListeners: IDisposable[];
50-
private active: boolean;
50+
private active: boolean = false;
51+
private pending: boolean = false;
52+
private triggerChars = new CharacterSet();
53+
54+
private triggerContext: modes.SignatureHelpContext | undefined;
5155
private throttledDelayer: RunOnceScheduler;
52-
private provideSignatureHelpRequest?: CancelablePromise<SignatureHelp>;
56+
private provideSignatureHelpRequest?: CancelablePromise<modes.SignatureHelp>;
5357

54-
constructor(editor: ICodeEditor) {
58+
constructor(
59+
editor: ICodeEditor,
60+
delay: number = ParameterHintsModel.DEFAULT_DELAY
61+
) {
5562
super();
5663

5764
this.editor = editor;
5865
this.enabled = false;
5966
this.triggerCharactersListeners = [];
6067

61-
this.throttledDelayer = new RunOnceScheduler(() => this.doTrigger(), ParameterHintsModel.DELAY);
62-
63-
this.active = false;
68+
this.throttledDelayer = new RunOnceScheduler(() => this.doTrigger(), delay);
6469

6570
this._register(this.editor.onDidChangeConfiguration(() => this.onEditorConfigurationChange()));
6671
this._register(this.editor.onDidChangeModel(e => this.onModelChanged()));
6772
this._register(this.editor.onDidChangeModelLanguage(_ => this.onModelChanged()));
6873
this._register(this.editor.onDidChangeCursorSelection(e => this.onCursorChange(e)));
6974
this._register(this.editor.onDidChangeModelContent(e => this.onModelContentChange()));
70-
this._register(SignatureHelpProviderRegistry.onDidChange(this.onModelChanged, this));
75+
this._register(modes.SignatureHelpProviderRegistry.onDidChange(this.onModelChanged, this));
76+
this._register(this.editor.onDidType(text => this.onDidType(text)));
7177

7278
this.onEditorConfigurationChange();
7379
this.onModelChanged();
7480
}
7581

7682
cancel(silent: boolean = false): void {
7783
this.active = false;
84+
this.pending = false;
85+
this.triggerContext = undefined;
7886

7987
this.throttledDelayer.cancel();
8088

@@ -88,12 +96,13 @@ export class ParameterHintsModel extends Disposable {
8896
}
8997
}
9098

91-
trigger(delay = ParameterHintsModel.DELAY): void {
92-
if (!SignatureHelpProviderRegistry.has(this.editor.getModel())) {
99+
trigger(context: modes.SignatureHelpContext, delay?: number): void {
100+
if (!modes.SignatureHelpProviderRegistry.has(this.editor.getModel())) {
93101
return;
94102
}
95103

96104
this.cancel(true);
105+
this.triggerContext = context;
97106
return this.throttledDelayer.schedule(delay);
98107
}
99108

@@ -102,9 +111,17 @@ export class ParameterHintsModel extends Disposable {
102111
this.provideSignatureHelpRequest.cancel();
103112
}
104113

105-
this.provideSignatureHelpRequest = createCancelablePromise(token => provideSignatureHelp(this.editor.getModel(), this.editor.getPosition(), token));
114+
this.pending = true;
115+
116+
const triggerContext = this.triggerContext || { triggerReason: modes.SignatureHelpTriggerReason.Invoke };
117+
this.triggerContext = undefined;
118+
119+
this.provideSignatureHelpRequest = createCancelablePromise(token =>
120+
provideSignatureHelp(this.editor.getModel(), this.editor.getPosition(), triggerContext, token));
106121

107122
this.provideSignatureHelpRequest.then(result => {
123+
this.pending = false;
124+
108125
if (!result || !result.signatures || result.signatures.length === 0) {
109126
this.cancel();
110127
this._onCancel.fire(void 0);
@@ -116,54 +133,63 @@ export class ParameterHintsModel extends Disposable {
116133
this._onHint.fire(event);
117134
return true;
118135

119-
}).catch(onUnexpectedError);
136+
}).catch(error => {
137+
this.pending = false;
138+
onUnexpectedError(error);
139+
});
120140
}
121141

122-
isTriggered(): boolean {
123-
return this.active || this.throttledDelayer.isScheduled();
142+
private get isTriggered(): boolean {
143+
return this.active || this.pending || this.throttledDelayer.isScheduled();
124144
}
125145

126146
private onModelChanged(): void {
127147
this.cancel();
128148

129-
this.triggerCharactersListeners = dispose(this.triggerCharactersListeners);
149+
// Update trigger characters
150+
this.triggerChars = new CharacterSet();
130151

131152
const model = this.editor.getModel();
132153
if (!model) {
133154
return;
134155
}
135156

136-
const triggerChars = new CharacterSet();
137-
for (const support of SignatureHelpProviderRegistry.ordered(model)) {
157+
for (const support of modes.SignatureHelpProviderRegistry.ordered(model)) {
138158
if (Array.isArray(support.signatureHelpTriggerCharacters)) {
139159
for (const ch of support.signatureHelpTriggerCharacters) {
140-
triggerChars.add(ch.charCodeAt(0));
160+
this.triggerChars.add(ch.charCodeAt(0));
141161
}
142162
}
143163
}
164+
}
144165

145-
this.triggerCharactersListeners.push(this.editor.onDidType((text: string) => {
146-
if (!this.enabled) {
147-
return;
148-
}
166+
private onDidType(text: string) {
167+
if (!this.enabled) {
168+
return;
169+
}
149170

150-
if (triggerChars.has(text.charCodeAt(text.length - 1))) {
151-
this.trigger();
152-
}
153-
}));
171+
const lastCharIndex = text.length - 1;
172+
if (this.triggerChars.has(text.charCodeAt(lastCharIndex))) {
173+
this.trigger({
174+
triggerReason: this.isTriggered
175+
? modes.SignatureHelpTriggerReason.Retrigger
176+
: modes.SignatureHelpTriggerReason.TriggerCharacter,
177+
triggerCharacter: text.charAt(lastCharIndex)
178+
});
179+
}
154180
}
155181

156182
private onCursorChange(e: ICursorSelectionChangedEvent): void {
157183
if (e.source === 'mouse') {
158184
this.cancel();
159-
} else if (this.isTriggered()) {
160-
this.trigger();
185+
} else if (this.isTriggered) {
186+
this.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Retrigger });
161187
}
162188
}
163189

164190
private onModelContentChange(): void {
165-
if (this.isTriggered()) {
166-
this.trigger();
191+
if (this.isTriggered) {
192+
this.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Retrigger });
167193
}
168194
}
169195

@@ -198,7 +224,7 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable {
198224
private overloads: HTMLElement;
199225
private currentSignature: number;
200226
private visible: boolean;
201-
private hints: SignatureHelp;
227+
private hints: modes.SignatureHelp;
202228
private announcedLabel: string;
203229
private scrollbar: DomScrollableElement;
204230
private disposables: IDisposable[];
@@ -405,7 +431,7 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable {
405431
this.scrollbar.scanDomNode();
406432
}
407433

408-
private renderParameters(parent: HTMLElement, signature: SignatureInformation, currentParameter: number): void {
434+
private renderParameters(parent: HTMLElement, signature: modes.SignatureInformation, currentParameter: number): void {
409435
let end = signature.label.length;
410436
let idx = 0;
411437
let element: HTMLSpanElement;
@@ -526,8 +552,8 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable {
526552
return ParameterHintsWidget.ID;
527553
}
528554

529-
trigger(): void {
530-
this.model.trigger(0);
555+
trigger(context: modes.SignatureHelpContext): void {
556+
this.model.trigger(context, 0);
531557
}
532558

533559
private updateMaxHeight(): void {

src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors';
88
import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions';
99
import { Position } from 'vs/editor/common/core/position';
1010
import { ITextModel } from 'vs/editor/common/model';
11-
import { SignatureHelp, SignatureHelpProviderRegistry } from 'vs/editor/common/modes';
11+
import * as modes from 'vs/editor/common/modes';
1212
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
1313
import { CancellationToken } from 'vs/base/common/cancellation';
1414

@@ -17,13 +17,14 @@ export const Context = {
1717
MultipleSignatures: new RawContextKey<boolean>('parameterHintsMultipleSignatures', false),
1818
};
1919

20-
export function provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken): Promise<SignatureHelp> {
20+
export function provideSignatureHelp(model: ITextModel, position: Position, context: modes.SignatureHelpContext, token: CancellationToken): Promise<modes.SignatureHelp> {
2121

22-
const supports = SignatureHelpProviderRegistry.ordered(model);
22+
const supports = modes.SignatureHelpProviderRegistry.ordered(model);
2323

2424
return first2(supports.map(support => () => {
25-
return Promise.resolve(support.provideSignatureHelp(model, position, token)).catch(onUnexpectedExternalError);
25+
return Promise.resolve(support.provideSignatureHelp(model, position, token, context)).catch(onUnexpectedExternalError);
2626
}));
2727
}
2828

29-
registerDefaultLanguageCommand('_executeSignatureHelpProvider', (model, position) => provideSignatureHelp(model, position, CancellationToken.None));
29+
registerDefaultLanguageCommand('_executeSignatureHelpProvider', (model, position) =>
30+
provideSignatureHelp(model, position, { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None));

0 commit comments

Comments
 (0)