configuration.ts 37.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, 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';
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 } 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
import { IJSONSchema } from 'vs/base/common/jsonSchema';
39

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

58 59
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);

60
const configurationEntrySchema: IJSONSchema = {
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
	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
};


// 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
		}
	]
109 110 111 112
});
configurationExtPoint.setHandler(extensions => {
	const configurations: IConfigurationNode[] = [];

113 114
	function handleConfiguration(node: IConfigurationNode, id: string, collector: ExtensionMessageCollector) {
		let configuration = objects.clone(node);
115 116 117 118 119 120 121

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

		validateProperties(configuration, collector);

122
		configuration.id = id;
123
		configurations.push(configuration);
124
	};
125

126 127 128 129 130 131 132 133 134 135
	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));
		}
	}
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 161 162 163 164 165 166 167 168 169 170 171 172 173 174
	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 已提交
175
			const propertyConfiguration = configuration.properties[key];
S
Sandeep Somavarapu 已提交
176
			propertyConfiguration.scope = propertyConfiguration.scope && propertyConfiguration.scope.toString() === 'resource' ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW;
177 178 179 180 181 182 183 184
			if (message) {
				collector.warn(message);
				delete properties[key];
			}
		}
	}
	let subNodes = configuration.allOf;
	if (subNodes) {
185
		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."));
186 187 188 189 190 191
		for (let node of subNodes) {
			validateProperties(node, collector);
		}
	}
}

192
export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService {
193 194 195

	public _serviceBrand: any;

196 197
	protected workspace: Workspace = null;
	protected _configuration: Configuration<any>;
198

199
	protected readonly _onDidUpdateConfiguration: Emitter<IConfigurationServiceEvent> = this._register(new Emitter<IConfigurationServiceEvent>());
200 201
	public readonly onDidUpdateConfiguration: Event<IConfigurationServiceEvent> = this._onDidUpdateConfiguration.event;

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

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

208
	constructor() {
209
		super();
210
		this._configuration = new Configuration(new BaseConfiguration(new ConfigurationModel<any>(), new ConfigurationModel<any>()), new ConfigurationModel<any>(), new StrictResourceMap<FolderConfigurationModel<any>>(), this.workspace);
211 212
	}

B
Benjamin Pasero 已提交
213
	public getWorkspace(): IWorkspace {
214 215 216
		return this.workspace;
	}

217
	public getWorkbenchState(): WorkbenchState {
218 219 220 221 222 223
		// Workspace has configuration file
		if (this.workspace.configuration) {
			return WorkbenchState.WORKSPACE;
		}

		// Folder has single root
S
Sandeep Somavarapu 已提交
224
		if (this.workspace.folders.length === 1) {
225
			return WorkbenchState.FOLDER;
226
		}
227 228

		// Empty
229
		return WorkbenchState.EMPTY;
230 231
	}

S
Sandeep Somavarapu 已提交
232 233
	public getWorkspaceFolder(resource: URI): URI {
		return this.workspace.getFolder(resource);
234 235
	}

236
	public isInsideWorkspace(resource: URI): boolean {
S
Sandeep Somavarapu 已提交
237
		return !!this.getWorkspaceFolder(resource);
238 239
	}

240
	public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean {
241 242
		switch (this.getWorkbenchState()) {
			case WorkbenchState.FOLDER:
S
Sandeep Somavarapu 已提交
243
				return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.folders[0].fsPath, workspaceIdentifier);
244 245
			case WorkbenchState.WORKSPACE:
				return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id;
246
		}
247
		return false;
248 249
	}

250
	public toResource(workspaceRelativePath: string): URI {
S
Sandeep Somavarapu 已提交
251 252
		if (this.workspace.folders.length) {
			return URI.file(paths.join(this.workspace.folders[0].fsPath, workspaceRelativePath));
253 254
		}
		return null;
255 256
	}

257 258 259 260 261 262 263 264 265 266 267 268
	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));
269 270
	}

271 272
	public getConfigurationData<T>(): IConfigurationData<T> {
		return this._configuration.toData();
273 274
	}

275 276
	public getConfiguration<C>(section?: string, overrides?: IConfigurationOverrides): C {
		return this._configuration.getValue<C>(section, overrides);
277 278
	}

