提交 8e1e3225 编写于 作者: B Benjamin Pasero

merge workbench and platform config service

上级 d0238031
/*---------------------------------------------------------------------------------------------
* 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<IConfigurationServiceEvent>();
protected workspaceSettingsRootFolder: string;
private cachedConfig: ILoadConfigResult;
private bulkFetchFromWorkspacePromise: TPromise<any>;
private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<IConfigFile> };
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<IConfigurationServiceEvent> {
return this._onDidUpdateConfiguration.event;
}
protected registerListeners(): void {
const unbind = this.eventService.addListener2(EventType.FILE_CHANGES, (events) => this.handleFileEvents(events));
const subscription = Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidRegisterConfiguration(() => this.onDidRegisterConfiguration());
this.callOnDispose = {
dispose: () => {
unbind.dispose();
subscription.dispose();
}
};
}
public initialize(): TPromise<void> {
return this.doLoadConfiguration().then(() => null);
}
protected abstract resolveContents(resource: uri[]): TPromise<IContent[]>;
protected abstract resolveContent(resource: uri): TPromise<IContent>;
protected abstract resolveStat(resource: uri): TPromise<IStat>;
public abstract setUserConfiguration(key: string | JSONPath, value: any): Thenable<void>;
public getConfiguration<T>(section?: string): T {
let result = section ? this.cachedConfig.config[section] : this.cachedConfig.config;
const parseErrors = this.cachedConfig.parseErrors;
if (parseErrors && parseErrors.length > 0) {
if (!result) {
result = {};
}
result.$parseErrors = parseErrors;
}
return result;
}
public loadConfiguration(section?: string): TPromise<any> {
// Reset caches to ensure we are hitting the disk
this.bulkFetchFromWorkspacePromise = null;
this.workspaceFilePathToConfiguration = Object.create(null);
// Load configuration
return this.doLoadConfiguration(section);
}
private doLoadConfiguration(section?: string): TPromise<any> {
// Load globals
const globals = this.loadGlobalConfiguration();
// Load workspace locals
return this.loadWorkspaceConfiguration().then((values) => {
// Consolidate
const consolidated = consolidate(values);
// Override with workspace locals
const merged = objects.mixin(
objects.clone(globals.contents), // target: global/default values (but dont modify!)
consolidated.contents, // source: workspace configured values
true // overwrite
);
let parseErrors = [];
if (consolidated.parseErrors) {
parseErrors = consolidated.parseErrors;
}
if (globals.parseErrors) {
parseErrors.push.apply(parseErrors, globals.parseErrors);
}
return {
config: merged,
parseErrors
};
}).then((res: ILoadConfigResult) => {
this.cachedConfig = res;
return this.getConfiguration(section);
});
}
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
......@@ -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<IContent[]> {
return TPromise.as(resources.map((resource) => {
return {
resource: resource,
value: ''
};
}));
}
protected resolveContent(resource: URI): TPromise<IContent> {
return TPromise.as({
resource: resource,
value: ''
});
}
protected resolveStat(resource: URI): TPromise<IStat> {
return TPromise.as({
resource: resource,
isDirectory: false
});
}
public loadConfiguration<T>(section?: string): TPromise<T> {
return TPromise.as(this.getConfiguration());
}
......
......@@ -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<IConfigurationServiceEvent>();
private workspaceSettingsRootFolder: string;
private toDispose: IDisposable;
private cachedConfig: ILoadConfigResult;
private bulkFetchFromWorkspacePromise: TPromise<any>;
private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<IConfigFile> };
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<IConfigurationServiceEvent> {
return this._onDidUpdateConfiguration.event;
}
private registerListeners(): void {
const unbind = this.eventService.addListener2(FileEventType.FILE_CHANGES, (events) => this.handleFileEvents(events));
const subscription = Registry.as<IConfigurationRegistry>(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<IContent[]> {
public initialize(): TPromise<void> {
return this.doLoadConfiguration().then(() => null);
}
private resolveContents(resources: uri[]): TPromise<IContent[]> {
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<IContent> {
return readFile(resource.fsPath).then(contents => ({resource, value: contents.toString()}));
private resolveContent(resource: uri): TPromise<IContent> {
return readFile(resource.fsPath).then(contents => ({ resource, value: contents.toString() }));
}
protected resolveStat(resource: uri): TPromise<IStat> {
private resolveStat(resource: uri): TPromise<IStat> {
return new TPromise<IStat>((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<void> {
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' ? (<string>key).split('.') : <JSONPath>key;
const edits = setProperty(content, path, value, { insertSpaces, tabSize, eol: '\n' });
content = applyEdits(content, edits);
return writeFile(appSettingsPath, content, 'utf8');
});
}
public getConfiguration<T>(section?: string): T {
let result = section ? this.cachedConfig.config[section] : this.cachedConfig.config;
const parseErrors = this.cachedConfig.parseErrors;
if (parseErrors && parseErrors.length > 0) {
if (!result) {
result = {};
}
result.$parseErrors = parseErrors;
}
return super.loadWorkspaceConfiguration(section);
return result;
}
public loadConfiguration(section?: string): TPromise<any> {
// Reset caches to ensure we are hitting the disk
this.bulkFetchFromWorkspacePromise = null;
this.workspaceFilePathToConfiguration = Object.create(null);
// Load configuration
return this.doLoadConfiguration(section);
}
protected loadGlobalConfiguration(): { contents: any; parseErrors?: string[]; } {
const defaults = super.loadGlobalConfiguration();
private doLoadConfiguration(section?: string): TPromise<any> {
// Load globals
const globals = this.loadGlobalConfiguration();
// Load workspace locals
return this.loadWorkspaceConfiguration().then((values) => {
// Consolidate
const consolidated = consolidate(values);
// Override with workspace locals
const merged = objects.mixin(
objects.clone(globals.contents), // target: global/default values (but dont modify!)
consolidated.contents, // source: workspace configured values
true // overwrite
);
let parseErrors = [];
if (consolidated.parseErrors) {
parseErrors = consolidated.parseErrors;
}
if (globals.parseErrors) {
parseErrors.push.apply(parseErrors, globals.parseErrors);
}
return {
config: merged,
parseErrors
};
}).then((res: ILoadConfigResult) => {
this.cachedConfig = res;
return this.getConfiguration(section);
});
}
private loadGlobalConfiguration(): { contents: any; parseErrors?: string[]; } {
const globalSettings = (<LegacyWorkspaceContextService>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<void> {
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' ? (<string> key).split('.') : <JSONPath> 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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册