From 8ba62c61c0c8b20ccc5fdc2a7016eef59ca0ce76 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 17 Jan 2017 11:26:10 +0100 Subject: [PATCH] #1587 Update config model and services to handle language based settings - Contirbute parsing of config as an option to config watcher - Introduce Config model to hold configurations , overrides and necessary data for services - Refactor config services to use config models - Add an api in config services to get configuration for a language --- src/vs/base/node/config.ts | 3 +- .../browser/standalone/simpleServices.ts | 2 +- .../configuration/common/configuration.ts | 24 +++ src/vs/platform/configuration/common/model.ts | 183 ++++++++++++++++++ .../node/configurationService.ts | 107 +++++----- .../configuration/test/common/model.test.ts | 26 +++ .../services/configuration/common/model.ts | 111 ++++------- .../node/configurationService.ts | 145 +++++++------- .../configuration/test/common/model.test.ts | 101 ++++------ 9 files changed, 435 insertions(+), 267 deletions(-) create mode 100644 src/vs/platform/configuration/test/common/model.test.ts diff --git a/src/vs/base/node/config.ts b/src/vs/base/node/config.ts index e2a65bbf162..603ab53fcae 100644 --- a/src/vs/base/node/config.ts +++ b/src/vs/base/node/config.ts @@ -28,6 +28,7 @@ export interface IConfigWatcher { export interface IConfigOptions { defaultConfig?: T; changeBufferDelay?: number; + parse?: (content: string, errors: any[]) => T; } /** @@ -104,7 +105,7 @@ export class ConfigWatcher implements IConfigWatcher, IDisposable { let res: T; try { this.parseErrors = []; - res = json.parse(raw, this.parseErrors); + res = this.options.parse ? this.options.parse(raw, this.parseErrors) : json.parse(raw, this.parseErrors); } catch (error) { // Ignore parsing errors } diff --git a/src/vs/editor/browser/standalone/simpleServices.ts b/src/vs/editor/browser/standalone/simpleServices.ts index 2e43cc59605..bc5801ffaeb 100644 --- a/src/vs/editor/browser/standalone/simpleServices.ts +++ b/src/vs/editor/browser/standalone/simpleServices.ts @@ -375,7 +375,7 @@ export class SimpleConfigurationService implements IConfigurationService { this._config = getDefaultConfiguration(); } - public getConfiguration(section?: string): T { + public getConfiguration(section?: any): T { return this._config; } diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index fe9e02d2647..f892f756020 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -4,11 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { TPromise } from 'vs/base/common/winjs.base'; +import URI from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import Event from 'vs/base/common/event'; export const IConfigurationService = createDecorator('configurationService'); +export interface IConfigurationOptions { + language?: string; + section?: string; +} + export interface IConfigurationService { _serviceBrand: any; @@ -17,6 +23,7 @@ export interface IConfigurationService { * This will be an object keyed off the section name. */ getConfiguration(section?: string): T; + getConfiguration(options?: IConfigurationOptions): T; /** * Resolves a configuration key to its values in the different scopes @@ -94,3 +101,20 @@ export function getConfigurationValue(config: any, settingPath: string, defau return typeof result === 'undefined' ? defaultValue : result; } + +export interface IConfigModel { + contents: T; + overrides: IOverrides[]; + keys: string[]; + raw: any; + errors: any[]; + + merge(other: IConfigModel, overwrite?: boolean): IConfigModel; + config(section: string): IConfigModel; + languageConfig(language: string, section?: string): IConfigModel; +} + +export interface IOverrides { + contents: T; + languages: string[]; +} \ No newline at end of file diff --git a/src/vs/platform/configuration/common/model.ts b/src/vs/platform/configuration/common/model.ts index a3c6df777b1..51703bee94c 100644 --- a/src/vs/platform/configuration/common/model.ts +++ b/src/vs/platform/configuration/common/model.ts @@ -5,7 +5,11 @@ 'use strict'; import { Registry } from 'vs/platform/platform'; +import * as types from 'vs/base/common/types'; +import * as json from 'vs/base/common/json'; +import * as objects from 'vs/base/common/objects'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigModel, IOverrides } from 'vs/platform/configuration/common/configuration'; export function getDefaultValues(): any { const valueTreeRoot: any = Object.create(null); @@ -61,4 +65,183 @@ export function getConfigurationKeys(): string[] { const properties = Registry.as(Extensions.Configuration).getConfigurationProperties(); return Object.keys(properties); +} + +export function merge(base: any, add: any, overwrite: boolean): void { + Object.keys(add).forEach(key => { + if (key in base) { + if (types.isObject(base[key]) && types.isObject(add[key])) { + merge(base[key], add[key], overwrite); + } else if (overwrite) { + base[key] = add[key]; + } + } else { + base[key] = add[key]; + } + }); +} + +interface _IOverrides extends IOverrides { + raw: any; +} + +export class ConfigModel implements IConfigModel { + + protected _contents: T; + protected _overrides: IOverrides[] = null; + + private _raw: any = {}; + private _parseErrors: any[] = []; + + constructor(content: string, private name: string = '') { + if (content) { + this.update(content); + } + } + + public get contents(): T { + return this._contents || {}; + } + + public get overrides(): any { + return this._overrides; + } + + public get keys(): string[] { + return Object.keys(this._raw) + } + + public get raw(): T { + return this._raw; + } + + public get errors(): any[] { + return this._parseErrors; + } + + public merge(other: IConfigModel, overwrite: boolean = true): ConfigModel { + const mergedModel = new ConfigModel(null); + mergedModel._contents = objects.clone(this.contents); + merge(mergedModel.contents, other.contents, overwrite); + mergedModel._overrides = other.overrides ? other.overrides : this.overrides; + return mergedModel; + } + + public config(section: string): ConfigModel { + const result = new ConfigModel(null); + result._contents = objects.clone(this.contents[section]); + return result; + } + + public languageConfig(language: string): ConfigModel { + const result = new ConfigModel(null); + const contents = objects.clone(this.contents); + for (const override of this._overrides) { + if (override.languages.indexOf(language) !== -1) { + merge(contents, override.contents, true); + } + } + result._contents = contents; + return result; + } + + public update(content: string): void { + let overrides: _IOverrides[] = null; + let currentProperty: string = null; + let currentParent: any = []; + let previousParents: any[] = []; + let parseErrors: json.ParseError[] = []; + + function onValue(value: any) { + if (Array.isArray(currentParent)) { + (currentParent).push(value); + } else if (currentProperty) { + currentParent[currentProperty] = value; + if (currentParent['overrideSettings']) { + onOverrideSettingsValue(currentProperty, value); + } + } + } + + function onOverrideSettingsValue(property: string, value: any): void { + if (property.indexOf('languages:') === 0) { + overrides.push({ + languages: property.substring('languages:'.length).split(','), + raw: value, + contents: null + }) + } + } + + let visitor: json.JSONVisitor = { + onObjectBegin: () => { + let object = {}; + if (currentProperty === 'settings.override') { + overrides = []; + object['overrideSettings'] = true; + } + onValue(object); + previousParents.push(currentParent); + currentParent = object; + currentProperty = null; + }, + onObjectProperty: (name: string) => { + currentProperty = name; + }, + onObjectEnd: () => { + currentParent = previousParents.pop(); + if (currentParent['overrideSettings']) { + delete currentParent['overrideSettings']; + } + }, + onArrayBegin: () => { + let array = []; + onValue(array); + previousParents.push(currentParent); + currentParent = array; + currentProperty = null; + }, + onArrayEnd: () => { + currentParent = previousParents.pop(); + }, + onLiteralValue: onValue, + onError: (error: json.ParseErrorCode) => { + parseErrors.push({ error: error }); + } + }; + try { + json.visit(content, visitor); + this._raw = currentParent[0]; + } catch (e) { + console.error(`Error while parsing settings file ${this.name}: ${e}`) + this._raw = {}; + this._parseErrors = [e]; + } + this._contents = toValuesTree(this._raw, message => console.error(`Conflict in settings file ${this.name}: ${message}`)); + this._overrides = overrides ? overrides.map<_IOverrides>(override => { + return { + languages: override.languages, + contents: toValuesTree(override.raw, message => console.error(`Conflict in settings file ${this.name}: ${message}`)), + raw: override.raw + }; + }) : null; + } +} + +export class DefaultConfigModel extends ConfigModel { + constructor() { + super(null); + } + + protected get _contents(): T { + return getDefaultValues(); // defaults coming from contributions to registries + } + + protected set _contents(arg: T) { + //no op + } + + public get keys(): string[] { + return getConfigurationKeys(); + } } \ No newline at end of file diff --git a/src/vs/platform/configuration/node/configurationService.ts b/src/vs/platform/configuration/node/configurationService.ts index fd08d96a29a..a216be09f68 100644 --- a/src/vs/platform/configuration/node/configurationService.ts +++ b/src/vs/platform/configuration/node/configurationService.ts @@ -5,47 +5,51 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; +import URI from 'vs/base/common/uri'; +import * as glob from 'vs/base/common/glob'; import * as objects from 'vs/base/common/objects'; -import { getDefaultValues, toValuesTree, getConfigurationKeys } from 'vs/platform/configuration/common/model'; import { ConfigWatcher } from 'vs/base/node/config'; import { Registry } from 'vs/platform/platform'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { ConfigurationSource, IConfigurationService, IConfigurationServiceEvent, IConfigurationValue, getConfigurationValue, IConfigurationKeys } from 'vs/platform/configuration/common/configuration'; +import { IDisposable, dispose, toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { ConfigurationSource, IConfigurationService, IConfigurationServiceEvent, IConfigurationValue, getConfigurationValue, IConfigurationKeys, IConfigModel, IConfigurationOptions } from 'vs/platform/configuration/common/configuration'; +import { ConfigModel, DefaultConfigModel } from 'vs/platform/configuration/common/model'; import Event, { Emitter } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -interface ICache { - defaults: T; - user: T; - consolidated: T; +export interface ICache { + defaults: IConfigModel; + user: IConfigModel; + consolidated: IConfigModel; } -export class ConfigurationService implements IConfigurationService, IDisposable { +export class ConfigurationService extends Disposable implements IConfigurationService, IDisposable { _serviceBrand: any; - private disposables: IDisposable[]; - - private rawConfig: ConfigWatcher; private cache: ICache; + private userConfigModelWatcher: ConfigWatcher>; - private _onDidUpdateConfiguration: Emitter; + private _onDidUpdateConfiguration: Emitter = this._register(new Emitter()); + public readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; constructor( @IEnvironmentService environmentService: IEnvironmentService ) { - this.disposables = []; - - this._onDidUpdateConfiguration = new Emitter(); - this.disposables.push(this._onDidUpdateConfiguration); - - this.rawConfig = new ConfigWatcher(environmentService.appSettingsPath, { changeBufferDelay: 300, defaultConfig: Object.create(null) }); - this.disposables.push(toDisposable(() => this.rawConfig.dispose())); + super(); + + this.userConfigModelWatcher = new ConfigWatcher(environmentService.appSettingsPath, { + changeBufferDelay: 300, defaultConfig: new ConfigModel(null, environmentService.appSettingsPath), parse: (content: string, parseErrors: any[]) => { + const userConfigModel = new ConfigModel(content, environmentService.appSettingsPath); + parseErrors = [...userConfigModel.errors]; + return userConfigModel; + } + }); + this._register(toDisposable(() => this.userConfigModelWatcher.dispose())); // Listeners - this.disposables.push(this.rawConfig.onDidUpdateConfiguration(() => this.onConfigurationChange(ConfigurationSource.User))); - this.disposables.push(Registry.as(Extensions.Configuration).onDidRegisterConfiguration(() => this.onConfigurationChange(ConfigurationSource.Default))); + this._register(this.userConfigModelWatcher.onDidUpdateConfiguration(() => this.onConfigurationChange(ConfigurationSource.User))); + this._register(Registry.as(Extensions.Configuration).onDidRegisterConfiguration(() => this.onConfigurationChange(ConfigurationSource.Default))); } private onConfigurationChange(source: ConfigurationSource): void { @@ -56,17 +60,13 @@ export class ConfigurationService implements IConfigurationService, IDisposab this._onDidUpdateConfiguration.fire({ config: this.getConfiguration(), source, - sourceConfig: source === ConfigurationSource.Default ? cache.defaults : cache.user + sourceConfig: source === ConfigurationSource.Default ? cache.defaults.contents : cache.user.contents }); } - public get onDidUpdateConfiguration(): Event { - return this._onDidUpdateConfiguration.event; - } - public reloadConfiguration(section?: string): TPromise { return new TPromise(c => { - this.rawConfig.reload(() => { + this.userConfigModelWatcher.reload(() => { this.cache = void 0; // reset our caches c(this.getConfiguration(section)); @@ -74,14 +74,13 @@ export class ConfigurationService implements IConfigurationService, IDisposab }); } - public getConfiguration(section?: string): C { + public getConfiguration(section?: string): C + public getConfiguration(options?: IConfigurationOptions): C + public getConfiguration(arg?: any): C { + const options = this.toOptions(arg); const cache = this.getCache(); - - return section ? cache.consolidated[section] : cache.consolidated; - } - - private getCache(): ICache { - return this.cache || (this.cache = this.consolidateConfigurations()); + const configModel = options.language ? cache.consolidated.languageConfig(options.language) : cache.consolidated; + return options.section ? configModel.config(options.section).contents : configModel.contents; } public lookup(key: string): IConfigurationValue { @@ -89,33 +88,39 @@ export class ConfigurationService implements IConfigurationService, IDisposab // make sure to clone the configuration so that the receiver does not tamper with the values return { - default: objects.clone(getConfigurationValue(cache.defaults, key)), - user: objects.clone(getConfigurationValue(cache.user, key)), - value: objects.clone(getConfigurationValue(cache.consolidated, key)) + default: objects.clone(getConfigurationValue(cache.defaults.contents, key)), + user: objects.clone(getConfigurationValue(cache.user.contents, key)), + value: objects.clone(getConfigurationValue(cache.consolidated.contents, key)) }; } public keys(): IConfigurationKeys { + const cache = this.getCache(); + return { - default: getConfigurationKeys(), - user: Object.keys(this.rawConfig.getConfig()) + default: cache.defaults.keys, + user: cache.user.keys }; } - private consolidateConfigurations(): ICache { - const defaults = getDefaultValues(); // defaults coming from contributions to registries - const user = toValuesTree(this.rawConfig.getConfig(), message => console.error(`Conflict in user settings: ${message}`)); // user configured settings - - const consolidated = objects.mixin( - objects.clone(defaults), // target: default values (but dont modify!) - user, // source: user settings - true // overwrite - ); + public getCache(): ICache { + return this.cache || (this.cache = this.consolidateConfigurations()); + } - return { defaults, user, consolidated }; + private toOptions(arg: any): IConfigurationOptions { + if (typeof arg === 'string') { + return { section: arg }; + } + if (typeof arg === 'object') { + return arg; + } + return {}; } - public dispose(): void { - this.disposables = dispose(this.disposables); + private consolidateConfigurations(): ICache { + const defaults = new DefaultConfigModel(); + const user = this.userConfigModelWatcher.getConfig(); + const consolidated = defaults.merge(user); + return { defaults, user, consolidated }; } } \ No newline at end of file diff --git a/src/vs/platform/configuration/test/common/model.test.ts b/src/vs/platform/configuration/test/common/model.test.ts new file mode 100644 index 00000000000..5ae67445615 --- /dev/null +++ b/src/vs/platform/configuration/test/common/model.test.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as assert from 'assert'; +import * as model from 'vs/platform/configuration/common/model'; + +suite('ConfigurationService - Model', () => { + + test('simple merge', () => { + let base = { 'a': 1, 'b': 2 }; + model.merge(base, { 'a': 3, 'c': 4 }, true); + assert.deepEqual(base, { 'a': 3, 'b': 2, 'c': 4 }); + base = { 'a': 1, 'b': 2 }; + model.merge(base, { 'a': 3, 'c': 4 }, false); + assert.deepEqual(base, { 'a': 1, 'b': 2, 'c': 4 }); + }); + + test('Recursive merge', () => { + const base = { 'a': { 'b': 1 } }; + model.merge(base, { 'a': { 'b': 2 } }, true); + assert.deepEqual(base, { 'a': { 'b': 2 } }); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/common/model.ts b/src/vs/workbench/services/configuration/common/model.ts index 466751a606c..79d11ea950f 100644 --- a/src/vs/workbench/services/configuration/common/model.ts +++ b/src/vs/workbench/services/configuration/common/model.ts @@ -4,90 +4,51 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import objects = require('vs/base/common/objects'); -import types = require('vs/base/common/types'); -import json = require('vs/base/common/json'); -import { toValuesTree } from 'vs/platform/configuration/common/model'; -import { CONFIG_DEFAULT_NAME, WORKSPACE_CONFIG_DEFAULT_PATH } from 'vs/workbench/services/configuration/common/configuration'; +import { ConfigModel } from 'vs/platform/configuration/common/model'; +import { IConfigModel } from 'vs/platform/configuration/common/configuration'; +import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration'; -export interface IConfigFile { - contents: any; - raw?: any; - parseError?: any; -} +export class ScopedConfigModel extends ConfigModel { -export function newConfigFile(value: string, fileName: string): IConfigFile { - try { - const contents = json.parse(value) || {}; - return { - contents: toValuesTree(contents, message => console.error(`Conflict in settings file ${fileName}: ${message}`)), - raw: contents - }; - } catch (e) { - return { - contents: {}, - parseError: e - }; + constructor(content: string, name: string, public readonly scope: string) { + super(null, name); + this.update(content); } -} - -export function merge(base: any, add: any, overwrite: boolean): void { - Object.keys(add).forEach(key => { - if (key in base) { - if (types.isObject(base[key]) && types.isObject(add[key])) { - merge(base[key], add[key], overwrite); - } else if (overwrite) { - base[key] = add[key]; - } - } else { - base[key] = add[key]; - } - }); -} - -export function consolidate(configMap: { [key: string]: IConfigFile; }): { contents: any; parseErrors: string[]; } { - const finalConfig: any = Object.create(null); - const parseErrors: string[] = []; - const regexp = /\/([^\.]*)*\.json/; - // We want to use the default settings file as base and let all other config - // files overwrite the base one - const configurationFiles = Object.keys(configMap); - const defaultIndex = configurationFiles.indexOf(WORKSPACE_CONFIG_DEFAULT_PATH); - if (defaultIndex > 0) { - configurationFiles.unshift(configurationFiles.splice(defaultIndex, 1)[0]); + public update(content: string): void { + super.update(content); + const contents = Object.create(null); + contents[this.scope] = this.contents; + this._contents = contents; } - // For each config file in .vscode folder - configurationFiles.forEach(configFileName => { - const config = objects.clone(configMap[configFileName]); - const matches = regexp.exec(configFileName); - if (!matches || !config) { - return; - } +} - // Extract the config key from the file name (except for settings.json which is the default) - let configElement: any = finalConfig; - if (matches && matches[1] && matches[1] !== CONFIG_DEFAULT_NAME) { +export class WorkspaceConfigModel extends ConfigModel { - // Use the name of the file as top level config section for all settings inside - const configSection = matches[1]; - let element = configElement[configSection]; - if (!element) { - element = Object.create(null); - configElement[configSection] = element; - } - configElement = element; - } + constructor(private workspaceSettingsConfig: IConfigModel, private scopedConfigs: ScopedConfigModel[]) { + super(null); + this.consolidate(); + } - merge(configElement, config.contents, true); - if (config.parseError) { - parseErrors.push(configFileName); + private consolidate(): void { + let result = new ConfigModel(null).merge(this.workspaceSettingsConfig); + for (const configModel of this.scopedConfigs) { + result = result.merge(configModel); } - }); + this._contents = result.contents; + this._overrides = result.overrides; + } - return { - contents: finalConfig, - parseErrors: parseErrors - }; + public get keys(): string[] { + const keys: string[] = [...this.workspaceSettingsConfig.keys]; + this.scopedConfigs.forEach(scopedConfigModel => { + Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS).forEach(scope => { + if (scopedConfigModel.scope === scope) { + keys.push(...scopedConfigModel.keys.map(key => `${scope}.${key}`)); + } + }); + }); + return keys; + } } \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index b1ef6c8fa37..1fdeaa97349 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -14,11 +14,12 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import collections = require('vs/base/common/collections'); import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { readFile } from 'vs/base/node/pfs'; import errors = require('vs/base/common/errors'); -import { IConfigFile, consolidate, newConfigFile } from 'vs/workbench/services/configuration/common/model'; -import { IConfigurationServiceEvent, ConfigurationSource, getConfigurationValue } from 'vs/platform/configuration/common/configuration'; +import { ScopedConfigModel, WorkspaceConfigModel } from 'vs/workbench/services/configuration/common/model'; +import { IConfigurationServiceEvent, ConfigurationSource, getConfigurationValue, IConfigModel, IOverrides, IConfigurationOptions } from 'vs/platform/configuration/common/configuration'; +import { ConfigModel } from 'vs/platform/configuration/common/model'; import { ConfigurationService as BaseConfigurationService } from 'vs/platform/configuration/node/configurationService'; import { IWorkspaceConfigurationValues, IWorkspaceConfigurationService, IWorkspaceConfigurationValue, CONFIG_DEFAULT_NAME, WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME, WORKSPACE_STANDALONE_CONFIGURATIONS, WORKSPACE_CONFIG_DEFAULT_PATH } from 'vs/workbench/services/configuration/common/configuration'; import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; @@ -35,30 +36,28 @@ interface IContent { value: string; } -interface IConfiguration { +interface IWorkspaceConfiguration { workspace: T; - consolidated: T; + consolidated: any; } /** * Wraps around the basic configuration service and adds knowledge about workspace settings. */ -export class WorkspaceConfigurationService implements IWorkspaceConfigurationService, IDisposable { +export class WorkspaceConfigurationService extends Disposable implements IWorkspaceConfigurationService, IDisposable { public _serviceBrand: any; private static RELOAD_CONFIGURATION_DELAY = 50; private _onDidUpdateConfiguration: Emitter; - private toDispose: IDisposable[]; private baseConfigurationService: BaseConfigurationService; - private cachedConfig: any; - private cachedWorkspaceConfig: any; - private cachedWorkspaceKeys: string[]; + private cachedConfig: ConfigModel; + private cachedWorkspaceConfig: WorkspaceConfigModel; private bulkFetchFromWorkspacePromise: TPromise; - private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise }; + private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise> }; private reloadConfigurationScheduler: RunOnceScheduler; constructor( @@ -66,52 +65,43 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer @IEnvironmentService environmentService: IEnvironmentService, private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME ) { - this.toDispose = []; + super(); this.workspaceFilePathToConfiguration = Object.create(null); - this.cachedConfig = Object.create(null); - this.cachedWorkspaceConfig = Object.create(null); + this.cachedConfig = new ConfigModel(null); + this.cachedWorkspaceConfig = new WorkspaceConfigModel(new ConfigModel(null), []); - this._onDidUpdateConfiguration = new Emitter(); - this.toDispose.push(this._onDidUpdateConfiguration); + this._onDidUpdateConfiguration = this._register(new Emitter()); - this.baseConfigurationService = new BaseConfigurationService(environmentService); - this.toDispose.push(this.baseConfigurationService); + this.baseConfigurationService = this._register(new BaseConfigurationService(environmentService)); - this.reloadConfigurationScheduler = new RunOnceScheduler(() => this.doLoadConfiguration() + this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.doLoadConfiguration() .then(config => this._onDidUpdateConfiguration.fire({ config: config.consolidated, source: ConfigurationSource.Workspace, sourceConfig: config.workspace })) - .done(null, errors.onUnexpectedError), WorkspaceConfigurationService.RELOAD_CONFIGURATION_DELAY); - this.toDispose.push(this.reloadConfigurationScheduler); + .done(null, errors.onUnexpectedError), WorkspaceConfigurationService.RELOAD_CONFIGURATION_DELAY)); - this.registerListeners(); + this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e))); } get onDidUpdateConfiguration(): Event { return this._onDidUpdateConfiguration.event; } - private registerListeners(): void { - this.toDispose.push(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e))); - } - private onBaseConfigurationChanged(e: IConfigurationServiceEvent): void { // update cached config when base config changes - const newConfig = objects.mixin( - objects.clone(this.baseConfigurationService.getConfiguration()), // target: global/default values (do NOT modify) - this.cachedWorkspaceConfig, // source: workspace configured values - true // overwrite - ); + const newConfig = new ConfigModel(null) + .merge(this.baseConfigurationService.getCache().consolidated) // global/default values (do NOT modify) + .merge(this.cachedWorkspaceConfig); // workspace configured values // emit this as update to listeners if changed - if (!objects.equals(this.cachedConfig, newConfig)) { + if (!objects.equals(this.cachedConfig.contents, newConfig.contents)) { this.cachedConfig = newConfig; this._onDidUpdateConfiguration.fire({ - config: this.cachedConfig, + config: this.cachedConfig.contents, source: e.source, sourceConfig: e.sourceConfig }); @@ -122,8 +112,12 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer return this.doLoadConfiguration().then(() => null); } - public getConfiguration(section?: string): T { - return section ? this.cachedConfig[section] : this.cachedConfig; + public getConfiguration(section?: string): C + public getConfiguration(options?: IConfigurationOptions): C + public getConfiguration(arg?: any): C { + const options = this.toOptions(arg); + const configModel = options.language ? this.cachedConfig.languageConfig(options.language) : this.cachedConfig; + return options.section ? configModel.config(options.section).contents : configModel.contents; } public lookup(key: string): IWorkspaceConfigurationValue { @@ -132,8 +126,8 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer return { default: configurationValue.default, user: configurationValue.user, - workspace: getConfigurationValue(this.cachedWorkspaceConfig, key), - value: getConfigurationValue(this.cachedConfig, key) + workspace: getConfigurationValue(this.cachedWorkspaceConfig.contents, key), + value: getConfigurationValue(this.cachedConfig.contents, key) }; } @@ -143,7 +137,7 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer return { default: keys.default, user: keys.user, - workspace: this.cachedWorkspaceKeys + workspace: this.cachedWorkspaceConfig.keys }; } @@ -177,40 +171,35 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer }); } - private doLoadConfiguration(): TPromise> { + private toOptions(arg: any): IConfigurationOptions { + if (typeof arg === 'string') { + return { section: arg }; + } + if (typeof arg === 'object') { + return arg; + } + return {}; + } + + private doLoadConfiguration(): TPromise> { // Load workspace locals return this.loadWorkspaceConfigFiles().then(workspaceConfigFiles => { // Consolidate (support *.json files in the workspace settings folder) - const workspaceConfig = consolidate(workspaceConfigFiles).contents; - this.cachedWorkspaceConfig = workspaceConfig; - - // Cache keys - const workspaceConfigKeys: string[] = []; - Object.keys(workspaceConfigFiles).forEach(path => { - if (path === WORKSPACE_CONFIG_DEFAULT_PATH) { - workspaceConfigKeys.push(...Object.keys(workspaceConfigFiles[path].raw)); - } else { - const workspaceConfigs = Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS); - workspaceConfigs.forEach(workspaceConfig => { - if (path === WORKSPACE_STANDALONE_CONFIGURATIONS[workspaceConfig]) { - workspaceConfigKeys.push(...Object.keys(workspaceConfigFiles[path].raw).map(key => `${workspaceConfig}.${key}`)); - } - }); - } - }); - this.cachedWorkspaceKeys = workspaceConfigKeys; + let workspaceSettingsModel: IConfigModel = >workspaceConfigFiles[WORKSPACE_CONFIG_DEFAULT_PATH] || new ConfigModel(null); + let otherConfigModels = Object.keys(workspaceConfigFiles).filter(key => key !== WORKSPACE_CONFIG_DEFAULT_PATH).map(key => >workspaceConfigFiles[key]); + + this.cachedWorkspaceConfig = new WorkspaceConfigModel(workspaceSettingsModel, otherConfigModels); // Override base (global < user) with workspace locals (global < user < workspace) - this.cachedConfig = objects.mixin( - objects.clone(this.baseConfigurationService.getConfiguration()), // target: global/default values (do NOT modify) - this.cachedWorkspaceConfig, // source: workspace configured values - true // overwrite - ); + this.cachedConfig = new ConfigModel(null) + .merge(this.baseConfigurationService.getCache().consolidated) // global/default values (do NOT modify) + .merge(this.cachedWorkspaceConfig); // workspace configured values + return { - consolidated: this.cachedConfig, - workspace: this.cachedWorkspaceConfig + consolidated: this.cachedConfig.contents, + workspace: this.cachedWorkspaceConfig.contents }; }); } @@ -219,11 +208,7 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer return !!this.workspaceFilePathToConfiguration[`${this.workspaceSettingsRootFolder}/${CONFIG_DEFAULT_NAME}.json`]; } - public dispose(): void { - this.toDispose = dispose(this.toDispose); - } - - private loadWorkspaceConfigFiles(): TPromise<{ [relativeWorkspacePath: string]: IConfigFile }> { + private loadWorkspaceConfigFiles(): TPromise<{ [relativeWorkspacePath: string]: IConfigModel }> { // Return early if we don't have a workspace if (!this.contextService.hasWorkspace()) { @@ -245,9 +230,10 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer return this.isWorkspaceConfigurationFile(this.contextService.toWorkspaceRelativePath(stat.resource)); // only workspace config files }).map(stat => stat.resource)); - }, err => [] /* never fail this call */).then((contents: IContent[]) => { - contents.forEach(content => this.workspaceFilePathToConfiguration[this.contextService.toWorkspaceRelativePath(content.resource)] = TPromise.as(newConfigFile(content.value, content.resource.toString()))); - }, errors.onUnexpectedError); + }, 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); } // on change: join on *all* configuration file promises so that we can merge them into a single configuration object. this @@ -292,7 +278,7 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer break; case FileChangeType.UPDATED: case FileChangeType.ADDED: - this.workspaceFilePathToConfiguration[workspacePath] = resolveContent(resource).then(content => newConfigFile(content.value, content.resource.toString()), errors.onUnexpectedError); + this.workspaceFilePathToConfiguration[workspacePath] = resolveContent(resource).then(content => this.createConfigModel(content), errors.onUnexpectedError); affectedByChanges = true; } } @@ -303,6 +289,19 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer } } + private createConfigModel(content: IContent): IConfigModel { + const path = this.contextService.toWorkspaceRelativePath(content.resource); + if (path === WORKSPACE_CONFIG_DEFAULT_PATH) { + return new ConfigModel(content.value, content.resource.toString()); + } else { + const matches = /\/([^\.]*)*\.json/.exec(path); + if (matches && matches[1]) { + return new ScopedConfigModel(content.value, content.resource.toString(), matches[1]); + } + } + return new ConfigModel(null); + } + private isWorkspaceConfigurationFile(workspaceRelativePath: string): boolean { return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS.launch, WORKSPACE_STANDALONE_CONFIGURATIONS.tasks].some(p => p === workspaceRelativePath); } diff --git a/src/vs/workbench/services/configuration/test/common/model.test.ts b/src/vs/workbench/services/configuration/test/common/model.test.ts index 2c5ba252cb3..44441e34ecc 100644 --- a/src/vs/workbench/services/configuration/test/common/model.test.ts +++ b/src/vs/workbench/services/configuration/test/common/model.test.ts @@ -4,38 +4,20 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import assert = require('assert'); -import model = require('vs/workbench/services/configuration/common/model'); +import * as assert from 'assert'; +import { ConfigModel } from 'vs/platform/configuration/common/model'; +import { WorkspaceConfigModel, ScopedConfigModel } from 'vs/workbench/services/configuration/common/model'; suite('ConfigurationService - Model', () => { - test('simple merge', () => { - let base = { 'a': 1, 'b': 2 }; - model.merge(base, { 'a': 3, 'c': 4 }, true); - assert.deepEqual(base, { 'a': 3, 'b': 2, 'c': 4 }); - base = { 'a': 1, 'b': 2 }; - model.merge(base, { 'a': 3, 'c': 4 }, false); - assert.deepEqual(base, { 'a': 1, 'b': 2, 'c': 4 }); - }); - - test('Recursive merge', () => { - const base = { 'a': { 'b': 1 } }; - model.merge(base, { 'a': { 'b': 2 } }, true); - assert.deepEqual(base, { 'a': { 'b': 2 } }); - }); - test('Test consolidate (settings and tasks)', () => { - const settingsConfig: model.IConfigFile = { - contents: { - awesome: true - } - }; + const settingsConfig = new ConfigModel(JSON.stringify({ + awesome: true + })); - const tasksConfig: model.IConfigFile = { - contents: { - awesome: false - } - }; + const tasksConfig = new ScopedConfigModel(JSON.stringify({ + awesome: false + }), '', 'tasks'); const expected = { awesome: true, @@ -44,22 +26,17 @@ suite('ConfigurationService - Model', () => { } }; - assert.deepEqual(model.consolidate({ '.vscode/settings.json': settingsConfig, '.vscode/tasks.json': tasksConfig }).contents, expected); - assert.deepEqual(model.consolidate({ '.vscode/tasks.json': tasksConfig, '.vscode/settings.json': settingsConfig }).contents, expected); + assert.deepEqual(new WorkspaceConfigModel(settingsConfig, [tasksConfig]).contents, expected); }); test('Test consolidate (settings and launch)', () => { - const settingsConfig: model.IConfigFile = { - contents: { - awesome: true - } - }; + const settingsConfig = new ConfigModel(JSON.stringify({ + awesome: true + })); - const launchConfig: model.IConfigFile = { - contents: { - awesome: false - } - }; + const launchConfig = new ScopedConfigModel(JSON.stringify({ + awesome: false + }), '', 'launch'); const expected = { awesome: true, @@ -68,36 +45,29 @@ suite('ConfigurationService - Model', () => { } }; - assert.deepEqual(model.consolidate({ '.vscode/settings.json': settingsConfig, '.vscode/launch.json': launchConfig }).contents, expected); - assert.deepEqual(model.consolidate({ '.vscode/launch.json': launchConfig, '.vscode/settings.json': settingsConfig }).contents, expected); + assert.deepEqual(new WorkspaceConfigModel(settingsConfig, [launchConfig]).contents, expected); }); test('Test consolidate (settings and launch and tasks) - launch/tasks wins over settings file', () => { - const settingsConfig: model.IConfigFile = { - contents: { - awesome: true, - launch: { - launchConfig: 'defined', - otherLaunchConfig: 'alsoDefined' - }, - tasks: { - taskConfig: 'defined', - otherTaskConfig: 'alsoDefined' - } + const settingsConfig = new ConfigModel(JSON.stringify({ + awesome: true, + launch: { + launchConfig: 'defined', + otherLaunchConfig: 'alsoDefined' + }, + tasks: { + taskConfig: 'defined', + otherTaskConfig: 'alsoDefined' } - }; + })); - const tasksConfig: model.IConfigFile = { - contents: { - taskConfig: 'overwritten', - } - }; + const tasksConfig = new ScopedConfigModel(JSON.stringify({ + taskConfig: 'overwritten', + }), '', 'tasks'); - const launchConfig: model.IConfigFile = { - contents: { - launchConfig: 'overwritten', - } - }; + const launchConfig = new ScopedConfigModel(JSON.stringify({ + launchConfig: 'overwritten', + }), '', 'launch'); const expected = { awesome: true, @@ -111,8 +81,7 @@ suite('ConfigurationService - Model', () => { } }; - assert.deepEqual(model.consolidate({ '.vscode/settings.json': settingsConfig, '.vscode/launch.json': launchConfig, '.vscode/tasks.json': tasksConfig }).contents, expected); - assert.deepEqual(model.consolidate({ '.vscode/launch.json': launchConfig, '.vscode/tasks.json': tasksConfig, '.vscode/settings.json': settingsConfig }).contents, expected); - assert.deepEqual(model.consolidate({ '.vscode/tasks.json': tasksConfig, '.vscode/launch.json': launchConfig, '.vscode/settings.json': settingsConfig }).contents, expected); + assert.deepEqual(new WorkspaceConfigModel(settingsConfig, [launchConfig, tasksConfig]).contents, expected); + assert.deepEqual(new WorkspaceConfigModel(settingsConfig, [tasksConfig, launchConfig]).contents, expected); }); }); \ No newline at end of file -- GitLab