configurationService.ts 13.9 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

J
Johannes Rieken 已提交
8
import { TPromise } from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
9 10
import uri from 'vs/base/common/uri';
import paths = require('vs/base/common/paths');
11
import extfs = require('vs/base/node/extfs');
E
Erich Gamma 已提交
12
import objects = require('vs/base/common/objects');
J
Johannes Rieken 已提交
13
import { RunOnceScheduler } from 'vs/base/common/async';
14
import collections = require('vs/base/common/collections');
J
Johannes Rieken 已提交
15 16
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
S
Sandeep Somavarapu 已提交
17
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
J
Johannes Rieken 已提交
18
import { readFile } from 'vs/base/node/pfs';
19
import errors = require('vs/base/common/errors');
20
import { ScopedConfigModel, WorkspaceConfigModel, WorkspaceSettingsConfigModel } from 'vs/workbench/services/configuration/common/configurationModels';
S
Sandeep Somavarapu 已提交
21
import { IConfigurationServiceEvent, ConfigurationSource, getConfigurationValue, IConfigModel, IConfigurationOptions } from 'vs/platform/configuration/common/configuration';
22
import { ConfigModel } from 'vs/platform/configuration/common/model';
J
Johannes Rieken 已提交
23
import { ConfigurationService as BaseConfigurationService } from 'vs/platform/configuration/node/configurationService';
24
import { IWorkspaceConfigurationValues, IWorkspaceConfigurationService, IWorkspaceConfigurationValue, WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME, WORKSPACE_STANDALONE_CONFIGURATIONS, WORKSPACE_CONFIG_DEFAULT_PATH } from 'vs/workbench/services/configuration/common/configuration';
B
wip  
Benjamin Pasero 已提交
25
import { FileChangeType, FileChangesEvent, isEqual } from 'vs/platform/files/common/files';
J
Johannes Rieken 已提交
26
import Event, { Emitter } from 'vs/base/common/event';
K
kieferrm 已提交
27

E
Erich Gamma 已提交
28

29 30
interface IStat {
	resource: uri;
B
Benjamin Pasero 已提交
31
	isDirectory?: boolean;
32 33 34 35 36 37 38 39
	children?: { resource: uri; }[];
}

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

40
interface IWorkspaceConfiguration<T> {
41
	workspace: T;
42
	consolidated: any;
43 44
}

B
Benjamin Pasero 已提交
45 46 47
/**
 * Wraps around the basic configuration service and adds knowledge about workspace settings.
 */
48
export class WorkspaceConfigurationService extends Disposable implements IWorkspaceConfigurationService, IDisposable {
E
Erich Gamma 已提交
49

50
	public _serviceBrand: any;
51

52 53
	private static RELOAD_CONFIGURATION_DELAY = 50;

54
	private _onDidUpdateConfiguration: Emitter<IConfigurationServiceEvent>;
55
	private baseConfigurationService: BaseConfigurationService<any>;
56

57 58
	private cachedConfig: ConfigModel<any>;
	private cachedWorkspaceConfig: WorkspaceConfigModel<any>;
59 60

	private bulkFetchFromWorkspacePromise: TPromise<any>;
61
	private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<IConfigModel<any>> };
62
	private reloadConfigurationScheduler: RunOnceScheduler;
E
Erich Gamma 已提交
63

B
Benjamin Pasero 已提交
64
	constructor(
65
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
66
		@IEnvironmentService environmentService: IEnvironmentService,
B
Benjamin Pasero 已提交
67
		private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME
B
Benjamin Pasero 已提交
68
	) {
69
		super();
70 71
		this.workspaceFilePathToConfiguration = Object.create(null);

72
		this.cachedConfig = new ConfigModel<any>(null);
73
		this.cachedWorkspaceConfig = new WorkspaceConfigModel(new WorkspaceSettingsConfigModel(null), []);
74

75
		this._onDidUpdateConfiguration = this._register(new Emitter<IConfigurationServiceEvent>());
76

77
		this.baseConfigurationService = this._register(new BaseConfigurationService(environmentService));
78

79
		this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.doLoadConfiguration()
80 81 82 83 84
			.then(config => this._onDidUpdateConfiguration.fire({
				config: config.consolidated,
				source: ConfigurationSource.Workspace,
				sourceConfig: config.workspace
			}))
85
			.done(null, errors.onUnexpectedError), WorkspaceConfigurationService.RELOAD_CONFIGURATION_DELAY));
86

87
		this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e)));
E
Erich Gamma 已提交
88 89
	}

90 91 92 93
	get onDidUpdateConfiguration(): Event<IConfigurationServiceEvent> {
		return this._onDidUpdateConfiguration.event;
	}

