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 } 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, 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 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 209 210
	protected readonly _onDidChangeWorkbenchState: Emitter<WorkbenchState> = this._register(new Emitter<WorkbenchState>());
	public readonly onDidChangeWorkbenchState: Event<WorkbenchState> = this._onDidChangeWorkbenchState.event;

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

B
Benjamin Pasero 已提交
216
	public getWorkspace(): IWorkspace {
217 218 219
		return this.workspace;
	}

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

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

		// Empty
232
		return WorkbenchState.EMPTY;
233 234
	}

235
	public getWorkspaceFolder(resource: URI): WorkspaceFolder {
S
Sandeep Somavarapu 已提交
236
		return this.workspace.getFolder(resource);
237 238
	}

239
	public isInsideWorkspace(resource: URI): boolean {
S
Sandeep Somavarapu 已提交
240
		return !!this.getWorkspaceFolder(resource);
241 242
	}

243
	public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean {
244 245
		switch (this.getWorkbenchState()) {
			case WorkbenchState.FOLDER:
246
				return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && this.pathEquals(this.workspace.folders[0].uri.fsPath, workspaceIdentifier);
247 248
			case WorkbenchState.WORKSPACE:
				return isWorkspaceIdentifier(workspaceIdentifier) && this.workspace.id === workspaceIdentifier.id;
249
		}
250
		return false;
251 252
	}

253 254
	public toResource(workspaceRelativePath: string, workspaceFolder: WorkspaceFolder): URI {
		return URI.file(paths.join(workspaceFolder.uri.fsPath, workspaceRelativePath));
255 256
	}

257 258
	public reloadConfiguration(section?: string): TPromise<any> {
		return TPromise.as(this.getConfiguration(section));
259 260
	}

261 262
	public getConfigurationData<T>(): IConfigurationData<T> {
		return this._configuration.toData();
263 264
	}

265 266
	public getConfiguration<C>(section?: string, overrides?: IConfigurationOverrides): C {
		return this._configuration.getValue<C>(section, overrides);
267 268
	}

S
Sandeep Somavarapu 已提交
269 270
	public lookup<C>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<C> {
		return this._configuration.lookup<C>(key, overrides);
271 272
	}

273 274
	public keys(overrides?: IConfigurationOverrides): IConfigurationKeys {
		return this._configuration.keys(overrides);
275
	}
276

277
	public values<V>(): IConfigurationValues {
278
		return this._configuration.values();
279 280
	}

281
	public getUnsupportedWorkspaceKeys(): string[] {
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
		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);
	}
305 306 307 308 309 310 311 312 313

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

		return path1 === path2;
	}
314 315 316 317 318 319 320 321 322 323
}

export class WorkspaceServiceImpl extends WorkspaceService {

	public _serviceBrand: any;

	private baseConfigurationService: GlobalConfigurationService<any>;
	private workspaceConfiguration: WorkspaceConfiguration;
	private cachedFolderConfigs: StrictResourceMap<FolderConfiguration<any>>;

324
	constructor(private environmentService: IEnvironmentService, private workspacesService: IWorkspacesService, private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME) {
325
		super();
326
		this.workspaceConfiguration = this._register(new WorkspaceConfiguration());
327
		this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService));
328
		this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e)));
329 330 331
	}

	public getUnsupportedWorkspaceKeys(): string[] {
332
		return this.getWorkbenchState() === WorkbenchState.FOLDER ? this._configuration.getFolderConfigurationModel(this.workspace.folders[0].uri).workspaceSettingsConfig.unsupportedKeys : [];
333 334
	}

