diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 99d6ff44b21..cb7cccb5470 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1426,17 +1426,22 @@ Actual: ${stringify(fullActual)}`); } } - public verifyNoSignatureHelp(triggerCharacter: ts.SignatureHelpTriggerCharacter | undefined, 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(triggerCharacter, ts.emptyArray); + this.verifySignatureHelpPresence(expectPresent, triggerReason, ts.emptyArray); } return; } - const actual = this.getSignatureHelp({ triggerCharacter }); - 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.") + } } } @@ -1455,7 +1460,7 @@ Actual: ${stringify(fullActual)}`); } private verifySignatureHelpWorker(options: FourSlashInterface.VerifySignatureHelpOptions) { - const help = this.getSignatureHelp({ triggerCharacter: options.triggerCharacter })!; + 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; @@ -1497,7 +1502,7 @@ Actual: ${stringify(fullActual)}`); const allKeys: ReadonlyArray = [ "marker", - "triggerCharacter", + "triggerReason", "overloadsCount", "docComment", "text", @@ -1769,9 +1774,9 @@ Actual: ${stringify(fullActual)}`); Harness.IO.log(stringify(help.items[help.selectedItemIndex])); } - private getSignatureHelp({ triggerCharacter }: FourSlashInterface.VerifySignatureHelpOptions): ts.SignatureHelpItems | undefined { + private getSignatureHelp({ triggerReason }: FourSlashInterface.VerifySignatureHelpOptions): ts.SignatureHelpItems | undefined { return this.languageService.getSignatureHelpItems(this.activeFile.fileName, this.currentCaretPosition, { - triggerCharacter + triggerReason }); } @@ -1870,7 +1875,12 @@ Actual: ${stringify(fullActual)}`); if (highFidelity) { if (ch === "(" || ch === "," || ch === "<") { /* Signature help*/ - this.languageService.getSignatureHelpItems(this.activeFile.fileName, offset, { triggerCharacter: ch }); + this.languageService.getSignatureHelpItems(this.activeFile.fileName, offset, { + triggerReason: { + kind: "characterTyped", + triggerCharacter: ch + } + }); } else if (prevChar === " " && /A-Za-z_/.test(ch)) { /* Completions */ @@ -4081,11 +4091,15 @@ namespace FourSlashInterface { } public noSignatureHelp(...markers: string[]): void { - this.state.verifyNoSignatureHelp(/*triggerCharacter*/ undefined, markers); + this.state.verifySignatureHelpPresence(/*expectPresent*/ false, /*triggerReason*/ undefined, markers); } - public noSignatureHelpForTriggerCharacter(triggerCharacter: ts.SignatureHelpTriggerCharacter, ...markers: string[]): void { - this.state.verifyNoSignatureHelp(triggerCharacter, 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 { @@ -4816,7 +4830,7 @@ namespace FourSlashInterface { readonly isVariadic?: boolean; /** @default ts.emptyArray */ readonly tags?: ReadonlyArray; - readonly triggerCharacter?: ts.SignatureHelpTriggerCharacter; + readonly triggerReason?: ts.SignatureHelpTriggerReason; } export type ArrayOrSingle = T | ReadonlyArray; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 5b5ba4fa5a6..ad70ca4f258 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2067,16 +2067,57 @@ namespace ts.server.protocol { } export type SignatureHelpTriggerCharacter = "," | "(" | "<"; + export type SignatureHelpRetriggerCharacter = SignatureHelpTriggerCharacter | ")"; - /** - * Arguments of a signature help request. - */ + /** + * Arguments of a signature help request. + */ export interface SignatureHelpRequestArgs extends FileLocationRequestArgs { /** - * Character that was responsible for triggering signature help. - * Should be `undefined` if a user manually requested completion. + * Reason why signature help was invoked. + * See each individual possible */ - triggerCharacter?: SignatureHelpTriggerCharacter; + 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, } /** diff --git a/src/services/services.ts b/src/services/services.ts index 9278f41b82d..b690febdb27 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1756,12 +1756,12 @@ namespace ts { /** * This is a semantic operation. */ - function getSignatureHelpItems(fileName: string, position: number, { triggerCharacter }: SignatureHelpItemsOptions = emptyOptions): SignatureHelpItems | undefined { + function getSignatureHelpItems(fileName: string, position: number, { triggerReason }: SignatureHelpItemsOptions = emptyOptions): SignatureHelpItems | undefined { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); - return SignatureHelp.getSignatureHelpItems(program, sourceFile, position, triggerCharacter, cancellationToken); + return SignatureHelp.getSignatureHelpItems(program, sourceFile, position, triggerReason, cancellationToken); } /// Syntactic features diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index a183cdab1b4..2884f7a7a0c 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, triggerCharacter: SignatureHelpTriggerCharacter | undefined, 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,9 +29,11 @@ namespace ts.SignatureHelp { return undefined; } - // In the middle of a string, don't provide signature help unless the user explicitly requested it. - if (triggerCharacter !== undefined && isInString(sourceFile, position, startingToken)) { - 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); @@ -55,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 d9a305895db..e586f486a0b 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -383,13 +383,50 @@ namespace ts { } 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", /** - * If the editor is asking for signature help because a certain character was typed - * (as opposed to when the user explicitly requested them) this should be set. + * Character that was responsible for triggering signature help. */ - triggerCharacter?: SignatureHelpTriggerCharacter; + 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 { diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 2cdd88bdb9c..9c68127d744 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,15 @@ 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; - noSignatureHelpForTriggerCharacter(triggerCharacter: string, ...markers: string[]): 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; @@ -338,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 }; @@ -359,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; @@ -514,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]` */ @@ -567,7 +562,47 @@ declare namespace FourSlashInterface { argumentCount?: number; isVariadic?: boolean; tags?: ReadonlyArray; - triggerCharacter?: string; + 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 { @@ -576,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_;