diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json index 461cb773a4d..b039fac9631 100644 --- a/extensions/typescript/package.json +++ b/extensions/typescript/package.json @@ -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": { diff --git a/extensions/typescript/src/features/jsDocCompletionProvider.ts b/extensions/typescript/src/features/jsDocCompletionProvider.ts index da02e942bfc..502cf89c42c 100644 --- a/extensions/typescript/src/features/jsDocCompletionProvider.ts +++ b/extensions/typescript/src/features/jsDocCompletionProvider.ts @@ -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 { - 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) { diff --git a/extensions/typescript/src/features/taskProvider.ts b/extensions/typescript/src/features/taskProvider.ts new file mode 100644 index 00000000000..76528021983 --- /dev/null +++ b/extensions/typescript/src/features/taskProvider.ts @@ -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 => + new Promise((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 { + 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 { + 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 { + 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 { + 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; + } +} \ No newline at end of file diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 9949d0db9d2..07a666936b9 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -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(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(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; 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); diff --git a/extensions/typescript/src/typescriptServiceClient.ts b/extensions/typescript/src/typescriptServiceClient.ts index cf2b7c3be6f..5e22350feb0 100644 --- a/extensions/typescript/src/typescriptServiceClient.ts +++ b/extensions/typescript/src/typescriptServiceClient.ts @@ -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. diff --git a/extensions/typescript/src/utils/versionStatus.ts b/extensions/typescript/src/utils/versionStatus.ts index e92b87d0f24..21f3a1c34f1 100644 --- a/extensions/typescript/src/utils/versionStatus.ts +++ b/extensions/typescript/src/utils/versionStatus.ts @@ -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'; -} diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 5f23efc8f80..aa6497bc410 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -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) {