projectStatus.ts 6.8 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';
J
Johannes Rieken 已提交
7 8
import { ITypescriptServiceClient } from '../typescriptService';
import { loadMessageBundle } from 'vscode-nls';
9 10
import { dirname } from 'path';
import { openOrCreateConfigFile, isImplicitProjectConfigFile } from './tsconfig';
11 12 13 14 15 16 17 18 19

const localize = loadMessageBundle();
const selector = ['javascript', 'javascriptreact'];


interface Hint {
	message: string;
}

20 21 22 23
interface ProjectHintedMap {
	[k: string]: boolean;
}

24 25
const fileLimit = 500;

26
class ExcludeHintItem {
27
	public configFileName?: string;
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
	private _item: vscode.StatusBarItem;
	private _client: ITypescriptServiceClient;
	private _currentHint: Hint;

	constructor(client: ITypescriptServiceClient) {
		this._client = client;
		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();
	}

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

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

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

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

81 82 83 84
			item.hide();
			return;
		}

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

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

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

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

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

121 122 123
	toDispose.push(vscode.window.onDidChangeActiveTextEditor(onEditor));
	onEditor(vscode.window.activeTextEditor);

124 125 126 127
	return toDispose;
}

function createLargeProjectMonitorFromTypeScript(item: ExcludeHintItem, client: ITypescriptServiceClient): vscode.Disposable {
128 129 130 131 132

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

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

155 156 157 158 159 160 161 162 163 164 165 166 167 168
function onConfigureExcludesSelected(client: ITypescriptServiceClient, configFileName: string) {
	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,
				root);
		}
	}
}

169 170 171
export function create(client: ITypescriptServiceClient, isOpen: (path: string) => Promise<boolean>, memento: vscode.Memento) {
	const toDispose: vscode.Disposable[] = [];

172
	const item = new ExcludeHintItem(client);
173
	toDispose.push(vscode.commands.registerCommand('js.projectStatus.command', () => {
174 175 176
		if (item.configFileName) {
			onConfigureExcludesSelected(client, item.configFileName);
		}
177 178
		let { message } = item.getCurrentHint();
		return vscode.window.showInformationMessage(message);
179 180 181 182 183 184 185 186
	}));

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

187 188 189
	return vscode.Disposable.from(...toDispose);
}

J
Johannes Rieken 已提交
190
function computeLargeRoots(configFileName: string, fileNames: string[]): string[] {
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210

	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] });
	}
211 212 213 214

	data
		.sort((a, b) => b.count - a.count)
		.filter(s => s.root === 'src' || s.root === 'test' || s.root === 'tests');
215 216 217 218 219 220 221 222 223 224 225 226 227

	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;
}