Skip to content

Commit ec05f29

Browse files
authored
Make signature help node building cancellable (#23543)
* Make token building cancellable * Scope cancellation token, make find all refs and quickinfo cancellable * Make completion entry details cancellable * Actually accept public API update * Add test verifying cancellations within checker for select language service operations * Document runWithCancellationToken a bit more * Add post-cancellation verification to test
1 parent 583bcea commit ec05f29

File tree

9 files changed

+152
-14
lines changed

9 files changed

+152
-14
lines changed

src/compiler/checker.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,16 @@ namespace ts {
325325
return diagnostics;
326326
}
327327
},
328+
329+
runWithCancellationToken: (token, callback) => {
330+
try {
331+
cancellationToken = token;
332+
return callback(checker);
333+
}
334+
finally {
335+
cancellationToken = undefined;
336+
}
337+
}
328338
};
329339

330340
const tupleTypes: GenericType[] = [];
@@ -2988,6 +2998,9 @@ namespace ts {
29882998
}
29892999

29903000
function typeToTypeNodeHelper(type: Type, context: NodeBuilderContext): TypeNode {
3001+
if (cancellationToken && cancellationToken.throwIfCancellationRequested) {
3002+
cancellationToken.throwIfCancellationRequested();
3003+
}
29913004
const inTypeAlias = context.flags & NodeBuilderFlags.InTypeAlias;
29923005
context.flags &= ~NodeBuilderFlags.InTypeAlias;
29933006

src/compiler/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3000,6 +3000,13 @@ namespace ts {
30003000
* Others are added in computeSuggestionDiagnostics.
30013001
*/
30023002
/* @internal */ getSuggestionDiagnostics(file: SourceFile): ReadonlyArray<Diagnostic>;
3003+
3004+
/**
3005+
* Depending on the operation performed, it may be appropriate to throw away the checker
3006+
* if the cancellation token is triggered. Typically, if it is used for error checking
3007+
* and the operation is cancelled, then it should be discarded, otherwise it is safe to keep.
3008+
*/
3009+
runWithCancellationToken<T>(token: CancellationToken, cb: (checker: TypeChecker) => T): T;
30033010
}
30043011

30053012
/* @internal */
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/// <reference path="..\harness.ts" />
2+
3+
namespace ts {
4+
describe("cancellableLanguageServiceOperations", () => {
5+
const file = `
6+
function foo(): void;
7+
function foo<T>(x: T): T;
8+
function foo<T>(x?: T): T | void {}
9+
foo(f);
10+
`;
11+
it("can cancel signature help mid-request", () => {
12+
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
13+
service.getSignatureHelpItems("file.ts", file.lastIndexOf("f")),
14+
r => assert.exists(r.items[0])
15+
);
16+
});
17+
18+
it("can cancel find all references mid-request", () => {
19+
verifyOperationCancelledAfter(file, 3, service => // Two calls are top-level in services, one is the root type
20+
service.findReferences("file.ts", file.lastIndexOf("o")),
21+
r => assert.exists(r[0].definition)
22+
);
23+
});
24+
25+
it("can cancel quick info mid-request", () => {
26+
verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for quickinfo, so the first check is within the checker
27+
service.getQuickInfoAtPosition("file.ts", file.lastIndexOf("o")),
28+
r => assert.exists(r.displayParts)
29+
);
30+
});
31+
32+
it("can cancel completion entry details mid-request", () => {
33+
const options: FormatCodeSettings = {
34+
indentSize: 4,
35+
tabSize: 4,
36+
newLineCharacter: "\n",
37+
convertTabsToSpaces: true,
38+
indentStyle: IndentStyle.Smart,
39+
insertSpaceAfterConstructor: false,
40+
insertSpaceAfterCommaDelimiter: true,
41+
insertSpaceAfterSemicolonInForStatements: true,
42+
insertSpaceBeforeAndAfterBinaryOperators: true,
43+
insertSpaceAfterKeywordsInControlFlowStatements: true,
44+
insertSpaceAfterFunctionKeywordForAnonymousFunctions: false,
45+
insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false,
46+
insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false,
47+
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true,
48+
insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false,
49+
insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false,
50+
insertSpaceBeforeFunctionParenthesis: false,
51+
placeOpenBraceOnNewLineForFunctions: false,
52+
placeOpenBraceOnNewLineForControlBlocks: false,
53+
};
54+
verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for completion entry details, so the first check is within the checker
55+
service.getCompletionEntryDetails("file.ts", file.lastIndexOf("f"), "foo", options, /*content*/ undefined, {}),
56+
r => assert.exists(r.displayParts)
57+
);
58+
});
59+
});
60+
61+
function verifyOperationCancelledAfter<T>(content: string, cancelAfter: number, operation: (service: LanguageService) => T, validator: (arg: T) => void) {
62+
let checks = 0;
63+
const token: HostCancellationToken = {
64+
isCancellationRequested() {
65+
checks++;
66+
const result = checks >= cancelAfter;
67+
if (result) {
68+
checks = -Infinity; // Cancel just once, then disable cancellation, effectively
69+
}
70+
return result;
71+
}
72+
};
73+
const adapter = new Harness.LanguageService.NativeLanguageServiceAdapter(token);
74+
const host = adapter.getHost();
75+
host.addScript("file.ts", content, /*isRootFile*/ true);
76+
const service = adapter.getLanguageService();
77+
assertCancelled(() => operation(service));
78+
validator(operation(service));
79+
}
80+
81+
/**
82+
* We don't just use `assert.throws` because it doesn't validate instances for thrown objects which do not inherit from `Error`
83+
*/
84+
function assertCancelled(cb: () => void) {
85+
let caught: any;
86+
try {
87+
cb();
88+
}
89+
catch (e) {
90+
caught = e;
91+
}
92+
assert.exists(caught, "Expected operation to be cancelled, but was not");
93+
assert.instanceOf(caught, OperationCanceledException);
94+
}
95+
}

