configuration.ts 37.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
/*---------------------------------------------------------------------------------------------
 *  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 URI from 'vs/base/common/uri';
import * as paths from 'vs/base/common/paths';
import { TPromise } from 'vs/base/common/winjs.base';
import Event, { Emitter } from 'vs/base/common/event';
11
import { StrictResourceMap } from 'vs/base/common/map';
12
import { equals, coalesce } from 'vs/base/common/arrays';
13 14 15
import * as objects from 'vs/base/common/objects';
import * as errors from 'vs/base/common/errors';
import * as collections from 'vs/base/common/collections';
16
import { Disposable, toDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
17
import { RunOnceScheduler } from 'vs/base/common/async';
18
import { readFile, stat } from 'vs/base/node/pfs';
19
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
20
import * as extfs from 'vs/base/node/extfs';
21
import { IWorkspaceContextService, IWorkspace, Workspace, WorkbenchState } from 'vs/platform/workspace/common/workspace';
22
import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
23
import { isLinux } from 'vs/base/common/platform';
24
import { ConfigWatcher } from 'vs/base/node/config';
25
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
26
import { CustomConfigurationModel } from 'vs/platform/configuration/common/model';
27
import { WorkspaceConfigurationModel, ScopedConfigurationModel, FolderConfigurationModel, FolderSettingsModel } from 'vs/workbench/services/configuration/common/configurationModels';
28
import { IConfigurationServiceEvent, ConfigurationSource, IConfigurationKeys, IConfigurationValue, ConfigurationModel, IConfigurationOverrides, Configuration as BaseConfiguration, IConfigurationValues, IConfigurationData } from 'vs/platform/configuration/common/configuration';
29
import { IWorkspaceConfigurationService, WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME, WORKSPACE_STANDALONE_CONFIGURATIONS, WORKSPACE_CONFIG_DEFAULT_PATH } from 'vs/workbench/services/configuration/common/configuration';
30
import { ConfigurationService as GlobalConfigurationService } from 'vs/platform/configuration/node/configurationService';
31
import * as nls from 'vs/nls';
32 33
import { Registry } from 'vs/platform/registry/common/platform';
import { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/platform/extensions/common/extensionsRegistry';
34
import { IConfigurationNode, IConfigurationRegistry, Extensions, editorConfigurationSchemaId, IDefaultConfigurationExtension, validateProperty, ConfigurationScope, schemaId } from 'vs/platform/configuration/common/configurationRegistry';
35
import { createHash } from 'crypto';
36
import { getWorkspaceLabel, IWorkspacesService, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IStoredWorkspaceFolder, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
37
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
38

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
interface IStat {
	resource: URI;
	isDirectory?: boolean;
	children?: { resource: URI; }[];
}

interface IContent {
	resource: URI;
	value: string;
}

interface IWorkspaceConfiguration<T> {
	workspace: T;
	consolidated: any;
}

type IWorkspaceFoldersConfiguration = { [rootFolder: string]: { folders: string[]; } };

57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);

// BEGIN VSCode extension point `configuration`
const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>('configuration', [], {
	description: nls.localize('vscode.extension.contributes.configuration', 'Contributes configuration settings.'),
	type: 'object',
	defaultSnippets: [{ body: { title: '', properties: {} } }],
	properties: {
		title: {
			description: nls.localize('vscode.extension.contributes.configuration.title', 'A summary of the settings. This label will be used in the settings file as separating comment.'),
			type: 'string'
		},
		properties: {
			description: nls.localize('vscode.extension.contributes.configuration.properties', 'Description of the configuration properties.'),
			type: 'object',
			additionalProperties: {
				anyOf: [
					{ $ref: 'http://json-schema.org/draft-04/schema#' },
					{
						type: 'object',
						properties: {
							isExecutable: {
								type: 'boolean'
S
Sandeep Somavarapu 已提交
80 81 82
							},
							scope: {
								type: 'string',
S
Sandeep Somavarapu 已提交
83 84
								enum: ['window', 'resource'],
								default: 'window',
S
Sandeep Somavarapu 已提交
85
								enumDescriptions: [
S
Sandeep Somavarapu 已提交
86
									nls.localize('scope.window.description', "Window specific configuration, which can be configured in the User or Workspace settings."),
S
Sandeep Somavarapu 已提交
87
									nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the User, Workspace or Folder settings.")
S
Sandeep Somavarapu 已提交
88
								],
S
Sandeep Somavarapu 已提交
89
								description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `window` and `resource`.")
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
							}
						}
					}
				]
			}
		}
	}
});
configurationExtPoint.setHandler(extensions => {
	const configurations: IConfigurationNode[] = [];


	for (let i = 0; i < extensions.length; i++) {
		const configuration = <IConfigurationNode>objects.clone(extensions[i].value);
		const collector = extensions[i].collector;

		if (configuration.type && configuration.type !== 'object') {
			collector.warn(nls.localize('invalid.type', "if set, 'configuration.type' must be set to 'object"));
		} else {
			configuration.type = 'object';
		}

		if (configuration.title && (typeof configuration.title !== 'string')) {
			collector.error(nls.localize('invalid.title', "'configuration.title' must be a string"));
		}

		validateProperties(configuration, collector);

		configuration.id = extensions[i].description.id;
		configurations.push(configuration);
	}

	configurationRegistry.registerConfigurations(configurations, false);
});
// END VSCode extension point `configuration`

// BEGIN VSCode extension point `configurationDefaults`
const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>('configurationDefaults', [], {
	description: nls.localize('vscode.extension.contributes.defaultConfiguration', 'Contributes default editor configuration settings by language.'),
	type: 'object',
	defaultSnippets: [{ body: {} }],
	patternProperties: {
		'\\[.*\\]$': {
			type: 'object',
			default: {},
			$ref: editorConfigurationSchemaId,
		}
	}
});
defaultConfigurationExtPoint.setHandler(extensions => {
	const defaultConfigurations: IDefaultConfigurationExtension[] = extensions.map(extension => {
		const id = extension.description.id;
		const name = extension.description.name;
		const defaults = objects.clone(extension.value);
		return <IDefaultConfigurationExtension>{
			id, name, defaults
		};
	});
	configurationRegistry.registerDefaultConfigurations(defaultConfigurations);
});
// END VSCode extension point `configurationDefaults`

function validateProperties(configuration: IConfigurationNode, collector: ExtensionMessageCollector): void {
	let properties = configuration.properties;
	if (properties) {
		if (typeof properties !== 'object') {
			collector.error(nls.localize('invalid.properties', "'configuration.properties' must be an object"));
			configuration.properties = {};
		}
		for (let key in properties) {
			const message = validateProperty(key);
S
Sandeep Somavarapu 已提交
161
			const propertyConfiguration = configuration.properties[key];
S
Sandeep Somavarapu 已提交
162
			propertyConfiguration.scope = propertyConfiguration.scope && propertyConfiguration.scope.toString() === 'resource' ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW;
163 164 165 166 167 168 169 170 171 172 173 174 175 176
			if (message) {
				collector.warn(message);
				delete properties[key];
			}
		}
	}
	let subNodes = configuration.allOf;
	if (subNodes) {
		for (let node of subNodes) {
			validateProperties(node, collector);
		}
	}
}

177
export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService {
178 179 180

	public _serviceBrand: any;

181 182
	protected workspace: Workspace = null;
	protected _configuration: Configuration<any>;
183

184
	protected readonly _onDidUpdateConfiguration: Emitter<IConfigurationServiceEvent> = this._register(new Emitter<IConfigurationServiceEvent>());
185 186
	public readonly onDidUpdateConfiguration: Event<IConfigurationServiceEvent> = this._onDidUpdateConfiguration.event;

187 188
	protected readonly _onDidChangeWorkspaceRoots: Emitter<void> = this._register(new Emitter<void>());
	public readonly onDidChangeWorkspaceRoots: Event<void> = this._onDidChangeWorkspaceRoots.event;
189

S
Sandeep Somavarapu 已提交
190 191 192
	protected readonly _onDidChangeWorkspaceName: Emitter<void> = this._register(new Emitter<void>());
	public readonly onDidChangeWorkspaceName: Event<void> = this._onDidChangeWorkspaceName.event;

193
	constructor() {
194
		super();
195
		this._configuration = new Configuration(new BaseConfiguration(new ConfigurationModel<any>(), new ConfigurationModel<any>()), new ConfigurationModel<any>(), new StrictResourceMap<FolderConfigurationModel<any>>(), this.workspace);
196 197
	}

B
Benjamin Pasero 已提交
198
	public getWorkspace(): IWorkspace {
199 200 201
		return this.workspace;
	}

202
	public getWorkbenchState(): WorkbenchState {
203 204 205 206 207 208 209
		// Workspace has configuration file
		if (this.workspace.configuration) {
			return WorkbenchState.WORKSPACE;
		}

		// Folder has single root
		if (this.workspace.roots.length === 1) {
210
			return WorkbenchState.FOLDER;
211
		}
212 213

		// Empty
214
		return WorkbenchState.EMPTY;
215 216
	}

217
	public getRoot(resource: URI): URI {
218
		return this.workspace.getRoot(resource);
219 220
	}

221
	public isInsideWorkspace(resource: URI): boolean {
222
		return !!this.getRoot(resource);
223 224
	}

225
	public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean {
226 227 228 229 230
		switch (this.getWorkbenchState()) {
			case WorkbenchState.FOLDER:
				return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.roots[0].fsPath, workspaceIdentifier);
			case WorkbenchState.WORKSPACE:
				return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id;
231
		}
232
		return false;
233 234
	}

235
	public toResource(workspaceRelativePath: string): URI {
236
		if (this.workspace.roots.length) {
237 238 239
			return URI.file(paths.join(this.workspace.roots[0].fsPath, workspaceRelativePath));
		}
		return null;
240 241
	}

242 243 244 245 246 247 248 249 250 251 252 253
	public initialize(trigger: boolean = true): TPromise<any> {
		this.resetCaches();
		return this.updateConfiguration()
			.then(() => {
				if (trigger) {
					this.triggerConfigurationChange();
				}
			});
	}

	public reloadConfiguration(section?: string): TPromise<any> {
		return TPromise.as(this.getConfiguration(section));
254 255
	}

256 257
	public getConfigurationData<T>(): IConfigurationData<T> {
		return this._configuration.toData();
258 259
	}

260 261
	public getConfiguration<C>(section?: string, overrides?: IConfigurationOverrides): C {
		return this._configuration.getValue<C>(section, overrides);
262 263
	}

S
Sandeep Somavarapu 已提交
264 265
	public lookup<C>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<C> {
		return this._configuration.lookup<C>(key, overrides);
266 267
	}

268 269
	public keys(overrides?: IConfigurationOverrides): IConfigurationKeys {
		return this._configuration.keys(overrides);
270
	}
271

272
	public values<V>(): IConfigurationValues {
273
		return this._configuration.values();
274 275
	}

276
	public getUnsupportedWorkspaceKeys(): string[] {
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
		return [];
	}

	public isInWorkspaceContext(): boolean {
		return false;
	}

	protected triggerConfigurationChange(): void {
		this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.Workspace, sourceConfig: void 0 });
	}

	public handleWorkspaceFileEvents(event: FileChangesEvent): void {
		// implemented by sub classes
	}

	protected resetCaches(): void {
		// implemented by sub classes
	}

	protected updateConfiguration(): TPromise<boolean> {
		// implemented by sub classes
		return TPromise.as(false);
	}
300 301 302 303 304 305 306 307 308

	private pathEquals(path1: string, path2: string): boolean {
		if (!isLinux) {
			path1 = path1.toLowerCase();
			path2 = path2.toLowerCase();
		}

		return path1 === path2;
	}
309 310 311 312 313 314
}

export class EmptyWorkspaceServiceImpl extends WorkspaceService {

	private baseConfigurationService: GlobalConfigurationService<any>;

315
	constructor(configuration: IWindowConfiguration, environmentService: IEnvironmentService) {
316
		super();
317 318
		let id = configuration.backupPath ? URI.from({ path: paths.basename(configuration.backupPath), scheme: 'empty' }).toString() : '';
		this.workspace = new Workspace(id, '', []);
319 320 321
		this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService));
		this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e)));
		this.resetCaches();
322 323
	}

324 325 326
	public reloadConfiguration(section?: string): TPromise<any> {
		const current = this._configuration;
		return this.baseConfigurationService.reloadConfiguration()
327
			.then(() => this.initialize(false)) // Reinitialize to ensure we are hitting the disk
328 329 330 331 332 333 334
			.then(() => {
				// Check and trigger
				if (!this._configuration.equals(current)) {
					this.triggerConfigurationChange();
				}
				return super.reloadConfiguration(section);
			});
335 336
	}

337 338 339
	private onBaseConfigurationChanged({ source, sourceConfig }: IConfigurationServiceEvent): void {
		if (this._configuration.updateBaseConfiguration(<any>this.baseConfigurationService.configuration())) {
			this._onDidUpdateConfiguration.fire({ source, sourceConfig });
340
		}
341
	}
342

343 344 345 346 347 348 349 350 351 352 353 354 355
	protected resetCaches(): void {
		this._configuration = new Configuration(<any>this.baseConfigurationService.configuration(), new ConfigurationModel<any>(), new StrictResourceMap<FolderConfigurationModel<any>>(), null);
	}

	protected triggerConfigurationChange(): void {
		this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.User, sourceConfig: this._configuration.user.contents });
	}
}

export class WorkspaceServiceImpl extends WorkspaceService {

	public _serviceBrand: any;

356 357
	private workspaceConfigPath: URI;
	private folderPath: URI;
358 359 360 361
	private baseConfigurationService: GlobalConfigurationService<any>;
	private workspaceConfiguration: WorkspaceConfiguration;
	private cachedFolderConfigs: StrictResourceMap<FolderConfiguration<any>>;

362
	constructor(private workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, private environmentService: IEnvironmentService, private workspacesService: IWorkspacesService, private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME) {
363
		super();
364 365 366 367 368 369 370

		if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {
			this.folderPath = URI.file(workspaceIdentifier);
		} else {
			this.workspaceConfigPath = URI.file(workspaceIdentifier.configPath);
		}

371
		this.workspaceConfiguration = this._register(new WorkspaceConfiguration());
372 373 374 375
		this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService));
	}

	public getUnsupportedWorkspaceKeys(): string[] {
376
		return this.getWorkbenchState() === WorkbenchState.FOLDER ? this._configuration.getFolderConfigurationModel(this.workspace.roots[0]).workspaceSettingsConfig.unsupportedKeys : [];
377 378
	}

379
	public initialize(trigger: boolean = true): TPromise<any> {
380 381 382 383 384
		if (!this.workspace) {
			return this.initializeWorkspace()
				.then(() => super.initialize(trigger));
		}

385
		if (this.workspaceConfigPath) {
386
			return this.workspaceConfiguration.load(this.workspaceConfigPath)
387 388 389 390 391 392 393 394 395 396
				.then(() => super.initialize(trigger));
		}

		return super.initialize(trigger);
	}

	public reloadConfiguration(section?: string): TPromise<any> {
		const current = this._configuration;
		return this.baseConfigurationService.reloadConfiguration()
			.then(() => this.initialize(false)) // Reinitialize to ensure we are hitting the disk
397
			.then(() => {
398 399 400
				// Check and trigger
				if (!this._configuration.equals(current)) {
					this.triggerConfigurationChange();
401
				}
402
				return super.reloadConfiguration(section);
403
			});
404 405
	}

406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
	public handleWorkspaceFileEvents(event: FileChangesEvent): void {
		TPromise.join(this.workspace.roots.map(folder => this.cachedFolderConfigs.get(folder).handleWorkspaceFileEvents(event))) // handle file event for each folder
			.then(folderConfigurations =>
				folderConfigurations.map((configuration, index) => ({ configuration, folder: this.workspace.roots[index] }))
					.filter(folderConfiguration => !!folderConfiguration.configuration) // Filter folders which are not impacted by events
					.map(folderConfiguration => this.updateFolderConfiguration(folderConfiguration.folder, folderConfiguration.configuration, true)) // Update the configuration of impacted folders
					.reduce((result, value) => result || value, false)) // Check if the effective configuration of folder is changed
			.then(changed => changed ? this.triggerConfigurationChange() : void 0); // Trigger event if changed
	}

	protected resetCaches(): void {
		this.cachedFolderConfigs = new StrictResourceMap<FolderConfiguration<any>>();
		this._configuration = new Configuration(<any>this.baseConfigurationService.configuration(), new ConfigurationModel<any>(), new StrictResourceMap<FolderConfigurationModel<any>>(), this.workspace);
		this.initCachesForFolders(this.workspace.roots);
	}

	private initializeWorkspace(): TPromise<void> {
		return (this.workspaceConfigPath ? this.initializeMulitFolderWorkspace() : this.initializeSingleFolderWorkspace())
			.then(() => {
				this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e)));
			});
	}

429 430 431 432
	// TODO@Sandeep use again once we can change workspace without window reload
	// private onWorkspaceChange(configPath: URI): TPromise<void> {
	// 	let workspaceName = this.workspace.name;
	// 	this.workspaceConfigPath = configPath;
433

434 435 436 437 438
	// 	// Reset the workspace if current workspace is single folder
	// 	if (this.hasFolderWorkspace()) {
	// 		this.folderPath = null;
	// 		this.workspace = null;
	// 	}
439

440 441 442 443 444
	// 	// Update workspace configuration path with new path
	// 	else {
	// 		this.workspace.configuration = configPath;
	// 		this.workspace.name = getWorkspaceLabel({ id: this.workspace.id, configPath: this.workspace.configuration.fsPath }, this.environmentService);
	// 	}
445

446 447 448 449 450 451
	// 	return this.initialize().then(() => {
	// 		if (workspaceName !== this.workspace.name) {
	// 			this._onDidChangeWorkspaceName.fire();
	// 		}
	// 	});
	// }
452

453
	private initializeMulitFolderWorkspace(): TPromise<void> {
454
		this.registerWorkspaceConfigSchema();
455
		return this.workspaceConfiguration.load(this.workspaceConfigPath)
456 457
			.then(() => {
				const workspaceConfigurationModel = this.workspaceConfiguration.workspaceConfigurationModel;
458 459
				const workspaceFolders = this.parseWorkspaceFolders(workspaceConfigurationModel.folders);
				if (!workspaceFolders.length) {
460 461
					return TPromise.wrapError<void>(new Error('Invalid workspace configuraton file ' + this.workspaceConfigPath));
				}
462 463
				const workspaceId = (this.workspaceIdentifier as IWorkspaceIdentifier).id;
				const workspaceName = getWorkspaceLabel({ id: workspaceId, configPath: this.workspaceConfigPath.fsPath }, this.environmentService);
464
				this.workspace = new Workspace(workspaceId, workspaceName, workspaceFolders, this.workspaceConfigPath);
465 466 467 468 469
				this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged()));
				return null;
			});
	}

470 471 472 473 474
	private parseWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[]): URI[] {
		return coalesce(configuredFolders.map(configuredFolder => {
			const path = configuredFolder.path;
			if (!path) {
				return void 0;
475 476
			}

477 478 479 480 481 482
			if (paths.isAbsolute(path)) {
				return URI.file(path);
			}

			return URI.file(paths.join(paths.dirname(this.workspaceConfigPath.fsPath), path));
		}));
483 484
	}

485 486 487 488 489 490
	private registerWorkspaceConfigSchema(): void {
		const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
		if (!contributionRegistry.getSchemaContributions().schemas['vscode://schemas/workspaceConfig']) {
			contributionRegistry.registerSchema('vscode://schemas/workspaceConfig', {
				default: {
					folders: [
491 492 493
						{
							path: ''
						}
494 495 496 497
					],
					settings: {
					}
				},
498
				required: ['folders'],
499 500 501 502
				properties: {
					'folders': {
						minItems: 1,
						uniqueItems: true,
503
						description: nls.localize('workspaceConfig.folders.description', "List of folders to be loaded in the workspace. Must be a file path. e.g. `/root/folderA` or `./folderA` for a relative path that will be resolved against the location of the workspace file."),
504
						items: {
505 506 507 508 509 510 511 512
							type: 'object',
							default: { path: '' },
							properties: {
								path: {
									type: 'string',
									description: nls.localize('workspaceConfig.folder.description', "A file path. e.g. `/root/folderA` or `./folderA` for a relative path that will be resolved against the location of the workspace file.")
								}
							}
513 514 515 516 517
						}
					},
					'settings': {
						type: 'object',
						default: {},
518
						description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),
519 520 521 522 523 524 525
						$ref: schemaId
					}
				}
			});
		}
	}

526
	private initializeSingleFolderWorkspace(): TPromise<void> {
527
		return stat(this.folderPath.fsPath)
528 529
			.then(workspaceStat => {
				const ctime = isLinux ? workspaceStat.ino : workspaceStat.birthtime.getTime(); // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead!
530 531
				const id = createHash('md5').update(this.folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex');
				const folder = URI.file(this.folderPath.fsPath);
532
				this.workspace = new Workspace(id, paths.basename(this.folderPath.fsPath), [folder], null, ctime);
533 534 535 536 537 538
				return TPromise.as(null);
			});
	}

	private initCachesForFolders(folders: URI[]): void {
		for (const folder of folders) {
539
			this.cachedFolderConfigs.set(folder, this._register(new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.getWorkbenchState() === WorkbenchState.WORKSPACE ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW)));
S
Sandeep Somavarapu 已提交
540
			this.updateFolderConfiguration(folder, new FolderConfigurationModel<any>(new FolderSettingsModel<any>(null), [], ConfigurationScope.RESOURCE), false);
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
		}
	}

	protected updateConfiguration(folders: URI[] = this.workspace.roots): TPromise<boolean> {
		return TPromise.join([...folders.map(folder => this.cachedFolderConfigs.get(folder).loadConfiguration()
			.then(configuration => this.updateFolderConfiguration(folder, configuration, true)))])
			.then(changed => changed.reduce((result, value) => result || value, false))
			.then(changed => this.updateWorkspaceConfiguration(true) || changed);
	}

	private onBaseConfigurationChanged({ source, sourceConfig }: IConfigurationServiceEvent): void {
		if (source === ConfigurationSource.Default) {
			this.workspace.roots.forEach(folder => this._configuration.getFolderConfigurationModel(folder).update());
		}
		if (this._configuration.updateBaseConfiguration(<any>this.baseConfigurationService.configuration())) {
			this._onDidUpdateConfiguration.fire({ source, sourceConfig });
		}
	}

	private onWorkspaceConfigurationChanged(): void {
561 562
		let configuredFolders = this.parseWorkspaceFolders(this.workspaceConfiguration.workspaceConfigurationModel.folders);
		const foldersChanged = !equals(this.workspace.roots, configuredFolders, (r1, r2) => r1.fsPath === r2.fsPath);
563
		if (foldersChanged) { // TODO@Sandeep be smarter here about detecting changes
564
			this.workspace.roots = configuredFolders;
S
Sandeep Somavarapu 已提交
565 566 567 568 569 570 571 572 573 574 575 576
			this.onFoldersChanged()
				.then(configurationChanged => {
					this._onDidChangeWorkspaceRoots.fire();
					if (configurationChanged) {
						this.triggerConfigurationChange();
					}
				});
		} else {
			const configurationChanged = this.updateWorkspaceConfiguration(true);
			if (configurationChanged) {
				this.triggerConfigurationChange();
			}
577 578 579
		}
	}

S
Sandeep Somavarapu 已提交
580
	private onFoldersChanged(): TPromise<boolean> {
S
Sandeep Somavarapu 已提交
581
		let configurationChangedOnRemoval = false;
582

583 584 585 586 587
		// Remove the configurations of deleted folders
		for (const key of this.cachedFolderConfigs.keys()) {
			if (!this.workspace.roots.filter(folder => folder.toString() === key.toString())[0]) {
				this.cachedFolderConfigs.delete(key);
				if (this._configuration.deleteFolderConfiguration(key)) {
S
Sandeep Somavarapu 已提交
588
					configurationChangedOnRemoval = true;
589 590 591 592
				}
			}
		}

593 594 595 596
		// Initialize the newly added folders
		const toInitialize = this.workspace.roots.filter(folder => !this.cachedFolderConfigs.has(folder));
		if (toInitialize.length) {
			this.initCachesForFolders(toInitialize);
S
Sandeep Somavarapu 已提交
597 598
			return this.updateConfiguration(toInitialize)
				.then(changed => configurationChangedOnRemoval || changed);
S
Sandeep Somavarapu 已提交
599 600
		} else if (configurationChangedOnRemoval) {
			this.updateWorkspaceConfiguration(false);
S
Sandeep Somavarapu 已提交
601
			return TPromise.as(true);
602
		}
S
Sandeep Somavarapu 已提交
603
		return TPromise.as(false);
604 605
	}

606 607
	private updateFolderConfiguration(folder: URI, folderConfiguration: FolderConfigurationModel<any>, compare: boolean): boolean {
		let configurationChanged = this._configuration.updateFolderConfiguration(folder, folderConfiguration, compare);
608
		if (this.getWorkbenchState() === WorkbenchState.FOLDER) {
609 610 611 612
			// Workspace configuration changed
			configurationChanged = this.updateWorkspaceConfiguration(compare) || configurationChanged;
		}
		return configurationChanged;
613 614
	}

615
	private updateWorkspaceConfiguration(compare: boolean): boolean {
616
		const workspaceConfiguration = this.getWorkbenchState() === WorkbenchState.WORKSPACE ? this.workspaceConfiguration.workspaceConfigurationModel.workspaceConfiguration : this._configuration.getFolderConfigurationModel(this.workspace.roots[0]);
617
		return this._configuration.updateWorkspaceConfiguration(workspaceConfiguration, compare);
618
	}
619

620 621
	protected triggerConfigurationChange(): void {
		this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.Workspace, sourceConfig: this._configuration.getFolderConfigurationModel(this.workspace.roots[0]).contents });
622
	}
623
}
624

625
class WorkspaceConfiguration extends Disposable {
626

627
	private _workspaceConfigPath: URI;
628
	private _workspaceConfigurationWatcher: ConfigWatcher<WorkspaceConfigurationModel<any>>;
629
	private _workspaceConfigurationWatcherDisposables: IDisposable[] = [];
630 631 632 633

	private _onDidUpdateConfiguration: Emitter<void> = this._register(new Emitter<void>());
	public readonly onDidUpdateConfiguration: Event<void> = this._onDidUpdateConfiguration.event;

634

635 636
	load(workspaceConfigPath: URI): TPromise<void> {
		if (this._workspaceConfigPath && this._workspaceConfigPath.fsPath === workspaceConfigPath.fsPath) {
637
			return this._reload();
638
		}
639

640 641 642
		this._workspaceConfigPath = workspaceConfigPath;

		this._workspaceConfigurationWatcherDisposables = dispose(this._workspaceConfigurationWatcherDisposables);
643
		return new TPromise<void>((c, e) => {
644
			this._workspaceConfigurationWatcher = new ConfigWatcher(this._workspaceConfigPath.fsPath, {
645
				changeBufferDelay: 300, onError: error => errors.onUnexpectedError(error), defaultConfig: new WorkspaceConfigurationModel(null, this._workspaceConfigPath.fsPath), parse: (content: string, parseErrors: any[]) => {
646
					const workspaceConfigurationModel = new WorkspaceConfigurationModel(content, this._workspaceConfigPath.fsPath);
647 648 649 650
					parseErrors = [...workspaceConfigurationModel.errors];
					return workspaceConfigurationModel;
				}, initCallback: () => c(null)
			});
651 652
			this._workspaceConfigurationWatcherDisposables.push(toDisposable(() => this._workspaceConfigurationWatcher.dispose()));
			this._workspaceConfigurationWatcher.onDidUpdateConfiguration(() => this._onDidUpdateConfiguration.fire(), this, this._workspaceConfigurationWatcherDisposables);
653
		});
654 655
	}

656 657 658 659 660
	get workspaceConfigurationModel(): WorkspaceConfigurationModel<any> {
		return this._workspaceConfigurationWatcher ? this._workspaceConfigurationWatcher.getConfig() : new WorkspaceConfigurationModel();
	}

	private _reload(): TPromise<void> {
661 662 663 664 665 666
		return new TPromise<void>(c => this._workspaceConfigurationWatcher.reload(() => c(null)));
	}

	dispose(): void {
		dispose(this._workspaceConfigurationWatcherDisposables);
		super.dispose();
667
	}
668
}
669

670
class FolderConfiguration<T> extends Disposable {
671

672
	private static RELOAD_CONFIGURATION_DELAY = 50;
673

674 675 676 677 678 679
	private bulkFetchFromWorkspacePromise: TPromise<any>;
	private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<ConfigurationModel<any>> };

	private reloadConfigurationScheduler: RunOnceScheduler;
	private reloadConfigurationEventEmitter: Emitter<FolderConfigurationModel<T>> = new Emitter<FolderConfigurationModel<T>>();

680
	constructor(private folder: URI, private configFolderRelativePath: string, private scope: ConfigurationScope) {
681
		super();
682

683 684 685
		this.workspaceFilePathToConfiguration = Object.create(null);
		this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.loadConfiguration().then(configuration => this.reloadConfigurationEventEmitter.fire(configuration), errors.onUnexpectedError), FolderConfiguration.RELOAD_CONFIGURATION_DELAY));
	}
686

687 688 689 690 691 692
	loadConfiguration(): TPromise<FolderConfigurationModel<T>> {
		// Load workspace locals
		return this.loadWorkspaceConfigFiles().then(workspaceConfigFiles => {
			// Consolidate (support *.json files in the workspace settings folder)
			const workspaceSettingsConfig = <FolderSettingsModel<T>>workspaceConfigFiles[WORKSPACE_CONFIG_DEFAULT_PATH] || new FolderSettingsModel<T>(null);
			const otherConfigModels = Object.keys(workspaceConfigFiles).filter(key => key !== WORKSPACE_CONFIG_DEFAULT_PATH).map(key => <ScopedConfigurationModel<T>>workspaceConfigFiles[key]);
693
			return new FolderConfigurationModel<T>(workspaceSettingsConfig, otherConfigModels, this.scope);
694 695 696 697
		});
	}

	private loadWorkspaceConfigFiles<T>(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModel<T> }> {
698 699
		// once: when invoked for the first time we fetch json files that contribute settings
		if (!this.bulkFetchFromWorkspacePromise) {
700
			this.bulkFetchFromWorkspacePromise = resolveStat(this.toResource(this.configFolderRelativePath)).then(stat => {
701 702 703 704 705 706 707 708 709 710
				if (!stat.isDirectory) {
					return TPromise.as([]);
				}

				return resolveContents(stat.children.filter(stat => {
					const isJson = paths.extname(stat.resource.fsPath) === '.json';
					if (!isJson) {
						return false; // only JSON files
					}

B
Benjamin Pasero 已提交
711
					return this.isWorkspaceConfigurationFile(this.toFolderRelativePath(stat.resource)); // only workspace config files
712 713 714
				}).map(stat => stat.resource));
			}, err => [] /* never fail this call */)
				.then((contents: IContent[]) => {
B
Benjamin Pasero 已提交
715
					contents.forEach(content => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(content.resource)] = TPromise.as(this.createConfigModel(content)));
716 717 718 719 720 721 722 723
				}, errors.onUnexpectedError);
		}

		// on change: join on *all* configuration file promises so that we can merge them into a single configuration object. this
		// happens whenever a config file changes, is deleted, or added
		return this.bulkFetchFromWorkspacePromise.then(() => TPromise.join(this.workspaceFilePathToConfiguration));
	}

