Skip to content

Stop computing outdated diagnostics with CancellationToken #2332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion server/src/embeddedSupport/languageModes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Diagnostic[]>;
getCodeActions?(
document: TextDocument,
range: Range,
Expand Down
29 changes: 25 additions & 4 deletions server/src/modes/script/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -145,18 +146,38 @@ export async function getJavascriptMode(
}
},

doValidation(doc: TextDocument): Diagnostic[] {
async doValidation(doc: TextDocument, cancellationToken?: VCancellationToken): Promise<Diagnostic[]> {
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)
];
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return rawScriptDiagnostics.map(diag => {
const tags: DiagnosticTag[] = [];

Expand Down
2 changes: 1 addition & 1 deletion server/src/modes/style/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function getStyleMode(
languageService.configure(c && c.css);
config = c;
},
doValidation(document) {
async doValidation(document) {
if (languageId === 'postcss') {
return [];
} else {
Expand Down
9 changes: 8 additions & 1 deletion server/src/modes/template/htmlMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,16 +61,22 @@ 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) {
diagnostics.push(...doPropValidation(document, this.vueDocuments.refreshAndGet(document), info));
}
}

if (await isVCancellationRequested(cancellationToken)) {
return diagnostics;
}
if (this.config.vetur.validation.template) {
const embedded = this.embeddedDocuments.refreshAndGet(document);
diagnostics.push(...doESLintValidation(embedded, this.lintEngine));
Expand Down
8 changes: 6 additions & 2 deletions server/src/modes/template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VueDocumentRegions>;

Expand Down Expand Up @@ -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);
Expand Down
11 changes: 10 additions & 1 deletion server/src/modes/template/interpolationMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,14 +53,18 @@ export class VueInterpolationMode implements LanguageMode {
return this.serviceHost.queryVirtualFileInfo(fileName, currFileText);
}

doValidation(document: TextDocument): Diagnostic[] {
async doValidation(document: TextDocument, cancellationToken?: VCancellationToken): Promise<Diagnostic[]> {
if (
!_.get(this.config, ['vetur', 'experimental', 'templateInterpolationService'], true) ||
!this.config.vetur.validation.interpolation
) {
return [];
}

if (await isVCancellationRequested(cancellationToken)) {
return [];
}

// Add suffix to process this doc as vue template.
const templateDoc = TextDocument.create(
document.uri + '.template',
Expand All @@ -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.
Expand Down
49 changes: 36 additions & 13 deletions server/src/services/vls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
CompletionTriggerKind,
ExecuteCommandParams,
ApplyWorkspaceEditRequest,
FoldingRangeParams
FoldingRangeParams,
CancellationTokenSource
} from 'vscode-languageserver';
import {
ColorInformation,
Expand All @@ -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';
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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 [];
});
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down
39 changes: 39 additions & 0 deletions server/src/utils/cancellationToken.ts
Original file line number Diff line number Diff line change
@@ -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));
}
});
}