configurationService.ts 12.1 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';

8
import {TPromise} from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
9 10 11 12
import uri from 'vs/base/common/uri';
import strings = require('vs/base/common/strings');
import platform = require('vs/base/common/platform');
import paths = require('vs/base/common/paths');
13
import extfs = require('vs/base/node/extfs');
E
Erich Gamma 已提交
14
import objects = require('vs/base/common/objects');
15 16
import {RunOnceScheduler} from 'vs/base/common/async';
import collections = require('vs/base/common/collections');
17 18
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
import {LegacyWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService';
19
import {IEnvironmentService} from 'vs/platform/environment/common/environment';
E
Erich Gamma 已提交
20 21
import {OptionsChangeEvent, EventType} from 'vs/workbench/common/events';
import {IEventService} from 'vs/platform/event/common/event';
22
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
23 24 25 26
import {readFile, writeFile} from 'vs/base/node/pfs';
import {JSONPath} from 'vs/base/common/json';
import {applyEdits} from 'vs/base/common/jsonFormatter';
import {setProperty} from 'vs/base/common/jsonEdit';
27 28
import errors = require('vs/base/common/errors');
import {IConfigFile, consolidate, CONFIG_DEFAULT_NAME, newConfigFile, getDefaultValues} from 'vs/platform/configuration/common/model';
29 30
import {IConfigurationServiceEvent}  from 'vs/platform/configuration/common/configuration';
import {IWorkbenchConfigurationService} from 'vs/workbench/services/configuration/common/configuration';
31 32 33 34
import {EventType as FileEventType, FileChangeType, FileChangesEvent} from 'vs/platform/files/common/files';
import {IConfigurationRegistry, Extensions} from 'vs/platform/configuration/common/configurationRegistry';
import {Registry} from 'vs/platform/platform';
import Event, {Emitter} from 'vs/base/common/event';
E
Erich Gamma 已提交
35

36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
interface IStat {
	resource: uri;
	isDirectory: boolean;
	children?: { resource: uri; }[];
}

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

interface ILoadConfigResult {
	config: any;
	parseErrors?: string[];
}

52
export class ConfigurationService implements IWorkbenchConfigurationService, IDisposable {
E
Erich Gamma 已提交
53

54
	public _serviceBrand: any;
55

56 57
	private static RELOAD_CONFIGURATION_DELAY = 50;

58
	private _onDidUpdateConfiguration: Emitter<IConfigurationServiceEvent>;
59

60 61 62 63
	private cachedConfig: ILoadConfigResult;

	private bulkFetchFromWorkspacePromise: TPromise<any>;
	private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<IConfigFile> };
64
	private toDispose: IDisposable[];
65
	private reloadConfigurationScheduler: RunOnceScheduler;
E
Erich Gamma 已提交
66

B
Benjamin Pasero 已提交
67
	constructor(
68 69 70
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IEventService private eventService: IEventService,
		@IEnvironmentService private environmentService: IEnvironmentService,
71
		private workspaceSettingsRootFolder: string = '.vscode'
B
Benjamin Pasero 已提交
72
	) {
73 74 75 76 77
		this.toDispose = [];

		this._onDidUpdateConfiguration = new Emitter<IConfigurationServiceEvent>();
		this.toDispose.push(this._onDidUpdateConfiguration);

78 79 80 81
		this.workspaceFilePathToConfiguration = Object.create(null);
		this.cachedConfig = {
			config: {}
		};
82

E
Erich Gamma 已提交
83 84 85
		this.registerListeners();
	}

86 87 88 89 90
	get onDidUpdateConfiguration(): Event<IConfigurationServiceEvent> {
		return this._onDidUpdateConfiguration.event;
	}

	private registerListeners(): void {
91 92 93
		this.toDispose.push(this.eventService.addListener2(FileEventType.FILE_CHANGES, (events) => this.handleFileEvents(events)));
		this.toDispose.push(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidRegisterConfiguration(() => this.onDidRegisterConfiguration()));
		this.toDispose.push(this.eventService.addListener2(EventType.WORKBENCH_OPTIONS_CHANGED, (e) => this.onOptionsChanged(e)));
94 95 96 97 98 99 100 101
	}

	private onOptionsChanged(e: OptionsChangeEvent): void {
		if (e.key === 'globalSettings') {
			this.handleConfigurationChange();
		}
	}

102 103 104 105 106
	public initialize(): TPromise<void> {
		return this.doLoadConfiguration().then(() => null);
	}

	private resolveContents(resources: uri[]): TPromise<IContent[]> {
B
Benjamin Pasero 已提交
107
		const contents: IContent[] = [];
E
Erich Gamma 已提交
108

109
		return TPromise.join(resources.map((resource) => {
E
Erich Gamma 已提交
110 111 112 113 114 115
			return this.resolveContent(resource).then((content) => {
				contents.push(content);
			});
		})).then(() => contents);
	}

116 117
	private resolveContent(resource: uri): TPromise<IContent> {
		return readFile(resource.fsPath).then(contents => ({ resource, value: contents.toString() }));
E
Erich Gamma 已提交
118 119
	}

120
	private resolveStat(resource: uri): TPromise<IStat> {
E
Erich Gamma 已提交
121
		return new TPromise<IStat>((c, e) => {
122
			extfs.readdir(resource.fsPath, (error, children) => {
E
Erich Gamma 已提交
123
				if (error) {
124
					if ((<any>error).code === 'ENOTDIR') {
E
Erich Gamma 已提交
125
						c({
126
							resource,
E
Erich Gamma 已提交
127 128 129 130 131 132 133
							isDirectory: false
						});
					} else {
						e(error);
					}
				} else {
					c({
134
						resource,
E
Erich Gamma 已提交
135
						isDirectory: true,
136
						children: children.map((child) => {
E
Erich Gamma 已提交
137 138 139 140 141 142 143 144 145 146 147 148 149 150
							if (platform.isMacintosh) {
								child = strings.normalizeNFC(child); // Mac: uses NFD unicode form on disk, but we want NFC
							}

							return {
								resource: uri.file(paths.join(resource.fsPath, child))
							};
						})
					});
				}
			});
		});
	}

151 152
	public setUserConfiguration(key: any, value: any): Thenable<void> {
		const appSettingsPath = this.environmentService.appSettingsPath;
E
Erich Gamma 已提交
153

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
		return readFile(appSettingsPath, 'utf8').then(content => {
			const {tabSize, insertSpaces} = this.getConfiguration<{ tabSize: number; insertSpaces: boolean }>('editor');
			const path: JSONPath = typeof key === 'string' ? (<string>key).split('.') : <JSONPath>key;
			const edits = setProperty(content, path, value, { insertSpaces, tabSize, eol: '\n' });

			content = applyEdits(content, edits);

			return writeFile(appSettingsPath, content, 'utf8');
		});
	}

	public getConfiguration<T>(section?: string): T {
		let result = section ? this.cachedConfig.config[section] : this.cachedConfig.config;

		const parseErrors = this.cachedConfig.parseErrors;
		if (parseErrors && parseErrors.length > 0) {
			if (!result) {
				result = {};
			}
			result.$parseErrors = parseErrors;
E
Erich Gamma 已提交
174 175
		}

176 177 178 179 180 181 182 183 184 185 186
		return result;
	}

	public loadConfiguration(section?: string): TPromise<any> {

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

		// Load configuration
		return this.doLoadConfiguration(section);
E
Erich Gamma 已提交
187 188
	}

189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
	private doLoadConfiguration(section?: string): TPromise<any> {

		// Load globals
		const globals = this.loadGlobalConfiguration();

		// Load workspace locals
		return this.loadWorkspaceConfiguration().then((values) => {

			// Consolidate
			const consolidated = consolidate(values);

			// Override with workspace locals
			const merged = objects.mixin(
				objects.clone(globals.contents), 	// target: global/default values (but dont modify!)
				consolidated.contents,				// source: workspace configured values
				true								// overwrite
			);

			let parseErrors = [];
			if (consolidated.parseErrors) {
				parseErrors = consolidated.parseErrors;
			}

			if (globals.parseErrors) {
				parseErrors.push.apply(parseErrors, globals.parseErrors);
			}

			return {
				config: merged,
				parseErrors
			};
		}).then((res: ILoadConfigResult) => {
			this.cachedConfig = res;

			return this.getConfiguration(section);
		});
	}

	private loadGlobalConfiguration(): { contents: any; parseErrors?: string[]; } {
228
		const globalSettings = (<LegacyWorkspaceContextService>this.contextService).getOptions().globalSettings;
229 230 231

		return {
			contents: objects.mixin(
232
				objects.clone(getDefaultValues()),	// target: default values (but don't modify!)
233
				globalSettings,						// source: global configured values
234 235
				true								// overwrite
			),
236
			parseErrors: []
237
		};
E
Erich Gamma 已提交
238 239
	}

240 241 242
	public hasWorkspaceConfiguration(): boolean {
		return !!this.workspaceFilePathToConfiguration[`.vscode/${CONFIG_DEFAULT_NAME}.json`];
	}
243

244
	private loadWorkspaceConfiguration(section?: string): TPromise<{ [relativeWorkspacePath: string]: IConfigFile }> {
B
Benjamin Pasero 已提交
245

246 247 248 249
		// Return early if we don't have a workspace
		if (!this.contextService.getWorkspace()) {
			return TPromise.as({});
		}
B
Benjamin Pasero 已提交
250

251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
		// once: when invoked for the first time we fetch *all* json
		// files using the bulk stats and content routes
		if (!this.bulkFetchFromWorkspacePromise) {
			this.bulkFetchFromWorkspacePromise = this.resolveStat(this.contextService.toResource(this.workspaceSettingsRootFolder)).then((stat) => {
				if (!stat.isDirectory) {
					return TPromise.as([]);
				}

				return this.resolveContents(stat.children.filter((stat) => paths.extname(stat.resource.fsPath) === '.json').map(stat => stat.resource));
			}, (err) => {
				if (err) {
					return []; // never fail this call
				}
			}).then((contents: IContent[]) => {
				contents.forEach(content => this.workspaceFilePathToConfiguration[this.contextService.toWorkspaceRelativePath(content.resource)] = TPromise.as(newConfigFile(content.value)));
			}, 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(() => {
			return TPromise.join(this.workspaceFilePathToConfiguration);
274 275 276
		});
	}

277 278 279 280 281 282 283
	private onDidRegisterConfiguration(): void {

		// a new configuration was registered (e.g. from an extension) and this means we do have a new set of
		// configuration defaults. since we already loaded the merged set of configuration (defaults < global < workspace),
		// we want to update the defaults with the new values. So we take our cached config and mix it into the new
		// defaults that we got, overwriting any value present.
		this.cachedConfig.config = objects.mixin(objects.clone(getDefaultValues()), this.cachedConfig.config, true /* overwrite */);
284

285 286 287 288 289 290 291 292 293
		// emit this as update to listeners
		this._onDidUpdateConfiguration.fire({ config: this.cachedConfig.config });
	}

	private handleConfigurationChange(): void {
		if (!this.reloadConfigurationScheduler) {
			this.reloadConfigurationScheduler = new RunOnceScheduler(() => {
				this.doLoadConfiguration().then((config) => this._onDidUpdateConfiguration.fire({ config: config })).done(null, errors.onUnexpectedError);
			}, ConfigurationService.RELOAD_CONFIGURATION_DELAY);
294 295

			this.toDispose.push(this.reloadConfigurationScheduler);
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 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
		}

		if (!this.reloadConfigurationScheduler.isScheduled()) {
			this.reloadConfigurationScheduler.schedule();
		}
	}

	private handleFileEvents(event: FileChangesEvent): void {
		const events = event.changes;
		let affectedByChanges = false;

		for (let i = 0, len = events.length; i < len; i++) {
			const workspacePath = this.contextService.toWorkspaceRelativePath(events[i].resource);
			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;
			}

			// outside my folder or not a *.json file
			if (paths.extname(workspacePath) !== '.json' || !paths.isEqualOrParent(workspacePath, this.workspaceSettingsRootFolder)) {
				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] = this.resolveContent(events[i].resource).then(content => newConfigFile(content.value), errors.onUnexpectedError);
					affectedByChanges = true;
			}
		}

		if (affectedByChanges) {
			this.handleConfigurationChange();
		}
	}

	public dispose(): void {
343
		this.toDispose = dispose(this.toDispose);
E
Erich Gamma 已提交
344 345
	}
}