configuration.ts 38.8 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 } 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';
B
Benjamin Pasero 已提交
16
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
17
import { RunOnceScheduler } from 'vs/base/common/async';
18
import { readFile, stat, writeFile } 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, WorkspaceFolder, toWorkspaceFolders } 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, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
37
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
38
import { IJSONSchema } from 'vs/base/common/jsonSchema';
39 40 41 42
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { ICommandService } from 'vs/platform/commands/common/commands';
import product from 'vs/platform/node/product';
import pkg from 'vs/platform/node/package';
43

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
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[]; } };

62 63
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);

64
const configurationEntrySchema: IJSONSchema = {
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
	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 已提交
83 84 85
							},
							scope: {
								type: 'string',
S
Sandeep Somavarapu 已提交
86 87
								enum: ['window', 'resource'],
								default: 'window',
S
Sandeep Somavarapu 已提交
88
								enumDescriptions: [
S
Sandeep Somavarapu 已提交
89
									nls.localize('scope.window.description', "Window specific configuration, which can be configured in the User or Workspace settings."),
S
Sandeep Somavarapu 已提交
90
									nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the User, Workspace or Folder settings.")
S
Sandeep Somavarapu 已提交
91
								],
S
Sandeep Somavarapu 已提交
92
								description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `window` and `resource`.")
93 94 95 96 97 98 99
							}
						}
					}
				]
			}
		}
	}
100 101 102 103 104 105 106 107 108 109 110 111 112
};


// BEGIN VSCode extension point `configuration`
const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>('configuration', [], {
	description: nls.localize('vscode.extension.contributes.configuration', 'Contributes configuration settings.'),
	oneOf: [
		configurationEntrySchema,
		{
			type: 'array',
			items: configurationEntrySchema
		}
	]
113 114 115 116
});
configurationExtPoint.setHandler(extensions => {
	const configurations: IConfigurationNode[] = [];

117 118
	function handleConfiguration(node: IConfigurationNode, id: string, collector: ExtensionMessageCollector) {
		let configuration = objects.clone(node);
119 120 121 122 123 124 125

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

		validateProperties(configuration, collector);

126
		configuration.id = id;
127
		configurations.push(configuration);
128
	};
129

130 131 132 133 134 135 136 137 138 139
	for (let extension of extensions) {
		const collector = extension.collector;
		const value = <IConfigurationNode | IConfigurationNode[]>extension.value;
		const id = extension.description.id;
		if (!Array.isArray(value)) {
			handleConfiguration(value, id, collector);
		} else {
			value.forEach(v => handleConfiguration(v, id, collector));
		}
	}
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
	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 已提交
179
			const propertyConfiguration = configuration.properties[key];
S
Sandeep Somavarapu 已提交
180
			propertyConfiguration.scope = propertyConfiguration.scope && propertyConfiguration.scope.toString() === 'resource' ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW;
181 182 183 184 185 186 187 188
			if (message) {
				collector.warn(message);
				delete properties[key];
			}
		}
	}
	let subNodes = configuration.allOf;
	if (subNodes) {
189
		collector.error(nls.localize('invalid.allOf', "'configuration.allOf' is deprecated and should no longer be used. Instead, pass multiple configuration sections as an array to the 'configuration' contribution point."));
190 191 192 193 194 195
		for (let node of subNodes) {
			validateProperties(node, collector);
		}
	}
}

S
Sandeep Somavarapu 已提交
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
contributionRegistry.registerSchema('vscode://schemas/workspaceConfig', {
	default: {
		folders: [
			{
				path: ''
			}
		],
		settings: {
		}
	},
	required: ['folders'],
	properties: {
		'folders': {
			minItems: 1,
			uniqueItems: true,
			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."),
			items: {
				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.")
220 221 222 223
					},
					name: {
						type: 'string',
						description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")
S
Sandeep Somavarapu 已提交
224 225 226 227 228 229 230 231 232
					}
				}
			}
		},
		'settings': {
			type: 'object',
			default: {},
			description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),
			$ref: schemaId
233 234 235 236 237 238
		},
		'extensions': {
			type: 'object',
			default: {},
			description: nls.localize('workspaceConfig.extensions.description', "Workspace extensions"),
			$ref: 'vscode://schemas/extensions'
S
Sandeep Somavarapu 已提交
239 240 241 242
		}
	}
});