335
	public initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise<any> {
336 337 338 339 340 341 342 343
		const handleStateChange = !!this.workspace;

		if (!this.workspace) {
			this.workspace = new Workspace('', '', []);
		}

		const currentState = this.getWorkbenchState();
		const currentFolders = this.workspace.folders;
B
Benjamin Pasero 已提交
344
		const currentName = this.workspace.name;
345 346 347 348 349 350

		return this.initializeWorkspace(arg).then(() => this.initializeConfiguration(true)).then(() => {
			if (!handleStateChange) {
				return;
			}

351
			// Emit event for STATE transition
352 353 354 355 356
			const newState = this.getWorkbenchState();
			if (newState !== currentState) {
				this._onDidChangeWorkbenchState.fire(newState);
			}

357
			// Emit event for FOLDERS change
358 359 360 361
			const foldersChanged = !equals(this.workspace.folders, currentFolders, (folder1, folder2) => folder1.uri.fsPath === folder2.uri.fsPath);
			if (foldersChanged) {
				this._onDidChangeWorkspaceFolders.fire();
			}
362 363

			// Emit event for workspace NAME change
B
Benjamin Pasero 已提交
364 365
			const newName = this.workspace.name;
			if (currentName !== newName) {
366 367
				this._onDidChangeWorkspaceName.fire();
			}
368
		});
369 370 371 372
	}

	public reloadConfiguration(section?: string): TPromise<any> {
		const current = this._configuration;
373
		// Reload and reinitialize to ensure we are hitting the disk
374
		return this.baseConfigurationService.reloadConfiguration()
375 376 377 378 379 380 381
			.then(() => {
				if (this.workspace.configuration) {
					return this.workspaceConfiguration.load(this.workspace.configuration)
						.then(() => this.initializeConfiguration(false));
				}
				return this.initializeConfiguration(false);
			})
382
			.then(() => {
383 384 385
				// Check and trigger
				if (!this._configuration.equals(current)) {
					this.triggerConfigurationChange();
386
				}
387
				return super.reloadConfiguration(section);
388
			});
389 390
	}

391
	public handleWorkspaceFileEvents(event: FileChangesEvent): void {
392
		TPromise.join(this.workspace.folders.map(folder => this.cachedFolderConfigs.get(folder.uri).handleWorkspaceFileEvents(event))) // handle file event for each folder
393
			.then(folderConfigurations =>
S
Sandeep Somavarapu 已提交
394
				folderConfigurations.map((configuration, index) => ({ configuration, folder: this.workspace.folders[index] }))
395 396 397 398 399 400
					.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
	}

401 402 403 404 405 406 407 408 409 410
	private initializeConfiguration(trigger: boolean = true): TPromise<any> {
		this.resetCaches();
		return this.updateConfiguration()
			.then(() => {
				if (trigger) {
					this.triggerConfigurationChange();
				}
			});
	}

411 412
	protected resetCaches(): void {
		this.cachedFolderConfigs = new StrictResourceMap<FolderConfiguration<any>>();
413
		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
S
Sandeep Somavarapu 已提交
414
		this.initCachesForFolders(this.workspace.folders);
415 416
	}

417 418 419 420 421 422 423 424 425 426
	private initializeWorkspace(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise<void> {
		if (isWorkspaceIdentifier(arg)) {
			return this.initializeMulitFolderWorkspace(arg);
		}

		if (isSingleFolderWorkspaceIdentifier(arg)) {
			return this.initializeSingleFolderWorkspace(arg);
		}

		return this.initializeEmptyWorkspace(arg);
427 428
	}

429
	private initializeMulitFolderWorkspace(workspaceIdentifier: IWorkspaceIdentifier): TPromise<void> {
430
		this.registerWorkspaceConfigSchema();
431
		const workspaceConfigPath = URI.file(workspaceIdentifier.configPath);
432
		return this.workspaceConfiguration.load(workspaceConfigPath)
433 434
			.then(() => {
				const workspaceConfigurationModel = this.workspaceConfiguration.workspaceConfigurationModel;
435
				const workspaceFolders = toWorkspaceFolders(workspaceConfigurationModel.folders, URI.file(paths.dirname(workspaceConfigPath.fsPath)));
436
				if (!workspaceFolders.length) {
437
					return TPromise.wrapError<void>(new Error('Invalid workspace configuraton file ' + workspaceConfigPath));
438
				}
439
				const workspaceId = workspaceIdentifier.id;
440
				const workspaceName = getWorkspaceLabel({ id: workspaceId, configPath: workspaceConfigPath.fsPath }, this.environmentService);
441 442
				this.workspace.update(workspaceId, workspaceName, workspaceFolders, workspaceConfigPath);

443
				this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged(workspaceConfigPath)));
444 445 446 447
				return null;
			});
	}