S
Sandeep Somavarapu 已提交
279 280
	public lookup<C>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<C> {
		return this._configuration.lookup<C>(key, overrides);
281 282
	}

283 284
	public keys(overrides?: IConfigurationOverrides): IConfigurationKeys {
		return this._configuration.keys(overrides);
285
	}
286

287
	public values<V>(): IConfigurationValues {
288
		return this._configuration.values();
289 290
	}

291
	public getUnsupportedWorkspaceKeys(): string[] {
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
		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);
	}
315 316 317 318 319 320 321 322 323

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

		return path1 === path2;
	}
324 325 326 327 328 329
}

export class EmptyWorkspaceServiceImpl extends WorkspaceService {

	private baseConfigurationService: GlobalConfigurationService<any>;

330
	constructor(configuration: IWindowConfiguration, environmentService: IEnvironmentService) {
331
		super();
332 333
		let id = configuration.backupPath ? URI.from({ path: paths.basename(configuration.backupPath), scheme: 'empty' }).toString() : '';
		this.workspace = new Workspace(id, '', []);
334 335 336
		this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService));
		this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e)));
		this.resetCaches();
337 338
	}

339 340 341
	public reloadConfiguration(section?: string): TPromise<any> {
		const current = this._configuration;
		return this.baseConfigurationService.reloadConfiguration()
342
			.then(() => this.initialize(false)) // Reinitialize to ensure we are hitting the disk
343 344 345 346 347 348 349
			.then(() => {
				// Check and trigger
				if (!this._configuration.equals(current)) {
					this.triggerConfigurationChange();
				}
				return super.reloadConfiguration(section);
			});
350 351
	}

352 353 354
	private onBaseConfigurationChanged({ source, sourceConfig }: IConfigurationServiceEvent): void {
		if (this._configuration.updateBaseConfiguration(<any>this.baseConfigurationService.configuration())) {
			this._onDidUpdateConfiguration.fire({ source, sourceConfig });
355
		}
356
	}
357

358 359 360 361 362 363 364 365 366 367 368 369 370
	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;

371 372
	private workspaceConfigPath: URI;
	private folderPath: URI;
373 374 375 376
	private baseConfigurationService: GlobalConfigurationService<any>;
	private workspaceConfiguration: WorkspaceConfiguration;
	private cachedFolderConfigs: StrictResourceMap<FolderConfiguration<any>>;

377
	constructor(private workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, private environmentService: IEnvironmentService, private workspacesService: IWorkspacesService, private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME) {
378
		super();
379 380 381 382 383 384 385

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

386
		this.workspaceConfiguration = this._register(new WorkspaceConfiguration());
387 388 389 390
		this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService));
	}

	public getUnsupportedWorkspaceKeys(): string[] {
S
Sandeep Somavarapu 已提交
391
		return this.getWorkbenchState() === WorkbenchState.FOLDER ? this._configuration.getFolderConfigurationModel(this.workspace.folders[0]).workspaceSettingsConfig.unsupportedKeys : [];
392 393
	}