S
Sandeep Somavarapu 已提交
243
export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService {
244 245 246

	public _serviceBrand: any;

247 248 249 250 251
	private workspace: Workspace;
	private _configuration: Configuration<any>;
	private baseConfigurationService: GlobalConfigurationService<any>;
	private workspaceConfiguration: WorkspaceConfiguration;
	private cachedFolderConfigs: StrictResourceMap<FolderConfiguration<any>>;
252

253
	protected readonly _onDidUpdateConfiguration: Emitter<IConfigurationServiceEvent> = this._register(new Emitter<IConfigurationServiceEvent>());
254 255
	public readonly onDidUpdateConfiguration: Event<IConfigurationServiceEvent> = this._onDidUpdateConfiguration.event;

S
Sandeep Somavarapu 已提交
256 257
	protected readonly _onDidChangeWorkspaceFolders: Emitter<void> = this._register(new Emitter<void>());
	public readonly onDidChangeWorkspaceFolders: Event<void> = this._onDidChangeWorkspaceFolders.event;
258

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

262 263 264
	protected readonly _onDidChangeWorkbenchState: Emitter<WorkbenchState> = this._register(new Emitter<WorkbenchState>());
	public readonly onDidChangeWorkbenchState: Event<WorkbenchState> = this._onDidChangeWorkbenchState.event;

265
	constructor(private environmentService: IEnvironmentService, private workspacesService: IWorkspacesService, private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME) {
266
		super();
267 268 269 270 271 272

		this.workspaceConfiguration = this._register(new WorkspaceConfiguration());
		this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged()));

		this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService));
		this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e)));
273 274
	}

B
Benjamin Pasero 已提交
275
	public getWorkspace(): IWorkspace {
276 277 278
		return this.workspace;
	}

279
	public getWorkbenchState(): WorkbenchState {
280 281 282 283 284 285
		// Workspace has configuration file
		if (this.workspace.configuration) {
			return WorkbenchState.WORKSPACE;
		}

		// Folder has single root
S
Sandeep Somavarapu 已提交
286
		if (this.workspace.folders.length === 1) {
287
			return WorkbenchState.FOLDER;
288
		}
289 290

		// Empty
291
		return WorkbenchState.EMPTY;
292 293
	}

294
	public getWorkspaceFolder(resource: URI): WorkspaceFolder {
S
Sandeep Somavarapu 已提交
295
		return this.workspace.getFolder(resource);
296 297
	}

298
	public isInsideWorkspace(resource: URI): boolean {
S
Sandeep Somavarapu 已提交
299
		return !!this.getWorkspaceFolder(resource);
300 301
	}

302
	public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean {
303 304
		switch (this.getWorkbenchState()) {
			case WorkbenchState.FOLDER:
305
				return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.folders[0].uri.fsPath, workspaceIdentifier);
306 307
			case WorkbenchState.WORKSPACE:
				return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id;
308
		}
309
		return false;
310 311
	}

312 313
	public toResource(workspaceRelativePath: string, workspaceFolder: WorkspaceFolder): URI {
		return URI.file(paths.join(workspaceFolder.uri.fsPath, workspaceRelativePath));
314 315
	}

316 317
	public getConfigurationData<T>(): IConfigurationData<T> {
		return this._configuration.toData();
318 319
	}

320 321
	public getConfiguration<C>(section?: string, overrides?: IConfigurationOverrides): C {
		return this._configuration.getValue<C>(section, overrides);
322 323
	}