448 449 450 451 452 453
	private registerWorkspaceConfigSchema(): void {
		const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
		if (!contributionRegistry.getSchemaContributions().schemas['vscode://schemas/workspaceConfig']) {
			contributionRegistry.registerSchema('vscode://schemas/workspaceConfig', {
				default: {
					folders: [
454 455 456
						{
							path: ''
						}
457 458 459 460
					],
					settings: {
					}
				},
461
				required: ['folders'],
462 463 464 465
				properties: {
					'folders': {
						minItems: 1,
						uniqueItems: true,
466
						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."),
467
						items: {
468 469 470 471 472 473 474 475
							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.")
								}
							}
476 477 478 479 480
						}
					},
					'settings': {
						type: 'object',
						default: {},
481
						description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),
482 483 484 485 486 487 488
						$ref: schemaId
					}
				}
			});
		}
	}

489 490
	private initializeSingleFolderWorkspace(singleFolderWorkspaceIdentifier: ISingleFolderWorkspaceIdentifier): TPromise<void> {
		const folderPath = URI.file(singleFolderWorkspaceIdentifier);
491
		return stat(folderPath.fsPath)
492 493
			.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!
494 495
				const id = createHash('md5').update(folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex');
				const folder = URI.file(folderPath.fsPath);
496
				this.workspace.update(id, paths.basename(folderPath.fsPath), toWorkspaceFolders([{ path: folder.fsPath }]), null, ctime);
497 498 499 500
				return TPromise.as(null);
			});
	}

501 502
	private initializeEmptyWorkspace(configuration: IWindowConfiguration): TPromise<void> {
		let id = configuration.backupPath ? URI.from({ path: paths.basename(configuration.backupPath), scheme: 'empty' }).toString() : '';
503
		this.workspace.update(id, '', []);
504 505 506
		return TPromise.as(null);
	}

507
	private initCachesForFolders(folders: WorkspaceFolder[]): void {
508
		for (const folder of folders) {
509
			this.cachedFolderConfigs.set(folder.uri, this._register(new FolderConfiguration(folder.uri, this.workspaceSettingsRootFolder, this.getWorkbenchState() === WorkbenchState.WORKSPACE ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW)));
S
Sandeep Somavarapu 已提交
510
			this.updateFolderConfiguration(folder, new FolderConfigurationModel<any>(new FolderSettingsModel<any>(null), [], ConfigurationScope.RESOURCE), false);
511 512 513
		}
	}

