diff --git a/src/vs/platform/configuration/common/configurationService.ts b/src/vs/platform/configuration/common/configurationService.ts deleted file mode 100644 index 1e05f587c2cfd89a4286dcdfe9b297cd0115fb0b..0000000000000000000000000000000000000000 --- a/src/vs/platform/configuration/common/configurationService.ts +++ /dev/null @@ -1,272 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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'); -import {TPromise} from 'vs/base/common/winjs.base'; -import objects = require('vs/base/common/objects'); -import errors = require('vs/base/common/errors'); -import uri from 'vs/base/common/uri'; -import {IConfigFile, consolidate, CONFIG_DEFAULT_NAME, newConfigFile, getDefaultValues} from 'vs/platform/configuration/common/model'; -import {RunOnceScheduler} from 'vs/base/common/async'; -import {IDisposable} from 'vs/base/common/lifecycle'; -import collections = require('vs/base/common/collections'); -import {IConfigurationService, IConfigurationServiceEvent} from './configuration'; -import {IEventService} from 'vs/platform/event/common/event'; -import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; -import {EventType, 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'; -import {JSONPath} from 'vs/base/common/json'; - -// ---- service abstract implementation - -export interface IStat { - resource: uri; - isDirectory: boolean; - children?: { resource: uri; }[]; -} - -export interface IContent { - resource: uri; - value: string; -} - -interface ILoadConfigResult { - config: any; - parseErrors?: string[]; -} - -export abstract class ConfigurationService implements IConfigurationService, IDisposable { - - public _serviceBrand: any; - - private static RELOAD_CONFIGURATION_DELAY = 50; - - private _onDidUpdateConfiguration = new Emitter(); - - protected workspaceSettingsRootFolder: string; - - private cachedConfig: ILoadConfigResult; - - private bulkFetchFromWorkspacePromise: TPromise; - private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise }; - private callOnDispose: IDisposable; - private reloadConfigurationScheduler: RunOnceScheduler; - - constructor( - protected contextService: IWorkspaceContextService, - protected eventService: IEventService, - workspaceSettingsRootFolder: string = '.vscode' - ) { - this.workspaceSettingsRootFolder = workspaceSettingsRootFolder; - this.workspaceFilePathToConfiguration = Object.create(null); - this.cachedConfig = { - config: {} - }; - - this.registerListeners(); - } - - get onDidUpdateConfiguration(): Event { - return this._onDidUpdateConfiguration.event; - } - - protected registerListeners(): void { - const unbind = this.eventService.addListener2(EventType.FILE_CHANGES, (events) => this.handleFileEvents(events)); - const subscription = Registry.as(Extensions.Configuration).onDidRegisterConfiguration(() => this.onDidRegisterConfiguration()); - - this.callOnDispose = { - dispose: () => { - unbind.dispose(); - subscription.dispose(); - } - }; - } - - public initialize(): TPromise { - return this.doLoadConfiguration().then(() => null); - } - - protected abstract resolveContents(resource: uri[]): TPromise; - - protected abstract resolveContent(resource: uri): TPromise; - - protected abstract resolveStat(resource: uri): TPromise; - - public abstract setUserConfiguration(key: string | JSONPath, value: any): Thenable; - - public getConfiguration(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; - } - - return result; - } - - public loadConfiguration(section?: string): TPromise { - - // 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 { - - // 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); - }); - } - - protected loadGlobalConfiguration(): { contents: any; parseErrors?: string[]; } { - return { - contents: getDefaultValues() - }; - } - - public hasWorkspaceConfiguration(): boolean { - return !!this.workspaceFilePathToConfiguration[`.vscode/${CONFIG_DEFAULT_NAME}.json`]; - } - - protected loadWorkspaceConfiguration(section?: string): TPromise<{ [relativeWorkspacePath: string]: IConfigFile }> { - - // 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); - }); - } - - 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 */); - - // emit this as update to listeners - this._onDidUpdateConfiguration.fire({ config: this.cachedConfig.config }); - } - - protected 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); - } - - 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 { - if (this.reloadConfigurationScheduler) { - this.reloadConfigurationScheduler.dispose(); - } - this.callOnDispose.dispose(); - this._onDidUpdateConfiguration.dispose(); - } -} \ No newline at end of file diff --git a/src/vs/test/utils/servicesTestUtils.ts b/src/vs/test/utils/servicesTestUtils.ts index 01a86a97eccc8bb4e5471d1c0a78188b82d442cd..4b357e390fe660f2c534fa734acb625b5dce3ef2 100644 --- a/src/vs/test/utils/servicesTestUtils.ts +++ b/src/vs/test/utils/servicesTestUtils.ts @@ -16,7 +16,6 @@ import {EditorInputEvent, IEditorGroup} from 'vs/workbench/common/editor'; import Event, {Emitter} from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; -import {IContent, IStat} from 'vs/platform/configuration/common/configurationService'; import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage'; import WorkbenchEditorService = require('vs/workbench/services/editor/common/editorService'); import QuickOpenService = require('vs/workbench/services/quickopen/common/quickOpenService'); @@ -544,29 +543,6 @@ export class TestConfigurationService extends EventEmitter.EventEmitter implemen private configuration = Object.create(null); - protected resolveContents(resources: URI[]): TPromise { - return TPromise.as(resources.map((resource) => { - return { - resource: resource, - value: '' - }; - })); - } - - protected resolveContent(resource: URI): TPromise { - return TPromise.as({ - resource: resource, - value: '' - }); - } - - protected resolveStat(resource: URI): TPromise { - return TPromise.as({ - resource: resource, - isDirectory: false - }); - } - public loadConfiguration(section?: string): TPromise { return TPromise.as(this.getConfiguration()); } diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index abd86dd19dde851362d8a452cbea4142c173ee19..f89cac49f9e2613e091a317d6fcaedee700c0151 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -11,9 +11,9 @@ import strings = require('vs/base/common/strings'); import platform = require('vs/base/common/platform'); import paths = require('vs/base/common/paths'); import extfs = require('vs/base/node/extfs'); -import {IConfigFile} from 'vs/platform/configuration/common/model'; import objects = require('vs/base/common/objects'); -import {IStat, IContent, ConfigurationService as CommonConfigurationService} from 'vs/platform/configuration/common/configurationService'; +import {RunOnceScheduler} from 'vs/base/common/async'; +import collections = require('vs/base/common/collections'); import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; import {LegacyWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService'; import {IEnvironmentService} from 'vs/platform/environment/common/environment'; @@ -24,29 +24,79 @@ 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'; +import errors = require('vs/base/common/errors'); +import {IConfigFile, consolidate, CONFIG_DEFAULT_NAME, newConfigFile, getDefaultValues} from 'vs/platform/configuration/common/model'; +import {IConfigurationService, IConfigurationServiceEvent} from 'vs/platform/configuration/common/configuration'; +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'; -export class ConfigurationService extends CommonConfigurationService { +interface IStat { + resource: uri; + isDirectory: boolean; + children?: { resource: uri; }[]; +} + +interface IContent { + resource: uri; + value: string; +} + +interface ILoadConfigResult { + config: any; + parseErrors?: string[]; +} + +export class ConfigurationService implements IConfigurationService, IDisposable { public _serviceBrand: any; - protected contextService: IWorkspaceContextService; + private static RELOAD_CONFIGURATION_DELAY = 50; + + private _onDidUpdateConfiguration = new Emitter(); + + private workspaceSettingsRootFolder: string; - private toDispose: IDisposable; + private cachedConfig: ILoadConfigResult; + + private bulkFetchFromWorkspacePromise: TPromise; + private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise }; + private callOnDispose: IDisposable; + private reloadConfigurationScheduler: RunOnceScheduler; constructor( - contextService: IWorkspaceContextService, - eventService: IEventService, - private environmentService: IEnvironmentService + @IWorkspaceContextService private contextService: IWorkspaceContextService, + @IEventService private eventService: IEventService, + @IEnvironmentService private environmentService: IEnvironmentService, + workspaceSettingsRootFolder: string = '.vscode' ) { - super(contextService, eventService); + this.workspaceSettingsRootFolder = workspaceSettingsRootFolder; + this.workspaceFilePathToConfiguration = Object.create(null); + this.cachedConfig = { + config: {} + }; this.registerListeners(); } - protected registerListeners(): void { - super.registerListeners(); + get onDidUpdateConfiguration(): Event { + return this._onDidUpdateConfiguration.event; + } + + private registerListeners(): void { + const unbind = this.eventService.addListener2(FileEventType.FILE_CHANGES, (events) => this.handleFileEvents(events)); + const subscription = Registry.as(Extensions.Configuration).onDidRegisterConfiguration(() => this.onDidRegisterConfiguration()); + + const unbind2 = this.eventService.addListener2(EventType.WORKBENCH_OPTIONS_CHANGED, (e) => this.onOptionsChanged(e)); - this.toDispose = this.eventService.addListener2(EventType.WORKBENCH_OPTIONS_CHANGED, (e) => this.onOptionsChanged(e)); + this.callOnDispose = { + dispose: () => { + unbind.dispose(); + subscription.dispose(); + unbind2.dispose(); + } + }; } private onOptionsChanged(e: OptionsChangeEvent): void { @@ -55,7 +105,11 @@ export class ConfigurationService extends CommonConfigurationService { } } - protected resolveContents(resources: uri[]): TPromise { + public initialize(): TPromise { + return this.doLoadConfiguration().then(() => null); + } + + private resolveContents(resources: uri[]): TPromise { const contents: IContent[] = []; return TPromise.join(resources.map((resource) => { @@ -65,11 +119,11 @@ export class ConfigurationService extends CommonConfigurationService { })).then(() => contents); } - protected resolveContent(resource: uri): TPromise { - return readFile(resource.fsPath).then(contents => ({resource, value: contents.toString()})); + private resolveContent(resource: uri): TPromise { + return readFile(resource.fsPath).then(contents => ({ resource, value: contents.toString() })); } - protected resolveStat(resource: uri): TPromise { + private resolveStat(resource: uri): TPromise { return new TPromise((c, e) => { extfs.readdir(resource.fsPath, (error, children) => { if (error) { @@ -100,23 +154,88 @@ export class ConfigurationService extends CommonConfigurationService { }); } - protected loadWorkspaceConfiguration(section?: string): TPromise<{ [relativeWorkspacePath: string]: IConfigFile }> { + public setUserConfiguration(key: any, value: any): Thenable { + const appSettingsPath = this.environmentService.appSettingsPath; - // Return early if we don't have a workspace - if (!this.contextService.getWorkspace()) { - return TPromise.as({}); + return readFile(appSettingsPath, 'utf8').then(content => { + const {tabSize, insertSpaces} = this.getConfiguration<{ tabSize: number; insertSpaces: boolean }>('editor'); + const path: JSONPath = typeof key === 'string' ? (key).split('.') : key; + const edits = setProperty(content, path, value, { insertSpaces, tabSize, eol: '\n' }); + + content = applyEdits(content, edits); + + return writeFile(appSettingsPath, content, 'utf8'); + }); + } + + public getConfiguration(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; } - return super.loadWorkspaceConfiguration(section); + return result; + } + + public loadConfiguration(section?: string): TPromise { + + // Reset caches to ensure we are hitting the disk + this.bulkFetchFromWorkspacePromise = null; + this.workspaceFilePathToConfiguration = Object.create(null); + + // Load configuration + return this.doLoadConfiguration(section); } - protected loadGlobalConfiguration(): { contents: any; parseErrors?: string[]; } { - const defaults = super.loadGlobalConfiguration(); + private doLoadConfiguration(section?: string): TPromise { + + // 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[]; } { const globalSettings = (this.contextService).getOptions().globalSettings; return { contents: objects.mixin( - objects.clone(defaults.contents), // target: default values (but don't modify!) + objects.clone(getDefaultValues()), // target: default values (but don't modify!) globalSettings, // source: global configured values true // overwrite ), @@ -124,23 +243,112 @@ export class ConfigurationService extends CommonConfigurationService { }; } - public setUserConfiguration(key: any, value: any) : Thenable { - const appSettingsPath = this.environmentService.appSettingsPath; + public hasWorkspaceConfiguration(): boolean { + return !!this.workspaceFilePathToConfiguration[`.vscode/${CONFIG_DEFAULT_NAME}.json`]; + } - return readFile(appSettingsPath, 'utf8').then(content => { - const {tabSize, insertSpaces} = this.getConfiguration<{ tabSize: number; insertSpaces: boolean }>('editor'); - const path: JSONPath = typeof key === 'string' ? ( key).split('.') : key; - const edits = setProperty(content, path, value, {insertSpaces, tabSize, eol: '\n'}); + private loadWorkspaceConfiguration(section?: string): TPromise<{ [relativeWorkspacePath: string]: IConfigFile }> { - content = applyEdits(content, edits); + // Return early if we don't have a workspace + if (!this.contextService.getWorkspace()) { + return TPromise.as({}); + } - return writeFile(appSettingsPath, content, 'utf8'); + // 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); }); } - public dispose(): void { - super.dispose(); + 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 */); - this.toDispose.dispose(); + // 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); + } + + 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 { + if (this.reloadConfigurationScheduler) { + this.reloadConfigurationScheduler.dispose(); + } + + this.callOnDispose.dispose(); + this._onDidUpdateConfiguration.dispose(); } } \ No newline at end of file