S
Sandeep Somavarapu 已提交
324 325
	public lookup<C>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<C> {
		return this._configuration.lookup<C>(key, overrides);
326 327
	}

328 329
	public keys(overrides?: IConfigurationOverrides): IConfigurationKeys {
		return this._configuration.keys(overrides);
330
	}
331

332
	public values<V>(): IConfigurationValues {
333
		return this._configuration.values();
334 335
	}

336 337
	public reloadConfiguration(section?: string): TPromise<any> {
		const current = this._configuration;
338
		// Reload and reinitialize to ensure we are hitting the disk
339
		return this.baseConfigurationService.reloadConfiguration()
340 341 342 343 344 345 346
			.then(() => {
				if (this.workspace.configuration) {
					return this.workspaceConfiguration.load(this.workspace.configuration)
						.then(() => this.initializeConfiguration(false));
				}
				return this.initializeConfiguration(false);
			})
347
			.then(() => {
348 349 350
				// Check and trigger
				if (!this._configuration.equals(current)) {
					this.triggerConfigurationChange();
351
				}
352
				return this.getConfiguration(section);
353
			});
354 355
	}

356 357 358 359
	public getUnsupportedWorkspaceKeys(): string[] {
		return this.getWorkbenchState() === WorkbenchState.FOLDER ? this._configuration.getFolderConfigurationModel(this.workspace.folders[0].uri).workspaceSettingsConfig.unsupportedKeys : [];
	}