514 515
	protected updateConfiguration(folders: WorkspaceFolder[] = this.workspace.folders): TPromise<boolean> {
		return TPromise.join([...folders.map(folder => this.cachedFolderConfigs.get(folder.uri).loadConfiguration()
516 517 518 519 520 521 522
			.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) {
523
			this.workspace.folders.forEach(folder => this._configuration.getFolderConfigurationModel(folder.uri).update());
524 525 526 527 528 529
		}
		if (this._configuration.updateBaseConfiguration(<any>this.baseConfigurationService.configuration())) {
			this._onDidUpdateConfiguration.fire({ source, sourceConfig });
		}
	}

530 531
	private onWorkspaceConfigurationChanged(workspaceConfigPath: URI): void {
		let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.workspaceConfigurationModel.folders, URI.file(paths.dirname(workspaceConfigPath.fsPath)));
532
		const foldersChanged = !equals(this.workspace.folders, configuredFolders, (folder1, folder2) => folder1.uri.fsPath === folder2.uri.fsPath);
533
		if (foldersChanged) { // TODO@Sandeep be smarter here about detecting changes
S
Sandeep Somavarapu 已提交
534
			this.workspace.folders = configuredFolders;
S
Sandeep Somavarapu 已提交
535 536
			this.onFoldersChanged()
				.then(configurationChanged => {
S
Sandeep Somavarapu 已提交
537
					this._onDidChangeWorkspaceFolders.fire();
S
Sandeep Somavarapu 已提交
538 539 540 541 542 543 544 545 546
					if (configurationChanged) {
						this.triggerConfigurationChange();
					}
				});
		} else {
			const configurationChanged = this.updateWorkspaceConfiguration(true);
			if (configurationChanged) {
				this.triggerConfigurationChange();
			}
547 548 549
		}
	}

S
Sandeep Somavarapu 已提交
550
	private onFoldersChanged(): TPromise<boolean> {
S
Sandeep Somavarapu 已提交
551
		let configurationChangedOnRemoval = false;
552

553 554
		// Remove the configurations of deleted folders
		for (const key of this.cachedFolderConfigs.keys()) {
555
			if (!this.workspace.folders.filter(folder => folder.uri.toString() === key.toString())[0]) {
556 557
				this.cachedFolderConfigs.delete(key);
				if (this._configuration.deleteFolderConfiguration(key)) {
S
Sandeep Somavarapu 已提交
558
					configurationChangedOnRemoval = true;
559 560 561 562
				}
			}
		}

563
		// Initialize the newly added folders
564
		const toInitialize = this.workspace.folders.filter(folder => !this.cachedFolderConfigs.has(folder.uri));
565 566
		if (toInitialize.length) {
			this.initCachesForFolders(toInitialize);
S
Sandeep Somavarapu 已提交
567 568
			return this.updateConfiguration(toInitialize)
				.then(changed => configurationChangedOnRemoval || changed);
S
Sandeep Somavarapu 已提交
569 570
		} else if (configurationChangedOnRemoval) {
			this.updateWorkspaceConfiguration(false);
S
Sandeep Somavarapu 已提交
571
			return TPromise.as(true);
572
		}
S
Sandeep Somavarapu 已提交
573
		return TPromise.as(false);
574 575
	}

576 577
	private updateFolderConfiguration(folder: WorkspaceFolder, folderConfiguration: FolderConfigurationModel<any>, compare: boolean): boolean {
		let configurationChanged = this._configuration.updateFolderConfiguration(folder.uri, folderConfiguration, compare);
578
		if (this.getWorkbenchState() === WorkbenchState.FOLDER) {
579 580 581 582
			// Workspace configuration changed
			configurationChanged = this.updateWorkspaceConfiguration(compare) || configurationChanged;
		}
		return configurationChanged;
583 584
	}

585
	private updateWorkspaceConfiguration(compare: boolean): boolean {
586 587 588 589 590 591
		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);
592
		return this._configuration.updateWorkspaceConfiguration(workspaceConfiguration, compare);
593
	}
594

595
	protected triggerConfigurationChange(): void {
596 597 598 599 600
		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 });
		}
601
	}
602
}
603

