diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ad969e2f..fac5155330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - 🙌 Complete with `?.` for optional properies in completion. Thanks to contribution from [@yoyo930021](https://github.com/yoyo930021). #2326 and #2357. - 🙌 Respect typescript language settings. Thanks to contribution from [@yoyo930021](https://github.com/yoyo930021). #2109 and #2375. - 🙌 Slim syntax highlighting. Thanks to contribution from [@Antti](https://github.com/Antti). +- 🙌 Stop computing outdated diagnostics with CancellationToken. Thanks to contribution from [@yoyo930021](https://github.com/yoyo930021). #1263 and #2332. ### 0.28.0 | 2020-09-23 | [VSIX](https://marketplace.visualstudio.com/_apis/public/gallery/publishers/octref/vsextensions/vetur/0.28.0/vspackage) diff --git a/server/src/embeddedSupport/languageModes.ts b/server/src/embeddedSupport/languageModes.ts index 13d455e1f5..78ae8f222b 100644 --- a/server/src/embeddedSupport/languageModes.ts +++ b/server/src/embeddedSupport/languageModes.ts @@ -38,6 +38,7 @@ import { getServiceHost, IServiceHost } from '../services/typescriptService/serv import { VLSFullConfig } from '../config'; import { SassLanguageMode } from '../modes/style/sass/sassLanguageMode'; import { getPugMode } from '../modes/pug'; +import { VCancellationToken } from '../utils/cancellationToken'; export interface VLSServices { infoService?: VueInfoService; @@ -49,7 +50,7 @@ export interface LanguageMode { configure?(options: VLSFullConfig): void; updateFileInfo?(doc: TextDocument): void; - doValidation?(document: TextDocument): Diagnostic[]; + doValidation?(document: TextDocument, cancellationToken?: VCancellationToken): Promise; getCodeActions?( document: TextDocument, range: Range, diff --git a/server/src/modes/script/javascript.ts b/server/src/modes/script/javascript.ts index 06b19f1f33..8ba0cb2983 100644 --- a/server/src/modes/script/javascript.ts +++ b/server/src/modes/script/javascript.ts @@ -46,6 +46,7 @@ import { RefactorAction } from '../../types'; import { IServiceHost } from '../../services/typescriptService/serviceHost'; import { toCompletionItemKind, toSymbolKind } from '../../services/typescriptService/util'; import * as Previewer from './previewer'; +import { isVCancellationRequested, VCancellationToken } from '../../utils/cancellationToken'; // Todo: After upgrading to LS server 4.0, use CompletionContext for filtering trigger chars // https://microsoft.github.io/language-server-protocol/specification#completion-request-leftwards_arrow_with_hook @@ -145,18 +146,38 @@ export async function getJavascriptMode( } }, - doValidation(doc: TextDocument): Diagnostic[] { + async doValidation(doc: TextDocument, cancellationToken?: VCancellationToken): Promise { + if (await isVCancellationRequested(cancellationToken)) { + return []; + } const { scriptDoc, service } = updateCurrentVueTextDocument(doc); if (!languageServiceIncludesFile(service, doc.uri)) { return []; } + if (await isVCancellationRequested(cancellationToken)) { + return []; + } const fileFsPath = getFileFsPath(doc.uri); - const rawScriptDiagnostics = [ - ...service.getSyntacticDiagnostics(fileFsPath), - ...service.getSemanticDiagnostics(fileFsPath) + const program = service.getProgram(); + const sourceFile = program?.getSourceFile(fileFsPath); + if (!program || !sourceFile) { + return []; + } + + let rawScriptDiagnostics = [ + ...program.getSyntacticDiagnostics(sourceFile, cancellationToken?.tsToken), + ...program.getSemanticDiagnostics(sourceFile, cancellationToken?.tsToken) ]; + const compilerOptions = program.getCompilerOptions(); + if (compilerOptions.declaration || compilerOptions.composite) { + rawScriptDiagnostics = [ + ...rawScriptDiagnostics, + ...program.getDeclarationDiagnostics(sourceFile, cancellationToken?.tsToken) + ]; + } + return rawScriptDiagnostics.map(diag => { const tags: DiagnosticTag[] = []; diff --git a/server/src/modes/style/index.ts b/server/src/modes/style/index.ts index cda2dcdb82..c21f019db9 100644 --- a/server/src/modes/style/index.ts +++ b/server/src/modes/style/index.ts @@ -69,7 +69,7 @@ function getStyleMode( languageService.configure(c && c.css); config = c; }, - doValidation(document) { + async doValidation(document) { if (languageId === 'postcss') { return []; } else { diff --git a/server/src/modes/template/htmlMode.ts b/server/src/modes/template/htmlMode.ts index c31dee2c21..b84c9b1f9b 100644 --- a/server/src/modes/template/htmlMode.ts +++ b/server/src/modes/template/htmlMode.ts @@ -26,6 +26,7 @@ import { getComponentInfoTagProvider } from './tagProviders/componentInfoTagProv import { VueVersion } from '../../services/typescriptService/vueVersion'; import { doPropValidation } from './services/vuePropValidation'; import { getFoldingRanges } from './services/htmlFolding'; +import { isVCancellationRequested, VCancellationToken } from '../../utils/cancellationToken'; export class HTMLMode implements LanguageMode { private tagProviderSettings: CompletionConfiguration; @@ -60,9 +61,12 @@ export class HTMLMode implements LanguageMode { this.config = c; } - doValidation(document: TextDocument) { + async doValidation(document: TextDocument, cancellationToken?: VCancellationToken) { const diagnostics = []; + if (await isVCancellationRequested(cancellationToken)) { + return []; + } if (this.config.vetur.validation.templateProps) { const info = this.vueInfoService ? this.vueInfoService.getInfo(document) : undefined; if (info && info.componentInfo.childComponents) { @@ -70,6 +74,9 @@ export class HTMLMode implements LanguageMode { } } + if (await isVCancellationRequested(cancellationToken)) { + return diagnostics; + } if (this.config.vetur.validation.template) { const embedded = this.embeddedDocuments.refreshAndGet(document); diagnostics.push(...doESLintValidation(embedded, this.lintEngine)); diff --git a/server/src/modes/template/index.ts b/server/src/modes/template/index.ts index 31af1df5d2..ed9794fef9 100644 --- a/server/src/modes/template/index.ts +++ b/server/src/modes/template/index.ts @@ -18,6 +18,7 @@ import { IServiceHost } from '../../services/typescriptService/serviceHost'; import { T_TypeScript } from '../../services/dependencyService'; import { HTMLDocument, parseHTMLDocument } from './parser/htmlParser'; import { inferVueVersion } from '../../services/typescriptService/vueVersion'; +import { VCancellationToken } from '../../utils/cancellationToken'; type DocumentRegionCache = LanguageModelCache; @@ -47,8 +48,11 @@ export class VueHTMLMode implements LanguageMode { queryVirtualFileInfo(fileName: string, currFileText: string) { return this.vueInterpolationMode.queryVirtualFileInfo(fileName, currFileText); } - doValidation(document: TextDocument) { - return this.htmlMode.doValidation(document).concat(this.vueInterpolationMode.doValidation(document)); + async doValidation(document: TextDocument, cancellationToken?: VCancellationToken) { + return Promise.all([ + this.vueInterpolationMode.doValidation(document, cancellationToken), + this.htmlMode.doValidation(document, cancellationToken) + ]).then(result => [...result[0], ...result[1]]); } doComplete(document: TextDocument, position: Position) { const htmlList = this.htmlMode.doComplete(document, position); diff --git a/server/src/modes/template/interpolationMode.ts b/server/src/modes/template/interpolationMode.ts index a4cc0cfaaa..278fe54894 100644 --- a/server/src/modes/template/interpolationMode.ts +++ b/server/src/modes/template/interpolationMode.ts @@ -23,6 +23,7 @@ import { mapBackRange, mapFromPositionToOffset } from '../../services/typescript import { createTemplateDiagnosticFilter } from '../../services/typescriptService/templateDiagnosticFilter'; import { toCompletionItemKind } from '../../services/typescriptService/util'; import { VueInfoService } from '../../services/vueInfoService'; +import { isVCancellationRequested, VCancellationToken } from '../../utils/cancellationToken'; import { getFileFsPath } from '../../utils/paths'; import { NULL_COMPLETION } from '../nullMode'; import { languageServiceIncludesFile } from '../script/javascript'; @@ -52,7 +53,7 @@ export class VueInterpolationMode implements LanguageMode { return this.serviceHost.queryVirtualFileInfo(fileName, currFileText); } - doValidation(document: TextDocument): Diagnostic[] { + async doValidation(document: TextDocument, cancellationToken?: VCancellationToken): Promise { if ( !_.get(this.config, ['vetur', 'experimental', 'templateInterpolationService'], true) || !this.config.vetur.validation.interpolation @@ -60,6 +61,10 @@ export class VueInterpolationMode implements LanguageMode { return []; } + if (await isVCancellationRequested(cancellationToken)) { + return []; + } + // Add suffix to process this doc as vue template. const templateDoc = TextDocument.create( document.uri + '.template', @@ -81,6 +86,10 @@ export class VueInterpolationMode implements LanguageMode { return []; } + if (await isVCancellationRequested(cancellationToken)) { + return []; + } + const templateFileFsPath = getFileFsPath(templateDoc.uri); // We don't need syntactic diagnostics because // compiled template is always valid JavaScript syntax. diff --git a/server/src/services/vls.ts b/server/src/services/vls.ts index aeb9244ce3..37730c954d 100644 --- a/server/src/services/vls.ts +++ b/server/src/services/vls.ts @@ -21,7 +21,8 @@ import { CompletionTriggerKind, ExecuteCommandParams, ApplyWorkspaceEditRequest, - FoldingRangeParams + FoldingRangeParams, + CancellationTokenSource } from 'vscode-languageserver'; import { ColorInformation, @@ -46,7 +47,7 @@ import { URI } from 'vscode-uri'; import { LanguageModes, LanguageModeRange, LanguageMode } from '../embeddedSupport/languageModes'; import { NULL_COMPLETION, NULL_HOVER, NULL_SIGNATURE } from '../modes/nullMode'; import { VueInfoService } from './vueInfoService'; -import { DependencyService } from './dependencyService'; +import { DependencyService, State } from './dependencyService'; import * as _ from 'lodash'; import { DocumentContext, RefactorAction } from '../types'; import { DocumentService } from './documentService'; @@ -55,6 +56,7 @@ import { logger } from '../log'; import { getDefaultVLSConfig, VLSFullConfig, VLSConfig } from '../config'; import { LanguageId } from '../embeddedSupport/embeddedSupport'; import { APPLY_REFACTOR_COMMAND } from '../modes/script/javascript'; +import { VCancellationToken, VCancellationTokenSource } from '../utils/cancellationToken'; export class VLS { // @Todo: Remove this and DocumentContext @@ -67,6 +69,7 @@ export class VLS { private languageModes: LanguageModes; private pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; + private cancellationTokenValidationRequests: { [uri: string]: VCancellationTokenSource } = {}; private validationDelayMs = 200; private validation: { [k: string]: boolean } = { 'vue-html': true, @@ -173,10 +176,11 @@ export class VLS { return (this.languageModes.getMode('vue-html') as VueHTMLMode).queryVirtualFileInfo(fileName, currFileText); }); - this.lspConnection.onRequest('$/getDiagnostics', params => { + this.lspConnection.onRequest('$/getDiagnostics', async params => { const doc = this.documentService.getDocument(params.uri); if (doc) { - return this.doValidate(doc); + const diagnostics = await this.doValidate(doc); + return diagnostics ?? []; } return []; }); @@ -509,12 +513,26 @@ export class VLS { } this.cleanPendingValidation(textDocument); + this.cancelPastValidation(textDocument); this.pendingValidationRequests[textDocument.uri] = setTimeout(() => { delete this.pendingValidationRequests[textDocument.uri]; - this.validateTextDocument(textDocument); + const tsDep = this.dependencyService.getDependency('typescript'); + if (tsDep?.state === State.Loaded) { + this.cancellationTokenValidationRequests[textDocument.uri] = new VCancellationTokenSource(tsDep.module); + this.validateTextDocument(textDocument, this.cancellationTokenValidationRequests[textDocument.uri].token); + } }, this.validationDelayMs); } + cancelPastValidation(textDocument: TextDocument): void { + const source = this.cancellationTokenValidationRequests[textDocument.uri]; + if (source) { + source.cancel(); + source.dispose(); + delete this.cancellationTokenValidationRequests[textDocument.uri]; + } + } + cleanPendingValidation(textDocument: TextDocument): void { const request = this.pendingValidationRequests[textDocument.uri]; if (request) { @@ -523,25 +541,30 @@ export class VLS { } } - validateTextDocument(textDocument: TextDocument): void { - const diagnostics: Diagnostic[] = this.doValidate(textDocument); - this.lspConnection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + async validateTextDocument(textDocument: TextDocument, cancellationToken?: VCancellationToken) { + const diagnostics = await this.doValidate(textDocument, cancellationToken); + if (diagnostics) { + this.lspConnection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + } } - doValidate(doc: TextDocument): Diagnostic[] { + async doValidate(doc: TextDocument, cancellationToken?: VCancellationToken) { const diagnostics: Diagnostic[] = []; if (doc.languageId === 'vue') { - this.languageModes.getAllLanguageModeRangesInDocument(doc).forEach(lmr => { + for (const lmr of this.languageModes.getAllLanguageModeRangesInDocument(doc)) { if (lmr.mode.doValidation) { if (this.validation[lmr.mode.getId()]) { - pushAll(diagnostics, lmr.mode.doValidation(doc)); + pushAll(diagnostics, await lmr.mode.doValidation(doc, cancellationToken)); } // Special case for template type checking else if (lmr.mode.getId() === 'vue-html' && this.templateInterpolationValidation) { - pushAll(diagnostics, lmr.mode.doValidation(doc)); + pushAll(diagnostics, await lmr.mode.doValidation(doc, cancellationToken)); } } - }); + } + } + if (cancellationToken?.isCancellationRequested) { + return null; } return diagnostics; } diff --git a/server/src/utils/cancellationToken.ts b/server/src/utils/cancellationToken.ts new file mode 100644 index 0000000000..825c8f007d --- /dev/null +++ b/server/src/utils/cancellationToken.ts @@ -0,0 +1,39 @@ +import type { T_TypeScript } from '../services/dependencyService'; +import { CancellationToken as TSCancellationToken } from 'typescript'; +import { CancellationTokenSource, CancellationToken as LSPCancellationToken } from 'vscode-languageserver'; + +export interface VCancellationToken extends LSPCancellationToken { + tsToken: TSCancellationToken; +} + +export class VCancellationTokenSource extends CancellationTokenSource { + constructor(private tsModule: T_TypeScript) { + super(); + } + + get token(): VCancellationToken { + const operationCancelException = this.tsModule.OperationCanceledException; + const token = super.token as VCancellationToken; + token.tsToken = { + isCancellationRequested() { + return token.isCancellationRequested; + }, + throwIfCancellationRequested() { + if (token.isCancellationRequested) { + throw new operationCancelException(); + } + } + }; + return token; + } +} + +export function isVCancellationRequested(token?: VCancellationToken) { + return new Promise(resolve => { + if (!token) { + resolve(false); + } else { + setImmediate(() => resolve(token.isCancellationRequested)); + } + }); +}