diff --git a/extensions/typescript/src/typescriptService.ts b/extensions/typescript/src/typescriptService.ts index 4c466d0b93d..a815a86a601 100644 --- a/extensions/typescript/src/typescriptService.ts +++ b/extensions/typescript/src/typescriptService.ts @@ -5,7 +5,7 @@ 'use strict'; -import { CancellationToken, Uri } from 'vscode'; +import { CancellationToken, Uri, Event } from 'vscode'; import * as Proto from './protocol'; import * as semver from 'semver'; @@ -53,6 +53,10 @@ export class API { public has208Features(): boolean { return semver.gte(this._version, '2.0.8'); } + + public has213Features(): boolean { + return semver.gte(this._version, '2.1.3'); + } } export interface ITypescriptServiceClient { @@ -63,6 +67,8 @@ export interface ITypescriptServiceClient { warn(message: string, data?: any): void; error(message: string, data?: any): void; + onProjectLanguageServiceStateChanged: Event; + logTelemetry(eventName: string, properties?: { [prop: string]: string }); experimentalAutoBuild: boolean; diff --git a/extensions/typescript/src/typescriptServiceClient.ts b/extensions/typescript/src/typescriptServiceClient.ts index 5535bf49eb4..870925b19ea 100644 --- a/extensions/typescript/src/typescriptServiceClient.ts +++ b/extensions/typescript/src/typescriptServiceClient.ts @@ -12,7 +12,7 @@ import * as fs from 'fs'; import * as electron from './utils/electron'; import { Reader } from './utils/wireProtocol'; -import { workspace, window, Uri, CancellationToken, OutputChannel, Memento, MessageItem } from 'vscode'; +import { workspace, window, Uri, CancellationToken, OutputChannel, Memento, MessageItem, EventEmitter, Event } from 'vscode'; import * as Proto from './protocol'; import { ITypescriptServiceClient, ITypescriptServiceClientHost, API } from './typescriptService'; @@ -102,6 +102,7 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient private requestQueue: RequestItem[]; private pendingResponses: number; private callbacks: CallbackMap; + private _onProjectLanguageServiceStateChanged = new EventEmitter(); private _packageInfo: IPackageInfo | null; private _apiVersion: API; @@ -148,6 +149,10 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient this.startService(); } + get onProjectLanguageServiceStateChanged(): Event { + return this._onProjectLanguageServiceStateChanged.event; + } + private get output(): OutputChannel { if (!this._output) { this._output = window.createOutputChannel(localize('channelName', 'TypeScript')); @@ -684,6 +689,11 @@ export default class TypeScriptServiceClient implements ITypescriptServiceClient break; } this.logTelemetry(telemetryData.telemetryEventName, properties); + } else if (event.event === 'projectLanguageServiceState') { + const data = (event as Proto.ProjectLanguageServiceStateEvent).body; + if (data) { + this._onProjectLanguageServiceStateChanged.fire(data); + } } } else { throw new Error('Unknown message type ' + message.type + ' recevied'); diff --git a/extensions/typescript/src/utils/projectStatus.ts b/extensions/typescript/src/utils/projectStatus.ts index 360ae2d5c45..b60a1ff697d 100644 --- a/extensions/typescript/src/utils/projectStatus.ts +++ b/extensions/typescript/src/utils/projectStatus.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { ITypescriptServiceClient } from '../typescriptService'; import { loadMessageBundle } from 'vscode-nls'; -import { dirname, join } from 'path'; +import { dirname } from 'path'; const localize = loadMessageBundle(); const selector = ['javascript', 'javascriptreact']; @@ -22,37 +22,69 @@ interface Hint { options: Option[]; } +interface ProjectHintedMap { + [k: string]: boolean; +} + const fileLimit = 500; -export function create(client: ITypescriptServiceClient, isOpen: (path: string) => Promise, memento: vscode.Memento) { +class ExcludeHintItem { + 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(); + } + + public show(configFileName: string, largeRoots: string, onExecute: () => void) { + this._currentHint = { + message: largeRoots.length > 0 + ? localize('hintExclude', "For better performance exclude folders with many files, like: {0}", largeRoots) + : localize('hintExclude.generic', "For better performance exclude folders with many files."), + options: [{ + title: localize('open', "Configure Excludes"), + execute: () => { + this._client.logTelemetry('js.hintProjectExcludes.accepted'); + onExecute(); + this._item.hide(); + + return vscode.workspace.openTextDocument(configFileName) + .then(vscode.window.showTextDocument); + } + }] + }; + this._item.tooltip = this._currentHint.message; + this._item.text = localize('large.label', "Configure Excludes"); + this._item.tooltip = localize('hintExclude.tooltip', "For better performance exclude folders with many files."); + this._item.color = '#A5DF3B'; + this._item.show(); + this._client.logTelemetry('js.hintProjectExcludes'); + } +} + +function createLargeProjectMonitorForProject(item: ExcludeHintItem, client: ITypescriptServiceClient, isOpen: (path: string) => Promise, memento: vscode.Memento): vscode.Disposable[] { const toDispose: vscode.Disposable[] = []; - const projectHinted: { [k: string]: boolean } = Object.create(null); + const projectHinted: ProjectHintedMap = Object.create(null); const projectHintIgnoreList = memento.get('projectHintIgnoreList', []); for (let path of projectHintIgnoreList) { - if (!path) { + if (path === null) { path = 'undefined'; } projectHinted[path] = true; } - let currentHint: Hint; - let item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, Number.MIN_VALUE); - item.command = 'js.projectStatus.command'; - toDispose.push(vscode.commands.registerCommand('js.projectStatus.command', () => { - let {message, options} = currentHint; - return vscode.window.showInformationMessage(message, ...options).then(selection => { - if (selection) { - return selection.execute(); - } - }); - })); - - toDispose.push(vscode.workspace.onDidChangeTextDocument(e => { - delete projectHinted[e.document.fileName]; - })); - function onEditor(editor: vscode.TextEditor | undefined): void { if (!editor || !vscode.languages.match(selector, editor.document) @@ -66,56 +98,26 @@ export function create(client: ITypescriptServiceClient, isOpen: (path: string) if (!file) { return; } - isOpen(file).then(value => { if (!value) { return; } - return client.execute('projectInfo', { file, needFileNameList: true }).then(res => { + return client.execute('projectInfo', { file, needFileNameList: true } as protocol.ProjectInfoRequestArgs).then(res => { if (!res.body) { return; } let {configFileName, fileNames} = res.body; - if (projectHinted[configFileName] === true) { + if (projectHinted[configFileName] === true || !fileNames) { return; } - if (fileNames && fileNames.length > fileLimit) { + if (fileNames.length > fileLimit || res.body.languageServiceDisabled) { let largeRoots = computeLargeRoots(configFileName, fileNames).map(f => `'/${f}/'`).join(', '); - - currentHint = { - message: largeRoots.length > 0 - ? localize('hintExclude', "For better performance exclude folders with many files, like: {0}", largeRoots) - : localize('hintExclude.generic', "For better performance exclude folders with many files."), - options: [{ - title: localize('open', "Configure Excludes"), - execute: () => { - client.logTelemetry('js.hintProjectExcludes.accepted'); - projectHinted[configFileName] = true; - item.hide(); - - let configFileUri: vscode.Uri; - let rootPath = vscode.workspace.rootPath; - if (rootPath && dirname(configFileName).indexOf('' + rootPath) === 0) { - configFileUri = vscode.Uri.file(configFileName); - } else { - configFileUri = vscode.Uri.parse('untitled://' + join(rootPath, 'jsconfig.json')); - } - - return vscode.workspace.openTextDocument(configFileUri) - .then(vscode.window.showTextDocument); - } - }] - }; - item.tooltip = currentHint.message; - item.text = localize('large.label', "Configure Excludes"); - item.tooltip = localize('hintExclude.tooltip', "For better performance exclude folders with many files."); - item.color = '#A5DF3B'; - item.show(); - client.logTelemetry('js.hintProjectExcludes'); - + item.show(configFileName, largeRoots, () => { + projectHinted[configFileName] = true; + }); } else { item.hide(); } @@ -125,9 +127,45 @@ export function create(client: ITypescriptServiceClient, isOpen: (path: string) }); } + toDispose.push(vscode.workspace.onDidChangeTextDocument(e => { + delete projectHinted[e.document.fileName]; + })); + toDispose.push(vscode.window.onDidChangeActiveTextEditor(onEditor)); onEditor(vscode.window.activeTextEditor); + return toDispose; +} + +function createLargeProjectMonitorFromTypeScript(item: ExcludeHintItem, client: ITypescriptServiceClient): vscode.Disposable { + return client.onProjectLanguageServiceStateChanged(body => { + if (body.languageServiceEnabled) { + item.hide(); + } else { + item.show(body.projectName, '', () => { }); + } + }); +} + +export function create(client: ITypescriptServiceClient, isOpen: (path: string) => Promise, memento: vscode.Memento) { + const toDispose: vscode.Disposable[] = []; + + let item = new ExcludeHintItem(client); + toDispose.push(vscode.commands.registerCommand('js.projectStatus.command', () => { + let {message, options} = item.getCurrentHint(); + return vscode.window.showInformationMessage(message, ...options).then(selection => { + if (selection) { + return selection.execute(); + } + }); + })); + + if (client.apiVersion.has213Features()) { + toDispose.push(createLargeProjectMonitorFromTypeScript(item, client)); + } else { + toDispose.push(...createLargeProjectMonitorForProject(item, client, isOpen, memento)); + } + return vscode.Disposable.from(...toDispose); }