src/services/completions.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ namespace ts.Completions {
534534
formatContext: formatting.FormatContext,
535535
getCanonicalFileName: GetCanonicalFileName,
536536
preferences: UserPreferences,
537+
cancellationToken: CancellationToken,
537538
): CompletionEntryDetails {
538539
const typeChecker = program.getTypeChecker();
539540
const compilerOptions = program.getCompilerOptions();
@@ -544,7 +545,7 @@ namespace ts.Completions {
544545
const stringLiteralCompletions = !contextToken || !isStringLiteralLike(contextToken)
545546
? undefined
546547
: getStringLiteralCompletionEntries(sourceFile, contextToken, position, typeChecker, compilerOptions, host);
547-
return stringLiteralCompletions && stringLiteralCompletionDetails(name, contextToken, stringLiteralCompletions, sourceFile, typeChecker);
548+
return stringLiteralCompletions && stringLiteralCompletionDetails(name, contextToken, stringLiteralCompletions, sourceFile, typeChecker, cancellationToken);
548549
}
549550

550551
// Compute all the completion symbols again.
@@ -566,28 +567,31 @@ namespace ts.Completions {
566567
case "symbol": {
567568
const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion;
568569
const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, previousToken, formatContext, getCanonicalFileName, program.getSourceFiles(), preferences);
569-
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, codeActions, sourceDisplay);
570+
return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, cancellationToken, codeActions, sourceDisplay);
570571
}
571572
case "none":
572573
// Didn't find a symbol with this name. See if we can find a keyword instead.
573574
return allKeywordsCompletions().some(c => c.name === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.keyword, [displayPart(name, SymbolDisplayPartKind.keyword)]) : undefined;
574575
}
575576
}
576577

577-
function createCompletionDetailsForSymbol(symbol: Symbol, checker: TypeChecker, sourceFile: SourceFile, location: Node, codeActions?: CodeAction[], sourceDisplay?: SymbolDisplayPart[]): CompletionEntryDetails {
578-
const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, sourceFile, location, location, SemanticMeaning.All);
578+
function createCompletionDetailsForSymbol(symbol: Symbol, checker: TypeChecker, sourceFile: SourceFile, location: Node, cancellationToken: CancellationToken, codeActions?: CodeAction[], sourceDisplay?: SymbolDisplayPart[]): CompletionEntryDetails {
579+
const { displayParts, documentation, symbolKind, tags } =
580+
checker.runWithCancellationToken(cancellationToken, checker =>
581+
SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(checker, symbol, sourceFile, location, location, SemanticMeaning.All)
582+
);
579583
return createCompletionDetails(symbol.name, SymbolDisplay.getSymbolModifiers(symbol), symbolKind, displayParts, documentation, tags, codeActions, sourceDisplay);
580584
}
581585

