projectStatus.ts 7.0 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
7
import { ITypeScriptServiceClient } from '../typescriptService';
J
Johannes Rieken 已提交
8
import { loadMessageBundle } from 'vscode-nls';
9 10
import { dirname } from 'path';
import { openOrCreateConfigFile, isImplicitProjectConfigFile } from './tsconfig';
11
import * as languageModeIds from '../utils/languageModeIds';
12
import TelemetryReporter from './telemetry';
13 14

const localize = loadMessageBundle();
15
const selector = [languageModeIds.javascript, languageModeIds.javascriptreact];
16 17 18 19 20 21


interface Hint {
	message: string;
}

22 23 24 25
interface ProjectHintedMap {
	[k: string]: boolean;
}

26 27
const fileLimit = 500;

28
class ExcludeHintItem {
29
	public configFileName?: string;
30 31 32
	private _item: vscode.StatusBarItem;
	private _currentHint: Hint;

33 34 35
	constructor(
		private readonly telemetryReporter: TelemetryReporter
	) {
36 37 38 39 40 41 42 43 44 45 46 47
		this._item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, Number.MIN_VALUE);
		this._item.command = 'js.projectStatus.command';
	}

	public getCurrentHint(): Hint {
		return this._currentHint;
	}

	public hide() {
		this._item.hide();
	}

48
	public show(largeRoots?: string) {
49
		this._currentHint = {
50
			message: largeRoots
51
				? localize('hintExclude', "To enable project-wide JavaScript/TypeScript language features, exclude folders with many files, like: {0}", largeRoots)
52
				: localize('hintExclude.generic', "To enable project-wide JavaScript/TypeScript language features, exclude large folders with source files that you do not work on.")
53 54 55
		};
		this._item.tooltip = this._currentHint.message;
		this._item.text = localize('large.label', "Configure Excludes");
56
		this._item.tooltip = localize('hintExclude.tooltip', "To enable project-wide JavaScript/TypeScript language features, exclude large folders with source files that you do not work on.");
57 58
		this._item.color = '#A5DF3B';
		this._item.show();
K
kieferrm 已提交
59
		/* __GDPR__
K
kieferrm 已提交
60 61
			"js.hintProjectExcludes" : {}
		*/
62
		this.telemetryReporter.logTelemetry('js.hintProjectExcludes');
63 64
	}
}
65

66
function createLargeProjectMonitorForProject(item: ExcludeHintItem, client: ITypeScriptServiceClient, isOpen: (path: string) => Promise<boolean>, memento: vscode.Memento): vscode.Disposable[] {
67
	const toDispose: vscode.Disposable[] = [];
68
	const projectHinted: ProjectHintedMap = Object.create(null);
69 70 71

	const projectHintIgnoreList = memento.get<string[]>('projectHintIgnoreList', []);
	for (let path of projectHintIgnoreList) {
72
		if (path === null) {
73
			path = 'undefined';
74 75 76
		}
		projectHinted[path] = true;
	}
77

78
	function onEditor(editor: vscode.TextEditor | undefined): void {
79 80
		if (!editor
			|| !vscode.languages.match(selector, editor.document)
81
			|| !client.normalizePath(editor.document.uri)) {
82

83 84 85 86
			item.hide();
			return;
		}

87
		const file = client.normalizePath(editor.document.uri);
88 89 90
		if (!file) {
			return;
		}
91 92
		isOpen(file).then(value => {
			if (!value) {
93 94 95
				return;
			}

96
			return client.execute('projectInfo', { file, needFileNameList: true } as protocol.ProjectInfoRequestArgs).then(res => {
97 98 99
				if (!res.body) {
					return;
				}
A
Alex Dima 已提交
100
				let { configFileName, fileNames } = res.body;
101

102
				if (projectHinted[configFileName] === true || !fileNames) {
103 104 105
					return;
				}

106
				if (fileNames.length > fileLimit || res.body.languageServiceDisabled) {
107
					let largeRoots = computeLargeRoots(configFileName, fileNames).map(f => `'/${f}/'`).join(', ');
108 109
					item.show(largeRoots);
					projectHinted[configFileName] = true;
110 111 112 113
				} else {
					item.hide();
				}
			});
114
		}).catch(err => {
115
			client.logger.warn(err);
116 117 118
		});
	}

119 120 121 122
	toDispose.push(vscode.workspace.onDidChangeTextDocument(e => {
		delete projectHinted[e.document.fileName];
	}));

123 124 125
	toDispose.push(vscode.window.onDidChangeActiveTextEditor(onEditor));
	onEditor(vscode.window.activeTextEditor);

126 127 128
	return toDispose;
}