360
	public handleWorkspaceFileEvents(event: FileChangesEvent): void {
361
		TPromise.join(this.workspace.folders.map(folder => this.cachedFolderConfigs.get(folder.uri).handleWorkspaceFileEvents(event))) // handle file event for each folder
362
			.then(folderConfigurations =>
S
Sandeep Somavarapu 已提交
363
				folderConfigurations.map((configuration, index) => ({ configuration, folder: this.workspace.folders[index] }))
364 365 366 367 368 369
					.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
	}

370 371 372 373
	public initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise<any> {
		return this.createWorkspace(arg)
			.then(workspace => this.setWorkspace(workspace))
			.then(() => this.initializeConfiguration(true));
374 375
	}

376
	private createWorkspace(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise<Workspace> {
377
		if (isWorkspaceIdentifier(arg)) {
378
			return this.createMulitFolderWorkspace(arg);
379 380 381
		}

		if (isSingleFolderWorkspaceIdentifier(arg)) {
382
			return this.createSingleFolderWorkspace(arg);
383 384
		}

385
		return this.createEmptyWorkspace(arg);
386 387
	}

388
	private createMulitFolderWorkspace(workspaceIdentifier: IWorkspaceIdentifier): TPromise<Workspace> {
389
		const workspaceConfigPath = URI.file(workspaceIdentifier.configPath);
390
		return this.workspaceConfiguration.load(workspaceConfigPath)
391 392
			.then(() => {
				const workspaceConfigurationModel = this.workspaceConfiguration.workspaceConfigurationModel;
393
				const workspaceFolders = toWorkspaceFolders(workspaceConfigurationModel.folders, URI.file(paths.dirname(workspaceConfigPath.fsPath)));
394
				if (!workspaceFolders.length) {
395
					return TPromise.wrapError<Workspace>(new Error('Invalid workspace configuraton file ' + workspaceConfigPath));
396
				}
397
				const workspaceId = workspaceIdentifier.id;
398
				const workspaceName = getWorkspaceLabel({ id: workspaceId, configPath: workspaceConfigPath.fsPath }, this.environmentService);
399
				return new Workspace(workspaceId, workspaceName, workspaceFolders, workspaceConfigPath);
400 401 402
			});
	}

403
	private createSingleFolderWorkspace(singleFolderWorkspaceIdentifier: ISingleFolderWorkspaceIdentifier): TPromise<Workspace> {
404
		const folderPath = URI.file(singleFolderWorkspaceIdentifier);
405
		return stat(folderPath.fsPath)
406 407
			.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!
408 409
				const id = createHash('md5').update(folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex');
				const folder = URI.file(folderPath.fsPath);
410
				return new Workspace(id, paths.basename(folderPath.fsPath), toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime);
411 412 413
			});
	}

414
	private createEmptyWorkspace(configuration: IWindowConfiguration): TPromise<Workspace> {
415
		let id = configuration.backupPath ? URI.from({ path: paths.basename(configuration.backupPath), scheme: 'empty' }).toString() : '';
416 417 418 419 420 421 422 423 424 425
		return TPromise.as(new Workspace(id));
	}

	private setWorkspace(workspace: Workspace): void {
		if (!this.workspace) {
			this.workspace = workspace;
			return;
		}

		const currentState = this.getWorkbenchState();
426
		const currentWorkspacePath = this.workspace.configuration ? this.workspace.configuration.fsPath : void 0;
427 428 429 430 431 432 433 434 435
		const currentFolders = this.workspace.folders;

		this.workspace.update(workspace);

		const newState = this.getWorkbenchState();
		if (newState !== currentState) {
			this._onDidChangeWorkbenchState.fire(newState);
		}

436 437
		const newWorkspacePath = this.workspace.configuration ? this.workspace.configuration.fsPath : void 0;
		if (newWorkspacePath !== currentWorkspacePath || newState !== currentState) {
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
			this._onDidChangeWorkspaceName.fire();
		}

		if (!equals(this.workspace.folders, currentFolders, (folder1, folder2) => folder1.uri.fsPath === folder2.uri.fsPath)) {
			this._onDidChangeWorkspaceFolders.fire();
		}
	}

	private initializeConfiguration(trigger: boolean = true): TPromise<any> {
		this.resetCaches();
		return this.updateConfiguration()
			.then(() => {
				if (trigger) {
					this.triggerConfigurationChange();
				}
			});
	}

	private resetCaches(): void {
		this.cachedFolderConfigs = new StrictResourceMap<FolderConfiguration<any>>();
		this._configuration = new Configuration(<any>this.baseConfigurationService.configuration(), new ConfigurationModel<any>(), new StrictResourceMap<FolderConfigurationModel<any>>(), this.getWorkbenchState() !== WorkbenchState.EMPTY ? this.workspace : null); //TODO: @Sandy Avoid passing null
		this.initCachesForFolders(this.workspace.folders);
460 461
	}

462
	private initCachesForFolders(folders: WorkspaceFolder[]): void {
463
		for (const folder of folders) {
464
			this.cachedFolderConfigs.set(folder.uri, this._register(new FolderConfiguration(folder.uri, this.workspaceSettingsRootFolder, this.getWorkbenchState() === WorkbenchState.WORKSPACE ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW)));
S
Sandeep Somavarapu 已提交
465
			this.updateFolderConfiguration(folder, new FolderConfigurationModel<any>(new FolderSettingsModel<any>(null), [], ConfigurationScope.RESOURCE), false);
466 467 468
		}
	}

469
	private updateConfiguration(folders: WorkspaceFolder[] = this.workspace.folders): TPromise<boolean> {
470
		return TPromise.join([...folders.map(folder => this.cachedFolderConfigs.get(folder.uri).loadConfiguration()
471 472 473 474 475 476
			.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 {
477 478 479 480 481 482 483
		if (this.workspace) {
			if (source === ConfigurationSource.Default) {
				this.workspace.folders.forEach(folder => this._configuration.getFolderConfigurationModel(folder.uri).update());
			}
			if (this._configuration.updateBaseConfiguration(<any>this.baseConfigurationService.configuration())) {
				this._onDidUpdateConfiguration.fire({ source, sourceConfig });
			}
484 485 486
		}
	}

487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
	private onWorkspaceConfigurationChanged(): void {
		if (this.workspace && this.workspace.configuration) {
			let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.workspaceConfigurationModel.folders, URI.file(paths.dirname(this.workspace.configuration.fsPath)));
			const foldersChanged = !equals(this.workspace.folders, configuredFolders, (folder1, folder2) => folder1.uri.fsPath === folder2.uri.fsPath);
			if (foldersChanged) { // TODO@Sandeep be smarter here about detecting changes
				this.workspace.folders = configuredFolders;
				this.onFoldersChanged()
					.then(configurationChanged => {
						this._onDidChangeWorkspaceFolders.fire();
						if (configurationChanged) {
							this.triggerConfigurationChange();
						}
					});
			} else {
				const configurationChanged = this.updateWorkspaceConfiguration(true);
				if (configurationChanged) {
					this.triggerConfigurationChange();
				}
S
Sandeep Somavarapu 已提交
505
			}
506 507 508
		}
	}

S
Sandeep Somavarapu 已提交
509
	private onFoldersChanged(): TPromise<boolean> {
S
Sandeep Somavarapu 已提交
510
		let configurationChangedOnRemoval = false;
511

512 513
		// Remove the configurations of deleted folders
		for (const key of this.cachedFolderConfigs.keys()) {
514
			if (!this.workspace.folders.filter(folder => folder.uri.toString() === key.toString())[0]) {
515 516
				this.cachedFolderConfigs.delete(key);
				if (this._configuration.deleteFolderConfiguration(key)) {
S
Sandeep Somavarapu 已提交
517
					configurationChangedOnRemoval = true;
518 519 520 521
				}
			}
		}

522
		// Initialize the newly added folders
523
		const toInitialize = this.workspace.folders.filter(folder => !this.cachedFolderConfigs.has(folder.uri));
524 525
		if (toInitialize.length) {
			this.initCachesForFolders(toInitialize);
S
Sandeep Somavarapu 已提交
526 527
			return this.updateConfiguration(toInitialize)
				.then(changed => configurationChangedOnRemoval || changed);
S
Sandeep Somavarapu 已提交
528 529
		} else if (configurationChangedOnRemoval) {
			this.updateWorkspaceConfiguration(false);
S
Sandeep Somavarapu 已提交
530
			return TPromise.as(true);
531
		}
S
Sandeep Somavarapu 已提交
532
		return TPromise.as(false);
533 534
	}

535 536
	private updateFolderConfiguration(folder: WorkspaceFolder, folderConfiguration: FolderConfigurationModel<any>, compare: boolean): boolean {
		let configurationChanged = this._configuration.updateFolderConfiguration(folder.uri, folderConfiguration, compare);
537
		if (this.getWorkbenchState() === WorkbenchState.FOLDER) {
538 539 540 541
			// Workspace configuration changed
			configurationChanged = this.updateWorkspaceConfiguration(compare) || configurationChanged;
		}
		return configurationChanged;
542 543
	}

544
	private updateWorkspaceConfiguration(compare: boolean): boolean {
545 546 547 548 549 550
		const workbennchState = this.getWorkbenchState();
		if (workbennchState === WorkbenchState.EMPTY) {
			return false;
		}

		const workspaceConfiguration = workbennchState === WorkbenchState.WORKSPACE ? this.workspaceConfiguration.workspaceConfigurationModel.workspaceConfiguration : this._configuration.getFolderConfigurationModel(this.workspace.folders[0].uri);
551
		return this._configuration.updateWorkspaceConfiguration(workspaceConfiguration, compare);
552
	}
553

554
	private triggerConfigurationChange(): void {
555 556 557 558 559
		if (this.getWorkbenchState() === WorkbenchState.EMPTY) {
			this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.User, sourceConfig: this._configuration.user.contents });
		} else {
			this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.Workspace, sourceConfig: this._configuration.getFolderConfigurationModel(this.workspace.folders[0].uri).contents });
		}
560
	}