604
class WorkspaceConfiguration extends Disposable {
605

606
	private _workspaceConfigPath: URI;
607
	private _workspaceConfigurationWatcher: ConfigWatcher<WorkspaceConfigurationModel<any>>;
608
	private _workspaceConfigurationWatcherDisposables: IDisposable[] = [];
609 610 611 612

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

613

614 615
	load(workspaceConfigPath: URI): TPromise<void> {
		if (this._workspaceConfigPath && this._workspaceConfigPath.fsPath === workspaceConfigPath.fsPath) {
616
			return this._reload();
617
		}
618

619 620 621
		this._workspaceConfigPath = workspaceConfigPath;

		this._workspaceConfigurationWatcherDisposables = dispose(this._workspaceConfigurationWatcherDisposables);
622
		return new TPromise<void>((c, e) => {
623
			this._workspaceConfigurationWatcher = new ConfigWatcher(this._workspaceConfigPath.fsPath, {
624
				changeBufferDelay: 300, onError: error => errors.onUnexpectedError(error), defaultConfig: new WorkspaceConfigurationModel(null, this._workspaceConfigPath.fsPath), parse: (content: string, parseErrors: any[]) => {
625
					const workspaceConfigurationModel = new WorkspaceConfigurationModel(content, this._workspaceConfigPath.fsPath);
626 627 628 629
					parseErrors = [...workspaceConfigurationModel.errors];
					return workspaceConfigurationModel;
				}, initCallback: () => c(null)
			});
B
Benjamin Pasero 已提交
630
			this._workspaceConfigurationWatcherDisposables.push(this._workspaceConfigurationWatcher);
631
			this._workspaceConfigurationWatcher.onDidUpdateConfiguration(() => this._onDidUpdateConfiguration.fire(), this, this._workspaceConfigurationWatcherDisposables);
632
		});
633 634
	}

635 636 637 638 639
	get workspaceConfigurationModel(): WorkspaceConfigurationModel<any> {
		return this._workspaceConfigurationWatcher ? this._workspaceConfigurationWatcher.getConfig() : new WorkspaceConfigurationModel();
	}

	private _reload(): TPromise<void> {
640 641 642 643 644 645
		return new TPromise<void>(c => this._workspaceConfigurationWatcher.reload(() => c(null)));
	}

	dispose(): void {
		dispose(this._workspaceConfigurationWatcherDisposables);
		super.dispose();
646
	}
647
}
648

649
class FolderConfiguration<T> extends Disposable {
650

651
	private static RELOAD_CONFIGURATION_DELAY = 50;
652

653 654 655 656 657 658
	private bulkFetchFromWorkspacePromise: TPromise<any>;
	private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<ConfigurationModel<any>> };

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

659
	constructor(private folder: URI, private configFolderRelativePath: string, private scope: ConfigurationScope) {
660
		super();
661

662 663 664
		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));
	}
665

666 667 668 669 670 671
	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]);
672
			return new FolderConfigurationModel<T>(workspaceSettingsConfig, otherConfigModels, this.scope);
673 674 675 676
		});
	}

	private loadWorkspaceConfigFiles<T>(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModel<T> }> {
677 678
		// once: when invoked for the first time we fetch json files that contribute settings
		if (!this.bulkFetchFromWorkspacePromise) {
679
			this.bulkFetchFromWorkspacePromise = resolveStat(this.toResource(this.configFolderRelativePath)).then(stat => {
680 681 682 683 684 685 686 687 688 689
				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 已提交
690
					return this.isWorkspaceConfigurationFile(this.toFolderRelativePath(stat.resource)); // only workspace config files
691 692 693
				}).map(stat => stat.resource));
			}, err => [] /* never fail this call */)
				.then((contents: IContent[]) => {
B
Benjamin Pasero 已提交
694
					contents.forEach(content => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(content.resource)] = TPromise.as(this.createConfigModel(content)));
695 696 697 698 699 700 701 702
				}, 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));
	}

703
	public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise<FolderConfigurationModel<T>> {
704 705 706 707 708 709 710
		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';
711
			const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && paths.isEqual(paths.basename(resource.fsPath), this.configFolderRelativePath));
712 713 714 715
			if (!isJson && !isDeletedSettingsFolder) {
				continue; // only JSON files or the actual settings folder
			}

B
Benjamin Pasero 已提交
716
			const workspacePath = this.toFolderRelativePath(resource);
717 718 719 720 721
			if (!workspacePath) {
				continue; // event is not inside workspace
			}

			// Handle case where ".vscode" got deleted
722
			if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) {
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744
				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;
			}
		}

745 746
		if (!affectedByChanges) {
			return TPromise.as(null);
747
		}
748 749 750 751 752 753 754 755 756 757 758

		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();
			}
		});
