task.ts 8.3 KB
Newer Older
M
Matt Bierner 已提交
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
9
import * as nls from 'vscode-nls';
10
import { ITypeScriptServiceClient } from '../typescriptService';
11
import { Lazy } from '../utils/lazy';
12
import { isImplicitProjectConfigFile } from '../utils/tsconfig';
13 14
import TsConfigProvider, { TSConfig } from '../utils/tsconfigProvider';

M
Matt Bierner 已提交
15

16 17
const localize = nls.loadMessageBundle();

18
type AutoDetect = 'on' | 'off' | 'build' | 'watch';
19

20

M
Matt Bierner 已提交
21 22 23 24 25 26 27
const exists = (file: string): Promise<boolean> =>
	new Promise<boolean>((resolve, _reject) => {
		fs.exists(file, (value: boolean) => {
			resolve(value);
		});
	});

28

D
Dirk Baeumer 已提交
29
interface TypeScriptTaskDefinition extends vscode.TaskDefinition {
D
Dirk Baeumer 已提交
30
	tsconfig: string;
31
	option?: string;
32 33
}

34 35 36 37
/**
 * Provides tasks for building `tsconfig.json` files in a project.
 */
class TscTaskProvider implements vscode.TaskProvider {
38
	private autoDetect: AutoDetect = 'on';
39
	private readonly tsconfigProvider: TsConfigProvider;
40
	private readonly disposables: vscode.Disposable[] = [];
M
Matt Bierner 已提交
41 42

	public constructor(
43
		private readonly client: Lazy<ITypeScriptServiceClient>
44 45
	) {
		this.tsconfigProvider = new TsConfigProvider();
46 47 48 49 50 51 52

		vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this, this.disposables);
		this.onConfigurationChanged();
	}

	dispose() {
		this.disposables.forEach(x => x.dispose());
53 54 55
	}

	public async provideTasks(token: vscode.CancellationToken): Promise<vscode.Task[]> {
M
Matt Bierner 已提交
56 57
		const folders = vscode.workspace.workspaceFolders;
		if (!folders || !folders.length) {
M
Matt Bierner 已提交
58 59 60
			return [];
		}

61 62 63 64 65
		const configPaths: Set<string> = new Set();
		const tasks: vscode.Task[] = [];
		for (const project of await this.getAllTsConfigs(token)) {
			if (!configPaths.has(project.path)) {
				configPaths.add(project.path);
66
				tasks.push(...(await this.getTasksForProject(project)));
67 68 69
			}
		}
		return tasks;
M
Matt Bierner 已提交
70 71
	}

D
Dirk Baeumer 已提交
72 73 74 75
	public resolveTask(_task: vscode.Task): vscode.Task | undefined {
		return undefined;
	}

76 77
	private async getAllTsConfigs(token: vscode.CancellationToken): Promise<TSConfig[]> {
		const out = new Set<TSConfig>();
78 79 80 81
		const configs = [
			...await this.getTsConfigForActiveFile(token),
			...await this.getTsConfigsInWorkspace()
		];
82
		for (const config of configs) {
83
			if (await exists(config.path)) {
84
				out.add(config);
85 86
			}
		}
87
		return Array.from(out);
88
	}
M
Matt Bierner 已提交
89

90
	private async getTsConfigForActiveFile(token: vscode.CancellationToken): Promise<TSConfig[]> {
M
Matt Bierner 已提交
91 92 93
		const editor = vscode.window.activeTextEditor;
		if (editor) {
			if (path.basename(editor.document.fileName).match(/^tsconfig\.(.\.)?json$/)) {
94
				const uri = editor.document.uri;
95
				return [{
96 97
					path: uri.fsPath,
					workspaceFolder: vscode.workspace.getWorkspaceFolder(uri)
98
				}];
M
Matt Bierner 已提交
99 100 101 102 103 104 105 106
			}
		}

		const file = this.getActiveTypeScriptFile();
		if (!file) {
			return [];
		}

107 108 109 110 111 112 113
		const response = await this.client.value.execute(
			'projectInfo',
			{ file, needFileNameList: false },
			token);
		if (response.type !== 'response' || !response.body) {
			return [];
		}
M
Matt Bierner 已提交
114

115 116 117 118 119 120 121 122 123
		const { configFileName } = response.body;
		if (configFileName && !isImplicitProjectConfigFile(configFileName)) {
			const normalizedConfigPath = path.normalize(configFileName);
			const uri = vscode.Uri.file(normalizedConfigPath);
			const folder = vscode.workspace.getWorkspaceFolder(uri);
			return [{
				path: normalizedConfigPath,
				workspaceFolder: folder
			}];
M
Matt Bierner 已提交
124
		}
125

M
Matt Bierner 已提交
126 127 128
		return [];
	}

129
	private async getTsConfigsInWorkspace(): Promise<TSConfig[]> {
130
		return Array.from(await this.tsconfigProvider.getConfigsForWorkspace());
M
Matt Bierner 已提交
131 132
	}

133
	private static async getCommand(project: TSConfig): Promise<string> {
134
		if (project.workspaceFolder) {
135 136 137 138 139 140 141 142
			const localTsc = await TscTaskProvider.getLocalTscAtPath(path.dirname(project.path));
			if (localTsc) {
				return localTsc;
			}

			const workspaceTsc = await TscTaskProvider.getLocalTscAtPath(project.workspaceFolder.uri.fsPath);
			if (workspaceTsc) {
				return workspaceTsc;
143
			}
M
Matt Bierner 已提交
144
		}
145 146

		// Use global tsc version
147
		return 'tsc';
M
Matt Bierner 已提交
148 149
	}