394
	public initialize(trigger: boolean = true): TPromise<any> {
395 396 397 398 399
		if (!this.workspace) {
			return this.initializeWorkspace()
				.then(() => super.initialize(trigger));
		}

400
		if (this.workspaceConfigPath) {
401
			return this.workspaceConfiguration.load(this.workspaceConfigPath)
402 403 404 405 406 407 408 409 410 411
				.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
412
			.then(() => {
413 414 415
				// Check and trigger
				if (!this._configuration.equals(current)) {
					this.triggerConfigurationChange();
416
				}
417
				return super.reloadConfiguration(section);
418
			});
419 420
	}

421
	public handleWorkspaceFileEvents(event: FileChangesEvent): void {
S
Sandeep Somavarapu 已提交
422
		TPromise.join(this.workspace.folders.map(folder => this.cachedFolderConfigs.get(folder).handleWorkspaceFileEvents(event))) // handle file event for each folder
423
			.then(folderConfigurations =>
S
Sandeep Somavarapu 已提交
424
				folderConfigurations.map((configuration, index) => ({ configuration, folder: this.workspace.folders[index] }))
425 426 427 428 429 430 431 432 433
					.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);
S
Sandeep Somavarapu 已提交
434
		this.initCachesForFolders(this.workspace.folders);
435 436 437 438 439 440 441 442 443
	}

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

444 445 446 447
	// 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;
448

449 450 451 452 453
	// 	// Reset the workspace if current workspace is single folder
	// 	if (this.hasFolderWorkspace()) {
	// 		this.folderPath = null;
	// 		this.workspace = null;
	// 	}
454

455 456 457 458 459
	// 	// 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);
	// 	}
460

461 462 463 464 465 466
	// 	return this.initialize().then(() => {
	// 		if (workspaceName !== this.workspace.name) {
	// 			this._onDidChangeWorkspaceName.fire();
	// 		}
	// 	});
	// }
467

468
	private initializeMulitFolderWorkspace(): TPromise<void> {
469
		this.registerWorkspaceConfigSchema();
470
		return this.workspaceConfiguration.load(this.workspaceConfigPath)
471 472
			.then(() => {
				const workspaceConfigurationModel = this.workspaceConfiguration.workspaceConfigurationModel;
473 474
				const workspaceFolders = this.parseWorkspaceFolders(workspaceConfigurationModel.folders);
				if (!workspaceFolders.length) {
475 476
					return TPromise.wrapError<void>(new Error('Invalid workspace configuraton file ' + this.workspaceConfigPath));
				}
477 478
				const workspaceId = (this.workspaceIdentifier as IWorkspaceIdentifier).id;
				const workspaceName = getWorkspaceLabel({ id: workspaceId, configPath: this.workspaceConfigPath.fsPath }, this.environmentService);
479
				this.workspace = new Workspace(workspaceId, workspaceName, workspaceFolders, this.workspaceConfigPath);
480 481 482 483 484
				this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged()));
				return null;
			});
	}

485 486 487 488 489
	private parseWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[]): URI[] {
		return coalesce(configuredFolders.map(configuredFolder => {
			const path = configuredFolder.path;
			if (!path) {
				return void 0;
490 491
			}

492 493 494 495 496 497
			if (paths.isAbsolute(path)) {
				return URI.file(path);
			}

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

500 501 502 503 504 505
	private registerWorkspaceConfigSchema(): void {
		const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
		if (!contributionRegistry.getSchemaContributions().schemas['vscode://schemas/workspaceConfig']) {
			contributionRegistry.registerSchema('vscode://schemas/workspaceConfig', {
				default: {
					folders: [
506 507 508
						{
							path: ''
						}
509 510 511 512
					],
					settings: {
					}
				},
513
				required: ['folders'],
514 515 516 517
				properties: {
					'folders': {
						minItems: 1,
						uniqueItems: true,
518
						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."),
519
						items: {
520 521 522 523 524 525 526 527
							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.")
								}
							}
528 529 530 531 532
						}
					},
					'settings': {
						type: 'object',
						default: {},
533
						description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),
534 535 536 537 538 539 540
						$ref: schemaId
					}
				}
			});
		}
	}

541
	private initializeSingleFolderWorkspace(): TPromise<void> {
542
		return stat(this.folderPath.fsPath)
543 544
			.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!
545 546
				const id = createHash('md5').update(this.folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex');
				const folder = URI.file(this.folderPath.fsPath);
547
				this.workspace = new Workspace(id, paths.basename(this.folderPath.fsPath), [folder], null, ctime);
548 549 550 551 552 553
				return TPromise.as(null);
			});
	}

	private initCachesForFolders(folders: URI[]): void {
		for (const folder of folders) {
554
			this.cachedFolderConfigs.set(folder, this._register(new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.getWorkbenchState() === WorkbenchState.WORKSPACE ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW)));
S
Sandeep Somavarapu 已提交
555
			this.updateFolderConfiguration(folder, new FolderConfigurationModel<any>(new FolderSettingsModel<any>(null), [], ConfigurationScope.RESOURCE), false);
556 557 558
		}
	}