724
	public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise<FolderConfigurationModel<T>> {
725 726 727 728 729 730 731
		const events = event.changes;
		let affectedByChanges = false;

		// Find changes that affect workspace configuration files
		for (let i = 0, len = events.length; i < len; i++) {
			const resource = events[i].resource;
			const isJson = paths.extname(resource.fsPath) === '.json';
732
			const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && paths.isEqual(paths.basename(resource.fsPath), this.configFolderRelativePath));
733 734 735 736
			if (!isJson && !isDeletedSettingsFolder) {
				continue; // only JSON files or the actual settings folder
			}

B
Benjamin Pasero 已提交
737
			const workspacePath = this.toFolderRelativePath(resource);
738 739 740 741 742
			if (!workspacePath) {
				continue; // event is not inside workspace
			}

			// Handle case where ".vscode" got deleted
743
			if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) {
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765
				this.workspaceFilePathToConfiguration = Object.create(null);
				affectedByChanges = true;
			}

			// only valid workspace config files
			if (!this.isWorkspaceConfigurationFile(workspacePath)) {
				continue;
			}

			// insert 'fetch-promises' for add and update events and
			// remove promises for delete events
			switch (events[i].type) {
				case FileChangeType.DELETED:
					affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, workspacePath);
					break;
				case FileChangeType.UPDATED:
				case FileChangeType.ADDED:
					this.workspaceFilePathToConfiguration[workspacePath] = resolveContent(resource).then(content => this.createConfigModel(content), errors.onUnexpectedError);
					affectedByChanges = true;
			}
		}