K
kieferrm 已提交
94
	private onBaseConfigurationChanged(event: IConfigurationServiceEvent): void {
95 96 97
		if (event.source === ConfigurationSource.Default) {
			this.cachedWorkspaceConfig.update();
		}
98 99

		// update cached config when base config changes
S
Sandeep Somavarapu 已提交
100 101
		const configModel = <ConfigModel<any>>this.baseConfigurationService.getCache().consolidated		// global/default values (do NOT modify)
			.merge(this.cachedWorkspaceConfig);		// workspace configured values
102 103

		// emit this as update to listeners if changed
K
kieferrm 已提交
104 105
		if (!objects.equals(this.cachedConfig.contents, configModel.contents)) {
			this.cachedConfig = configModel;
106
			this._onDidUpdateConfiguration.fire({
107
				config: this.cachedConfig.contents,
K
kieferrm 已提交
108 109
				source: event.source,
				sourceConfig: event.sourceConfig
110
			});
111 112 113
		}
	}

114 115 116 117
	public initialize(): TPromise<void> {
		return this.doLoadConfiguration().then(() => null);
	}

118 119 120 121
	public getConfiguration<C>(section?: string): C
	public getConfiguration<C>(options?: IConfigurationOptions): C
	public getConfiguration<C>(arg?: any): C {
		const options = this.toOptions(arg);
122
		const configModel = options.overrideIdentifier ? this.cachedConfig.configWithOverrides<C>(options.overrideIdentifier) : this.cachedConfig;
S
Sandeep Somavarapu 已提交
123
		return options.section ? configModel.getContentsFor<C>(options.section) : configModel.contents;
124 125
	}

126 127
	public lookup<C>(key: string, overrideIdentifier?: string): IWorkspaceConfigurationValue<C> {
		const configurationValue = this.baseConfigurationService.lookup<C>(key, overrideIdentifier);
B
Benjamin Pasero 已提交
128 129 130
		return {
			default: configurationValue.default,
			user: configurationValue.user,
131 132
			workspace: objects.clone(getConfigurationValue<C>(overrideIdentifier ? this.cachedWorkspaceConfig.configWithOverrides(overrideIdentifier).contents : this.cachedWorkspaceConfig.contents, key)),
			value: objects.clone(getConfigurationValue<C>(overrideIdentifier ? this.cachedConfig.configWithOverrides(overrideIdentifier).contents : this.cachedConfig.contents, key))
B
Benjamin Pasero 已提交
133 134 135
		};
	}

B
Benjamin Pasero 已提交
136 137 138 139 140 141
	public keys() {
		const keys = this.baseConfigurationService.keys();

		return {
			default: keys.default,
			user: keys.user,
142
			workspace: this.cachedWorkspaceConfig.keys
B
Benjamin Pasero 已提交
143 144 145
		};
	}

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
	public values(): IWorkspaceConfigurationValues {
		const result: IWorkspaceConfigurationValues = Object.create(null);
		const keyset = this.keys();
		const keys = [...keyset.workspace, ...keyset.user, ...keyset.default].sort();

		let lastKey: string;
		for (const key of keys) {
			if (key !== lastKey) {
				lastKey = key;
				result[key] = this.lookup(key);
			}
		}

		return result;
	}

162
	public reloadConfiguration(section?: string): TPromise<any> {
163 164 165 166 167 168

		// Reset caches to ensure we are hitting the disk
		this.bulkFetchFromWorkspacePromise = null;
		this.workspaceFilePathToConfiguration = Object.create(null);

		// Load configuration
169
		return this.baseConfigurationService.reloadConfiguration().then(() => {
S
Sandeep Somavarapu 已提交
170
			const current = this.cachedConfig;
171
			return this.doLoadConfiguration().then(configuration => {
S
Sandeep Somavarapu 已提交
172 173 174 175 176 177 178 179
				// emit this as update to listeners if changed
				if (!objects.equals(current, this.cachedConfig)) {
					this._onDidUpdateConfiguration.fire({
						config: configuration.consolidated,
						source: ConfigurationSource.Workspace,
						sourceConfig: configuration.workspace
					});
				}
180 181 182
				return section ? configuration.consolidated[section] : configuration.consolidated;
			});
		});
E
Erich Gamma 已提交
183 184
	}

185 186 187 188 189 190 191 192 193 194 195
	private toOptions(arg: any): IConfigurationOptions {
		if (typeof arg === 'string') {
			return { section: arg };
		}
		if (typeof arg === 'object') {
			return arg;
		}
		return {};
	}

	private doLoadConfiguration<T>(): TPromise<IWorkspaceConfiguration<T>> {
196 197

		// Load workspace locals
198
		return this.loadWorkspaceConfigFiles().then(workspaceConfigFiles => {
199

200
			// Consolidate (support *.json files in the workspace settings folder)
201 202 203
			const workspaceSettingsConfig = <WorkspaceSettingsConfigModel<T>>workspaceConfigFiles[WORKSPACE_CONFIG_DEFAULT_PATH] || new WorkspaceSettingsConfigModel<T>(null);
			const otherConfigModels = Object.keys(workspaceConfigFiles).filter(key => key !== WORKSPACE_CONFIG_DEFAULT_PATH).map(key => <ScopedConfigModel<T>>workspaceConfigFiles[key]);
			this.cachedWorkspaceConfig = new WorkspaceConfigModel<T>(workspaceSettingsConfig, otherConfigModels);
B
Benjamin Pasero 已提交
204

205
			// Override base (global < user) with workspace locals (global < user < workspace)
S
Sandeep Somavarapu 已提交
206 207
			this.cachedConfig = <ConfigModel<any>>this.baseConfigurationService.getCache().consolidated		// global/default values (do NOT modify)
				.merge(this.cachedWorkspaceConfig);		// workspace configured values
208

209
			return {
210 211
				consolidated: this.cachedConfig.contents,
				workspace: this.cachedWorkspaceConfig.contents
212
			};
213 214 215
		});
	}

