configuration.ts 37.0 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, WorkspaceState } 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

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
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[]; } };

56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
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 已提交
79 80 81
							},
							scope: {
								type: 'string',
S
Sandeep Somavarapu 已提交
82 83
								enum: ['window', 'resource'],
								default: 'window',
S
Sandeep Somavarapu 已提交
84
								enumDescriptions: [
S
Sandeep Somavarapu 已提交
85
									nls.localize('scope.window.description', "Window specific configuration, which can be configured in the User or Workspace settings."),
S
Sandeep Somavarapu 已提交
86
									nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the User, Workspace or Folder settings.")
S
Sandeep Somavarapu 已提交
87
								],
S
Sandeep Somavarapu 已提交
88
								description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `window` and `resource`.")
89 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
							}
						}
					}
				]
			}
		}
	}
});
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 已提交
160
			const propertyConfiguration = configuration.properties[key];
S
Sandeep Somavarapu 已提交
161
			propertyConfiguration.scope = propertyConfiguration.scope && propertyConfiguration.scope.toString() === 'resource' ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW;
162 163 164 165 166 167 168 169 170 171 172 173 174 175
			if (message) {
				collector.warn(message);
				delete properties[key];
			}
		}
	}
	let subNodes = configuration.allOf;
	if (subNodes) {
		for (let node of subNodes) {
			validateProperties(node, collector);
		}
	}
}

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

	public _serviceBrand: any;

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

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

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

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

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

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

201 202 203 204 205 206 207 208
	public getWorkspaceState(): WorkspaceState {
		if (this.workspace) {
			if (this.workspace.configuration) {
				return WorkspaceState.WORKSPACE;
			}
			return WorkspaceState.FOLDER;
		}
		return WorkspaceState.EMPTY;
209 210
	}

211
	public getRoot(resource: URI): URI {
212
		return this.workspace ? this.workspace.getRoot(resource) : null;
213 214 215 216 217 218
	}

	private get workspaceUri(): URI {
		return this.workspace ? this.workspace.roots[0] : null;
	}

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

223 224 225 226 227 228 229 230 231 232 233 234
	public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean {
		if (!this.workspace) {
			return false;
		}

		if (this.workspace.configuration) {
			return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id;
		}

		return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.roots[0].fsPath, workspaceIdentifier);
	}

235
	public toResource(workspaceRelativePath: string): URI {
236 237 238 239
		if (this.workspace && this.workspace.roots.length) {
			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 315 316 317 318 319
}

export class EmptyWorkspaceServiceImpl extends WorkspaceService {

	private baseConfigurationService: GlobalConfigurationService<any>;

	constructor(environmentService: IEnvironmentService) {
		super();
		this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService));
		this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e)));
		this.resetCaches();
320 321
	}

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

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

341 342 343 344 345 346 347 348 349 350 351 352 353
	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;

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

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

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

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

	public getUnsupportedWorkspaceKeys(): string[] {
374
		return this.getWorkspaceState() === WorkspaceState.FOLDER ? this._configuration.getFolderConfigurationModel(this.workspace.roots[0]).workspaceSettingsConfig.unsupportedKeys : [];
375 376
	}

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

383
		if (this.workspaceConfigPath) {
384
			return this.workspaceConfiguration.load(this.workspaceConfigPath)
385 386 387 388 389 390 391 392 393 394
				.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
395
			.then(() => {
396 397 398
				// Check and trigger
				if (!this._configuration.equals(current)) {
					this.triggerConfigurationChange();
399
				}
400
				return super.reloadConfiguration(section);
401
			});
402 403
	}

404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
	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)));
			});
	}

427 428 429 430
	// 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;
431

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

438 439 440 441 442
	// 	// 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);
	// 	}
443

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

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

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

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

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

483 484 485 486 487 488
	private registerWorkspaceConfigSchema(): void {
		const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
		if (!contributionRegistry.getSchemaContributions().schemas['vscode://schemas/workspaceConfig']) {
			contributionRegistry.registerSchema('vscode://schemas/workspaceConfig', {
				default: {
					folders: [
489 490 491
						{
							path: ''
						}
492 493 494 495
					],
					settings: {
					}
				},
496
				required: ['folders'],
497 498 499 500
				properties: {
					'folders': {
						minItems: 1,
						uniqueItems: true,
501
						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."),
502
						items: {
503 504 505 506 507 508 509 510
							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.")
								}
							}
511 512 513 514 515
						}
					},
					'settings': {
						type: 'object',
						default: {},
516
						description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),
517 518 519 520 521 522 523
						$ref: schemaId
					}
				}
			});
		}
	}

