Add TSC Task Provider (#27093)

* extract standardLanguageDescriptions to constant

* Remove client variable

* Move VersionStatus into a class

* Add TSC Task Provider

Fixes #26079

Adds a task provider for building typescript projects.

* Make ts loading lazy
This commit is contained in:
Matt Bierner
2017-05-26 09:28:00 +02:00
committed by Dirk Baeumer
parent 5860e3dcad
commit 64d676ccf7
7 changed files with 235 additions and 76 deletions
+4 -1
View File
@@ -34,7 +34,10 @@
"onCommand:typescript.selectTypeScriptVersion",
"onCommand:javascript.goToProjectConfig",
"onCommand:typescript.goToProjectConfig",
"onCommand:typescript.openTsServerLog"
"onCommand:typescript.openTsServerLog",
"onCommand:workbench.action.tasks.runTask",
"onCommand:workbench.action.tasks.build",
"onCommand:workbench.action.tasks.test"
],
"main": "./out/typescriptMain",
"contributes": {
@@ -88,7 +88,7 @@ export class TryCompleteJsDocCommand {
static COMMAND_NAME = '_typeScript.tryCompleteJsDoc';
constructor(
private client: ITypescriptServiceClient
private lazyClient: () => ITypescriptServiceClient
) { }
/**
@@ -96,7 +96,7 @@ export class TryCompleteJsDocCommand {
* if possible, otherwise falling back to a default comment format.
*/
public tryCompleteJsDoc(resource: Uri, start: Position, shouldGetJSDocFromTSServer: boolean): Thenable<boolean> {
const file = this.client.normalizePath(resource);
const file = this.lazyClient().normalizePath(resource);
if (!file) {
return Promise.resolve(false);
}
@@ -126,7 +126,7 @@ export class TryCompleteJsDocCommand {
offset: position.character + 1
};
return Promise.race([
this.client.execute('docCommentTemplate', args),
this.lazyClient().execute('docCommentTemplate', args),
new Promise((_, reject) => setTimeout(reject, 250))
]).then((res: DocCommandTemplateResponse) => {
if (!res || !res.body) {
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import * as Proto from '../protocol';
import TypeScriptServiceClient from '../typescriptServiceClient';
const exists = (file: string): Promise<boolean> =>
new Promise<boolean>((resolve, _reject) => {
fs.exists(file, (value: boolean) => {
resolve(value);
});
});
export default class TypeScriptTaskProvider implements vscode.TaskProvider {
public constructor(
private readonly lazyClient: () => TypeScriptServiceClient
) { }
async provideTasks(token: vscode.CancellationToken): Promise<vscode.Task[]> {
const rootPath = vscode.workspace.rootPath;
if (!rootPath) {
return [];
}
const projects = (await this.getConfigForActiveFile(token)).concat(await this.getConfigsForWorkspace());
const command = await this.getCommand();
return projects
.filter((x, i) => projects.indexOf(x) === i)
.map(configFile => {
const configFileName = path.relative(rootPath, configFile);
const buildTask = new vscode.ShellTask(`tsc: build ${configFileName}`, `${command} -p ${configFile}`, '$tsc');
buildTask.group = vscode.TaskGroup.Build;
return buildTask;
});
}
private async getConfigForActiveFile(token: vscode.CancellationToken): Promise<string[]> {
const editor = vscode.window.activeTextEditor;
if (editor) {
if (path.basename(editor.document.fileName).match(/^tsconfig\.(.\.)?json$/)) {
return [editor.document.fileName];
}
}
const file = this.getActiveTypeScriptFile();
if (!file) {
return [];
}
const res: Proto.ProjectInfoResponse = await this.lazyClient().execute(
'projectInfo',
{ file, needFileNameList: false } as protocol.ProjectInfoRequestArgs,
token);
if (!res || !res.body) {
return [];
}
const { configFileName } = res.body;
if (configFileName && configFileName.indexOf('/dev/null/') !== 0) {
return [configFileName];
}
return [];
}
private async getConfigsForWorkspace(): Promise<string[]> {
if (!vscode.workspace.rootPath) {
return [];
}
const rootTsConfig = path.join(vscode.workspace.rootPath, 'tsconfig.json');
if (!await exists(rootTsConfig)) {
return [];
}
return [rootTsConfig];
}
private async getCommand(): Promise<string> {
const platform = process.platform;
if (platform === 'win32' && await exists(path.join(vscode.workspace.rootPath!, 'node_modules', '.bin', 'tsc.cmd'))) {
return path.join('.', 'node_modules', '.bin', 'tsc.cmd');
} else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(vscode.workspace.rootPath!, 'node_modules', '.bin', 'tsc'))) {
return path.join('.', 'node_modules', '.bin', 'tsc');
} else {
return 'tsc';
}
}
private getActiveTypeScriptFile(): string | null {
const editor = vscode.window.activeTextEditor;
if (editor) {
const document = editor.document;
if (document && (document.languageId === 'typescript' || document.languageId === 'typescriptreact')) {
return this.lazyClient().normalizePath(document.uri);
}
}
return null;
}
}
+71 -38
View File
@@ -41,13 +41,14 @@ import CodeActionProvider from './features/codeActionProvider';
import ReferenceCodeLensProvider from './features/referencesCodeLensProvider';
import { JsDocCompletionProvider, TryCompleteJsDocCommand } from './features/jsDocCompletionProvider';
import { DirectiveCommentCompletionProvider } from './features/directiveCommentCompletionProvider';
import TypeScriptTaskProvider from './features/taskProvider';
import ImplementationCodeLensProvider from './features/implementationsCodeLensProvider';
import * as BuildStatus from './utils/buildStatus';
import * as ProjectStatus from './utils/projectStatus';
import TypingsStatus, { AtaProgressReporter } from './utils/typingsStatus';
import * as VersionStatus from './utils/versionStatus';
import VersionStatus from './utils/versionStatus';
import { getContributedTypeScriptServerPlugins, TypeScriptServerPlugin } from "./utils/plugins";
interface LanguageDescription {
@@ -67,72 +68,100 @@ interface ProjectConfigMessageItem extends MessageItem {
id: ProjectConfigAction;
}
const MODE_ID_TS = 'typescript';
const MODE_ID_TSX = 'typescriptreact';
const MODE_ID_JS = 'javascript';
const MODE_ID_JSX = 'javascriptreact';
const standardLanguageDescriptions: LanguageDescription[] = [
{
id: 'typescript',
diagnosticSource: 'ts',
modeIds: [MODE_ID_TS, MODE_ID_TSX],
configFile: 'tsconfig.json'
}, {
id: 'javascript',
diagnosticSource: 'js',
modeIds: [MODE_ID_JS, MODE_ID_JSX],
configFile: 'jsconfig.json'
}
];
export function activate(context: ExtensionContext): void {
const MODE_ID_TS = 'typescript';
const MODE_ID_TSX = 'typescriptreact';
const MODE_ID_JS = 'javascript';
const MODE_ID_JSX = 'javascriptreact';
const plugins = getContributedTypeScriptServerPlugins();
const clientHost = new TypeScriptServiceClientHost([
{
id: 'typescript',
diagnosticSource: 'ts',
modeIds: [MODE_ID_TS, MODE_ID_TSX],
configFile: 'tsconfig.json'
},
{
id: 'javascript',
diagnosticSource: 'js',
modeIds: [MODE_ID_JS, MODE_ID_JSX],
configFile: 'jsconfig.json'
}
], context.storagePath, context.globalState, context.workspaceState, plugins);
context.subscriptions.push(clientHost);
const client = clientHost.serviceClient;
const lazyClientHost = (() => {
let clientHost: TypeScriptServiceClientHost | undefined;
return () => {
if (!clientHost) {
clientHost = new TypeScriptServiceClientHost(standardLanguageDescriptions, context.storagePath, context.globalState, context.workspaceState, plugins);
context.subscriptions.push(clientHost);
const host = clientHost;
clientHost.serviceClient.onReady().then(() => {
context.subscriptions.push(ProjectStatus.create(host.serviceClient,
path => new Promise<boolean>(resolve => setTimeout(() => resolve(host.handles(path)), 750)),
context.workspaceState));
}, () => {
// Nothing to do here. The client did show a message;
});
}
return clientHost;
};
})();
context.subscriptions.push(commands.registerCommand('typescript.reloadProjects', () => {
clientHost.reloadProjects();
lazyClientHost().reloadProjects();
}));
context.subscriptions.push(commands.registerCommand('javascript.reloadProjects', () => {
clientHost.reloadProjects();
lazyClientHost().reloadProjects();
}));
context.subscriptions.push(commands.registerCommand('typescript.selectTypeScriptVersion', () => {
client.onVersionStatusClicked();
lazyClientHost().serviceClient.onVersionStatusClicked();
}));
context.subscriptions.push(commands.registerCommand('typescript.openTsServerLog', () => {
client.openTsServerLogFile();
lazyClientHost().serviceClient.openTsServerLogFile();
}));
context.subscriptions.push(commands.registerCommand('typescript.restartTsServer', () => {
client.restartTsServer();
lazyClientHost().serviceClient.restartTsServer();
}));
context.subscriptions.push(workspace.registerTaskProvider(new TypeScriptTaskProvider(() => lazyClientHost().serviceClient)));
const goToProjectConfig = (isTypeScript: boolean) => {
const editor = window.activeTextEditor;
if (editor) {
clientHost.goToProjectConfig(isTypeScript, editor.document.uri);
lazyClientHost().goToProjectConfig(isTypeScript, editor.document.uri);
}
};
context.subscriptions.push(commands.registerCommand('typescript.goToProjectConfig', goToProjectConfig.bind(null, true)));
context.subscriptions.push(commands.registerCommand('javascript.goToProjectConfig', goToProjectConfig.bind(null, false)));
const jsDocCompletionCommand = new TryCompleteJsDocCommand(client);
const jsDocCompletionCommand = new TryCompleteJsDocCommand(() => lazyClientHost().serviceClient);
context.subscriptions.push(commands.registerCommand(TryCompleteJsDocCommand.COMMAND_NAME, jsDocCompletionCommand.tryCompleteJsDoc, jsDocCompletionCommand));
window.onDidChangeActiveTextEditor(VersionStatus.showHideStatus, null, context.subscriptions);
client.onReady().then(() => {
context.subscriptions.push(ProjectStatus.create(client,
path => new Promise<boolean>(resolve => setTimeout(() => resolve(clientHost.handles(path)), 750)),
context.workspaceState));
}, () => {
// Nothing to do here. The client did show a message;
});
const supportedLanguage = [].concat.apply([], standardLanguageDescriptions.map(x => x.modeIds).concat(plugins.map(x => x.languages)));
function didOpenTextDocument(textDocument: TextDocument): boolean {
if (supportedLanguage.indexOf(textDocument.languageId) >= 0) {
openListener.dispose();
// Force activation
void lazyClientHost();
return true;
}
return false;
};
const openListener = workspace.onDidOpenTextDocument(didOpenTextDocument);
for (let textDocument of workspace.textDocuments) {
if (didOpenTextDocument(textDocument)) {
break;
}
}
BuildStatus.update({ queueLength: 0 });
}
@@ -423,6 +452,7 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {
private languages: LanguageProvider[] = [];
private languagePerId: ObjectMap<LanguageProvider>;
private readonly disposables: Disposable[] = [];
private readonly versionStatus: VersionStatus;
constructor(
descriptions: LanguageDescription[],
@@ -446,7 +476,10 @@ class TypeScriptServiceClientHost implements ITypescriptServiceClientHost {
configFileWatcher.onDidDelete(handleProjectCreateOrDelete, this, this.disposables);
configFileWatcher.onDidChange(handleProjectChange, this, this.disposables);
this.client = new TypeScriptServiceClient(this, storagePath, globalState, workspaceState, plugins, this.disposables);
this.versionStatus = new VersionStatus();
this.disposables.push(this.versionStatus);
this.client = new TypeScriptServiceClient(this, storagePath, globalState, workspaceState, this.versionStatus, plugins, this.disposables);
this.languagePerId = Object.create(null);
for (const description of descriptions) {
const manager = new LanguageProvider(this.client, description);
@@ -18,7 +18,7 @@ import { ITypescriptServiceClient, ITypescriptServiceClientHost, API } from './t
import { TypeScriptServerPlugin } from './utils/plugins';
import Logger from './utils/logger';
import * as VersionStatus from './utils/versionStatus';
import VersionStatus from './utils/versionStatus';
import * as is from './utils/is';
import * as nls from 'vscode-nls';
@@ -141,7 +141,9 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
host: ITypescriptServiceClientHost,
storagePath: string | undefined,
globalState: Memento,
private workspaceState: Memento,
private readonly workspaceState: Memento,
private readonly versionStatus: VersionStatus,
private plugins: TypeScriptServerPlugin[],
disposables: Disposable[]
) {
@@ -404,8 +406,8 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient
const label = version || localize('versionNumber.custom', 'custom');
const tooltip = modulePath;
this.modulePath = modulePath;
VersionStatus.showHideStatus();
VersionStatus.setInfo(label, tooltip);
this.versionStatus.showHideStatus();
this.versionStatus.setInfo(label, tooltip);
// This is backwards compatibility code to move the setting from the local
// store into the workspace setting file.
@@ -3,42 +3,53 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import vscode = require('vscode');
import * as vscode from 'vscode';
const versionBarEntry = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, Number.MIN_VALUE);
export function showHideStatus() {
if (!versionBarEntry) {
return;
}
if (!vscode.window.activeTextEditor) {
versionBarEntry.hide();
return;
export default class VersionStatus extends vscode.Disposable {
onChangeEditorSub: any;
private versionBarEntry: vscode.StatusBarItem;
constructor() {
super(() => this.dispose());
this.versionBarEntry = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, Number.MIN_VALUE);
this.onChangeEditorSub = vscode.window.onDidChangeActiveTextEditor(this.showHideStatus, this);
}
let doc = vscode.window.activeTextEditor.document;
if (vscode.languages.match('typescript', doc) || vscode.languages.match('typescriptreact', doc)) {
versionBarEntry.show();
return;
dispose() {
this.versionBarEntry.dispose();
this.onChangeEditorSub.dispose();
}
if (!vscode.window.activeTextEditor.viewColumn) {
// viewColumn is undefined for the debug/output panel, but we still want
// to show the version info
return;
showHideStatus() {
if (!this.versionBarEntry) {
return;
}
if (!vscode.window.activeTextEditor) {
this.versionBarEntry.hide();
return;
}
let doc = vscode.window.activeTextEditor.document;
if (vscode.languages.match('typescript', doc) || vscode.languages.match('typescriptreact', doc)) {
this.versionBarEntry.show();
return;
}
if (!vscode.window.activeTextEditor.viewColumn) {
// viewColumn is undefined for the debug/output panel, but we still want
// to show the version info
return;
}
this.versionBarEntry.hide();
}
versionBarEntry.hide();
}
export function disposeStatus() {
if (versionBarEntry) {
versionBarEntry.dispose();
public setInfo(message: string, tooltip: string) {
this.versionBarEntry.text = message;
this.versionBarEntry.tooltip = tooltip;
this.versionBarEntry.command = 'typescript.selectTypeScriptVersion';
}
}
export function setInfo(message: string, tooltip: string) {
versionBarEntry.text = message;
versionBarEntry.tooltip = tooltip;
versionBarEntry.command = 'typescript.selectTypeScriptVersion';
}
+1 -1
View File
@@ -163,7 +163,7 @@ namespace ProblemMatcher {
if (values === void 0 || values === null) {
return undefined;
}
let result: (string | Problems.ProblemMatcher)[];
let result: (string | Problems.ProblemMatcher)[] = [];
for (let value of values) {
let converted = typeof value === 'string' ? value : fromSingle(value);
if (converted) {