提交 8ba62c61 编写于 作者: S Sandeep Somavarapu

#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
上级 04f02ec2
......@@ -28,6 +28,7 @@ export interface IConfigWatcher<T> {
export interface IConfigOptions<T> {
defaultConfig?: T;
changeBufferDelay?: number;
parse?: (content: string, errors: any[]) => T;
}
/**
......@@ -104,7 +105,7 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, 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
}
......
......@@ -375,7 +375,7 @@ export class SimpleConfigurationService implements IConfigurationService {
this._config = getDefaultConfiguration();
}
public getConfiguration<T>(section?: string): T {
public getConfiguration<T>(section?: any): T {
return this._config;
}
......
......@@ -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<IConfigurationService>('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<T>(section?: string): T;
getConfiguration<T>(options?: IConfigurationOptions): T;
/**
* Resolves a configuration key to its values in the different scopes
......@@ -94,3 +101,20 @@ export function getConfigurationValue<T>(config: any, settingPath: string, defau
return typeof result === 'undefined' ? defaultValue : result;
}
export interface IConfigModel<T> {
contents: T;
overrides: IOverrides<T>[];
keys: string[];
raw: any;
errors: any[];
merge(other: IConfigModel<T>, overwrite?: boolean): IConfigModel<T>;
config<V>(section: string): IConfigModel<V>;
languageConfig<V>(language: string, section?: string): IConfigModel<V>;
}
export interface IOverrides<T> {
contents: T;
languages: string[];
}
\ No newline at end of file
......@@ -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);
......@@ -62,3 +66,182 @@ export function getConfigurationKeys(): string[] {
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<T> extends IOverrides<T> {
raw: any;
}
export class ConfigModel<T> implements IConfigModel<T> {
protected _contents: T;
protected _overrides: IOverrides<T>[] = 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 || <T>{};
}
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<T>, overwrite: boolean = true): ConfigModel<T> {
const mergedModel = new ConfigModel<T>(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<V>(section: string): ConfigModel<V> {
const result = new ConfigModel<V>(null);
result._contents = objects.clone(this.contents[section]);
return result;
}
public languageConfig<V>(language: string): ConfigModel<V> {
const result = new ConfigModel<V>(null);
const contents = objects.clone<any>(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<T>[] = null;
let currentProperty: string = null;
let currentParent: any = [];
let previousParents: any[] = [];
let parseErrors: json.ParseError[] = [];
function onValue(value: any) {
if (Array.isArray(currentParent)) {
(<any[]>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 = <T>{};
this._parseErrors = [e];
}
this._contents = toValuesTree(this._raw, message => console.error(`Conflict in settings file ${this.name}: ${message}`));
this._overrides = overrides ? overrides.map<_IOverrides<T>>(override => {
return {
languages: override.languages,
contents: <T>toValuesTree(override.raw, message => console.error(`Conflict in settings file ${this.name}: ${message}`)),
raw: override.raw
};
}) : null;
}
}
export class DefaultConfigModel<T> extends ConfigModel<T> {
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
......@@ -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<T> {
defaults: T;
user: T;
consolidated: T;
export interface ICache<T> {
defaults: IConfigModel<T>;
user: IConfigModel<T>;
consolidated: IConfigModel<any>;
}
export class ConfigurationService<T> implements IConfigurationService, IDisposable {
export class ConfigurationService<T> extends Disposable implements IConfigurationService, IDisposable {
_serviceBrand: any;
private disposables: IDisposable[];
private rawConfig: ConfigWatcher<T>;
private cache: ICache<T>;
private userConfigModelWatcher: ConfigWatcher<IConfigModel<T>>;
private _onDidUpdateConfiguration: Emitter<IConfigurationServiceEvent>;
private _onDidUpdateConfiguration: Emitter<IConfigurationServiceEvent> = this._register(new Emitter<IConfigurationServiceEvent>());
public readonly onDidUpdateConfiguration: Event<IConfigurationServiceEvent> = this._onDidUpdateConfiguration.event;
constructor(
@IEnvironmentService environmentService: IEnvironmentService
) {
this.disposables = [];
this._onDidUpdateConfiguration = new Emitter<IConfigurationServiceEvent>();
this.disposables.push(this._onDidUpdateConfiguration);
super();
this.rawConfig = new ConfigWatcher(environmentService.appSettingsPath, { changeBufferDelay: 300, defaultConfig: Object.create(null) });
this.disposables.push(toDisposable(() => this.rawConfig.dispose()));
this.userConfigModelWatcher = new ConfigWatcher(environmentService.appSettingsPath, {
changeBufferDelay: 300, defaultConfig: new ConfigModel<T>(null, environmentService.appSettingsPath), parse: (content: string, parseErrors: any[]) => {
const userConfigModel = new ConfigModel<T>(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<IConfigurationRegistry>(Extensions.Configuration).onDidRegisterConfiguration(() => this.onConfigurationChange(ConfigurationSource.Default)));
this._register(this.userConfigModelWatcher.onDidUpdateConfiguration(() => this.onConfigurationChange(ConfigurationSource.User)));
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidRegisterConfiguration(() => this.onConfigurationChange(ConfigurationSource.Default)));
}
private onConfigurationChange(source: ConfigurationSource): void {
......@@ -56,17 +60,13 @@ export class ConfigurationService<T> 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<IConfigurationServiceEvent> {
return this._onDidUpdateConfiguration.event;
}
public reloadConfiguration<C>(section?: string): TPromise<C> {
return new TPromise<C>(c => {
this.rawConfig.reload(() => {
this.userConfigModelWatcher.reload(() => {
this.cache = void 0; // reset our caches
c(this.getConfiguration<C>(section));
......@@ -74,14 +74,13 @@ export class ConfigurationService<T> implements IConfigurationService, IDisposab
});
}
public getConfiguration<C>(section?: string): C {
public getConfiguration<C>(section?: string): C
public getConfiguration<C>(options?: IConfigurationOptions): C
public getConfiguration<C>(arg?: any): C {
const options = this.toOptions(arg);
const cache = this.getCache();
return section ? cache.consolidated[section] : cache.consolidated;
}
private getCache(): ICache<T> {
return this.cache || (this.cache = this.consolidateConfigurations());
const configModel = options.language ? cache.consolidated.languageConfig<C>(options.language) : cache.consolidated;
return options.section ? configModel.config<C>(options.section).contents : configModel.contents;
}
public lookup<C>(key: string): IConfigurationValue<C> {
......@@ -89,33 +88,39 @@ export class ConfigurationService<T> implements IConfigurationService, IDisposab
// make sure to clone the configuration so that the receiver does not tamper with the values
return {
default: objects.clone(getConfigurationValue<C>(cache.defaults, key)),
user: objects.clone(getConfigurationValue<C>(cache.user, key)),
value: objects.clone(getConfigurationValue<C>(cache.consolidated, key))
default: objects.clone(getConfigurationValue<C>(cache.defaults.contents, key)),
user: objects.clone(getConfigurationValue<C>(cache.user.contents, key)),
value: objects.clone(getConfigurationValue<C>(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<T> {
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<T> {
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<T> {
const defaults = new DefaultConfigModel<T>();
const user = this.userConfigModelWatcher.getConfig();
const consolidated = defaults.merge(user);
return { defaults, user, consolidated };
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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
......@@ -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<T> extends ConfigModel<T> {
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];
public update(content: string): void {
super.update(content);
const contents = Object.create(null);
contents[this.scope] = this.contents;
this._contents = contents;
}
});
}
export function consolidate(configMap: { [key: string]: IConfigFile; }): { contents: any; parseErrors: string[]; } {
const finalConfig: any = Object.create(null);
const parseErrors: string[] = [];
const regexp = /\/([^\.]*)*\.json/;
export class WorkspaceConfigModel<T> extends ConfigModel<T> {
// 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]);
constructor(private workspaceSettingsConfig: IConfigModel<T>, private scopedConfigs: ScopedConfigModel<T>[]) {
super(null);
this.consolidate();
}
// 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;
private consolidate(): void {
let result = new ConfigModel<T>(null).merge(this.workspaceSettingsConfig);
for (const configModel of this.scopedConfigs) {
result = result.merge(configModel);
}
// 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) {
// 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;
this._contents = result.contents;
this._overrides = result.overrides;
}
merge(configElement, config.contents, true);
if (config.parseError) {
parseErrors.push(configFileName);
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 {
contents: finalConfig,
parseErrors: parseErrors
};
});
return keys;
}
}
\ No newline at end of file
......@@ -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<T> {
interface IWorkspaceConfiguration<T> {
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<IConfigurationServiceEvent>;
private toDispose: IDisposable[];
private baseConfigurationService: BaseConfigurationService<any>;
private cachedConfig: any;
private cachedWorkspaceConfig: any;
private cachedWorkspaceKeys: string[];
private cachedConfig: ConfigModel<any>;
private cachedWorkspaceConfig: WorkspaceConfigModel<any>;
private bulkFetchFromWorkspacePromise: TPromise<any>;
private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<IConfigFile> };
private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<IConfigModel<any>> };
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<any>(null);
this.cachedWorkspaceConfig = new WorkspaceConfigModel(new ConfigModel(null), []);
this._onDidUpdateConfiguration = new Emitter<IConfigurationServiceEvent>();
this.toDispose.push(this._onDidUpdateConfiguration);
this._onDidUpdateConfiguration = this._register(new Emitter<IConfigurationServiceEvent>());
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<IConfigurationServiceEvent> {
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<any>(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<T>(section?: string): T {
return section ? this.cachedConfig[section] : this.cachedConfig;
public getConfiguration<C>(section?: string): C
public getConfiguration<C>(options?: IConfigurationOptions): C
public getConfiguration<C>(arg?: any): C {
const options = this.toOptions(arg);
const configModel = options.language ? this.cachedConfig.languageConfig<C>(options.language) : this.cachedConfig;
return options.section ? configModel.config<C>(options.section).contents : configModel.contents;
}
public lookup<C>(key: string): IWorkspaceConfigurationValue<C> {
......@@ -132,8 +126,8 @@ export class WorkspaceConfigurationService implements IWorkspaceConfigurationSer
return {
default: configurationValue.default,
user: configurationValue.user,
workspace: getConfigurationValue<C>(this.cachedWorkspaceConfig, key),
value: getConfigurationValue<C>(this.cachedConfig, key)
workspace: getConfigurationValue<C>(this.cachedWorkspaceConfig.contents, key),
value: getConfigurationValue<C>(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<T>(): TPromise<IConfiguration<T>> {
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>> {
// 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;
let workspaceSettingsModel: IConfigModel<T> = <IConfigModel<T>>workspaceConfigFiles[WORKSPACE_CONFIG_DEFAULT_PATH] || new ConfigModel<T>(null);
let otherConfigModels = Object.keys(workspaceConfigFiles).filter(key => key !== WORKSPACE_CONFIG_DEFAULT_PATH).map(key => <ScopedConfigModel<T>>workspaceConfigFiles[key]);
// 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;
this.cachedWorkspaceConfig = new WorkspaceConfigModel<T>(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<T>(): TPromise<{ [relativeWorkspacePath: string]: IConfigModel<T> }> {
// Return early if we don't have a workspace
if (!this.contextService.hasWorkspace()) {
......@@ -245,8 +230,9 @@ 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())));
}, 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);
}
......@@ -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<T>(content: IContent): IConfigModel<T> {
const path = this.contextService.toWorkspaceRelativePath(content.resource);
if (path === WORKSPACE_CONFIG_DEFAULT_PATH) {
return new ConfigModel<T>(content.value, content.resource.toString());
} else {
const matches = /\/([^\.]*)*\.json/.exec(path);
if (matches && matches[1]) {
return new ScopedConfigModel<T>(content.value, content.resource.toString(), matches[1]);
}
}
return new ConfigModel<T>(null);
}
private isWorkspaceConfigurationFile(workspaceRelativePath: string): boolean {
return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS.launch, WORKSPACE_STANDALONE_CONFIGURATIONS.tasks].some(p => p === workspaceRelativePath);
}
......
......@@ -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: {
const settingsConfig = new ConfigModel(JSON.stringify({
awesome: true
}
};
}));
const tasksConfig: model.IConfigFile = {
contents: {
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: {
const settingsConfig = new ConfigModel(JSON.stringify({
awesome: true
}
};
}));
const launchConfig: model.IConfigFile = {
contents: {
const launchConfig = new ScopedConfigModel(JSON.stringify({
awesome: false
}
};
}), '', 'launch');
const expected = {
awesome: true,
......@@ -68,13 +45,11 @@ 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: {
const settingsConfig = new ConfigModel(JSON.stringify({
awesome: true,
launch: {
launchConfig: 'defined',
......@@ -84,20 +59,15 @@ suite('ConfigurationService - Model', () => {
taskConfig: 'defined',
otherTaskConfig: 'alsoDefined'
}
}
};
}));
const tasksConfig: model.IConfigFile = {
contents: {
const tasksConfig = new ScopedConfigModel(JSON.stringify({
taskConfig: 'overwritten',
}
};
}), '', 'tasks');
const launchConfig: model.IConfigFile = {
contents: {
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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册