150 151 152 153 154 155 156 157 158 159 160
	private static async getLocalTscAtPath(folderPath: string): Promise<string | undefined> {
		const platform = process.platform;
		const bin = path.join(folderPath, 'node_modules', '.bin');
		if (platform === 'win32' && await exists(path.join(bin, 'tsc.cmd'))) {
			return path.join(bin, 'tsc.cmd');
		} else if ((platform === 'linux' || platform === 'darwin') && await exists(path.join(bin, 'tsc'))) {
			return path.join(bin, 'tsc');
		}
		return undefined;
	}

M
Matt Bierner 已提交
161 162 163 164 165
	private getActiveTypeScriptFile(): string | null {
		const editor = vscode.window.activeTextEditor;
		if (editor) {
			const document = editor.document;
			if (document && (document.languageId === 'typescript' || document.languageId === 'typescriptreact')) {
M
Matt Bierner 已提交
166
				return this.client.value.toPath(document.uri);
M
Matt Bierner 已提交
167 168 169 170
			}
		}
		return null;
	}
171

172
	private async getTasksForProject(project: TSConfig): Promise<vscode.Task[]> {
173
		const command = await TscTaskProvider.getCommand(project);
174
		const label = this.getLabelForTasks(project);
175

176 177
		const tasks: vscode.Task[] = [];

178
		if (this.autoDetect === 'build' || this.autoDetect === 'on') {
179 180 181
			const buildTaskidentifier: TypeScriptTaskDefinition = { type: 'typescript', tsconfig: label };
			const buildTask = new vscode.Task(
				buildTaskidentifier,
182
				project.workspaceFolder || vscode.TaskScope.Workspace,
183 184
				localize('buildTscLabel', 'build - {0}', label),
				'tsc',
185
				new vscode.ShellExecution(command, ['-p', project.path]),
186 187 188 189 190 191
				'$tsc');
			buildTask.group = vscode.TaskGroup.Build;
			buildTask.isBackground = false;
			tasks.push(buildTask);
		}

192
		if (this.autoDetect === 'watch' || this.autoDetect === 'on') {
193 194 195
			const watchTaskidentifier: TypeScriptTaskDefinition = { type: 'typescript', tsconfig: label, option: 'watch' };
			const watchTask = new vscode.Task(
				watchTaskidentifier,
196
				project.workspaceFolder || vscode.TaskScope.Workspace,
197 198
				localize('buildAndWatchTscLabel', 'watch - {0}', label),
				'tsc',
199
				new vscode.ShellExecution(command, ['--watch', '-p', project.path]),
200 201 202 203 204 205 206 207 208 209
				'$tsc-watch');
			watchTask.group = vscode.TaskGroup.Build;
			watchTask.isBackground = true;
			tasks.push(watchTask);
		}

		return tasks;
	}

	private getLabelForTasks(project: TSConfig): string {
210
		if (project.workspaceFolder) {
211 212
			const projectFolder = project.workspaceFolder;
			const workspaceFolders = vscode.workspace.workspaceFolders;
213
			const relativePath = path.relative(project.workspaceFolder.uri.fsPath, project.path);
214 215 216
			if (workspaceFolders && workspaceFolders.length > 1) {
				// Use absolute path when we have multiple folders with the same name
				if (workspaceFolders.filter(x => x.name === projectFolder.name).length > 1) {
217
					return path.join(project.workspaceFolder.uri.fsPath, relativePath);
218
				} else {
219
					return path.join(project.workspaceFolder.name, relativePath);
220
				}
221
			} else {
222
				return relativePath;
223 224
			}
		}
225 226
		return project.path;
	}
227

228
	private onConfigurationChanged(): void {
229 230
		const type = vscode.workspace.getConfiguration('typescript.tsc').get<AutoDetect>('autoDetect');
		this.autoDetect = typeof type === 'undefined' ? 'on' : type;
231
	}
232 233 234
}

/**
235
 * Manages registrations of TypeScript task providers with VS Code.
236 237 238 239 240 241
 */
export default class TypeScriptTaskProviderManager {
	private taskProviderSub: vscode.Disposable | undefined = undefined;
	private readonly disposables: vscode.Disposable[] = [];

	constructor(
242
		private readonly client: Lazy<ITypeScriptServiceClient>
243 244 245 246 247 248 249 250 251 252 253 254 255 256
	) {
		vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged, this, this.disposables);
		this.onConfigurationChanged();
	}

	dispose() {
		if (this.taskProviderSub) {
			this.taskProviderSub.dispose();
			this.taskProviderSub = undefined;
		}
		this.disposables.forEach(x => x.dispose());
	}

	private onConfigurationChanged() {
257
		const autoDetect = vscode.workspace.getConfiguration('typescript.tsc').get<AutoDetect>('autoDetect');
258 259 260
		if (this.taskProviderSub && autoDetect === 'off') {
			this.taskProviderSub.dispose();
			this.taskProviderSub = undefined;
261
		} else if (!this.taskProviderSub && autoDetect !== 'off') {
262
			this.taskProviderSub = vscode.workspace.registerTaskProvider('typescript', new TscTaskProvider(this.client));
263 264
		}
	}
M
Matt Bierner 已提交
265
}