diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index a8f70d7d5c62d0d805cc965e3a2f29f15705b0a9..bb2b3a9aa5bac0935aa0d16e51ce054356f6771a 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import URI from 'vs/base/common/uri'; +import { createHash } from 'crypto'; import * as paths from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; import { Event, Emitter } from 'vs/base/common/event'; -import { readFile } from 'vs/base/node/pfs'; +import * as pfs from 'vs/base/node/pfs'; import * as errors from 'vs/base/common/errors'; import * as collections from 'vs/base/common/collections'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -21,11 +22,13 @@ import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LA import { IStoredWorkspace, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import * as extfs from 'vs/base/node/extfs'; import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService'; -import { WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { relative } from 'path'; import { equals } from 'vs/base/common/objects'; import { Schemas } from 'vs/base/common/network'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IConfigurationModel } from 'vs/platform/configuration/common/configuration'; export class WorkspaceConfiguration extends Disposable { @@ -129,18 +132,29 @@ function isWorkspaceConfigurationFile(resource: URI): boolean { return [`${FOLDER_SETTINGS_NAME}.json`, `${TASKS_CONFIGURATION_KEY}.json`, `${LAUNCH_CONFIGURATION_KEY}.json`].some(p => p === name);// only workspace config files } -export abstract class FolderConfiguration extends Disposable { +export interface IFolderConfiguration { + readonly onDidChange: Event; + loadConfiguration(): TPromise; + reprocess(): ConfigurationModel; + getUnsupportedKeys(): string[]; + dispose(): void; +} + +export abstract class AbstractFolderConfiguration extends Disposable implements IFolderConfiguration { private _folderSettingsModelParser: FolderSettingsModelParser; - private _standAloneConfigurations: ConfigurationModel[] = []; - private _cache: ConfigurationModel = new ConfigurationModel(); + private _standAloneConfigurations: ConfigurationModel[]; + private _cache: ConfigurationModel; protected readonly _onDidChange: Emitter = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; - constructor(protected readonly folder: URI, workbenchState: WorkbenchState) { + constructor(protected readonly folder: URI, workbenchState: WorkbenchState, from?: AbstractFolderConfiguration) { super(); - this._folderSettingsModelParser = new FolderSettingsModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? [ConfigurationScope.RESOURCE] : [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]); + + this._folderSettingsModelParser = from ? from._folderSettingsModelParser : new FolderSettingsModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? [ConfigurationScope.RESOURCE] : [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]); + this._standAloneConfigurations = from ? from._standAloneConfigurations : []; + this._cache = from ? from._cache : new ConfigurationModel(); } loadConfiguration(): TPromise { @@ -190,7 +204,7 @@ export abstract class FolderConfiguration extends Disposable { protected abstract loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]>; } -export class NodeBasedFolderConfiguration extends FolderConfiguration { +export class NodeBasedFolderConfiguration extends AbstractFolderConfiguration { private readonly folderConfigurationPath: URI; @@ -212,7 +226,7 @@ export class NodeBasedFolderConfiguration extends FolderConfiguration { private resolveContents(resources: URI[]): TPromise<{ resource: URI, value: string }[]> { return TPromise.join(resources.map(resource => - readFile(resource.fsPath) + pfs.readFile(resource.fsPath) .then(contents => ({ resource, value: contents.toString() })))); } @@ -237,15 +251,15 @@ export class NodeBasedFolderConfiguration extends FolderConfiguration { } } -export class FileServiceBasedFolderConfiguration extends FolderConfiguration { +export class FileServiceBasedFolderConfiguration extends AbstractFolderConfiguration { private bulkContentFetchromise: TPromise; private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise }; private reloadConfigurationScheduler: RunOnceScheduler; private readonly folderConfigurationPath: URI; - constructor(folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState, private fileService: IFileService) { - super(folder, workbenchState); + constructor(folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) { + super(folder, workbenchState, from); this.folderConfigurationPath = folder.with({ path: paths.join(this.folder.path, configFolderRelativePath) }); this.workspaceFilePathToConfiguration = Object.create(null); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); @@ -329,8 +343,124 @@ export class FileServiceBasedFolderConfiguration extends FolderConfiguration { } } -export class VoidFolderConfiguration extends FolderConfiguration { - protected loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]> { - return TPromise.as([]); +export class CachedFolderConfiguration extends Disposable implements IFolderConfiguration { + + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + private readonly cachedFolderPath: string; + private readonly cachedConfigurationPath: string; + private configurationModel: ConfigurationModel; + + constructor( + folder: URI, + configFolderRelativePath: string, + environmentService: IEnvironmentService) { + super(); + this.cachedFolderPath = paths.join(environmentService.appSettingsHome, createHash('md5').update(paths.join(folder.path, configFolderRelativePath)).digest('hex')); + this.cachedConfigurationPath = paths.join(this.cachedFolderPath, 'configuration.json'); + this.configurationModel = new ConfigurationModel(); + } + + loadConfiguration(): TPromise { + return pfs.readFile(this.cachedConfigurationPath) + .then(contents => { + const parsed: IConfigurationModel = JSON.parse(contents.toString()); + this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides); + return this.configurationModel; + }, () => this.configurationModel); + } + + updateConfiguration(configurationModel: ConfigurationModel): TPromise { + const raw = JSON.stringify(configurationModel.toJSON()); + return this.createCachedFolder().then(created => { + if (created) { + return configurationModel.keys.length ? pfs.writeFile(this.cachedConfigurationPath, raw) : pfs.rimraf(this.cachedFolderPath); + } + return null; + }); + } + + reprocess(): ConfigurationModel { + return this.configurationModel; + } + + getUnsupportedKeys(): string[] { + return []; + } + + private createCachedFolder(): TPromise { + return pfs.exists(this.cachedFolderPath) + .then(exists => exists ? exists : pfs.mkdirp(this.cachedFolderPath), () => pfs.mkdirp(this.cachedFolderPath)); + } +} + +export class FolderConfiguration extends Disposable implements IFolderConfiguration { + + protected readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + private folderConfiguration: IFolderConfiguration; + private cachedFolderConfiguration: CachedFolderConfiguration; + + constructor( + readonly workspaceFolder: IWorkspaceFolder, + private readonly configFolderRelativePath: string, + private readonly workbenchState: WorkbenchState, + private environmentService: IEnvironmentService, + fileService?: IFileService + ) { + super(); + + this.cachedFolderConfiguration = new CachedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.environmentService); + this.folderConfiguration = this.cachedFolderConfiguration; + if (fileService) { + this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService); + } else if (this.workspaceFolder.uri.scheme === Schemas.file) { + this.folderConfiguration = new NodeBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState); + } + this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange())); + } + + loadConfiguration(): TPromise { + return this.folderConfiguration.loadConfiguration(); + } + + reprocess(): ConfigurationModel { + return this.folderConfiguration.reprocess(); + } + + getUnsupportedKeys(): string[] { + return this.folderConfiguration.getUnsupportedKeys(); + } + + adopt(fileService: IFileService): boolean { + let result = false; + if (fileService && !(this.folderConfiguration instanceof FileServiceBasedFolderConfiguration)) { + if (this.folderConfiguration instanceof CachedFolderConfiguration) { + this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService); + this.updateCache(); + result = true; + } else { + const oldFolderConfiguration = this.folderConfiguration; + this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService, oldFolderConfiguration); + oldFolderConfiguration.dispose(); + } + this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange())); + } + return result; + } + + private onDidFolderConfigurationChange(): void { + this.updateCache(); + this._onDidChange.fire(); + } + + private updateCache(): TPromise { + if (this.workspaceFolder.uri.scheme !== Schemas.file && this.folderConfiguration instanceof FileServiceBasedFolderConfiguration) { + return this.folderConfiguration.loadConfiguration() + .then(configurationModel => this.cachedFolderConfiguration.updateConfiguration(configurationModel)); + } + return TPromise.as(null); } } \ 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 caf2352bcf1fba691442b8f279b7b9c7ac988ef5..5674e4c275af1eab9d3db674d29c4a291b52f6de 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -33,7 +33,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import product from 'vs/platform/node/product'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; -import { WorkspaceConfiguration, FolderConfiguration, FileServiceBasedFolderConfiguration, NodeBasedFolderConfiguration, VoidFolderConfiguration } from 'vs/workbench/services/configuration/node/configuration'; +import { WorkspaceConfiguration, FolderConfiguration } from 'vs/workbench/services/configuration/node/configuration'; import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService'; import { Schemas } from 'vs/base/common/network'; import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; @@ -309,7 +309,15 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat acquireFileService(fileService: IFileService): void { this.fileService = fileService; - this.reloadConfiguration(); + const changedWorkspaceFolders: IWorkspaceFolder[] = []; + this.cachedFolderConfigs.forEach(folderConfiguration => { + if (folderConfiguration.adopt(fileService)) { + changedWorkspaceFolders.push(folderConfiguration.workspaceFolder); + } + }); + for (const workspaceFolder of changedWorkspaceFolders) { + this.onWorkspaceFolderConfigurationChanged(workspaceFolder); + } } acquireInstantiationService(instantiationService: IInstantiationService): void { @@ -569,6 +577,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat // Remove the configurations of deleted folders for (const key of this.cachedFolderConfigs.keys()) { if (!this.workspace.folders.filter(folder => folder.uri.toString() === key.toString())[0]) { + const folderConfiguration = this.cachedFolderConfigs.get(key); + folderConfiguration.dispose(); this.cachedFolderConfigs.delete(key); changeEvent = changeEvent.change(this._configuration.compareAndDeleteFolderConfiguration(key)); } @@ -589,24 +599,16 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private loadFolderConfigurations(folders: IWorkspaceFolder[]): TPromise { return TPromise.join([...folders.map(folder => { - this.disposeFolderConfiguration(folder); - const folderConfiguration = this.createFolderConfiguration(folder); - this._register(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder))); - this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration)); + let folderConfiguration = this.cachedFolderConfigs.get(folder.uri); + if (!folderConfiguration) { + folderConfiguration = new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.getWorkbenchState(), this.environmentService, this.fileService); + this._register(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder))); + this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration)); + } return folderConfiguration.loadConfiguration(); })]); } - private createFolderConfiguration(folder: IWorkspaceFolder): FolderConfiguration { - if (this.fileService) { - return new FileServiceBasedFolderConfiguration(folder.uri, this.workspaceSettingsRootFolder, this.getWorkbenchState(), this.fileService); - } - if (folder.uri.scheme === Schemas.file) { - return new NodeBasedFolderConfiguration(folder.uri, this.workspaceSettingsRootFolder, this.getWorkbenchState()); - } - return new VoidFolderConfiguration(folder.uri, this.getWorkbenchState()); - } - private writeConfigurationValue(key: string, value: any, target: ConfigurationTarget, overrides: IConfigurationOverrides, donotNotifyError: boolean): TPromise { if (target === ConfigurationTarget.DEFAULT) { return TPromise.wrapError(new Error('Invalid configuration target')); @@ -689,13 +691,6 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return path1 === path2; } - - private disposeFolderConfiguration(folder: IWorkspaceFolder): void { - const folderConfiguration = this.cachedFolderConfigs.get(folder.uri); - if (folderConfiguration) { - folderConfiguration.dispose(); - } - } } interface IExportedConfigurationNode {