diff --git a/extensions/typescript-language-features/src/features/signatureHelp.ts b/extensions/typescript-language-features/src/features/signatureHelp.ts index f3a9a3e5620..d5698187e34 100644 --- a/extensions/typescript-language-features/src/features/signatureHelp.ts +++ b/extensions/typescript-language-features/src/features/signatureHelp.ts @@ -20,13 +20,17 @@ class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider { public async provideSignatureHelp( document: vscode.TextDocument, position: vscode.Position, - token: vscode.CancellationToken + token: vscode.CancellationToken, + context?: vscode.SignatureHelpContext, ): Promise { const filepath = this.client.toPath(document.uri); if (!filepath) { return undefined; } - const args: Proto.SignatureHelpRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position); + const args: Proto.SignatureHelpRequestArgs = { + ...typeConverters.Position.toFileLocationRequestArgs(filepath, position), + triggerReason: toTsTriggerReason(context!) + }; let info: Proto.SignatureHelpItems; try { @@ -71,6 +75,23 @@ class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider { } } +function toTsTriggerReason(context: vscode.SignatureHelpContext): Proto.SignatureHelpTriggerReason { + switch (context.triggerReason) { + case vscode.SignatureHelpTriggerReason.Retrigger: + return { kind: 'retrigger' }; + + case vscode.SignatureHelpTriggerReason.TriggerCharacter: + if (context.triggerCharacter) { + return { kind: 'characterTyped', triggerCharacter: context.triggerCharacter as any }; + } else { + return { kind: 'invoked' }; + } + + case vscode.SignatureHelpTriggerReason.Invoke: + default: + return { kind: 'invoked' }; + } +} export function register( selector: vscode.DocumentSelector, client: ITypeScriptServiceClient, diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 12eb263187c..97032a44ac8 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -447,6 +447,18 @@ export interface SignatureHelp { */ activeParameter: number; } + +export enum SignatureHelpTriggerReason { + Invoke = 1, + TriggerCharacter = 2, + Retrigger = 3, +} + +export interface SignatureHelpContext { + triggerReason: SignatureHelpTriggerReason; + triggerCharacter?: string; +} + /** * The signature help provider interface defines the contract between extensions and * the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature. @@ -458,7 +470,7 @@ export interface SignatureHelpProvider { /** * Provide help for the signature at the given position and document. */ - provideSignatureHelp(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideSignatureHelp(model: model.ITextModel, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult; } /** diff --git a/src/vs/editor/contrib/parameterHints/parameterHints.ts b/src/vs/editor/contrib/parameterHints/parameterHints.ts index 6ec4cda8857..e02faeb4cc5 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHints.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHints.ts @@ -16,6 +16,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ParameterHintsWidget } from './parameterHintsWidget'; import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import * as modes from 'vs/editor/common/modes'; class ParameterHintsController implements IEditorContribution { @@ -49,8 +50,8 @@ class ParameterHintsController implements IEditorContribution { this.widget.next(); } - trigger(): void { - this.widget.trigger(); + trigger(context: modes.SignatureHelpContext): void { + this.widget.trigger(context); } dispose(): void { @@ -77,7 +78,7 @@ export class TriggerParameterHintsAction extends EditorAction { public run(accessor: ServicesAccessor, editor: ICodeEditor): void { let controller = ParameterHintsController.get(editor); if (controller) { - controller.trigger(); + controller.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke }); } } } diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index b9a76891514..892eb88a8e9 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -10,7 +10,7 @@ import * as nls from 'vs/nls'; import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; -import { SignatureHelp, SignatureInformation, SignatureHelpProviderRegistry } from 'vs/editor/common/modes'; +import * as modes from 'vs/editor/common/modes'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { RunOnceScheduler, createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -31,12 +31,12 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; const $ = dom.$; export interface IHintEvent { - hints: SignatureHelp; + hints: modes.SignatureHelp; } export class ParameterHintsModel extends Disposable { - static DELAY = 120; // ms + private static readonly DEFAULT_DELAY = 120; // ms private readonly _onHint = this._register(new Emitter()); public readonly onHint: Event = this._onHint.event; @@ -47,27 +47,33 @@ export class ParameterHintsModel extends Disposable { private editor: ICodeEditor; private enabled: boolean; private triggerCharactersListeners: IDisposable[]; - private active: boolean; - private throttledDelayer: RunOnceScheduler; - private provideSignatureHelpRequest?: CancelablePromise; + private active: boolean = false; + private pending: boolean = false; + private triggerChars = new CharacterSet(); - constructor(editor: ICodeEditor) { + private triggerContext: modes.SignatureHelpContext | undefined; + private throttledDelayer: RunOnceScheduler; + private provideSignatureHelpRequest?: CancelablePromise; + + constructor( + editor: ICodeEditor, + delay: number = ParameterHintsModel.DEFAULT_DELAY + ) { super(); this.editor = editor; this.enabled = false; this.triggerCharactersListeners = []; - this.throttledDelayer = new RunOnceScheduler(() => this.doTrigger(), ParameterHintsModel.DELAY); - - this.active = false; + this.throttledDelayer = new RunOnceScheduler(() => this.doTrigger(), delay); this._register(this.editor.onDidChangeConfiguration(() => this.onEditorConfigurationChange())); this._register(this.editor.onDidChangeModel(e => this.onModelChanged())); this._register(this.editor.onDidChangeModelLanguage(_ => this.onModelChanged())); this._register(this.editor.onDidChangeCursorSelection(e => this.onCursorChange(e))); this._register(this.editor.onDidChangeModelContent(e => this.onModelContentChange())); - this._register(SignatureHelpProviderRegistry.onDidChange(this.onModelChanged, this)); + this._register(modes.SignatureHelpProviderRegistry.onDidChange(this.onModelChanged, this)); + this._register(this.editor.onDidType(text => this.onDidType(text))); this.onEditorConfigurationChange(); this.onModelChanged(); @@ -75,6 +81,8 @@ export class ParameterHintsModel extends Disposable { cancel(silent: boolean = false): void { this.active = false; + this.pending = false; + this.triggerContext = undefined; this.throttledDelayer.cancel(); @@ -88,12 +96,13 @@ export class ParameterHintsModel extends Disposable { } } - trigger(delay = ParameterHintsModel.DELAY): void { - if (!SignatureHelpProviderRegistry.has(this.editor.getModel())) { + trigger(context: modes.SignatureHelpContext, delay?: number): void { + if (!modes.SignatureHelpProviderRegistry.has(this.editor.getModel())) { return; } this.cancel(true); + this.triggerContext = context; return this.throttledDelayer.schedule(delay); } @@ -102,9 +111,17 @@ export class ParameterHintsModel extends Disposable { this.provideSignatureHelpRequest.cancel(); } - this.provideSignatureHelpRequest = createCancelablePromise(token => provideSignatureHelp(this.editor.getModel(), this.editor.getPosition(), token)); + this.pending = true; + + const triggerContext = this.triggerContext || { triggerReason: modes.SignatureHelpTriggerReason.Invoke }; + this.triggerContext = undefined; + + this.provideSignatureHelpRequest = createCancelablePromise(token => + provideSignatureHelp(this.editor.getModel(), this.editor.getPosition(), triggerContext, token)); this.provideSignatureHelpRequest.then(result => { + this.pending = false; + if (!result || !result.signatures || result.signatures.length === 0) { this.cancel(); this._onCancel.fire(void 0); @@ -116,54 +133,63 @@ export class ParameterHintsModel extends Disposable { this._onHint.fire(event); return true; - }).catch(onUnexpectedError); + }).catch(error => { + this.pending = false; + onUnexpectedError(error); + }); } - isTriggered(): boolean { - return this.active || this.throttledDelayer.isScheduled(); + private get isTriggered(): boolean { + return this.active || this.pending || this.throttledDelayer.isScheduled(); } private onModelChanged(): void { this.cancel(); - this.triggerCharactersListeners = dispose(this.triggerCharactersListeners); + // Update trigger characters + this.triggerChars = new CharacterSet(); const model = this.editor.getModel(); if (!model) { return; } - const triggerChars = new CharacterSet(); - for (const support of SignatureHelpProviderRegistry.ordered(model)) { + for (const support of modes.SignatureHelpProviderRegistry.ordered(model)) { if (Array.isArray(support.signatureHelpTriggerCharacters)) { for (const ch of support.signatureHelpTriggerCharacters) { - triggerChars.add(ch.charCodeAt(0)); + this.triggerChars.add(ch.charCodeAt(0)); } } } + } - this.triggerCharactersListeners.push(this.editor.onDidType((text: string) => { - if (!this.enabled) { - return; - } + private onDidType(text: string) { + if (!this.enabled) { + return; + } - if (triggerChars.has(text.charCodeAt(text.length - 1))) { - this.trigger(); - } - })); + const lastCharIndex = text.length - 1; + if (this.triggerChars.has(text.charCodeAt(lastCharIndex))) { + this.trigger({ + triggerReason: this.isTriggered + ? modes.SignatureHelpTriggerReason.Retrigger + : modes.SignatureHelpTriggerReason.TriggerCharacter, + triggerCharacter: text.charAt(lastCharIndex) + }); + } } private onCursorChange(e: ICursorSelectionChangedEvent): void { if (e.source === 'mouse') { this.cancel(); - } else if (this.isTriggered()) { - this.trigger(); + } else if (this.isTriggered) { + this.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Retrigger }); } } private onModelContentChange(): void { - if (this.isTriggered()) { - this.trigger(); + if (this.isTriggered) { + this.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Retrigger }); } } @@ -198,7 +224,7 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { private overloads: HTMLElement; private currentSignature: number; private visible: boolean; - private hints: SignatureHelp; + private hints: modes.SignatureHelp; private announcedLabel: string; private scrollbar: DomScrollableElement; private disposables: IDisposable[]; @@ -405,7 +431,7 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { this.scrollbar.scanDomNode(); } - private renderParameters(parent: HTMLElement, signature: SignatureInformation, currentParameter: number): void { + private renderParameters(parent: HTMLElement, signature: modes.SignatureInformation, currentParameter: number): void { let end = signature.label.length; let idx = 0; let element: HTMLSpanElement; @@ -526,8 +552,8 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable { return ParameterHintsWidget.ID; } - trigger(): void { - this.model.trigger(0); + trigger(context: modes.SignatureHelpContext): void { + this.model.trigger(context, 0); } private updateMaxHeight(): void { diff --git a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts index 4136612df1f..c75a78f58f9 100644 --- a/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts +++ b/src/vs/editor/contrib/parameterHints/provideSignatureHelp.ts @@ -8,7 +8,7 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; import { ITextModel } from 'vs/editor/common/model'; -import { SignatureHelp, SignatureHelpProviderRegistry } from 'vs/editor/common/modes'; +import * as modes from 'vs/editor/common/modes'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -17,13 +17,14 @@ export const Context = { MultipleSignatures: new RawContextKey('parameterHintsMultipleSignatures', false), }; -export function provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken): Promise { +export function provideSignatureHelp(model: ITextModel, position: Position, context: modes.SignatureHelpContext, token: CancellationToken): Promise { - const supports = SignatureHelpProviderRegistry.ordered(model); + const supports = modes.SignatureHelpProviderRegistry.ordered(model); return first2(supports.map(support => () => { - return Promise.resolve(support.provideSignatureHelp(model, position, token)).catch(onUnexpectedExternalError); + return Promise.resolve(support.provideSignatureHelp(model, position, token, context)).catch(onUnexpectedExternalError); })); } -registerDefaultLanguageCommand('_executeSignatureHelpProvider', (model, position) => provideSignatureHelp(model, position, CancellationToken.None)); +registerDefaultLanguageCommand('_executeSignatureHelpProvider', (model, position) => + provideSignatureHelp(model, position, { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None)); diff --git a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts index 0a734d96912..edd554911cf 100644 --- a/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts +++ b/src/vs/editor/contrib/parameterHints/test/parameterHintsModel.test.ts @@ -5,60 +5,214 @@ import * as assert from 'assert'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; +import { Handler } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { SignatureHelp, SignatureHelpProvider, SignatureHelpProviderRegistry } from 'vs/editor/common/modes'; -import { TestCodeEditor, createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import * as modes from 'vs/editor/common/modes'; +import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IStorageService, NullStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { ParameterHintsModel } from '../parameterHintsWidget'; -function createMockEditor(model: TextModel): TestCodeEditor { - return createTestCodeEditor({ - model: model, - serviceCollection: new ServiceCollection( - [ITelemetryService, NullTelemetryService], - [IStorageService, NullStorageService] - ) - }); -} +const mockFile = URI.parse('test:somefile.ttt'); +const mockFileSelector = { scheme: 'test' }; +const emptySigHelpResult = { + signatures: [{ + label: 'none', + parameters: [] + }], + activeParameter: 0, + activeSignature: 0 +}; suite('ParameterHintsModel', () => { let disposables: IDisposable[] = []; - setup(function () { disposables = dispose(disposables); }); - test('Should cancel existing request when new request comes in', () => { - const textModel = TextModel.createFromString('abc def', undefined, undefined, URI.parse('test:somefile.ttt')); + function createMockEditor(fileContents: string) { + const textModel = TextModel.createFromString(fileContents, undefined, undefined, mockFile); + const editor = createTestCodeEditor({ + model: textModel, + serviceCollection: new ServiceCollection( + [ITelemetryService, NullTelemetryService], + [IStorageService, NullStorageService] + ) + }); disposables.push(textModel); + disposables.push(editor); + return editor; + } - const editor = createMockEditor(textModel); + test('Provider should get trigger character on type', (done) => { + const triggerChar = '('; + + const editor = createMockEditor(''); + disposables.push(new ParameterHintsModel(editor)); + + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = [triggerChar]; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, triggerChar); + done(); + return undefined; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + }); + + test('Provider should be retriggered if already active', (done) => { + const triggerChar = '('; + + const editor = createMockEditor(''); + disposables.push(new ParameterHintsModel(editor)); + + let invokeCount = 0; + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = [triggerChar]; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + ++invokeCount; + if (invokeCount === 1) { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, triggerChar); + // Retrigger + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + } else { + assert.strictEqual(invokeCount, 2); + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.Retrigger); + assert.strictEqual(context.triggerCharacter, triggerChar); + done(); + } + return emptySigHelpResult; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + }); + + test('Provider should not be retriggered if previous help is canceled first', (done) => { + const triggerChar = '('; + + const editor = createMockEditor(''); + const hintModel = new ParameterHintsModel(editor); + disposables.push(hintModel); + + let invokeCount = 0; + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = [triggerChar]; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + ++invokeCount; + if (invokeCount === 1) { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, triggerChar); + + // Cancel and retrigger + hintModel.cancel(); + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + } else { + assert.strictEqual(invokeCount, 2); + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, triggerChar); + done(); + } + return emptySigHelpResult; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: triggerChar }); + }); + + test('Provider should get last trigger character when triggered multiple times and only be invoked once', (done) => { + const editor = createMockEditor(''); + disposables.push(new ParameterHintsModel(editor, 5)); + + let invokeCount = 0; + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = ['a', 'b', 'c']; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + ++invokeCount; + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.Retrigger); + assert.strictEqual(context.triggerCharacter, 'c'); + + // Give some time to allow for later triggers + setTimeout(() => { + assert.strictEqual(invokeCount, 1); + + done(); + }, 50); + return undefined; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: 'a' }); + editor.trigger('keyboard', Handler.Type, { text: 'b' }); + editor.trigger('keyboard', Handler.Type, { text: 'c' }); + }); + + test('Provider should be retriggered if already active', (done) => { + const editor = createMockEditor(''); + disposables.push(new ParameterHintsModel(editor, 5)); + + let invokeCount = 0; + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = ['a', 'b']; + + provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable { + ++invokeCount; + if (invokeCount === 1) { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter); + assert.strictEqual(context.triggerCharacter, 'a'); + + // retrigger after delay for widget to show up + setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: 'b' }), 50); + } else if (invokeCount === 2) { + assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.Retrigger); + assert.strictEqual(context.triggerCharacter, 'b'); + done(); + } else { + assert.fail('Unexpected invoke'); + } + + return emptySigHelpResult; + } + })); + + editor.trigger('keyboard', Handler.Type, { text: 'a' }); + }); + + test('Should cancel existing request when new request comes in', () => { + const editor = createMockEditor('abc def'); const hintsModel = new ParameterHintsModel(editor); let didRequestCancellationOf = -1; let invokeCount = 0; - const longRunningProvider = new class implements SignatureHelpProvider { - signatureHelpTriggerCharacters: string[] = []; + const longRunningProvider = new class implements modes.SignatureHelpProvider { + signatureHelpTriggerCharacters = []; - provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken): SignatureHelp | Thenable { + provideSignatureHelp(_model: ITextModel, _position: Position, token: CancellationToken): modes.SignatureHelp | Thenable { const count = invokeCount++; token.onCancellationRequested(() => { didRequestCancellationOf = count; }); // retrigger on first request if (count === 0) { - hintsModel.trigger(0); + hintsModel.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke }, 0); } - return new Promise(resolve => { + return new Promise(resolve => { setTimeout(() => { resolve({ signatures: [{ @@ -73,9 +227,9 @@ suite('ParameterHintsModel', () => { } }; - disposables.push(SignatureHelpProviderRegistry.register({ scheme: 'test' }, longRunningProvider)); + disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, longRunningProvider)); - hintsModel.trigger(0); + hintsModel.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke }, 0); assert.strictEqual(-1, didRequestCancellationOf); return new Promise((resolve, reject) => diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index b0287953b31..f7d7ccd8f40 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -880,6 +880,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { SymbolKind: modes.SymbolKind, IndentAction: IndentAction, SuggestTriggerKind: modes.SuggestTriggerKind, - FoldingRangeKind: modes.FoldingRangeKind + FoldingRangeKind: modes.FoldingRangeKind, + SignatureHelpTriggerReason: modes.SignatureHelpTriggerReason, }; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 24be3f11c28..0d9e49738e1 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4884,6 +4884,17 @@ declare namespace monaco.languages { activeParameter: number; } + export enum SignatureHelpTriggerReason { + Invoke = 1, + TriggerCharacter = 2, + Retrigger = 3 + } + + export interface SignatureHelpContext { + triggerReason: SignatureHelpTriggerReason; + triggerCharacter?: string; + } + /** * The signature help provider interface defines the contract between extensions and * the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature. @@ -4893,7 +4904,7 @@ declare namespace monaco.languages { /** * Provide help for the signature at the given position and document. */ - provideSignatureHelp(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideSignatureHelp(model: editor.ITextModel, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult; } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 17ed782108f..c1ac3219150 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -808,4 +808,52 @@ declare module 'vscode' { export const onDidRenameFile: Event; } //#endregion + + //#region Signature Help + /** + * How a [Signature provider](#SignatureHelpProvider) was triggered + */ + export enum SignatureHelpTriggerReason { + /** + * Signature help was invoked manually by the user or by a command. + */ + Invoke = 1, + + /** + * Signature help was triggered by a trigger character. + */ + TriggerCharacter = 2, + + /** + * Signature help was retriggered. + * + * Retriggers occur when the signature help is already active and can be caused by typing a trigger character + * or by a cursor move. + */ + Retrigger = 3, + } + + /** + * Contains additional information about the context in which a + * [signature help provider](#SignatureHelpProvider.provideSignatureHelp) is triggered. + */ + export interface SignatureHelpContext { + /** + * Action that caused signature help to be requested. + */ + readonly triggerReason: SignatureHelpTriggerReason; + + /** + * Character that caused signature help to be requested. + * + * This is `undefined` for manual triggers or retriggers for a cursor move. + */ + readonly triggerCharacter?: string; + } + + export interface SignatureHelpProvider { + provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult; + } + + //#endregion } diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index a7e1d46b3b8..5d14ececf3d 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -307,10 +307,9 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha signatureHelpTriggerCharacters: triggerCharacter, - provideSignatureHelp: (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable => { - return this._proxy.$provideSignatureHelp(handle, model.uri, position, token); + provideSignatureHelp: (model: ITextModel, position: EditorPosition, token: CancellationToken, context: modes.SignatureHelpContext): Thenable => { + return this._proxy.$provideSignatureHelp(handle, model.uri, position, context, token); } - }); } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 06ba33800ac..d445d204876 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -759,6 +759,7 @@ export function createApiFactory( Selection: extHostTypes.Selection, ShellExecution: extHostTypes.ShellExecution, ShellQuoting: extHostTypes.ShellQuoting, + SignatureHelpTriggerReason: extHostTypes.SignatureHelpTriggerReason, SignatureHelp: extHostTypes.SignatureHelp, SignatureInformation: extHostTypes.SignatureInformation, SnippetString: extHostTypes.SnippetString, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 20353081b85..fa9581beed1 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -843,7 +843,7 @@ export interface ExtHostLanguageFeaturesShape { $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.SuggestContext, token: CancellationToken): Thenable; $resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.ISuggestion, token: CancellationToken): Thenable; $releaseCompletionItems(handle: number, id: number): void; - $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable; + $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Thenable; $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Thenable; $resolveDocumentLink(handle: number, link: modes.ILink, token: CancellationToken): Thenable; $provideDocumentColors(handle: number, resource: UriComponents, token: CancellationToken): Thenable; diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index a1a1aad8138..a50bd234064 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -714,12 +714,12 @@ class SignatureHelpAdapter { private readonly _provider: vscode.SignatureHelpProvider ) { } - provideSignatureHelp(resource: URI, position: IPosition, token: CancellationToken): Thenable { + provideSignatureHelp(resource: URI, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Thenable { const doc = this._documents.getDocumentData(resource).document; const pos = typeConvert.Position.to(position); - return asThenable(() => this._provider.provideSignatureHelp(doc, pos, token)).then(value => { + return asThenable(() => this._provider.provideSignatureHelp(doc, pos, token, context)).then(value => { if (value) { return typeConvert.SignatureHelp.from(value); } @@ -1144,8 +1144,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable { - return this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.provideSignatureHelp(URI.revive(resource), position, token)); + $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Thenable { + return this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.provideSignatureHelp(URI.revive(resource), position, context, token)); } // --- links diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index b24f75e808a..b36fb948808 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1064,6 +1064,12 @@ export class SignatureHelp { } } +export enum SignatureHelpTriggerReason { + Invoke = 1, + TriggerCharacter = 2, + Retrigger = 3, +} + export enum CompletionTriggerKind { Invoke = 0, TriggerCharacter = 1, diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index c0936096f1e..18606175be9 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -24,7 +24,7 @@ import { IHeapService } from 'vs/workbench/api/electron-browser/mainThreadHeapSe import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; -import { DocumentSymbolProviderRegistry, DocumentHighlightKind, Hover, ResourceTextEdit } from 'vs/editor/common/modes'; +import * as modes from 'vs/editor/common/modes'; import { getCodeLensData } from 'vs/editor/contrib/codelens/codelens'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition } from 'vs/editor/contrib/goToDefinition/goToDefinition'; import { getHover } from 'vs/editor/contrib/hover/getHover'; @@ -133,7 +133,7 @@ suite('ExtHostLanguageFeatures', function () { // --- outline test('DocumentSymbols, register/deregister', function () { - assert.equal(DocumentSymbolProviderRegistry.all(model).length, 0); + assert.equal(modes.DocumentSymbolProviderRegistry.all(model).length, 0); let d1 = extHost.registerDocumentSymbolProvider(defaultSelector, { provideDocumentSymbols() { return []; @@ -141,7 +141,7 @@ suite('ExtHostLanguageFeatures', function () { }); return rpcProtocol.sync().then(() => { - assert.equal(DocumentSymbolProviderRegistry.all(model).length, 1); + assert.equal(modes.DocumentSymbolProviderRegistry.all(model).length, 1); d1.dispose(); return rpcProtocol.sync(); }); @@ -443,7 +443,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { return getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { assert.equal(value.length, 2); - let [first, second] = value as Hover[]; + let [first, second] = value as modes.Hover[]; assert.equal(first.contents[0].value, 'registered second'); assert.equal(second.contents[0].value, 'registered first'); }); @@ -489,7 +489,7 @@ suite('ExtHostLanguageFeatures', function () { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); - assert.equal(entry.kind, DocumentHighlightKind.Text); + assert.equal(entry.kind, modes.DocumentHighlightKind.Text); }); }); }); @@ -513,7 +513,7 @@ suite('ExtHostLanguageFeatures', function () { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); - assert.equal(entry.kind, DocumentHighlightKind.Text); + assert.equal(entry.kind, modes.DocumentHighlightKind.Text); }); }); }); @@ -537,7 +537,7 @@ suite('ExtHostLanguageFeatures', function () { assert.equal(value.length, 1); let [entry] = value; assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 3 }); - assert.equal(entry.kind, DocumentHighlightKind.Text); + assert.equal(entry.kind, modes.DocumentHighlightKind.Text); }); }); }); @@ -838,8 +838,8 @@ suite('ExtHostLanguageFeatures', function () { return rename(model, new EditorPosition(1, 1), 'newName').then(value => { // least relevant rename provider assert.equal(value.edits.length, 2); - assert.equal((value.edits[0]).edits.length, 1); - assert.equal((value.edits[1]).edits.length, 1); + assert.equal((value.edits[0]).edits.length, 1); + assert.equal((value.edits[1]).edits.length, 1); }); }); }); @@ -866,7 +866,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideSignatureHelp(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { + return provideSignatureHelp(model, new EditorPosition(1, 1), { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None).then(value => { assert.ok(value); }); }); @@ -881,7 +881,7 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { - return provideSignatureHelp(model, new EditorPosition(1, 1), CancellationToken.None).then(value => { + return provideSignatureHelp(model, new EditorPosition(1, 1), { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None).then(value => { assert.equal(value, undefined); }); });