766 767
		if (!affectedByChanges) {
			return TPromise.as(null);
768
		}
769 770 771 772 773 774 775 776 777 778 779

		return new TPromise((c, e) => {
			let disposable = this.reloadConfigurationEventEmitter.event(configuration => {
				disposable.dispose();
				c(configuration);
			});
			// trigger reload of the configuration if we are affected by changes
			if (!this.reloadConfigurationScheduler.isScheduled()) {
				this.reloadConfigurationScheduler.schedule();
			}
		});
780 781
	}

782
	private createConfigModel<T>(content: IContent): ConfigurationModel<T> {
B
Benjamin Pasero 已提交
783
		const path = this.toFolderRelativePath(content.resource);
784
		if (path === WORKSPACE_CONFIG_DEFAULT_PATH) {
785
			return new FolderSettingsModel<T>(content.value, content.resource.toString());
786 787 788
		} else {
			const matches = /\/([^\.]*)*\.json/.exec(path);
			if (matches && matches[1]) {
789
				return new ScopedConfigurationModel<T>(content.value, content.resource.toString(), matches[1]);
790 791 792
			}
		}

793
		return new CustomConfigurationModel<T>(null);
794 795
	}

B
Benjamin Pasero 已提交
796 797
	private isWorkspaceConfigurationFile(folderRelativePath: string): boolean {
		return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS.launch, WORKSPACE_STANDALONE_CONFIGURATIONS.tasks].some(p => p === folderRelativePath);