S
Sandeep Somavarapu 已提交
559
	protected updateConfiguration(folders: URI[] = this.workspace.folders): TPromise<boolean> {
560 561 562 563 564 565 566 567
		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) {
S
Sandeep Somavarapu 已提交
568
			this.workspace.folders.forEach(folder => this._configuration.getFolderConfigurationModel(folder).update());
569 570 571 572 573 574 575
		}
		if (this._configuration.updateBaseConfiguration(<any>this.baseConfigurationService.configuration())) {
			this._onDidUpdateConfiguration.fire({ source, sourceConfig });
		}
	}

	private onWorkspaceConfigurationChanged(): void {
576
		let configuredFolders = this.parseWorkspaceFolders(this.workspaceConfiguration.workspaceConfigurationModel.folders);
S
Sandeep Somavarapu 已提交
577
		const foldersChanged = !equals(this.workspace.folders, configuredFolders, (r1, r2) => r1.fsPath === r2.fsPath);
578
		if (foldersChanged) { // TODO@Sandeep be smarter here about detecting changes
S
Sandeep Somavarapu 已提交
579
			this.workspace.folders = configuredFolders;
S
Sandeep Somavarapu 已提交
580 581
			this.onFoldersChanged()
				.then(configurationChanged => {
S
Sandeep Somavarapu 已提交
582
					this._onDidChangeWorkspaceFolders.fire();
S
Sandeep Somavarapu 已提交
583 584 585 586 587 588 589 590 591
					if (configurationChanged) {
						this.triggerConfigurationChange();
					}
				});
		} else {
			const configurationChanged = this.updateWorkspaceConfiguration(true);
			if (configurationChanged) {
				this.triggerConfigurationChange();
			}
592 593 594
		}
	}

S
Sandeep Somavarapu 已提交
595
	private onFoldersChanged(): TPromise<boolean> {
S
Sandeep Somavarapu 已提交
596
		let configurationChangedOnRemoval = false;
597

598 599
		// Remove the configurations of deleted folders
		for (const key of this.cachedFolderConfigs.keys()) {
S
Sandeep Somavarapu 已提交
600
			if (!this.workspace.folders.filter(folder => folder.toString() === key.toString())[0]) {
601 602
				this.cachedFolderConfigs.delete(key);
				if (this._configuration.deleteFolderConfiguration(key)) {
S
Sandeep Somavarapu 已提交
603
					configurationChangedOnRemoval = true;
604 605 606 607
				}
			}
		}

608
		// Initialize the newly added folders
S
Sandeep Somavarapu 已提交
609
		const toInitialize = this.workspace.folders.filter(folder => !this.cachedFolderConfigs.has(folder));
610 611
		if (toInitialize.length) {
			this.initCachesForFolders(toInitialize);
S
Sandeep Somavarapu 已提交
612 613
			return this.updateConfiguration(toInitialize)
				.then(changed => configurationChangedOnRemoval || changed);
S
Sandeep Somavarapu 已提交
614 615
		} else if (configurationChangedOnRemoval) {
			this.updateWorkspaceConfiguration(false);
S
Sandeep Somavarapu 已提交
616
			return TPromise.as(true);
617
		}
S
Sandeep Somavarapu 已提交
618
		return TPromise.as(false);
619 620
	}

621 622
	private updateFolderConfiguration(folder: URI, folderConfiguration: FolderConfigurationModel<any>, compare: boolean): boolean {
		let configurationChanged = this._configuration.updateFolderConfiguration(folder, folderConfiguration, compare);
623
		if (this.getWorkbenchState() === WorkbenchState.FOLDER) {
624 625 626 627
			// Workspace configuration changed
			configurationChanged = this.updateWorkspaceConfiguration(compare) || configurationChanged;
		}
		return configurationChanged;
628 629
	}

630
	private updateWorkspaceConfiguration(compare: boolean): boolean {
S
Sandeep Somavarapu 已提交
631
		const workspaceConfiguration = this.getWorkbenchState() === WorkbenchState.WORKSPACE ? this.workspaceConfiguration.workspaceConfigurationModel.workspaceConfiguration : this._configuration.getFolderConfigurationModel(this.workspace.folders[0]);
632
		return this._configuration.updateWorkspaceConfiguration(workspaceConfiguration, compare);
633
	}
634

635
	protected triggerConfigurationChange(): void {
S
Sandeep Somavarapu 已提交
636
		this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.Workspace, sourceConfig: this._configuration.getFolderConfigurationModel(this.workspace.folders[0]).contents });
637
	}
638
}
639

