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

import paths = require('vs/base/common/paths');
8
import {TPromise} from 'vs/base/common/winjs.base';
E
Erich Gamma 已提交
9 10 11 12
import objects = require('vs/base/common/objects');
import errors = require('vs/base/common/errors');
import uri from 'vs/base/common/uri';
import model = require('./model');
13
import {RunOnceScheduler} from 'vs/base/common/async';
14
import {IDisposable, cAll} from 'vs/base/common/lifecycle';
E
Erich Gamma 已提交
15
import collections = require('vs/base/common/collections');
16
import {IConfigurationService, IConfigurationServiceEvent}  from './configuration';
E
Erich Gamma 已提交
17 18
import {IEventService} from 'vs/platform/event/common/event';
import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace';
19
import {EventType, FileChangeType, FileChangesEvent} from 'vs/platform/files/common/files';
E
Erich Gamma 已提交
20 21
import {IConfigurationRegistry, Extensions} from './configurationRegistry';
import {Registry} from 'vs/platform/platform';
22
import Event, {Emitter} from 'vs/base/common/event';
E
Erich Gamma 已提交
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38


// ---- service abstract implementation

export interface IStat {
	resource: uri;
	isDirectory: boolean;
	children?: { resource: uri; }[];
}

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

interface ILoadConfigResult {
39 40
	config: any;
	parseErrors?: string[];
E
Erich Gamma 已提交
41 42
}

43
export abstract class ConfigurationService implements IConfigurationService, IDisposable {
44

E
Erich Gamma 已提交
45 46
	public serviceId = IConfigurationService;

47 48
	private static RELOAD_CONFIGURATION_DELAY = 50;

49
	private _onDidUpdateConfiguration = new Emitter<IConfigurationServiceEvent>();
50

E
Erich Gamma 已提交
51 52 53 54
	protected contextService: IWorkspaceContextService;
	protected eventService: IEventService;
	protected workspaceSettingsRootFolder: string;

55 56 57 58
	private cachedConfig: ILoadConfigResult;

	private bulkFetchFromWorkspacePromise: TPromise<any>;
	private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<model.IConfigFile> };
E
Erich Gamma 已提交
59
	private callOnDispose: Function;
60
	private reloadConfigurationScheduler: RunOnceScheduler;
E
Erich Gamma 已提交
61 62 63 64 65 66 67 68

	constructor(contextService: IWorkspaceContextService, eventService: IEventService, workspaceSettingsRootFolder: string = '.vscode') {

		this.contextService = contextService;
		this.eventService = eventService;

		this.workspaceSettingsRootFolder = workspaceSettingsRootFolder;
		this.workspaceFilePathToConfiguration = Object.create(null);
69
		this.cachedConfig = {
70
			config: {}
71
		};
E
Erich Gamma 已提交
72

73 74 75
		this.registerListeners();
	}

76 77 78 79
	get onDidUpdateConfiguration(): Event<IConfigurationServiceEvent> {
		return this._onDidUpdateConfiguration.event;
	}

80 81
	protected registerListeners(): void {
		let unbind = this.eventService.addListener(EventType.FILE_CHANGES, (events) => this.handleFileEvents(events));
82
		let subscription = Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidRegisterConfiguration(() => this.onDidRegisterConfiguration());
E
Erich Gamma 已提交
83 84 85
		this.callOnDispose = () => {
			unbind();
			subscription.dispose();
B
Benjamin Pasero 已提交
86
		};
E
Erich Gamma 已提交
87 88
	}

89
	public initialize(): TPromise<void> {
90
		return this.doLoadConfiguration().then(() => null);
91
	}
E
Erich Gamma 已提交
92

93
	protected abstract resolveContents(resource: uri[]): TPromise<IContent[]>;
E
Erich Gamma 已提交
94

95
	protected abstract resolveContent(resource: uri): TPromise<IContent>;
96

97
	protected abstract resolveStat(resource: uri): TPromise<IStat>;
E
Erich Gamma 已提交
98

99
	public getConfiguration<T>(section?: string): T {
100
		let result = section ? this.cachedConfig.config[section] : this.cachedConfig.config;
E
Erich Gamma 已提交
101

102 103
		let parseErrors = this.cachedConfig.parseErrors;
		if (parseErrors && parseErrors.length > 0) {
104 105
			if (!result) {
				result = {};
E
Erich Gamma 已提交
106
			}
107 108
			result.$parseErrors = parseErrors;
		}
E
Erich Gamma 已提交
109

110 111
		return result;
	}
E
Erich Gamma 已提交
112