798 799
	}

B
Benjamin Pasero 已提交
800 801 802
	private toResource(folderRelativePath: string): URI {
		if (typeof folderRelativePath === 'string') {
			return URI.file(paths.join(this.folder.fsPath, folderRelativePath));
803 804 805
		}

		return null;
806 807
	}

B
Benjamin Pasero 已提交
808
	private toFolderRelativePath(resource: URI, toOSPath?: boolean): string {
809 810
		if (this.contains(resource)) {
			return paths.normalize(paths.relative(this.folder.fsPath, resource.fsPath), toOSPath);
811 812
		}

813
		return null;
814 815
	}

816 817
	private contains(resource: URI): boolean {
		if (resource) {
818
			return paths.isEqualOrParent(resource.fsPath, this.folder.fsPath, !isLinux /* ignorecase */);
819 820
		}

821
		return false;
822
	}
823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
}

// node.hs helper functions

function resolveContents(resources: URI[]): TPromise<IContent[]> {
	const contents: IContent[] = [];

	return TPromise.join(resources.map(resource => {
		return resolveContent(resource).then(content => {
			contents.push(content);
		});
	})).then(() => contents);
}

function resolveContent(resource: URI): TPromise<IContent> {
	return readFile(resource.fsPath).then(contents => ({ resource, value: contents.toString() }));
}