640
class WorkspaceConfiguration extends Disposable {
641

642
	private _workspaceConfigPath: URI;
643
	private _workspaceConfigurationWatcher: ConfigWatcher<WorkspaceConfigurationModel<any>>;
644
	private _workspaceConfigurationWatcherDisposables: IDisposable[] = [];
645 646 647 648

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

649

650 651
	load(workspaceConfigPath: URI): TPromise<void> {
		if (this._workspaceConfigPath && this._workspaceConfigPath.fsPath === workspaceConfigPath.fsPath) {
652
			return this._reload();
653
		}
654

655 656 657
		this._workspaceConfigPath = workspaceConfigPath;

		this._workspaceConfigurationWatcherDisposables = dispose(this._workspaceConfigurationWatcherDisposables);
658
		return new TPromise<void>((c, e) => {
659
			this._workspaceConfigurationWatcher = new ConfigWatcher(this._workspaceConfigPath.fsPath, {
660
				changeBufferDelay: 300, onError: error => errors.onUnexpectedError(error), defaultConfig: new WorkspaceConfigurationModel(null, this._workspaceConfigPath.fsPath), parse: (content: string, parseErrors: any[]) => {
661
					const workspaceConfigurationModel = new WorkspaceConfigurationModel(content, this._workspaceConfigPath.fsPath);
662 663 664 665
					parseErrors = [...workspaceConfigurationModel.errors];
					return workspaceConfigurationModel;
				}, initCallback: () => c(null)
			});
B
Benjamin Pasero 已提交
666
			this._workspaceConfigurationWatcherDisposables.push(this._workspaceConfigurationWatcher);
667
			this._workspaceConfigurationWatcher.onDidUpdateConfiguration(() => this._onDidUpdateConfiguration.fire(), this, this._workspaceConfigurationWatcherDisposables);
668
		});
669 670
	}

671 672 673 674 675
	get workspaceConfigurationModel(): WorkspaceConfigurationModel<any> {
		return this._workspaceConfigurationWatcher ? this._workspaceConfigurationWatcher.getConfig() : new WorkspaceConfigurationModel();
	}

	private _reload(): TPromise<void> {
676 677 678 679 680 681
		return new TPromise<void>(c => this._workspaceConfigurationWatcher.reload(() => c(null)));
	}

	dispose(): void {
		dispose(this._workspaceConfigurationWatcherDisposables);
		super.dispose();
682
	}
683
}
684

685
class FolderConfiguration<T> extends Disposable {
686

687
	private static RELOAD_CONFIGURATION_DELAY = 50;
688

689 690 691 692 693 694
	private bulkFetchFromWorkspacePromise: TPromise<any>;
	private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<ConfigurationModel<any>> };

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

695
	constructor(private folder: URI, private configFolderRelativePath: string, private scope: ConfigurationScope) {
696
		super();
697

698 699 700
		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));
	}
701

702 703 704 705 706 707
	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]);
708
			return new FolderConfigurationModel<T>(workspaceSettingsConfig, otherConfigModels, this.scope);
709 710 711 712
		});
	}

	private loadWorkspaceConfigFiles<T>(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModel<T> }> {
713 714
		// once: when invoked for the first time we fetch json files that contribute settings
		if (!this.bulkFetchFromWorkspacePromise) {
715
			this.bulkFetchFromWorkspacePromise = resolveStat(this.toResource(this.configFolderRelativePath)).then(stat => {
716 717 718 719 720 721 722 723 724 725
				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 已提交
726
					return this.isWorkspaceConfigurationFile(this.toFolderRelativePath(stat.resource)); // only workspace config files
727 728 729
				}).map(stat => stat.resource));
			}, err => [] /* never fail this call */)
				.then((contents: IContent[]) => {
B
Benjamin Pasero 已提交
730
					contents.forEach(content => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(content.resource)] = TPromise.as(this.createConfigModel(content)));
731 732 733 734 735 736 737 738
				}, 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));
	}

739
	public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise<FolderConfigurationModel<T>> {
740 741 742 743 744 745 746
		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';
747
			const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && paths.isEqual(paths.basename(resource.fsPath), this.configFolderRelativePath));
748 749 750 751
			if (!isJson && !isDeletedSettingsFolder) {
				continue; // only JSON files or the actual settings folder
			}

B
Benjamin Pasero 已提交
752
			const workspacePath = this.toFolderRelativePath(resource);
753 754 755 756 757
			if (!workspacePath) {
				continue; // event is not inside workspace
			}

			// Handle case where ".vscode" got deleted
