diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index f2ff965d4b2bd..f6196071cafa6 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1426,18 +1426,22 @@ Actual: ${stringify(fullActual)}`); } } - public verifyNoSignatureHelp(markers: ReadonlyArray) { + public verifySignatureHelpPresence(expectPresent: boolean, triggerReason: ts.SignatureHelpTriggerReason | undefined, markers: ReadonlyArray) { if (markers.length) { for (const marker of markers) { this.goToMarker(marker); - this.verifyNoSignatureHelp(ts.emptyArray); + this.verifySignatureHelpPresence(expectPresent, triggerReason, ts.emptyArray); } return; } - - const actual = this.getSignatureHelp(); - if (actual) { - this.raiseError(`Expected no signature help, but got "${stringify(actual)}"`); + const actual = this.getSignatureHelp({ triggerReason }); + if (expectPresent !== !!actual) { + if (actual) { + this.raiseError(`Expected no signature help, but got "${stringify(actual)}"`); + } + else { + this.raiseError("Expected signature help, but none was returned."); + } } } @@ -1456,7 +1460,7 @@ Actual: ${stringify(fullActual)}`); } private verifySignatureHelpWorker(options: FourSlashInterface.VerifySignatureHelpOptions) { - const help = this.getSignatureHelp()!; + const help = this.getSignatureHelp({ triggerReason: options.triggerReason })!; const selectedItem = help.items[help.selectedItemIndex]; // Argument index may exceed number of parameters const currentParameter = selectedItem.parameters[help.argumentIndex] as ts.SignatureHelpParameter | undefined; @@ -1498,6 +1502,7 @@ Actual: ${stringify(fullActual)}`); const allKeys: ReadonlyArray = [ "marker", + "triggerReason", "overloadsCount", "docComment", "text", @@ -1724,7 +1729,7 @@ Actual: ${stringify(fullActual)}`); } public printCurrentParameterHelp() { - const help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition); + const help = this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition, /*options*/ undefined); Harness.IO.log(stringify(help)); } @@ -1765,12 +1770,14 @@ Actual: ${stringify(fullActual)}`); } public printCurrentSignatureHelp() { - const help = this.getSignatureHelp()!; + const help = this.getSignatureHelp(ts.emptyOptions)!; Harness.IO.log(stringify(help.items[help.selectedItemIndex])); } - private getSignatureHelp(): ts.SignatureHelpItems | undefined { - return this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition); + private getSignatureHelp({ triggerReason }: FourSlashInterface.VerifySignatureHelpOptions): ts.SignatureHelpItems | undefined { + return this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition, { + triggerReason + }); } public printCompletionListMembers(preferences: ts.UserPreferences | undefined) { @@ -1866,13 +1873,18 @@ Actual: ${stringify(fullActual)}`); offset++; if (highFidelity) { - if (ch === "(" || ch === ",") { + if (ch === "(" || ch === "," || ch === "<") { /* Signature help*/ - this.languageService.getSignatureHelpItems(this.activeFile.fileName, offset); + this.languageService.getSignatureHelpItems(this.activeFile.fileName, offset, { + triggerReason: { + kind: "characterTyped", + triggerCharacter: ch + } + }); } else if (prevChar === " " && /A-Za-z_/.test(ch)) { /* Completions */ - this.languageService.getCompletionsAtPosition(this.activeFile.fileName, offset, ts.defaultPreferences); + this.languageService.getCompletionsAtPosition(this.activeFile.fileName, offset, ts.emptyOptions); } if (i % checkCadence === 0) { @@ -2505,7 +2517,7 @@ Actual: ${stringify(fullActual)}`); `Expected '${fixId}'. Available action ids: ${ts.mapDefined(this.getCodeFixes(this.activeFile.fileName), a => a.fixId)}`); ts.Debug.assertEqual(fixWithId!.fixAllDescription, fixAllDescription); - const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings, ts.defaultPreferences); + const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings, ts.emptyOptions); assert.deepEqual | undefined>(commands, expectedCommands); assert(changes.every(c => c.fileName === this.activeFile.fileName), "TODO: support testing codefixes that touch multiple files"); this.applyChanges(changes); @@ -2585,7 +2597,7 @@ Actual: ${stringify(fullActual)}`); * Rerieves a codefix satisfying the parameters, or undefined if no such codefix is found. * @param fileName Path to file where error should be retrieved from. */ - private getCodeFixes(fileName: string, errorCode?: number, preferences: ts.UserPreferences = ts.defaultPreferences): ts.CodeFixAction[] { + private getCodeFixes(fileName: string, errorCode?: number, preferences: ts.UserPreferences = ts.emptyOptions): ts.CodeFixAction[] { const diagnosticsForCodeFix = this.getDiagnostics(fileName, /*includeSuggestions*/ true).map(diagnostic => ({ start: diagnostic.start, length: diagnostic.length, @@ -3067,7 +3079,7 @@ Actual: ${stringify(fullActual)}`); this.raiseError(`Expected action description to be ${JSON.stringify(actionDescription)}, got: ${JSON.stringify(action.description)}`); } - const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactorName, actionName, ts.defaultPreferences)!; + const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactorName, actionName, ts.emptyOptions)!; for (const edit of editInfo.edits) { this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false); } @@ -3131,7 +3143,7 @@ Actual: ${stringify(fullActual)}`); const action = ts.first(refactor.actions); assert(action.name === "Move to a new file" && action.description === "Move to a new file"); - const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.defaultPreferences)!; + const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.emptyOptions)!; this.testNewFileContents(editInfo.edits, options.newFileContents, "move to new file"); } @@ -3173,14 +3185,14 @@ Actual: ${stringify(fullActual)}`); formattingOptions = formattingOptions || this.formatCodeSettings; const markerPos = this.getMarkerByName(markerName).position; - const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, markerPos, ts.defaultPreferences); + const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, markerPos, ts.emptyOptions); const applicableRefactorToApply = ts.find(applicableRefactors, refactor => refactor.name === refactorNameToApply); if (!applicableRefactorToApply) { this.raiseError(`The expected refactor: ${refactorNameToApply} is not available at the marker location.`); } - const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, formattingOptions, markerPos, refactorNameToApply, actionName, ts.defaultPreferences)!; + const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, formattingOptions, markerPos, refactorNameToApply, actionName, ts.emptyOptions)!; for (const edit of editInfo.edits) { this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false); @@ -3377,7 +3389,7 @@ Actual: ${stringify(fullActual)}`); public getEditsForFileRename({ oldPath, newPath, newFileContents }: FourSlashInterface.GetEditsForFileRenameOptions): void { const test = (fileContents: { readonly [fileName: string]: string }, description: string): void => { - const changes = this.languageService.getEditsForFileRename(oldPath, newPath, this.formatCodeSettings, ts.defaultPreferences); + const changes = this.languageService.getEditsForFileRename(oldPath, newPath, this.formatCodeSettings, ts.emptyOptions); this.testNewFileContents(changes, fileContents, description); }; @@ -3391,7 +3403,7 @@ Actual: ${stringify(fullActual)}`); test(renameKeys(newFileContents, key => pathUpdater(key) || key), "with file moved"); } - private getApplicableRefactors(positionOrRange: number | ts.TextRange, preferences = ts.defaultPreferences): ReadonlyArray { + private getApplicableRefactors(positionOrRange: number | ts.TextRange, preferences = ts.emptyOptions): ReadonlyArray { return this.languageService.getApplicableRefactors(this.activeFile.fileName, positionOrRange, preferences) || ts.emptyArray; } } @@ -4079,7 +4091,15 @@ namespace FourSlashInterface { } public noSignatureHelp(...markers: string[]): void { - this.state.verifyNoSignatureHelp(markers); + this.state.verifySignatureHelpPresence(/*expectPresent*/ false, /*triggerReason*/ undefined, markers); + } + + public noSignatureHelpForTriggerReason(reason: ts.SignatureHelpTriggerReason, ...markers: string[]): void { + this.state.verifySignatureHelpPresence(/*expectPresent*/ false, reason, markers); + } + + public signatureHelpPresentForTriggerReason(reason: ts.SignatureHelpTriggerReason, ...markers: string[]): void { + this.state.verifySignatureHelpPresence(/*expectPresent*/ true, reason, markers); } public signatureHelp(...options: VerifySignatureHelpOptions[]): void { @@ -4810,6 +4830,7 @@ namespace FourSlashInterface { readonly isVariadic?: boolean; /** @default ts.emptyArray */ readonly tags?: ReadonlyArray; + readonly triggerReason?: ts.SignatureHelpTriggerReason; } export type ArrayOrSingle = T | ReadonlyArray; diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index e6b16efe6c7ca..e5dda42ff8bde 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -449,8 +449,8 @@ namespace Harness.LanguageService { getBreakpointStatementAtPosition(fileName: string, position: number): ts.TextSpan { return unwrapJSONCallResult(this.shim.getBreakpointStatementAtPosition(fileName, position)); } - getSignatureHelpItems(fileName: string, position: number): ts.SignatureHelpItems { - return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position)); + getSignatureHelpItems(fileName: string, position: number, options: ts.SignatureHelpItemsOptions | undefined): ts.SignatureHelpItems { + return unwrapJSONCallResult(this.shim.getSignatureHelpItems(fileName, position, options)); } getRenameInfo(fileName: string, position: number): ts.RenameInfo { return unwrapJSONCallResult(this.shim.getRenameInfo(fileName, position)); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index af3ddfe33ec8f..9e86ef849a50a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -477,7 +477,7 @@ namespace ts.server { this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host), - preferences: defaultPreferences, + preferences: emptyOptions, hostInfo: "Unknown host", extraFileExtensions: [] }; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 23894a0f67df0..f4b251f870229 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1789,6 +1789,10 @@ namespace ts.server.protocol { * Optional prefix to apply to possible completions. */ prefix?: string; + /** + * Character that was responsible for triggering completion. + * Should be `undefined` if a user manually requested completion. + */ triggerCharacter?: CompletionsTriggerCharacter; /** * @deprecated Use UserPreferences.includeCompletionsForModuleExports @@ -2062,10 +2066,58 @@ namespace ts.server.protocol { argumentCount: number; } + export type SignatureHelpTriggerCharacter = "," | "(" | "<"; + export type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; + + /** + * Arguments of a signature help request. + */ + export interface SignatureHelpRequestArgs extends FileLocationRequestArgs { + /** + * Reason why signature help was invoked. + * See each individual possible + */ + triggerReason?: SignatureHelpTriggerReason; + } + + export type SignatureHelpTriggerReason = + | SignatureHelpInvokedReason + | SignatureHelpCharacterTypedReason + | SignatureHelpRetriggeredReason; + /** - * Arguments of a signature help request. + * Signals that the user manually requested signature help. + * The language service will unconditionally attempt to provide a result. */ - export interface SignatureHelpRequestArgs extends FileLocationRequestArgs { + export interface SignatureHelpInvokedReason { + kind: "invoked"; + triggerCharacter?: undefined; + } + + /** + * Signals that the signature help request came from a user typing a character. + * Depending on the character and the syntactic context, the request may or may not be served a result. + */ + export interface SignatureHelpCharacterTypedReason { + kind: "characterTyped"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter: SignatureHelpTriggerCharacter; + } + + /** + * Signals that this signature help request came from typing a character or moving the cursor. + * This should only occur if a signature help session was already active and the editor needs to see if it should adjust. + * The language service will unconditionally attempt to provide a result. + * `triggerCharacter` can be `undefined` for a retrigger caused by a cursor move. + */ + export interface SignatureHelpRetriggeredReason { + kind: "retrigger"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter?: SignatureHelpRetriggerCharacter; } /** diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 396c9963b54a7..f35ac090f295c 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -432,7 +432,7 @@ namespace ts.server { if (preferences) { if (!this.preferences) { - this.preferences = defaultPreferences; + this.preferences = emptyOptions; } this.preferences = { ...this.preferences, ...preferences }; } diff --git a/src/server/session.ts b/src/server/session.ts index 972743d392195..0e5ba24c21b5f 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1434,7 +1434,7 @@ namespace ts.server { const { file, project } = this.getFileAndProject(args); const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!; const position = this.getPosition(args, scriptInfo); - const helpItems = project.getLanguageService().getSignatureHelpItems(file, position); + const helpItems = project.getLanguageService().getSignatureHelpItems(file, position, args); if (!helpItems) { return undefined; } diff --git a/src/services/services.ts b/src/services/services.ts index b2a2fdaaae21e..b690febdb2701 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1396,7 +1396,7 @@ namespace ts { return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)]; } - function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = defaultPreferences): CompletionInfo | undefined { + function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = emptyOptions): CompletionInfo | undefined { // Convert from deprecated options names to new names const fullPreferences: UserPreferences = { ...identity(options), // avoid excess property check @@ -1414,7 +1414,7 @@ namespace ts { options.triggerCharacter); } - function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = defaultPreferences): CompletionEntryDetails | undefined { + function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions): CompletionEntryDetails | undefined { synchronizeHostData(); return Completions.getCompletionEntryDetails( program, @@ -1756,12 +1756,12 @@ namespace ts { /** * This is a semantic operation. */ - function getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined { + function getSignatureHelpItems(fileName: string, position: number, { triggerReason }: SignatureHelpItemsOptions = emptyOptions): SignatureHelpItems | undefined { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); - return SignatureHelp.getSignatureHelpItems(program, sourceFile, position, cancellationToken); + return SignatureHelp.getSignatureHelpItems(program, sourceFile, position, triggerReason, cancellationToken); } /// Syntactic features @@ -1940,7 +1940,7 @@ namespace ts { return []; } - function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings, preferences: UserPreferences = defaultPreferences): ReadonlyArray { + function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: ReadonlyArray, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): ReadonlyArray { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const span = createTextSpanFromBounds(start, end); @@ -1952,7 +1952,7 @@ namespace ts { }); } - function getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences = defaultPreferences): CombinedCodeActions { + function getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): CombinedCodeActions { synchronizeHostData(); Debug.assert(scope.type === "file"); const sourceFile = getValidSourceFile(scope.fileName); @@ -1961,7 +1961,7 @@ namespace ts { return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext, preferences }); } - function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences = defaultPreferences): ReadonlyArray { + function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): ReadonlyArray { synchronizeHostData(); Debug.assert(scope.type === "file"); const sourceFile = getValidSourceFile(scope.fileName); @@ -1970,7 +1970,7 @@ namespace ts { return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences); } - function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = defaultPreferences): ReadonlyArray { + function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): ReadonlyArray { return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions), preferences); } @@ -2219,7 +2219,7 @@ namespace ts { }; } - function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = defaultPreferences): ApplicableRefactorInfo[] { + function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): ApplicableRefactorInfo[] { synchronizeHostData(); const file = getValidSourceFile(fileName); return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences)); @@ -2231,7 +2231,7 @@ namespace ts { positionOrRange: number | TextRange, refactorName: string, actionName: string, - preferences: UserPreferences = defaultPreferences, + preferences: UserPreferences = emptyOptions, ): RefactorEditInfo | undefined { synchronizeHostData(); const file = getValidSourceFile(fileName); diff --git a/src/services/shims.ts b/src/services/shims.ts index 0ea27b4151f79..0776f55c9058f 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -158,7 +158,7 @@ namespace ts { getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): string; getBreakpointStatementAtPosition(fileName: string, position: number): string; - getSignatureHelpItems(fileName: string, position: number): string; + getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): string; /** * Returns a JSON-encoded value of the type: @@ -769,10 +769,10 @@ namespace ts { /// SIGNATUREHELP - public getSignatureHelpItems(fileName: string, position: number): string { + public getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): string { return this.forwardJSONCall( `getSignatureHelpItems('${fileName}', ${position})`, - () => this.languageService.getSignatureHelpItems(fileName, position) + () => this.languageService.getSignatureHelpItems(fileName, position, options) ); } diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index ac13a2c790447..2884f7a7a0ccc 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -19,7 +19,7 @@ namespace ts.SignatureHelp { argumentCount: number; } - export function getSignatureHelpItems(program: Program, sourceFile: SourceFile, position: number, cancellationToken: CancellationToken): SignatureHelpItems | undefined { + export function getSignatureHelpItems(program: Program, sourceFile: SourceFile, position: number, triggerReason: SignatureHelpTriggerReason | undefined, cancellationToken: CancellationToken): SignatureHelpItems | undefined { const typeChecker = program.getTypeChecker(); // Decide whether to show signature help @@ -29,6 +29,13 @@ namespace ts.SignatureHelp { return undefined; } + if (shouldCarefullyCheckContext(triggerReason)) { + // In the middle of a string, don't provide signature help unless the user explicitly requested it. + if (isInString(sourceFile, position, startingToken)) { + return undefined; + } + } + const argumentInfo = getContainingArgumentInfo(startingToken, position, sourceFile); if (!argumentInfo) return undefined; @@ -50,6 +57,11 @@ namespace ts.SignatureHelp { return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(candidateInfo.candidates, candidateInfo.resolvedSignature, argumentInfo, sourceFile, typeChecker)); } + function shouldCarefullyCheckContext(reason: SignatureHelpTriggerReason | undefined) { + // Only need to be careful if the user typed a character and signature help wasn't showing. + return !!reason && reason.kind === "characterTyped"; + } + function getCandidateInfo(argumentInfo: ArgumentListInfo, checker: TypeChecker): { readonly candidates: ReadonlyArray, readonly resolvedSignature: Signature } | undefined { const { invocation } = argumentInfo; if (invocation.kind === InvocationKind.Call) { diff --git a/src/services/types.ts b/src/services/types.ts index be897f20cd43c..10b342990b1ff 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -242,7 +242,7 @@ namespace ts { readonly allowTextChangesInNewFiles?: boolean; } /* @internal */ - export const defaultPreferences: UserPreferences = {}; + export const emptyOptions = {}; // // Public services of a language service instance associated @@ -292,7 +292,7 @@ namespace ts { getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; - getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined; + getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] | undefined; @@ -371,7 +371,10 @@ namespace ts { export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<"; export interface GetCompletionsAtPositionOptions extends UserPreferences { - /** If the editor is asking for completions because a certain character was typed, and not because the user explicitly requested them, this should be set. */ + /** + * If the editor is asking for completions because a certain character was typed + * (as opposed to when the user explicitly requested them) this should be set. + */ triggerCharacter?: CompletionsTriggerCharacter; /** @deprecated Use includeCompletionsForModuleExports */ includeExternalModuleExports?: boolean; @@ -379,6 +382,53 @@ namespace ts { includeInsertTextCompletions?: boolean; } + export type SignatureHelpTriggerCharacter = "," | "(" | "<"; + export type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; + + export interface SignatureHelpItemsOptions { + triggerReason?: SignatureHelpTriggerReason; + } + + export type SignatureHelpTriggerReason = + | SignatureHelpInvokedReason + | SignatureHelpCharacterTypedReason + | SignatureHelpRetriggeredReason; + + /** + * Signals that the user manually requested signature help. + * The language service will unconditionally attempt to provide a result. + */ + export interface SignatureHelpInvokedReason { + kind: "invoked"; + triggerCharacter?: undefined; + } + + /** + * Signals that the signature help request came from a user typing a character. + * Depending on the character and the syntactic context, the request may or may not be served a result. + */ + export interface SignatureHelpCharacterTypedReason { + kind: "characterTyped"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter: SignatureHelpTriggerCharacter; + } + + /** + * Signals that this signature help request came from typing a character or moving the cursor. + * This should only occur if a signature help session was already active and the editor needs to see if it should adjust. + * The language service will unconditionally attempt to provide a result. + * `triggerCharacter` can be `undefined` for a retrigger caused by a cursor move. + */ + export interface SignatureHelpRetriggeredReason { + kind: "retrigger"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter?: SignatureHelpRetriggerCharacter; + } + export interface ApplyCodeActionCommandResult { successMessage: string; } diff --git a/src/testRunner/unittests/cancellableLanguageServiceOperations.ts b/src/testRunner/unittests/cancellableLanguageServiceOperations.ts index a5947cc17e3dd..37f829d67a7b9 100644 --- a/src/testRunner/unittests/cancellableLanguageServiceOperations.ts +++ b/src/testRunner/unittests/cancellableLanguageServiceOperations.ts @@ -8,7 +8,7 @@ namespace ts { `; it("can cancel signature help mid-request", () => { verifyOperationCancelledAfter(file, 4, service => // Two calls are top-level in services, one is the root type, and the second should be for the parameter type - service.getSignatureHelpItems("file.ts", file.lastIndexOf("f"))!, + service.getSignatureHelpItems("file.ts", file.lastIndexOf("f"), emptyOptions)!, r => assert.exists(r.items[0]) ); }); diff --git a/src/testRunner/unittests/extractTestHelpers.ts b/src/testRunner/unittests/extractTestHelpers.ts index c36ac88e8e0ac..d812eb96f88b9 100644 --- a/src/testRunner/unittests/extractTestHelpers.ts +++ b/src/testRunner/unittests/extractTestHelpers.ts @@ -124,7 +124,7 @@ namespace ts { endPosition: selectionRange.end, host: notImplementedHost, formatContext: formatting.getFormatContext(testFormatOptions), - preferences: defaultPreferences, + preferences: emptyOptions, }; const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); assert.equal(rangeToExtract.errors, undefined, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); @@ -188,7 +188,7 @@ namespace ts { endPosition: selectionRange.end, host: notImplementedHost, formatContext: formatting.getFormatContext(testFormatOptions), - preferences: defaultPreferences, + preferences: emptyOptions, }; const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromRange(selectionRange)); assert.isUndefined(rangeToExtract.errors, rangeToExtract.errors && "Range error: " + rangeToExtract.errors[0].messageText); diff --git a/src/testRunner/unittests/organizeImports.ts b/src/testRunner/unittests/organizeImports.ts index b889f97ef1c6d..7fe99ffc784ee 100644 --- a/src/testRunner/unittests/organizeImports.ts +++ b/src/testRunner/unittests/organizeImports.ts @@ -270,7 +270,7 @@ export const Other = 1; content: "function F() { }", }; const languageService = makeLanguageService(testFile); - const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, testFormatOptions, defaultPreferences); + const changes = languageService.organizeImports({ type: "file", fileName: testFile.path }, testFormatOptions, emptyOptions); assert.isEmpty(changes); }); @@ -741,7 +741,7 @@ export * from "lib"; function runBaseline(baselinePath: string, testFile: TestFSWithWatch.File, ...otherFiles: TestFSWithWatch.File[]) { const { path: testPath, content: testContent } = testFile; const languageService = makeLanguageService(testFile, ...otherFiles); - const changes = languageService.organizeImports({ type: "file", fileName: testPath }, testFormatOptions, defaultPreferences); + const changes = languageService.organizeImports({ type: "file", fileName: testPath }, testFormatOptions, emptyOptions); assert.equal(changes.length, 1); assert.equal(changes[0].fileName, testPath); diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 44064f15c5da9..abd8259edc91a 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -1525,13 +1525,13 @@ namespace ts.projectSystem { service.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, defaultPreferences)!; + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; // should contain completions for string assert.isTrue(completions1.entries.some(e => e.name === "charAt"), "should contain 'charAt'"); assert.isFalse(completions1.entries.some(e => e.name === "toExponential"), "should not contain 'toExponential'"); service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, defaultPreferences)!; + const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 2, emptyOptions)!; // should contain completions for string assert.isFalse(completions2.entries.some(e => e.name === "charAt"), "should not contain 'charAt'"); assert.isTrue(completions2.entries.some(e => e.name === "toExponential"), "should contain 'toExponential'"); @@ -1557,11 +1557,11 @@ namespace ts.projectSystem { service.checkNumberOfProjects({ externalProjects: 1 }); checkProjectActualFiles(service.externalProjects[0], [f1.path, f2.path, libFile.path]); - const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, defaultPreferences)!; + const completions1 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; assert.isTrue(completions1.entries.some(e => e.name === "somelongname"), "should contain 'somelongname'"); service.closeClientFile(f2.path); - const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, defaultPreferences)!; + const completions2 = service.externalProjects[0].getLanguageService().getCompletionsAtPosition(f1.path, 0, emptyOptions)!; assert.isFalse(completions2.entries.some(e => e.name === "somelongname"), "should not contain 'somelongname'"); const sf2 = service.externalProjects[0].getLanguageService().getProgram()!.getSourceFile(f2.path)!; assert.equal(sf2.text, ""); @@ -2201,7 +2201,7 @@ namespace ts.projectSystem { // Check identifiers defined in HTML content are available in .ts file const project = configuredProjectAt(projectService, 0); - let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, defaultPreferences); + let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1, emptyOptions); assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`); // Close HTML file @@ -2215,7 +2215,7 @@ namespace ts.projectSystem { checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); // Check identifiers defined in HTML content are not available in .ts file - completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, defaultPreferences); + completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5, emptyOptions); assert(completions && completions.entries[0].name !== "hello", `unexpected hello entry in completion list`); }); @@ -8701,7 +8701,7 @@ export const x = 10;` Debug.assert(!!project.resolveModuleNames); - const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatOptions, defaultPreferences); + const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatOptions, emptyOptions); assert.deepEqual>(edits, [{ fileName: "/user.ts", textChanges: [{ diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 5f8ff55dae281..ee7b37373ca14 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -9959,7 +9959,7 @@ declare namespace ts { readonly importModuleSpecifierPreference?: "relative" | "non-relative"; readonly allowTextChangesInNewFiles?: boolean; } - const defaultPreferences: UserPreferences; + const emptyOptions: {}; interface LanguageService { cleanupSemanticCache(): void; getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[]; @@ -9983,7 +9983,7 @@ declare namespace ts { getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined; getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan | undefined; getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; - getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined; + getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] | undefined; getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] | undefined; @@ -10044,13 +10044,54 @@ declare namespace ts { type OrganizeImportsScope = CombinedCodeFixScope; type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<"; interface GetCompletionsAtPositionOptions extends UserPreferences { - /** If the editor is asking for completions because a certain character was typed, and not because the user explicitly requested them, this should be set. */ + /** + * If the editor is asking for completions because a certain character was typed + * (as opposed to when the user explicitly requested them) this should be set. + */ triggerCharacter?: CompletionsTriggerCharacter; /** @deprecated Use includeCompletionsForModuleExports */ includeExternalModuleExports?: boolean; /** @deprecated Use includeCompletionsWithInsertText */ includeInsertTextCompletions?: boolean; } + type SignatureHelpTriggerCharacter = "," | "(" | "<"; + type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; + interface SignatureHelpItemsOptions { + triggerReason?: SignatureHelpTriggerReason; + } + type SignatureHelpTriggerReason = SignatureHelpInvokedReason | SignatureHelpCharacterTypedReason | SignatureHelpRetriggeredReason; + /** + * Signals that the user manually requested signature help. + * The language service will unconditionally attempt to provide a result. + */ + interface SignatureHelpInvokedReason { + kind: "invoked"; + triggerCharacter?: undefined; + } + /** + * Signals that the signature help request came from a user typing a character. + * Depending on the character and the syntactic context, the request may or may not be served a result. + */ + interface SignatureHelpCharacterTypedReason { + kind: "characterTyped"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter: SignatureHelpTriggerCharacter; + } + /** + * Signals that this signature help request came from typing a character or moving the cursor. + * This should only occur if a signature help session was already active and the editor needs to see if it should adjust. + * The language service will unconditionally attempt to provide a result. + * `triggerCharacter` can be `undefined` for a retrigger caused by a cursor move. + */ + interface SignatureHelpRetriggeredReason { + kind: "retrigger"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter?: SignatureHelpRetriggerCharacter; + } interface ApplyCodeActionCommandResult { successMessage: string; } @@ -11231,7 +11272,7 @@ declare namespace ts.Rename { function getRenameInfo(program: Program, sourceFile: SourceFile, position: number): RenameInfo; } declare namespace ts.SignatureHelp { - function getSignatureHelpItems(program: Program, sourceFile: SourceFile, position: number, cancellationToken: CancellationToken): SignatureHelpItems | undefined; + function getSignatureHelpItems(program: Program, sourceFile: SourceFile, position: number, triggerReason: SignatureHelpTriggerReason | undefined, cancellationToken: CancellationToken): SignatureHelpItems | undefined; interface ArgumentInfoForCompletions { readonly invocation: CallLikeExpression; readonly argumentIndex: number; @@ -11926,7 +11967,7 @@ declare namespace ts { getQuickInfoAtPosition(fileName: string, position: number): string; getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): string; getBreakpointStatementAtPosition(fileName: string, position: number): string; - getSignatureHelpItems(fileName: string, position: number): string; + getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): string; /** * Returns a JSON-encoded value of the type: * { canRename: boolean, localizedErrorMessage: string, displayName: string, fullDisplayName: string, kind: string, kindModifiers: string, triggerSpan: { start; length } } @@ -12961,7 +13002,23 @@ declare namespace ts.server.protocol { argumentIndex: number; argumentCount: number; } + type SignatureHelpTriggerCharacter = "," | "(" | "<"; + type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; interface SignatureHelpRequestArgs extends FileLocationRequestArgs { + triggerReason?: SignatureHelpTriggerReason; + } + type SignatureHelpTriggerReason = SignatureHelpInvokedReason | SignatureHelpCharacterTypedReason | SignatureHelpRetriggeredReason; + interface SignatureHelpInvokedReason { + kind: "invoked"; + triggerCharacter?: undefined; + } + interface SignatureHelpCharacterTypedReason { + kind: "characterTyped"; + triggerCharacter: SignatureHelpTriggerCharacter; + } + interface SignatureHelpRetriggeredReason { + kind: "retrigger"; + triggerCharacter?: SignatureHelpRetriggerCharacter; } interface SignatureHelpRequest extends FileLocationRequest { command: CommandTypes.SignatureHelp; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 6b63e3febdac3..d129d7839a8c9 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4736,7 +4736,7 @@ declare namespace ts { getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined; getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan | undefined; getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined; - getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined; + getSignatureHelpItems(fileName: string, position: number, options: SignatureHelpItemsOptions | undefined): SignatureHelpItems | undefined; getRenameInfo(fileName: string, position: number): RenameInfo; findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] | undefined; getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] | undefined; @@ -4796,13 +4796,54 @@ declare namespace ts { type OrganizeImportsScope = CombinedCodeFixScope; type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<"; interface GetCompletionsAtPositionOptions extends UserPreferences { - /** If the editor is asking for completions because a certain character was typed, and not because the user explicitly requested them, this should be set. */ + /** + * If the editor is asking for completions because a certain character was typed + * (as opposed to when the user explicitly requested them) this should be set. + */ triggerCharacter?: CompletionsTriggerCharacter; /** @deprecated Use includeCompletionsForModuleExports */ includeExternalModuleExports?: boolean; /** @deprecated Use includeCompletionsWithInsertText */ includeInsertTextCompletions?: boolean; } + type SignatureHelpTriggerCharacter = "," | "(" | "<"; + type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; + interface SignatureHelpItemsOptions { + triggerReason?: SignatureHelpTriggerReason; + } + type SignatureHelpTriggerReason = SignatureHelpInvokedReason | SignatureHelpCharacterTypedReason | SignatureHelpRetriggeredReason; + /** + * Signals that the user manually requested signature help. + * The language service will unconditionally attempt to provide a result. + */ + interface SignatureHelpInvokedReason { + kind: "invoked"; + triggerCharacter?: undefined; + } + /** + * Signals that the signature help request came from a user typing a character. + * Depending on the character and the syntactic context, the request may or may not be served a result. + */ + interface SignatureHelpCharacterTypedReason { + kind: "characterTyped"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter: SignatureHelpTriggerCharacter; + } + /** + * Signals that this signature help request came from typing a character or moving the cursor. + * This should only occur if a signature help session was already active and the editor needs to see if it should adjust. + * The language service will unconditionally attempt to provide a result. + * `triggerCharacter` can be `undefined` for a retrigger caused by a cursor move. + */ + interface SignatureHelpRetriggeredReason { + kind: "retrigger"; + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter?: SignatureHelpRetriggerCharacter; + } interface ApplyCodeActionCommandResult { successMessage: string; } diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 0753ded25c903..9c68127d74437 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -139,7 +139,6 @@ declare namespace FourSlashInterface { file(name: string, content?: string, scriptKindName?: string): any; select(startMarker: string, endMarker: string): void; selectRange(range: Range): void; - selectAllInFile(fileName: string): void; } class verifyNegatable { private negative; @@ -179,7 +178,7 @@ declare namespace FourSlashInterface { isInCommentAtPosition(onlyMultiLineDiverges?: boolean): void; codeFix(options: { description: string, - newFileContent?: NewFileContent, + newFileContent?: string | { readonly [fileName: string]: string }, newRangeContent?: string, errorCode?: number, index?: number, @@ -191,7 +190,6 @@ declare namespace FourSlashInterface { applicableRefactorAvailableForRange(): void; refactorAvailable(name: string, actionName?: string): void; - refactorsAvailable(names: ReadonlyArray): void; refactor(options: { name: string; actionName: string; @@ -257,14 +255,16 @@ declare namespace FourSlashInterface { * For each of starts, asserts the ranges that are referenced from there. * This uses the 'findReferences' command instead of 'getReferencesAtPosition', so references are grouped by their definition. */ - referenceGroups(starts: ArrayOrSingle | ArrayOrSingle, parts: Array): void; + referenceGroups(starts: ArrayOrSingle | ArrayOrSingle, parts: Array<{ definition: ReferencesDefinition, ranges: Range[] }>): void; singleReferenceGroup(definition: ReferencesDefinition, ranges?: Range[]): void; rangesAreOccurrences(isWriteAccess?: boolean): void; rangesWithSameTextAreRenameLocations(): void; rangesAreRenameLocations(options?: Range[] | { findInStrings?: boolean, findInComments?: boolean, ranges?: Range[] }); findReferencesDefinitionDisplayPartsAtCaretAre(expected: ts.SymbolDisplayPart[]): void; noSignatureHelp(...markers: string[]): void; - signatureHelp(...options: VerifySignatureHelpOptions[]): void; + noSignatureHelpForTriggerReason(triggerReason: SignatureHelpTriggerReason, ...markers: string[]): void + signatureHelpPresentForTriggerReason(triggerReason: SignatureHelpTriggerReason, ...markers: string[]): void + signatureHelp(...options: VerifySignatureHelpOptions[], ): void; // Checks that there are no compile errors. noErrors(): void; numberOfErrorsInCurrentFile(expected: number): void; @@ -337,7 +337,7 @@ declare namespace FourSlashInterface { getEditsForFileRename(options: { oldPath: string; newPath: string; - newFileContents: { readonly [fileName: string]: string }; + newFileContents: { [fileName: string]: string }; }): void; moveToNewFile(options: { readonly newFileContents: { readonly [fileName: string]: string }; @@ -358,7 +358,7 @@ declare namespace FourSlashInterface { enableFormatting(): void; disableFormatting(): void; - applyRefactor(options: { refactorName: string, actionName: string, actionDescription: string, newContent: NewFileContent }): void; + applyRefactor(options: { refactorName: string, actionName: string, actionDescription: string, newContent: string }): void; } class debug { printCurrentParameterHelp(): void; @@ -513,10 +513,6 @@ declare namespace FourSlashInterface { text: string; range: Range; } - interface ReferenceGroup { - readonly definition: ReferencesDefinition; - readonly ranges: ReadonlyArray; - } interface Diagnostic { message: string; /** @default `test.ranges()[0]` */ @@ -566,6 +562,47 @@ declare namespace FourSlashInterface { argumentCount?: number; isVariadic?: boolean; tags?: ReadonlyArray; + triggerReason?: SignatureHelpTriggerReason; + } + + export type SignatureHelpTriggerReason = + | SignatureHelpInvokedReason + | SignatureHelpCharacterTypedReason + | SignatureHelpRetriggeredReason; + + /** + * Signals that the user manually requested signature help. + * The language service will unconditionally attempt to provide a result. + */ + export interface SignatureHelpInvokedReason { + kind: "invoked", + triggerCharacter?: undefined, + } + + /** + * Signals that the signature help request came from a user typing a character. + * Depending on the character and the syntactic context, the request may or may not be served a result. + */ + export interface SignatureHelpCharacterTypedReason { + kind: "characterTyped", + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter: string, + } + + /** + * Signals that this signature help request came from typing a character or moving the cursor. + * This should only occur if a signature help session was already active and the editor needs to see if it should adjust. + * The language service will unconditionally attempt to provide a result. + * `triggerCharacter` can be `undefined` for a retrigger caused by a cursor move. + */ + export interface SignatureHelpRetriggeredReason { + kind: "retrigger", + /** + * Character that was responsible for triggering signature help. + */ + triggerCharacter?: string, } interface JSDocTagInfo { @@ -574,7 +611,6 @@ declare namespace FourSlashInterface { } type ArrayOrSingle = T | ReadonlyArray; - type NewFileContent = string | { readonly [fileName: string]: string }; } declare function verifyOperationIsCancelled(f: any): void; declare var test: FourSlashInterface.test_; diff --git a/tests/cases/fourslash/signatureHelpFilteredTriggerCharacters01.ts b/tests/cases/fourslash/signatureHelpFilteredTriggerCharacters01.ts new file mode 100644 index 0000000000000..ed97ef020d07e --- /dev/null +++ b/tests/cases/fourslash/signatureHelpFilteredTriggerCharacters01.ts @@ -0,0 +1,23 @@ +/// + +////function foo(x: T): T { +//// throw null; +////} +//// +////foo("/**/") + +goTo.marker(); +for (const triggerCharacter of ["<", "(", ","]) { + edit.insert(triggerCharacter); + verify.noSignatureHelpForTriggerReason({ + kind: "characterTyped", + triggerCharacter, + }); + verify.signatureHelpPresentForTriggerReason({ + kind: "retrigger", + triggerCharacter, + }); + edit.backspace(); +} +verify.signatureHelpPresentForTriggerReason(/*triggerReason*/ undefined); +verify.signatureHelpPresentForTriggerReason({ kind: "invoked" }); \ No newline at end of file