function resolveStat(resource: URI): TPromise<IStat> {
	return new TPromise<IStat>((c, e) => {
		extfs.readdir(resource.fsPath, (error, children) => {
			if (error) {
				if ((<any>error).code === 'ENOTDIR') {
					c({ resource });
				} else {
					e(error);
				}
			} else {
				c({
					resource,
					isDirectory: true,
					children: children.map(child => { return { resource: URI.file(paths.join(resource.fsPath, child)) }; })
				});
			}
		});
	});
I
isidor 已提交
859
}
860

861
export class Configuration<T> extends BaseConfiguration<T> {
862

863
	constructor(private _baseConfiguration: BaseConfiguration<T>, workspaceConfiguration: ConfigurationModel<T>, protected folders: StrictResourceMap<FolderConfigurationModel<T>>, workspace: Workspace) {
864
		super(_baseConfiguration.defaults, _baseConfiguration.user, workspaceConfiguration, folders, workspace);
865 866
	}

867
	updateBaseConfiguration(baseConfiguration: BaseConfiguration<T>): boolean {
868
		const current = new Configuration(this._baseConfiguration, this._workspaceConfiguration, this.folders, this._workspace);
869

S
Sandeep Somavarapu 已提交
870 871 872
		this._baseConfiguration = baseConfiguration;
		this._defaults = this._baseConfiguration.defaults;
		this._user = this._baseConfiguration.user;
873 874 875 876 877
		this.merge();

		return !this.equals(current);
	}

878 879 880 881 882 883 884 885 886 887
	updateWorkspaceConfiguration(workspaceConfiguration: ConfigurationModel<T>, compare: boolean = true): boolean {
		const current = new Configuration(this._baseConfiguration, this._workspaceConfiguration, this.folders, this._workspace);

		this._workspaceConfiguration = workspaceConfiguration;
		this.merge();

		return compare && !this.equals(current);
	}