561 562 563 564 565 566 567 568 569

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

		return path1 === path2;
	}
570
}
571

572
class WorkspaceConfiguration extends Disposable {
573

574
	private _workspaceConfigPath: URI;
575
	private _workspaceConfigurationWatcher: ConfigWatcher<WorkspaceConfigurationModel<any>>;
576
	private _workspaceConfigurationWatcherDisposables: IDisposable[] = [];
577 578 579 580

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

581

582 583
	load(workspaceConfigPath: URI): TPromise<void> {
		if (this._workspaceConfigPath && this._workspaceConfigPath.fsPath === workspaceConfigPath.fsPath) {
584
			return this._reload();
585
		}
586

587 588 589
		this._workspaceConfigPath = workspaceConfigPath;

		this._workspaceConfigurationWatcherDisposables = dispose(this._workspaceConfigurationWatcherDisposables);
590
		return new TPromise<void>((c, e) => {
591
			this._workspaceConfigurationWatcher = new ConfigWatcher(this._workspaceConfigPath.fsPath, {
592
				changeBufferDelay: 300, onError: error => errors.onUnexpectedError(error), defaultConfig: new WorkspaceConfigurationModel(null, this._workspaceConfigPath.fsPath), parse: (content: string, parseErrors: any[]) => {
593
					const workspaceConfigurationModel = new WorkspaceConfigurationModel(content, this._workspaceConfigPath.fsPath);
594 595 596 597
					parseErrors = [...workspaceConfigurationModel.errors];
					return workspaceConfigurationModel;
				}, initCallback: () => c(null)
			});
B
Benjamin Pasero 已提交
598
			this._workspaceConfigurationWatcherDisposables.push(this._workspaceConfigurationWatcher);
599
			this._workspaceConfigurationWatcher.onDidUpdateConfiguration(() => this._onDidUpdateConfiguration.fire(), this, this._workspaceConfigurationWatcherDisposables);
600
		});
601 602
	}

603 604 605 606 607
	get workspaceConfigurationModel(): WorkspaceConfigurationModel<any> {
		return this._workspaceConfigurationWatcher ? this._workspaceConfigurationWatcher.getConfig() : new WorkspaceConfigurationModel();
	}