758
			if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) {
759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780
				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;
			}
		}

781 782
		if (!affectedByChanges) {
			return TPromise.as(null);
783
		}
784 785 786 787 788 789 790 791 792 793 794

		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();
			}
		});
795 796
	}

797
	private createConfigModel<T>(content: IContent): ConfigurationModel<T> {
B
Benjamin Pasero 已提交
798
		const path = this.toFolderRelativePath(content.resource);
799
		if (path === WORKSPACE_CONFIG_DEFAULT_PATH) {
800
			return new FolderSettingsModel<T>(content.value, content.resource.toString());
801 802 803
		} else {
			const matches = /\/([^\.]*)*\.json/.exec(path);
			if (matches && matches[1]) {
804
				return new ScopedConfigurationModel<T>(content.value, content.resource.toString(), matches[1]);
805 806 807
			}
		}

808
		return new CustomConfigurationModel<T>(null);
809 810
	}

B
Benjamin Pasero 已提交
811 812
	private isWorkspaceConfigurationFile(folderRelativePath: string): boolean {
		return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS.launch, WORKSPACE_STANDALONE_CONFIGURATIONS.tasks].some(p => p === folderRelativePath);
813 814
	}

B
Benjamin Pasero 已提交
815 816 817
	private toResource(folderRelativePath: string): URI {
		if (typeof folderRelativePath === 'string') {
			return URI.file(paths.join(this.folder.fsPath, folderRelativePath));
818 819 820
		}

		return null;
821 822
	}

B
Benjamin Pasero 已提交
823
	private toFolderRelativePath(resource: URI, toOSPath?: boolean): string {
824 825
		if (this.contains(resource)) {
			return paths.normalize(paths.relative(this.folder.fsPath, resource.fsPath), toOSPath);
826 827
		}

828
		return null;
829 830
	}

831 832
	private contains(resource: URI): boolean {
		if (resource) {
833
			return paths.isEqualOrParent(resource.fsPath, this.folder.fsPath, !isLinux /* ignorecase */);
834 835
		}

836
		return false;
837
	}
838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873
}

// 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 已提交
874
}
875

876
export class Configuration<T> extends BaseConfiguration<T> {
877

878
	constructor(private _baseConfiguration: BaseConfiguration<T>, workspaceConfiguration: ConfigurationModel<T>, protected folders: StrictResourceMap<FolderConfigurationModel<T>>, workspace: Workspace) {
879
		super(_baseConfiguration.defaults, _baseConfiguration.user, workspaceConfiguration, folders, workspace);
880 881
	}

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

S
Sandeep Somavarapu 已提交
885 886 887
		this._baseConfiguration = baseConfiguration;
		this._defaults = this._baseConfiguration.defaults;
		this._user = this._baseConfiguration.user;
888 889 890 891 892
		this.merge();

		return !this.equals(current);
	}

893 894 895 896 897 898 899 900 901 902
	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 {
903
		const current = this.getValue(null, { resource });
904 905

		this.folders.set(resource, configuration);
906
		this.mergeFolder(resource);
907

908
		return compare && !objects.equals(current, this.getValue(null, { resource }));
909 910 911
	}

	deleteFolderConfiguration(folder: URI): boolean {
S
Sandeep Somavarapu 已提交
912
		if (this._workspace && this._workspace.folders.length > 0 && this._workspace.folders[0].fsPath === folder.fsPath) {
913 914 915 916
			// Do not remove workspace configuration
			return false;
		}

S
Sandeep Somavarapu 已提交
917
		const changed = this.folders.get(folder).keys.length > 0;
918
		this.folders.delete(folder);
S
Sandeep Somavarapu 已提交
919 920
		this._foldersConsolidatedConfigurations.delete(folder);
		return changed;
921 922 923 924 925 926 927 928 929 930 931 932 933 934 935
	}

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

936
		if (this._foldersConsolidatedConfigurations.size !== other._foldersConsolidatedConfigurations.size) {
937 938 939
			return false;
		}

940
		for (const resource of this._foldersConsolidatedConfigurations.keys()) {
941
			if (!objects.equals(this.getValue(null, { resource }), other.getValue(null, { resource }))) {
942 943 944 945 946 947
				return false;
			}
		}

		return true;
	}
I
isidor 已提交
948
}