759 760
	}

761
	private createConfigModel<T>(content: IContent): ConfigurationModel<T> {
B
Benjamin Pasero 已提交
762
		const path = this.toFolderRelativePath(content.resource);
763
		if (path === WORKSPACE_CONFIG_DEFAULT_PATH) {
764
			return new FolderSettingsModel<T>(content.value, content.resource.toString());
765 766 767
		} else {
			const matches = /\/([^\.]*)*\.json/.exec(path);
			if (matches && matches[1]) {
768
				return new ScopedConfigurationModel<T>(content.value, content.resource.toString(), matches[1]);
769 770 771
			}
		}

772
		return new CustomConfigurationModel<T>(null);
773 774
	}

B
Benjamin Pasero 已提交
775 776
	private isWorkspaceConfigurationFile(folderRelativePath: string): boolean {
		return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS.launch, WORKSPACE_STANDALONE_CONFIGURATIONS.tasks].some(p => p === folderRelativePath);
777 778
	}

B
Benjamin Pasero 已提交
779 780 781
	private toResource(folderRelativePath: string): URI {
		if (typeof folderRelativePath === 'string') {
			return URI.file(paths.join(this.folder.fsPath, folderRelativePath));
782 783 784
		}

		return null;
785 786
	}

B
Benjamin Pasero 已提交
787
	private toFolderRelativePath(resource: URI, toOSPath?: boolean): string {
788 789
		if (this.contains(resource)) {
			return paths.normalize(paths.relative(this.folder.fsPath, resource.fsPath), toOSPath);
790 791
		}

792
		return null;
793 794
	}

795 796
	private contains(resource: URI): boolean {
		if (resource) {
797
			return paths.isEqualOrParent(resource.fsPath, this.folder.fsPath, !isLinux /* ignorecase */);
798 799
		}

800
		return false;
801
	}
802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837
}

// 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 已提交
838
}
839

840
export class Configuration<T> extends BaseConfiguration<T> {
841

842
	constructor(private _baseConfiguration: BaseConfiguration<T>, workspaceConfiguration: ConfigurationModel<T>, protected folders: StrictResourceMap<FolderConfigurationModel<T>>, workspace: Workspace) {
843
		super(_baseConfiguration.defaults, _baseConfiguration.user, workspaceConfiguration, folders, workspace);
844 845
	}

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

S
Sandeep Somavarapu 已提交
849 850 851
		this._baseConfiguration = baseConfiguration;
		this._defaults = this._baseConfiguration.defaults;
		this._user = this._baseConfiguration.user;
852 853 854 855 856
		this.merge();

		return !this.equals(current);
	}

857 858 859 860 861 862 863 864 865 866
	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 {
867
		const current = this.getValue(null, { resource });
868 869

		this.folders.set(resource, configuration);
870
		this.mergeFolder(resource);
871

872
		return compare && !objects.equals(current, this.getValue(null, { resource }));
873 874 875
	}

	deleteFolderConfiguration(folder: URI): boolean {
876
		if (this._workspace && this._workspace.folders.length > 0 && this._workspace.folders[0].uri.fsPath === folder.fsPath) {
877 878 879 880
			// Do not remove workspace configuration
			return false;
		}

S
Sandeep Somavarapu 已提交
881
		const changed = this.folders.get(folder).keys.length > 0;
882
		this.folders.delete(folder);
S
Sandeep Somavarapu 已提交
883 884
		this._foldersConsolidatedConfigurations.delete(folder);
		return changed;
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899
	}

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

900
		if (this._foldersConsolidatedConfigurations.size !== other._foldersConsolidatedConfigurations.size) {
901 902 903
			return false;
		}

904
		for (const resource of this._foldersConsolidatedConfigurations.keys()) {
905
			if (!objects.equals(this.getValue(null, { resource }), other.getValue(null, { resource }))) {
906 907 908 909 910 911
				return false;
			}
		}

		return true;
	}
I
isidor 已提交
912
}