	private _reload(): TPromise<void> {
608 609 610 611 612 613
		return new TPromise<void>(c => this._workspaceConfigurationWatcher.reload(() => c(null)));
	}

	dispose(): void {
		dispose(this._workspaceConfigurationWatcherDisposables);
		super.dispose();
614
	}
615
}
616

617
class FolderConfiguration<T> extends Disposable {
618

619
	private static RELOAD_CONFIGURATION_DELAY = 50;
620

621 622 623 624 625 626
	private bulkFetchFromWorkspacePromise: TPromise<any>;
	private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<ConfigurationModel<any>> };

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

627
	constructor(private folder: URI, private configFolderRelativePath: string, private scope: ConfigurationScope) {
628
		super();
629

630 631 632
		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));
	}
633

634 635 636 637 638 639
	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]);
640
			return new FolderConfigurationModel<T>(workspaceSettingsConfig, otherConfigModels, this.scope);
641 642 643 644
		});
	}

	private loadWorkspaceConfigFiles<T>(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModel<T> }> {
645 646
		// once: when invoked for the first time we fetch json files that contribute settings
		if (!this.bulkFetchFromWorkspacePromise) {
647
			this.bulkFetchFromWorkspacePromise = resolveStat(this.toResource(this.configFolderRelativePath)).then(stat => {
648 649 650 651 652 653 654 655 656 657
				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 已提交
658
					return this.isWorkspaceConfigurationFile(this.toFolderRelativePath(stat.resource)); // only workspace config files
659 660 661
				}).map(stat => stat.resource));
			}, err => [] /* never fail this call */)
				.then((contents: IContent[]) => {
B
Benjamin Pasero 已提交
662
					contents.forEach(content => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(content.resource)] = TPromise.as(this.createConfigModel(content)));
663 664 665 666 667 668 669 670
				}, 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));
	}

