Path: blob/main/src/vs/editor/contrib/parameterHints/browser/parameterHintsModel.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancelablePromise, createCancelablePromise, Delayer } from '../../../../base/common/async.js';6import { onUnexpectedError } from '../../../../base/common/errors.js';7import { Emitter } from '../../../../base/common/event.js';8import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';9import { ICodeEditor } from '../../../browser/editorBrowser.js';10import { EditorOption } from '../../../common/config/editorOptions.js';11import { CharacterSet } from '../../../common/core/characterClassifier.js';12import { ICursorSelectionChangedEvent } from '../../../common/cursorEvents.js';13import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';14import * as languages from '../../../common/languages.js';15import { provideSignatureHelp } from './provideSignatureHelp.js';1617export interface TriggerContext {18readonly triggerKind: languages.SignatureHelpTriggerKind;19readonly triggerCharacter?: string;20}2122namespace ParameterHintState {23export const enum Type {24Default,25Active,26Pending,27}2829export const Default = { type: Type.Default } as const;3031export class Pending {32readonly type = Type.Pending;33constructor(34readonly request: CancelablePromise<languages.SignatureHelpResult | undefined | null>,35readonly previouslyActiveHints: languages.SignatureHelp | undefined,36) { }37}3839export class Active {40readonly type = Type.Active;41constructor(42readonly hints: languages.SignatureHelp43) { }44}4546export type State = typeof Default | Pending | Active;47}4849export class ParameterHintsModel extends Disposable {5051private static readonly DEFAULT_DELAY = 120; // ms5253private readonly _onChangedHints = this._register(new Emitter<languages.SignatureHelp | undefined>());54public readonly onChangedHints = this._onChangedHints.event;5556private readonly editor: ICodeEditor;57private readonly providers: LanguageFeatureRegistry<languages.SignatureHelpProvider>;5859private triggerOnType = false;60private _state: ParameterHintState.State = ParameterHintState.Default;61private _pendingTriggers: TriggerContext[] = [];6263private readonly _lastSignatureHelpResult = this._register(new MutableDisposable<languages.SignatureHelpResult>());64private readonly triggerChars = new CharacterSet();65private readonly retriggerChars = new CharacterSet();6667private readonly throttledDelayer: Delayer<boolean>;68private triggerId = 0;6970constructor(71editor: ICodeEditor,72providers: LanguageFeatureRegistry<languages.SignatureHelpProvider>,73delay: number = ParameterHintsModel.DEFAULT_DELAY74) {75super();7677this.editor = editor;78this.providers = providers;7980this.throttledDelayer = new Delayer(delay);8182this._register(this.editor.onDidBlurEditorWidget(() => this.cancel()));83this._register(this.editor.onDidChangeConfiguration(() => this.onEditorConfigurationChange()));84this._register(this.editor.onDidChangeModel(e => this.onModelChanged()));85this._register(this.editor.onDidChangeModelLanguage(_ => this.onModelChanged()));86this._register(this.editor.onDidChangeCursorSelection(e => this.onCursorChange(e)));87this._register(this.editor.onDidChangeModelContent(e => this.onModelContentChange()));88this._register(this.providers.onDidChange(this.onModelChanged, this));89this._register(this.editor.onDidType(text => this.onDidType(text)));9091this.onEditorConfigurationChange();92this.onModelChanged();93}9495private get state() { return this._state; }96private set state(value: ParameterHintState.State) {97if (this._state.type === ParameterHintState.Type.Pending) {98this._state.request.cancel();99}100this._state = value;101}102103cancel(silent: boolean = false): void {104this.state = ParameterHintState.Default;105106this.throttledDelayer.cancel();107108if (!silent) {109this._onChangedHints.fire(undefined);110}111}112113trigger(context: TriggerContext, delay?: number): void {114const model = this.editor.getModel();115if (!model || !this.providers.has(model)) {116return;117}118119const triggerId = ++this.triggerId;120121this._pendingTriggers.push(context);122this.throttledDelayer.trigger(() => {123return this.doTrigger(triggerId);124}, delay)125.catch(onUnexpectedError);126}127128public next(): void {129if (this.state.type !== ParameterHintState.Type.Active) {130return;131}132133const length = this.state.hints.signatures.length;134const activeSignature = this.state.hints.activeSignature;135const last = (activeSignature % length) === (length - 1);136const cycle = this.editor.getOption(EditorOption.parameterHints).cycle;137138// If there is only one signature, or we're on last signature of list139if ((length < 2 || last) && !cycle) {140this.cancel();141return;142}143144this.updateActiveSignature(last && cycle ? 0 : activeSignature + 1);145}146147public previous(): void {148if (this.state.type !== ParameterHintState.Type.Active) {149return;150}151152const length = this.state.hints.signatures.length;153const activeSignature = this.state.hints.activeSignature;154const first = activeSignature === 0;155const cycle = this.editor.getOption(EditorOption.parameterHints).cycle;156157// If there is only one signature, or we're on first signature of list158if ((length < 2 || first) && !cycle) {159this.cancel();160return;161}162163this.updateActiveSignature(first && cycle ? length - 1 : activeSignature - 1);164}165166private updateActiveSignature(activeSignature: number) {167if (this.state.type !== ParameterHintState.Type.Active) {168return;169}170171this.state = new ParameterHintState.Active({ ...this.state.hints, activeSignature });172this._onChangedHints.fire(this.state.hints);173}174175private async doTrigger(triggerId: number): Promise<boolean> {176const isRetrigger = this.state.type === ParameterHintState.Type.Active || this.state.type === ParameterHintState.Type.Pending;177const activeSignatureHelp = this.getLastActiveHints();178this.cancel(true);179180if (this._pendingTriggers.length === 0) {181return false;182}183184const context: TriggerContext = this._pendingTriggers.reduce(mergeTriggerContexts);185this._pendingTriggers = [];186187const triggerContext = {188triggerKind: context.triggerKind,189triggerCharacter: context.triggerCharacter,190isRetrigger: isRetrigger,191activeSignatureHelp: activeSignatureHelp192};193194if (!this.editor.hasModel()) {195return false;196}197198const model = this.editor.getModel();199const position = this.editor.getPosition();200201this.state = new ParameterHintState.Pending(202createCancelablePromise(token => provideSignatureHelp(this.providers, model, position, triggerContext, token)),203activeSignatureHelp);204205try {206const result = await this.state.request;207208// Check that we are still resolving the correct signature help209if (triggerId !== this.triggerId) {210result?.dispose();211212return false;213}214215if (!result || !result.value.signatures || result.value.signatures.length === 0) {216result?.dispose();217this._lastSignatureHelpResult.clear();218this.cancel();219return false;220} else {221this.state = new ParameterHintState.Active(result.value);222this._lastSignatureHelpResult.value = result;223this._onChangedHints.fire(this.state.hints);224return true;225}226} catch (error) {227if (triggerId === this.triggerId) {228this.state = ParameterHintState.Default;229}230onUnexpectedError(error);231return false;232}233}234235private getLastActiveHints(): languages.SignatureHelp | undefined {236switch (this.state.type) {237case ParameterHintState.Type.Active: return this.state.hints;238case ParameterHintState.Type.Pending: return this.state.previouslyActiveHints;239default: return undefined;240}241}242243private get isTriggered(): boolean {244return this.state.type === ParameterHintState.Type.Active245|| this.state.type === ParameterHintState.Type.Pending246|| this.throttledDelayer.isTriggered();247}248249private onModelChanged(): void {250this.cancel();251252this.triggerChars.clear();253this.retriggerChars.clear();254255const model = this.editor.getModel();256if (!model) {257return;258}259260for (const support of this.providers.ordered(model)) {261for (const ch of support.signatureHelpTriggerCharacters || []) {262if (ch.length) {263const charCode = ch.charCodeAt(0);264this.triggerChars.add(charCode);265266// All trigger characters are also considered retrigger characters267this.retriggerChars.add(charCode);268}269}270271for (const ch of support.signatureHelpRetriggerCharacters || []) {272if (ch.length) {273this.retriggerChars.add(ch.charCodeAt(0));274}275}276}277}278279private onDidType(text: string) {280if (!this.triggerOnType) {281return;282}283284const lastCharIndex = text.length - 1;285const triggerCharCode = text.charCodeAt(lastCharIndex);286287if (this.triggerChars.has(triggerCharCode) || this.isTriggered && this.retriggerChars.has(triggerCharCode)) {288this.trigger({289triggerKind: languages.SignatureHelpTriggerKind.TriggerCharacter,290triggerCharacter: text.charAt(lastCharIndex),291});292}293}294295private onCursorChange(e: ICursorSelectionChangedEvent): void {296if (e.source === 'mouse') {297this.cancel();298} else if (this.isTriggered) {299this.trigger({ triggerKind: languages.SignatureHelpTriggerKind.ContentChange });300}301}302303private onModelContentChange(): void {304if (this.isTriggered) {305this.trigger({ triggerKind: languages.SignatureHelpTriggerKind.ContentChange });306}307}308309private onEditorConfigurationChange(): void {310this.triggerOnType = this.editor.getOption(EditorOption.parameterHints).enabled;311312if (!this.triggerOnType) {313this.cancel();314}315}316317override dispose(): void {318this.cancel(true);319super.dispose();320}321}322323function mergeTriggerContexts(previous: TriggerContext, current: TriggerContext) {324switch (current.triggerKind) {325case languages.SignatureHelpTriggerKind.Invoke:326// Invoke overrides previous triggers.327return current;328329case languages.SignatureHelpTriggerKind.ContentChange:330// Ignore content changes triggers331return previous;332333case languages.SignatureHelpTriggerKind.TriggerCharacter:334default:335return current;336}337}338339340