524
	private initializeSingleFolderWorkspace(): TPromise<void> {
525
		return stat(this.folderPath.fsPath)
526 527
			.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!
528 529
				const id = createHash('md5').update(this.folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex');
				const folder = URI.file(this.folderPath.fsPath);
530
				this.workspace = new Workspace(id, paths.basename(this.folderPath.fsPath), [folder], null, ctime);
531 532 533 534 535 536
				return TPromise.as(null);
			});
	}

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

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

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

581 582 583 584 585
		// 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 已提交
586
					configurationChangedOnRemoval = true;
587 588 589 590
				}
			}
		}

591 592 593 594
		// 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 已提交
595 596
			return this.updateConfiguration(toInitialize)
				.then(changed => configurationChangedOnRemoval || changed);
S
Sandeep Somavarapu 已提交
597 598
		} else if (configurationChangedOnRemoval) {
			this.updateWorkspaceConfiguration(false);
S
Sandeep Somavarapu 已提交
599
			return TPromise.as(true);
600
		}
S
Sandeep Somavarapu 已提交
601
		return TPromise.as(false);
602 603
	}

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

613
	private updateWorkspaceConfiguration(compare: boolean): boolean {
614
		const workspaceConfiguration = this.getWorkspaceState() === WorkspaceState.WORKSPACE ? this.workspaceConfiguration.workspaceConfigurationModel.workspaceConfiguration : this._configuration.getFolderConfigurationModel(this.workspace.roots[0]);
615
		return this._configuration.updateWorkspaceConfiguration(workspaceConfiguration, compare);
616
	}
617

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

623
class WorkspaceConfiguration extends Disposable {
624

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

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

632

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

638 639 640
		this._workspaceConfigPath = workspaceConfigPath;

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

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

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

	dispose(): void {
		dispose(this._workspaceConfigurationWatcherDisposables);
		super.dispose();
665
	}
666
}
667

668
class FolderConfiguration<T> extends Disposable {
669

670
	private static RELOAD_CONFIGURATION_DELAY = 50;
671

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

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

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

681 682 683
		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));
	}
684

685 686 687 688 689 690
	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]);
691
			return new FolderConfigurationModel<T>(workspaceSettingsConfig, otherConfigModels, this.scope);
692 693 694 695
		});
	}

	private loadWorkspaceConfigFiles<T>(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModel<T> }> {
696 697
		// once: when invoked for the first time we fetch json files that contribute settings
		if (!this.bulkFetchFromWorkspacePromise) {
698
			this.bulkFetchFromWorkspacePromise = resolveStat(this.toResource(this.configFolderRelativePath)).then(stat => {
699 700 701 702 703 704 705 706 707 708
				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 已提交
709
					return this.isWorkspaceConfigurationFile(this.toFolderRelativePath(stat.resource)); // only workspace config files
710 711 712
				}).map(stat => stat.resource));
			}, err => [] /* never fail this call */)
				.then((contents: IContent[]) => {
B
Benjamin Pasero 已提交
713
					contents.forEach(content => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(content.resource)] = TPromise.as(this.createConfigModel(content)));
714 715 716 717 718 719 720 721
				}, 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));
	}

722
	public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise<FolderConfigurationModel<T>> {
723 724 725 726 727 728 729
		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';
730
			const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && paths.isEqual(paths.basename(resource.fsPath), this.configFolderRelativePath));
731 732 733 734
			if (!isJson && !isDeletedSettingsFolder) {
				continue; // only JSON files or the actual settings folder
			}

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

			// Handle case where ".vscode" got deleted
741
			if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) {
742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763
				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;
			}
		}

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

		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();
			}
		});
778 779
	}

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

791
		return new CustomConfigurationModel<T>(null);
792 793
	}

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

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

		return null;
804 805
	}

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

811
		return null;
812 813
	}

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

819
		return false;
820
	}
821 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
}

// 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 已提交
857
}
858

859
export class Configuration<T> extends BaseConfiguration<T> {
860

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

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

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

		return !this.equals(current);
	}

876 877 878 879 880 881 882 883 884 885
	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 {
886
		const current = this.getValue(null, { resource });
887 888

		this.folders.set(resource, configuration);
889
		this.mergeFolder(resource);
890

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

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

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

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

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

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

		return true;
	}
I
isidor 已提交
931
}