671
	public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise<FolderConfigurationModel<T>> {
672 673 674 675 676 677 678
		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';
679
			const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && paths.isEqual(paths.basename(resource.fsPath), this.configFolderRelativePath));
680 681 682 683
			if (!isJson && !isDeletedSettingsFolder) {
				continue; // only JSON files or the actual settings folder
			}

B
Benjamin Pasero 已提交
684
			const workspacePath = this.toFolderRelativePath(resource);
685 686 687 688 689
			if (!workspacePath) {
				continue; // event is not inside workspace
			}

			// Handle case where ".vscode" got deleted
690
			if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) {
691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712
				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;
			}
		}

713 714
		if (!affectedByChanges) {
			return TPromise.as(null);
715
		}
716 717 718 719 720 721 722 723 724 725 726

		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();
			}
		});
727 728
	}

729
	private createConfigModel<T>(content: IContent): ConfigurationModel<T> {
B
Benjamin Pasero 已提交
730
		const path = this.toFolderRelativePath(content.resource);
731
		if (path === WORKSPACE_CONFIG_DEFAULT_PATH) {
732
			return new FolderSettingsModel<T>(content.value, content.resource.toString());
733 734 735
		} else {
			const matches = /\/([^\.]*)*\.json/.exec(path);
			if (matches && matches[1]) {
736
				return new ScopedConfigurationModel<T>(content.value, content.resource.toString(), matches[1]);
737 738 739
			}
		}

740
		return new CustomConfigurationModel<T>(null);
741 742
	}

B
Benjamin Pasero 已提交
743 744
	private isWorkspaceConfigurationFile(folderRelativePath: string): boolean {
		return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS.launch, WORKSPACE_STANDALONE_CONFIGURATIONS.tasks].some(p => p === folderRelativePath);
745 746
	}

B
Benjamin Pasero 已提交
747 748 749
	private toResource(folderRelativePath: string): URI {
		if (typeof folderRelativePath === 'string') {
			return URI.file(paths.join(this.folder.fsPath, folderRelativePath));
750 751 752
		}

		return null;
753 754
	}

B
Benjamin Pasero 已提交
755
	private toFolderRelativePath(resource: URI, toOSPath?: boolean): string {
756 757
		if (this.contains(resource)) {
			return paths.normalize(paths.relative(this.folder.fsPath, resource.fsPath), toOSPath);
758 759
		}

760
		return null;
761 762
	}

763 764
	private contains(resource: URI): boolean {
		if (resource) {
765
			return paths.isEqualOrParent(resource.fsPath, this.folder.fsPath, !isLinux /* ignorecase */);
766 767
		}

768
		return false;
769
	}
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
}

// 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 已提交
806
}
807

808
export class Configuration<T> extends BaseConfiguration<T> {
809

810
	constructor(private _baseConfiguration: BaseConfiguration<T>, workspaceConfiguration: ConfigurationModel<T>, protected folders: StrictResourceMap<FolderConfigurationModel<T>>, workspace: Workspace) {
811
		super(_baseConfiguration.defaults, _baseConfiguration.user, workspaceConfiguration, folders, workspace);
812 813
	}

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

S
Sandeep Somavarapu 已提交
817 818 819
		this._baseConfiguration = baseConfiguration;
		this._defaults = this._baseConfiguration.defaults;
		this._user = this._baseConfiguration.user;
820 821 822 823 824
		this.merge();

		return !this.equals(current);
	}