582-
function stringLiteralCompletionDetails(name: string, location: Node, completion: StringLiteralCompletion, sourceFile: SourceFile, checker: TypeChecker): CompletionEntryDetails | undefined {
586+
function stringLiteralCompletionDetails(name: string, location: Node, completion: StringLiteralCompletion, sourceFile: SourceFile, checker: TypeChecker, cancellationToken: CancellationToken): CompletionEntryDetails | undefined {
583587
switch (completion.kind) {
584588
case StringLiteralCompletionKind.Paths: {
585589
const match = find(completion.paths, p => p.name === name);
586590
return match && createCompletionDetails(name, ScriptElementKindModifier.none, match.kind, [textPart(name)]);
587591
}
588592
case StringLiteralCompletionKind.Properties: {
589593
const match = find(completion.symbols, s => s.name === name);
590-
return match && createCompletionDetailsForSymbol(match, checker, sourceFile, location);
594+
return match && createCompletionDetailsForSymbol(match, checker, sourceFile, location, cancellationToken);
591595
}
592596
case StringLiteralCompletionKind.Types:
593597
return find(completion.types, t => t.value === name) ? createCompletionDetails(name, ScriptElementKindModifier.none, ScriptElementKind.typeElement, [textPart(name)]) : undefined;

src/services/findAllReferences.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ namespace ts.FindAllReferences {
4545
const checker = program.getTypeChecker();
4646
return !referencedSymbols || !referencedSymbols.length ? undefined : mapDefined<SymbolAndEntries, ReferencedSymbol>(referencedSymbols, ({ definition, references }) =>
4747
// Only include referenced symbols that have a valid definition.
48-
definition && { definition: definitionToReferencedSymbolDefinitionInfo(definition, checker, node), references: references.map(toReferenceEntry) });
48+
definition && {
49+
definition: checker.runWithCancellationToken(cancellationToken, checker => definitionToReferencedSymbolDefinitionInfo(definition, checker, node)),
50+
references: references.map(toReferenceEntry)
51+
});
4952
}
5053

5154
export function getImplementationsAtPosition(program: Program, cancellationToken: CancellationToken, sourceFiles: ReadonlyArray<SourceFile>, sourceFile: SourceFile, position: number): ImplementationLocation[] {

src/services/services.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,7 +1424,9 @@ namespace ts {
14241424
host,
14251425
formattingOptions && formatting.getFormatContext(formattingOptions),
14261426
getCanonicalFileName,
1427-
preferences);
1427+
preferences,
1428+
cancellationToken,
1429+
);
14281430
}
14291431

14301432
function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol {
@@ -1465,7 +1467,7 @@ namespace ts {
14651467
kind: ScriptElementKind.unknown,
14661468
kindModifiers: ScriptElementKindModifier.none,
14671469
textSpan: createTextSpanFromNode(node, sourceFile),
1468-
displayParts: typeToDisplayParts(typeChecker, type, getContainerNode(node)),
1470+
displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => typeToDisplayParts(typeChecker, type, getContainerNode(node))),
14691471
documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined,
14701472
tags: type.symbol ? type.symbol.getJsDocTags() : undefined
14711473
};
@@ -1474,7 +1476,9 @@ namespace ts {
14741476
return undefined;
14751477
}
14761478

1477-
const { symbolKind, displayParts, documentation, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(node), node);
1479+
const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker =>
1480+
SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(node), node)
1481+
);
14781482
return {
14791483
kind: symbolKind,
14801484
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),

src/services/signatureHelp.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ namespace ts.SignatureHelp {
4141
// We didn't have any sig help items produced by the TS compiler. If this is a JS
4242
// file, then see if we can figure out anything better.
4343
if (isSourceFileJavaScript(sourceFile)) {
44-
return createJavaScriptSignatureHelpItems(argumentInfo, program);
44+
return createJavaScriptSignatureHelpItems(argumentInfo, program, cancellationToken);
4545
}
4646

4747
return undefined;
4848
}
4949

50-
return createSignatureHelpItems(candidates, resolvedSignature, argumentInfo, typeChecker);
50+
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(candidates, resolvedSignature, argumentInfo, typeChecker));
5151
}
5252

53-
function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program): SignatureHelpItems {
53+
function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program, cancellationToken: CancellationToken): SignatureHelpItems {
5454
if (argumentInfo.invocation.kind !== SyntaxKind.CallExpression) {
5555
return undefined;
5656
}
@@ -76,7 +76,7 @@ namespace ts.SignatureHelp {
7676
if (type) {
7777
const callSignatures = type.getCallSignatures();
7878
if (callSignatures && callSignatures.length) {
79-
return createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, typeChecker);
79+
return typeChecker.runWithCancellationToken(cancellationToken, typeChecker => createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, typeChecker));
8080
}
8181
}
8282
}

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,6 +1842,12 @@ declare namespace ts {
18421842
getSuggestionForNonexistentModule(node: Identifier, target: Symbol): string | undefined;
18431843
getBaseConstraintOfType(type: Type): Type | undefined;
18441844
getDefaultFromTypeParameter(type: Type): Type | undefined;
1845+
/**
1846+
* Depending on the operation performed, it may be appropriate to throw away the checker
1847+
* if the cancellation token is triggered. Typically, if it is used for error checking
1848+
* and the operation is cancelled, then it should be discarded, otherwise it is safe to keep.
1849+
*/
1850+
runWithCancellationToken<T>(token: CancellationToken, cb: (checker: TypeChecker) => T): T;
18451851
}
18461852
enum NodeBuilderFlags {
18471853
None = 0,

tests/baselines/reference/api/typescript.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,6 +1842,12 @@ declare namespace ts {
18421842
getSuggestionForNonexistentModule(node: Identifier, target: Symbol): string | undefined;
18431843
getBaseConstraintOfType(type: Type): Type | undefined;
18441844
getDefaultFromTypeParameter(type: Type): Type | undefined;
1845+
/**
1846+
* Depending on the operation performed, it may be appropriate to throw away the checker
1847+
* if the cancellation token is triggered. Typically, if it is used for error checking
1848+
* and the operation is cancelled, then it should be discarded, otherwise it is safe to keep.
1849+
*/
1850+
runWithCancellationToken<T>(token: CancellationToken, cb: (checker: TypeChecker) => T): T;
18451851
}
18461852
enum NodeBuilderFlags {
18471853
None = 0,

0 commit comments

Comments
 (0)