	updateFolderConfiguration(resource: URI, configuration: FolderConfigurationModel<T>, compare: boolean): boolean {
888
		const current = this.getValue(null, { resource });
889 890

		this.folders.set(resource, configuration);
891
		this.mergeFolder(resource);
892

893
		return compare && !objects.equals(current, this.getValue(null, { resource }));
894 895 896
	}

	deleteFolderConfiguration(folder: URI): boolean {
897
		if (this._workspace && this._workspace.roots.length > 0 && this._workspace.roots[0].fsPath === folder.fsPath) {
898 899 900 901
			// Do not remove workspace configuration
			return false;
		}

S
Sandeep Somavarapu 已提交
902
		const changed = this.folders.get(folder).keys.length > 0;
903
		this.folders.delete(folder);
S
Sandeep Somavarapu 已提交
904 905
		this._foldersConsolidatedConfigurations.delete(folder);
		return changed;
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920
	}

	getFolderConfigurationModel(folder: URI): FolderConfigurationModel<T> {
		return <FolderConfigurationModel<T>>this.folders.get(folder);
	}

	equals(other: any): boolean {
		if (!other || !(other instanceof Configuration)) {
			return false;
		}

		if (!objects.equals(this.getValue(), other.getValue())) {
			return false;
		}

921
		if (this._foldersConsolidatedConfigurations.size !== other._foldersConsolidatedConfigurations.size) {
922 923 924
			return false;
		}

925
		for (const resource of this._foldersConsolidatedConfigurations.keys()) {
926
			if (!objects.equals(this.getValue(null, { resource }), other.getValue(null, { resource }))) {
927 928 929 930 931 932
				return false;
			}
		}

		return true;
	}
I
isidor 已提交
933
}