129
function createLargeProjectMonitorFromTypeScript(item: ExcludeHintItem, client: ITypeScriptServiceClient): vscode.Disposable {
130 131 132 133 134

	interface LargeProjectMessageItem extends vscode.MessageItem {
		index: number;
	}

135 136 137 138
	return client.onProjectLanguageServiceStateChanged(body => {
		if (body.languageServiceEnabled) {
			item.hide();
		} else {
139
			item.show();
140 141
			const configFileName = body.projectName;
			if (configFileName) {
142
				item.configFileName = configFileName;
143 144 145 146 147
				vscode.window.showWarningMessage<LargeProjectMessageItem>(item.getCurrentHint().message,
					{
						title: localize('large.label', "Configure Excludes"),
						index: 0
					}).then(selected => {
148 149
						if (selected && selected.index === 0) {
							onConfigureExcludesSelected(client, configFileName);
150 151
						}
					});
152
			}
153 154 155 156
		}
	});
}

157
function onConfigureExcludesSelected(
158
	client: ITypeScriptServiceClient,
159 160
	configFileName: string
) {
161 162 163 164 165 166 167 168
	if (!isImplicitProjectConfigFile(configFileName)) {
		vscode.workspace.openTextDocument(configFileName)
			.then(vscode.window.showTextDocument);
	} else {
		const root = client.getWorkspaceRootForResource(vscode.Uri.file(configFileName));
		if (root) {
			openOrCreateConfigFile(
				configFileName.match(/tsconfig\.?.*\.json/) !== null,
169 170
				root,
				client.configuration);
171 172 173 174
		}
	}
}

175
export function create(
176
	client: ITypeScriptServiceClient,
177
	telemetryReporter: TelemetryReporter,
178 179 180
	isOpen: (path: string) => Promise<boolean>,
	memento: vscode.Memento
) {
181 182
	const toDispose: vscode.Disposable[] = [];

183
	const item = new ExcludeHintItem(telemetryReporter);
184
	toDispose.push(vscode.commands.registerCommand('js.projectStatus.command', () => {
185 186 187
		if (item.configFileName) {
			onConfigureExcludesSelected(client, item.configFileName);
		}
188 189
		let { message } = item.getCurrentHint();
		return vscode.window.showInformationMessage(message);
190 191 192 193 194 195 196 197
	}));

	if (client.apiVersion.has213Features()) {
		toDispose.push(createLargeProjectMonitorFromTypeScript(item, client));
	} else {
		toDispose.push(...createLargeProjectMonitorForProject(item, client, isOpen, memento));
	}

198 199 200
	return vscode.Disposable.from(...toDispose);
}

J
Johannes Rieken 已提交
201
function computeLargeRoots(configFileName: string, fileNames: string[]): string[] {
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221

	let roots: { [first: string]: number } = Object.create(null);
	let dir = dirname(configFileName);

	// console.log(dir, fileNames);

	for (let fileName of fileNames) {
		if (fileName.indexOf(dir) === 0) {
			let first = fileName.substring(dir.length + 1);
			first = first.substring(0, first.indexOf('/'));
			if (first) {
				roots[first] = (roots[first] || 0) + 1;
			}
		}
	}

	let data: { root: string; count: number }[] = [];
	for (let key in roots) {
		data.push({ root: key, count: roots[key] });
	}
222 223 224 225

	data
		.sort((a, b) => b.count - a.count)
		.filter(s => s.root === 'src' || s.root === 'test' || s.root === 'tests');
226 227 228 229 230 231 232 233 234 235 236 237 238

	let result: string[] = [];
	let sum = 0;
	for (let e of data) {
		sum += e.count;
		result.push(e.root);
		if (fileNames.length - sum < fileLimit) {
			break;
		}
	}

	return result;
}