Skip to content
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

Prototype TS Compile on Save #32103

Closed
wants to merge 6 commits into from
Closed
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
7 changes: 3 additions & 4 deletions extensions/typescript/src/features/taskProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import * as path from 'path';
import * as vscode from 'vscode';

import * as Proto from '../protocol';
import TypeScriptServiceClient from '../typescriptServiceClient';
import { ITypescriptServiceClient } from '../typescriptService';
import TsConfigProvider, { TSConfig } from '../utils/tsconfigProvider';
import { isImplicitProjectConfigFile } from '../utils/tsconfig';


import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();

Expand All @@ -37,7 +36,7 @@ class TscTaskProvider implements vscode.TaskProvider {
private readonly tsconfigProvider: TsConfigProvider;

public constructor(
private readonly lazyClient: () => TypeScriptServiceClient
private readonly lazyClient: () => ITypescriptServiceClient
) {
this.tsconfigProvider = new TsConfigProvider();
}
Expand Down Expand Up @@ -199,7 +198,7 @@ export default class TypeScriptTaskProviderManager {
private readonly disposables: vscode.Disposable[] = [];

constructor(
private readonly lazyClient: () => TypeScriptServiceClient
private readonly lazyClient: () => ITypescriptServiceClient
) {
vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this, this.disposables);
this.onConfigurationChanged();
Expand Down
43 changes: 22 additions & 21 deletions extensions/typescript/src/typescriptMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import SignatureHelpProvider from './features/signatureHelpProvider';
import RenameProvider from './features/renameProvider';
import { TypeScriptFormattingProvider, FormattingProviderManager } from './features/formattingProvider';
import BufferSyncSupport from './features/bufferSyncSupport';
import CompileOnSaveHelper from './utils/compileOnSave';
import CompletionItemProvider from './features/completionItemProvider';
import WorkspaceSymbolProvider from './features/workspaceSymbolProvider';
import CodeActionProvider from './features/codeActionProvider';
Expand Down Expand Up @@ -202,7 +203,9 @@ class LanguageProvider {

constructor(
private readonly client: TypeScriptServiceClient,
private readonly description: LanguageDescription
private readonly description: LanguageDescription,
syntaxDiagnosticsReceived: (file: string, diag: protocol.Diagnostic[]) => void,
semanticsDiagnosticsReceived: (file: string, diag: protocol.Diagnostic[]) => void
) {
this.bufferSyncSupport = new BufferSyncSupport(client, description.modeIds, {
delete: (file: string) => {
Expand All @@ -212,6 +215,10 @@ class LanguageProvider {
this.syntaxDiagnostics = Object.create(null);
this.currentDiagnostics = languages.createDiagnosticCollection(description.id);

if (!this.description.isExternal) {
this.disposables.push(new CompileOnSaveHelper(client, description.modeIds, syntaxDiagnosticsReceived, semanticsDiagnosticsReceived));
}

this.typingsStatus = new TypingsStatus(client);
new AtaProgressReporter(client);

Expand Down Expand Up @@ -479,7 +486,7 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {

this.languagePerId = new Map();
for (const description of descriptions) {
const manager = new LanguageProvider(this.client, description);
const manager = new LanguageProvider(this.client, description, this.syntaxDiagnosticsReceived.bind(this), this.semanticDiagnosticsReceived.bind(this));
this.languages.push(manager);
this.disposables.push(manager);
this.languagePerId.set(description.id, manager);
Expand All @@ -503,7 +510,7 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {
diagnosticSource: 'ts-plugins',
isExternal: true
};
const manager = new LanguageProvider(this.client, description);
const manager = new LanguageProvider(this.client, description, this.syntaxDiagnosticsReceived.bind(this), this.semanticDiagnosticsReceived.bind(this));
this.languages.push(manager);
this.disposables.push(manager);
this.languagePerId.set(description.id, manager);
Expand Down Expand Up @@ -631,26 +638,20 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {
});
}

/* internal */ syntaxDiagnosticsReceived(event: Proto.DiagnosticEvent): void {
const body = event.body;
if (body && body.diagnostics) {
this.findLanguage(body.file).then(language => {
if (language) {
language.syntaxDiagnosticsReceived(body.file, this.createMarkerDatas(body.diagnostics, language.diagnosticSource));
}
});
}
/* internal */ syntaxDiagnosticsReceived(file: string, diagnostics: Proto.Diagnostic[]): void {
this.findLanguage(file).then(language => {
if (language) {
language.syntaxDiagnosticsReceived(file, this.createMarkerDatas(diagnostics, language.diagnosticSource));
}
});
}

/* internal */ semanticDiagnosticsReceived(event: Proto.DiagnosticEvent): void {
const body = event.body;
if (body && body.diagnostics) {
this.findLanguage(body.file).then(language => {
if (language) {
language.semanticDiagnosticsReceived(body.file, this.createMarkerDatas(body.diagnostics, language.diagnosticSource));
}
});
}
/* internal */ semanticDiagnosticsReceived(file: string, diagnostics: Proto.Diagnostic[]): void {
this.findLanguage(file).then(language => {
if (language) {
language.semanticDiagnosticsReceived(file, this.createMarkerDatas(diagnostics, language.diagnosticSource));
}
});
}

/* internal */ configFileDiagnosticsReceived(event: Proto.ConfigFileDiagnosticEvent): void {
Expand Down
11 changes: 7 additions & 4 deletions extensions/typescript/src/typescriptService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import * as Proto from './protocol';
import API from './utils/api';

export interface ITypescriptServiceClientHost {
syntaxDiagnosticsReceived(event: Proto.DiagnosticEvent): void;
semanticDiagnosticsReceived(event: Proto.DiagnosticEvent): void;
syntaxDiagnosticsReceived(file: string, diagnostics: Proto.Diagnostic[]): void;
semanticDiagnosticsReceived(file: string, diagnostics: Proto.Diagnostic[]): void;
configFileDiagnosticsReceived(event: Proto.ConfigFileDiagnosticEvent): void;
populateService(): void;
}
Expand Down Expand Up @@ -63,7 +63,10 @@ export interface ITypescriptServiceClient {
execute(command: 'docCommentTemplate', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise<Proto.DocCommandTemplateResponse>;
execute(command: 'getApplicableRefactors', args: Proto.GetApplicableRefactorsRequestArgs, token?: CancellationToken): Promise<Proto.GetApplicableRefactorsResponse>;
execute(command: 'getEditsForRefactor', args: Proto.GetEditsForRefactorRequestArgs, token?: CancellationToken): Promise<Proto.GetEditsForRefactorResponse>;
// execute(command: 'compileOnSaveAffectedFileList', args: Proto.CompileOnSaveEmitFileRequestArgs, token?: CancellationToken): Promise<Proto.CompileOnSaveAffectedFileListResponse>;
// execute(command: 'compileOnSaveEmitFile', args: Proto.CompileOnSaveEmitFileRequestArgs, token?: CancellationToken): Promise<any>;
execute(command: 'compileOnSaveAffectedFileList', args: Proto.FileRequestArgs, token?: CancellationToken): Promise<Proto.CompileOnSaveAffectedFileListResponse>;
execute(command: 'compileOnSaveEmitFile', args: Proto.CompileOnSaveEmitFileRequestArgs, token?: CancellationToken): Promise<any>;
execute(command: 'semanticDiagnosticsSync', args: Proto.SemanticDiagnosticsSyncRequestArgs, token?: CancellationToken): Promise<Proto.SemanticDiagnosticsSyncResponse>;
execute(command: 'syntaxDiagnosticsSync', args: Proto.SyntacticDiagnosticsSyncRequestArgs, token?: CancellationToken): Promise<Proto.SyntacticDiagnosticsSyncResponse>;

execute(command: string, args: any, expectedResult: boolean | CancellationToken, token?: CancellationToken): Promise<any>;
}
12 changes: 10 additions & 2 deletions extensions/typescript/src/typescriptServiceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,9 +743,15 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient

private dispatchEvent(event: Proto.Event) {
if (event.event === 'syntaxDiag') {
this.host.syntaxDiagnosticsReceived(event as Proto.DiagnosticEvent);
const diagnosticEvent = event as Proto.DiagnosticEvent;
if (diagnosticEvent.body) {
this.host.syntaxDiagnosticsReceived(diagnosticEvent.body.file, diagnosticEvent.body.diagnostics);
}
} else if (event.event === 'semanticDiag') {
this.host.semanticDiagnosticsReceived(event as Proto.DiagnosticEvent);
const diagnosticEvent = event as Proto.DiagnosticEvent;
if (diagnosticEvent.body) {
this.host.semanticDiagnosticsReceived(diagnosticEvent.body.file, diagnosticEvent.body.diagnostics);
}
} else if (event.event === 'configFileDiag') {
this.host.configFileDiagnosticsReceived(event as Proto.ConfigFileDiagnosticEvent);
} else if (event.event === 'telemetry') {
Expand All @@ -771,6 +777,8 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
if (data) {
this._onTypesInstallerInitializationFailed.fire(data);
}
} else {
console.log(event.event);
}
}

Expand Down
135 changes: 135 additions & 0 deletions extensions/typescript/src/utils/compileOnSave.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as Proto from '../protocol';
import * as vscode from 'vscode';
import * as fs from 'fs';

import { ITypescriptServiceClient } from '../typescriptService';
import { Delayer } from './async';
import { isImplicitProjectConfigFile } from './tsconfig';

interface PendingProjectUpdate {
delayer: Delayer<void>;
files: Set<string>;
}

class ProjectDelayer {
private readonly pendingProjectUpdates = new Map<string, PendingProjectUpdate>();

constructor(
private task: (project: string, fileNames: Set<string>) => void
) { }

dispose() {
for (const update of this.pendingProjectUpdates.values()) {
update.delayer.cancel();
}
this.pendingProjectUpdates.clear();
}

public trigger(projectFileName: string, fileNames: string[]): void {
if (!fileNames.length) {
return;
}

let entry: PendingProjectUpdate | undefined = this.pendingProjectUpdates.get(projectFileName);
if (!entry) {
entry = {
delayer: new Delayer<void>(500),
files: new Set()
};
this.pendingProjectUpdates.set(projectFileName, entry);
}

for (const file of fileNames) {
entry.files.add(file);
}

entry.delayer.trigger(() => this.onDidTrigger(projectFileName));
}

private onDidTrigger(projectFileName: string): void {
const entry = this.pendingProjectUpdates.get(projectFileName);
if (!entry) {
return;
}
this.task(projectFileName, entry.files);
this.pendingProjectUpdates.delete(projectFileName);
}
}

export default class CompileOnSaveHelper {
private readonly saveSubscription: vscode.Disposable;

private readonly emitter: ProjectDelayer;

constructor(
private readonly client: ITypescriptServiceClient,
private readonly languages: string[],
private readonly syntaxDiagnosticsReceived: (file: string, diag: protocol.Diagnostic[]) => void,
private readonly semanticsDiagnosticsReceived: (file: string, diag: protocol.Diagnostic[]) => void
) {
this.emitter = new ProjectDelayer((project, files) => this.emit(project, files));
this.saveSubscription = vscode.workspace.onDidSaveTextDocument(this.onDidSave, this);
}

dispose() {
this.saveSubscription.dispose();
this.emitter.dispose();
}

private async onDidSave(textDocument: vscode.TextDocument) {
if (this.languages.indexOf(textDocument.languageId) === -1) {
return;
}

const file = this.client.normalizePath(textDocument.uri);
if (!file || !await this.isFileInCompileOnSaveEnabledProject(file)) {
return;
}

const affectedFileList = await this.client.execute('compileOnSaveAffectedFileList', { file });
if (!affectedFileList || !affectedFileList.body) {
return;
}

for (const project of affectedFileList.body) {
this.emitter.trigger(project.projectFileName, project.fileNames);
}
}

private async isFileInCompileOnSaveEnabledProject(file: string): Promise<boolean> {
const info = await this.client.execute('projectInfo', { file, needFileNameList: false });
if (!info || !info.body || !info.body.configFileName || isImplicitProjectConfigFile(info.body.configFileName)) {
return false;
}

try {
const config = JSON.parse(fs.readFileSync(info.body.configFileName, 'utf8'));
return config && config.compileOnSave;
} catch (e) {
return false;
}
}

private emit(project: string, files: Set<string>): void {
for (const file of files) {
this.client.execute('compileOnSaveEmitFile', { file }, false);

this.client.execute('semanticDiagnosticsSync', { file, projectFileName: project }).then(resp => {
if (resp && resp.body) {
this.semanticsDiagnosticsReceived(file, resp.body as Proto.Diagnostic[]);
}
});

this.client.execute('syntaxDiagnosticsSync', { file, projectFileName: project }).then(resp => {
if (resp && resp.body) {
this.syntaxDiagnosticsReceived(file, resp.body as Proto.Diagnostic[]);
}
});
}
}
}