113 114 115 116 117 118 119 120 121 122 123
	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);
	}

	private doLoadConfiguration(section?: string): TPromise<any> {
E
Erich Gamma 已提交
124 125

		// Load globals
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
		const globals = this.loadGlobalConfiguration();

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

			// Consolidate
			let consolidated = model.consolidate(values);

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

141 142 143 144 145 146 147 148
			let parseErrors = [];
			if (consolidated.parseErrors) {
				parseErrors = consolidated.parseErrors;
			}
			if (globals.parseErrors) {
				parseErrors.push.apply(parseErrors, globals.parseErrors);
			}

149
			return {
150 151
				config: merged,
				parseErrors
152
			};
153 154 155 156
		}).then((res: ILoadConfigResult) => {
			this.cachedConfig = res;

			return this.getConfiguration(section);
E
Erich Gamma 已提交
157 158 159
		});
	}

160 161
	protected loadGlobalConfiguration(): { contents: any; parseErrors?: string[]; } {
		return {
E
Erich Gamma 已提交
162
			contents: model.getDefaultValues()
163
		};
E
Erich Gamma 已提交
164 165 166 167 168 169
	}

	public hasWorkspaceConfiguration(): boolean {
		return !!this.workspaceFilePathToConfiguration['.vscode/' + model.CONFIG_DEFAULT_NAME + '.json'];
	}

170
	protected loadWorkspaceConfiguration(section?: string): TPromise<{ [relativeWorkspacePath: string]: model.IConfigFile }> {
E
Erich Gamma 已提交
171 172 173 174 175 176

		// 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) {
177
					return TPromise.as([]);
E
Erich Gamma 已提交
178 179 180 181 182 183 184 185
				}

				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[]) => {
186
				contents.forEach(content => this.workspaceFilePathToConfiguration[this.contextService.toWorkspaceRelativePath(content.resource)] = TPromise.as(model.newConfigFile(content.value)));
E
Erich Gamma 已提交
187 188 189 190 191 192 193
			}, 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(() => {
194
			return TPromise.join(this.workspaceFilePathToConfiguration);
E
Erich Gamma 已提交
195 196 197
		});
	}

198 199 200 201 202 203 204 205 206
	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(model.getDefaultValues()), this.cachedConfig.config, true /* overwrite */);

		// emit this as update to listeners
207
		this._onDidUpdateConfiguration.fire({ config: this.cachedConfig.config });
208 209
	}

210
	protected handleConfigurationChange(): void {
211 212
		if (!this.reloadConfigurationScheduler) {
			this.reloadConfigurationScheduler = new RunOnceScheduler(() => {
213
				this.doLoadConfiguration().then((config) => this._onDidUpdateConfiguration.fire({ config: config })).done(null, errors.onUnexpectedError);
214 215 216 217 218 219
			}, ConfigurationService.RELOAD_CONFIGURATION_DELAY);
		}

		if (!this.reloadConfigurationScheduler.isScheduled()) {
			this.reloadConfigurationScheduler.schedule();
		}
E
Erich Gamma 已提交
220 221
	}

222
	private handleFileEvents(event: FileChangesEvent): void {
B
Benjamin Pasero 已提交
223 224 225 226
		let events = event.changes;
		let affectedByChanges = false;
		for (let i = 0, len = events.length; i < len; i++) {
			let workspacePath = this.contextService.toWorkspaceRelativePath(events[i].resource);
E
Erich Gamma 已提交
227 228 229 230 231
			if (!workspacePath) {
				continue; // event is not inside workspace
			}

			// Handle case where ".vscode" got deleted
232
			if (workspacePath === this.workspaceSettingsRootFolder && events[i].type === FileChangeType.DELETED) {
E
Erich Gamma 已提交
233 234 235 236 237 238 239 240 241 242 243 244
				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) {
245
				case FileChangeType.DELETED:
E
Erich Gamma 已提交
246 247
					affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, workspacePath);
					break;
248 249
				case FileChangeType.UPDATED:
				case FileChangeType.ADDED:
E
Erich Gamma 已提交
250 251 252 253 254 255
					this.workspaceFilePathToConfiguration[workspacePath] = this.resolveContent(events[i].resource).then(content => model.newConfigFile(content.value), errors.onUnexpectedError);
					affectedByChanges = true;
			}
		}

		if (affectedByChanges) {
256
			this.handleConfigurationChange();
E
Erich Gamma 已提交
257 258
		}
	}
259 260 261 262 263 264

	public dispose(): void {
		if (this.reloadConfigurationScheduler) {
			this.reloadConfigurationScheduler.dispose();
		}
		this.callOnDispose = cAll(this.callOnDispose);
265
		this._onDidUpdateConfiguration.dispose();
266
	}
E
Erich Gamma 已提交
267
}