825 826 827 828 829 830 831 832 833 834
	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 {
835
		const current = this.getValue(null, { resource });
836 837

		this.folders.set(resource, configuration);
838
		this.mergeFolder(resource);
839

840
		return compare && !objects.equals(current, this.getValue(null, { resource }));
841 842 843
	}

	deleteFolderConfiguration(folder: URI): boolean {
844
		if (this._workspace && this._workspace.folders.length > 0 && this._workspace.folders[0].uri.fsPath === folder.fsPath) {
845 846 847 848
			// Do not remove workspace configuration
			return false;
		}

S
Sandeep Somavarapu 已提交
849
		const changed = this.folders.get(folder).keys.length > 0;
850
		this.folders.delete(folder);
S
Sandeep Somavarapu 已提交
851 852
		this._foldersConsolidatedConfigurations.delete(folder);
		return changed;
853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
	}

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

868
		if (this._foldersConsolidatedConfigurations.size !== other._foldersConsolidatedConfigurations.size) {
869 870 871
			return false;
		}

872
		for (const resource of this._foldersConsolidatedConfigurations.keys()) {
873
			if (!objects.equals(this.getValue(null, { resource }), other.getValue(null, { resource }))) {
874 875 876 877 878 879
				return false;
			}
		}

		return true;
	}
I
isidor 已提交
880
}
881

882 883 884 885 886 887 888 889 890 891 892 893 894
interface IExportedConfigurationNode {
	name: string;
	description: string;
	default: any;
	type: string | string[];
	enum?: any[];
	enumDescriptions?: string[];
}

interface IConfigurationExport {
	settings: IExportedConfigurationNode[];
	buildTime: number;
	commit: string;
895
	version: number;
896 897 898
}

export class DefaultConfigurationExportHelper {
899 900 901 902 903

	constructor(
		@IEnvironmentService environmentService: IEnvironmentService,
		@IExtensionService private extensionService: IExtensionService,
		@ICommandService private commandService: ICommandService) {
904 905
		if (environmentService.args['export-default-configuration']) {
			this.writeConfigModelAndQuit(environmentService.args['export-default-configuration']);
906 907 908 909 910 911 912 913 914 915 916
		}
	}

	private writeConfigModelAndQuit(targetPath: string): TPromise<void> {
		return this.extensionService.onReady()
			.then(() => this.writeConfigModel(targetPath))
			.then(() => this.commandService.executeCommand('workbench.action.quit'))
			.then(() => { });
	}

	private writeConfigModel(targetPath: string): TPromise<void> {
917 918 919 920 921 922 923
		const config = this.getConfigModel();

		const resultString = JSON.stringify(config, undefined, '  ');
		return writeFile(targetPath, resultString);
	}

	private getConfigModel(): IConfigurationExport {
924
		const configurations = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurations().slice();
925
		const settings: IExportedConfigurationNode[] = [];
926 927 928 929
		const processConfig = (config: IConfigurationNode) => {
			if (config.properties) {
				for (let name in config.properties) {
					const prop = config.properties[name];
930
					const propDetails: IExportedConfigurationNode = {
931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953
						name,
						description: prop.description,
						default: prop.default,
						type: prop.type
					};

					if (prop.enum) {
						propDetails.enum = prop.enum;
					}

					if (prop.enumDescriptions) {
						propDetails.enumDescriptions = prop.enumDescriptions;
					}

					settings.push(propDetails);
				}
			}

			if (config.allOf) {
				config.allOf.forEach(processConfig);
			}
		};

954
		configurations.forEach(processConfig);
955

956 957 958 959
		const result: IConfigurationExport = {
			settings: settings.sort((a, b) => a.name.localeCompare(b.name)),
			buildTime: Date.now(),
			commit: product.commit,
960
			version: versionStringToNumber(pkg.version)
961
		};
962

963
		return result;
964 965
	}
}
966 967 968 969 970 971 972 973 974 975

function versionStringToNumber(versionStr: string): number {
	const semverRegex = /(\d+)\.(\d+)\.(\d+)/;
	const match = versionStr.match(semverRegex);
	if (!match) {
		return 0;
	}

	return parseInt(match[1], 10) * 10000 + parseInt(match[2], 10) * 100 + parseInt(match[3], 10);
}