216
	private loadWorkspaceConfigFiles<T>(): TPromise<{ [relativeWorkspacePath: string]: IConfigModel<T> }> {
B
Benjamin Pasero 已提交
217

218
		// Return early if we don't have a workspace
B
Benjamin Pasero 已提交
219
		if (!this.contextService.hasWorkspace()) {
B
Benjamin Pasero 已提交
220
			return TPromise.as(Object.create(null));
221
		}
B
Benjamin Pasero 已提交
222

223
		// once: when invoked for the first time we fetch json files that contribute settings
224
		if (!this.bulkFetchFromWorkspacePromise) {
B
Benjamin Pasero 已提交
225
			this.bulkFetchFromWorkspacePromise = resolveStat(this.contextService.toResource(this.workspaceSettingsRootFolder)).then(stat => {
226 227 228 229
				if (!stat.isDirectory) {
					return TPromise.as([]);
				}

230 231 232 233 234 235 236 237
				return resolveContents(stat.children.filter(stat => {
					const isJson = paths.extname(stat.resource.fsPath) === '.json';
					if (!isJson) {
						return false; // only JSON files
					}

					return this.isWorkspaceConfigurationFile(this.contextService.toWorkspaceRelativePath(stat.resource)); // only workspace config files
				}).map(stat => stat.resource));
238 239 240 241
			}, err => [] /* never fail this call */)
				.then((contents: IContent[]) => {
					contents.forEach(content => this.workspaceFilePathToConfiguration[this.contextService.toWorkspaceRelativePath(content.resource)] = TPromise.as(this.createConfigModel(content)));
				}, errors.onUnexpectedError);
242 243
		}

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

249
	public handleWorkspaceFileEvents(event: FileChangesEvent): void {
250 251 252
		const events = event.changes;
		let affectedByChanges = false;

253
		// Find changes that affect workspace configuration files
254
		for (let i = 0, len = events.length; i < len; i++) {
255 256
			const resource = events[i].resource;
			const isJson = paths.extname(resource.fsPath) === '.json';
B
wip  
Benjamin Pasero 已提交
257
			const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && isEqual(paths.basename(resource.fsPath), this.workspaceSettingsRootFolder));
258 259
			if (!isJson && !isDeletedSettingsFolder) {
				continue; // only JSON files or the actual settings folder
260 261 262
			}

			const workspacePath = this.contextService.toWorkspaceRelativePath(resource);
263 264 265 266 267 268 269 270 271 272
			if (!workspacePath) {
				continue; // event is not inside workspace
			}

			// Handle case where ".vscode" got deleted
			if (workspacePath === this.workspaceSettingsRootFolder && events[i].type === FileChangeType.DELETED) {
				this.workspaceFilePathToConfiguration = Object.create(null);
				affectedByChanges = true;
			}

273 274
			// only valid workspace config files
			if (!this.isWorkspaceConfigurationFile(workspacePath)) {
275 276 277 278 279 280 281 282 283 284 285
				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:
286
					this.workspaceFilePathToConfiguration[workspacePath] = resolveContent(resource).then(content => this.createConfigModel(content), errors.onUnexpectedError);
287 288 289 290
					affectedByChanges = true;
			}
		}

291 292 293
		// trigger reload of the configuration if we are affected by changes
		if (affectedByChanges && !this.reloadConfigurationScheduler.isScheduled()) {
			this.reloadConfigurationScheduler.schedule();
294 295
		}
	}
296

297 298 299
	private createConfigModel<T>(content: IContent): IConfigModel<T> {
		const path = this.contextService.toWorkspaceRelativePath(content.resource);
		if (path === WORKSPACE_CONFIG_DEFAULT_PATH) {
300
			return new WorkspaceSettingsConfigModel<T>(content.value, content.resource.toString());
301 302 303 304 305 306
		} else {
			const matches = /\/([^\.]*)*\.json/.exec(path);
			if (matches && matches[1]) {
				return new ScopedConfigModel<T>(content.value, content.resource.toString(), matches[1]);
			}
		}
307
		return new ConfigModel<T>(null);
308 309
	}

310 311 312
	private isWorkspaceConfigurationFile(workspaceRelativePath: string): boolean {
		return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS.launch, WORKSPACE_STANDALONE_CONFIGURATIONS.tasks].some(p => p === workspaceRelativePath);
	}
K
kieferrm 已提交
313

314 315
	public getUnsupportedWorkspaceKeys(): string[] {
		return this.cachedWorkspaceConfig.workspaceSettingsConfig.unsupportedKeys;
K
kieferrm 已提交
316
	}
B
Benjamin Pasero 已提交
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
}

// 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)) }; })
				});
			}
		});
	});
E
Erich Gamma 已提交
353
}