From 90265e830581e72bc38f3e30832bf56d2ea27927 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Wed, 20 Feb 2019 11:10:10 +0200 Subject: [PATCH 1/5] Added ability to retrieve configuration for a resource. Signed-off-by: Oleksii Kurinnyi --- packages/plugin-ext/src/api/plugin-api.ts | 4 +- .../src/hosted/browser/hosted-plugin.ts | 12 +- .../src/hosted/node/plugin-reader.ts | 2 +- .../main/browser/preference-registry-main.ts | 32 ++- .../src/plugin/preference-registry.ts | 22 +- .../plugin/preferences/configuration.spec.ts | 250 ++++++++++++++++++ .../src/plugin/preferences/configuration.ts | 67 ++++- 7 files changed, 358 insertions(+), 31 deletions(-) create mode 100644 packages/plugin-ext/src/plugin/preferences/configuration.spec.ts diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index 7449f202b822f..d3c749cf309a1 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -730,12 +730,12 @@ export interface PreferenceRegistryMain { target: boolean | ConfigurationTarget | undefined, key: string, value: any, - resource: any | undefined + resource?: string ): PromiseLike; $removeConfigurationOption( target: boolean | ConfigurationTarget | undefined, key: string, - resource: any | undefined + resource?: string ): PromiseLike; } diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index a8c5665abe017..786246cff9556 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -34,6 +34,7 @@ import { StoragePathService } from '../../main/browser/storage-path-service'; import { getPreferences } from '../../main/browser/preference-registry-main'; import { PluginServer } from '../../common/plugin-protocol'; import { KeysToKeysToAnyValue } from '../../common/types'; +import { FileStat } from '@theia/filesystem/lib/common/filesystem'; @injectable() export class HostedPluginSupport { @@ -97,6 +98,7 @@ export class HostedPluginSupport { this.server.getExtPluginAPI(), this.pluginServer.keyValueStorageGetAll(true), this.pluginServer.keyValueStorageGetAll(false), + this.workspaceService.roots, ]).then(metadata => { const pluginsInitData: PluginsInitializationData = { plugins: metadata['0'], @@ -105,7 +107,8 @@ export class HostedPluginSupport { storagePath: metadata['3'], pluginAPIs: metadata['4'], globalStates: metadata['5'], - workspaceStates: metadata['6'] + workspaceStates: metadata['6'], + roots: metadata['7'] }; this.loadPlugins(pluginsInitData, this.container); }).catch(e => console.error(e)); @@ -130,7 +133,7 @@ export class HostedPluginSupport { const hostedExtManager = worker.rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); hostedExtManager.$init({ plugins: initData.plugins, - preferences: getPreferences(this.preferenceProviderProvider), + preferences: getPreferences(this.preferenceProviderProvider, initData.roots), globalState: initData.globalStates, workspaceState: initData.workspaceStates, env: { queryParams: getQueryParameters() }, @@ -171,7 +174,7 @@ export class HostedPluginSupport { const hostedExtManager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); hostedExtManager.$init({ plugins: plugins, - preferences: getPreferences(this.preferenceProviderProvider), + preferences: getPreferences(this.preferenceProviderProvider, initData.roots), globalState: initData.globalStates, workspaceState: initData.workspaceStates, env: { queryParams: getQueryParameters() }, @@ -236,5 +239,6 @@ interface PluginsInitializationData { storagePath: string | undefined, pluginAPIs: ExtPluginApi[], globalStates: KeysToKeysToAnyValue, - workspaceStates: KeysToKeysToAnyValue + workspaceStates: KeysToKeysToAnyValue, + roots: FileStat[], } diff --git a/packages/plugin-ext/src/hosted/node/plugin-reader.ts b/packages/plugin-ext/src/hosted/node/plugin-reader.ts index accab83885e09..0066da8cb970b 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-reader.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-reader.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ - // tslint:disable:no-any +// tslint:disable:no-any import * as path from 'path'; import * as fs from 'fs-extra'; diff --git a/packages/plugin-ext/src/main/browser/preference-registry-main.ts b/packages/plugin-ext/src/main/browser/preference-registry-main.ts index d2d8b3f5d0d21..9fff3cb77ed03 100644 --- a/packages/plugin-ext/src/main/browser/preference-registry-main.ts +++ b/packages/plugin-ext/src/main/browser/preference-registry-main.ts @@ -29,11 +29,23 @@ import { } from '../../api/plugin-api'; import { RPCProtocol } from '../../api/rpc-protocol'; import { ConfigurationTarget } from '../../plugin/types-impl'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { FileStat } from '@theia/filesystem/lib/common/filesystem'; -export function getPreferences(preferenceProviderProvider: PreferenceProviderProvider): PreferenceData { - return PreferenceScope.getScopes().reduce((result, scope) => { +export function getPreferences(preferenceProviderProvider: PreferenceProviderProvider, rootFolders: FileStat[]): PreferenceData { + const folders = rootFolders.map(root => root.uri.toString()); + /* tslint:disable-next-line:no-any */ + return PreferenceScope.getScopes().reduce((result: { [key: number]: any }, scope: PreferenceScope) => { + result[scope] = {}; const provider = preferenceProviderProvider(scope); - result[scope] = provider.getPreferences(); + if (scope === PreferenceScope.Folder) { + for (const f of folders) { + const folderPrefs = provider.getPreferences(f); + result[scope][f] = folderPrefs; + } + } else { + result[scope] = provider.getPreferences(); + } return result; }, {} as PreferenceData); } @@ -48,9 +60,11 @@ export class PreferenceRegistryMainImpl implements PreferenceRegistryMain { this.preferenceService = container.get(PreferenceService); this.preferenceProviderProvider = container.get(PreferenceProviderProvider); const preferenceServiceImpl = container.get(PreferenceServiceImpl); + const workspaceService = container.get(WorkspaceService); - preferenceServiceImpl.onPreferenceChanged(e => { - const data = getPreferences(this.preferenceProviderProvider); + preferenceServiceImpl.onPreferenceChanged(async e => { + const roots = await workspaceService.roots; + const data = getPreferences(this.preferenceProviderProvider, roots); this.proxy.$acceptConfigurationChanged(data, { preferenceName: e.preferenceName, newValue: e.newValue @@ -59,14 +73,14 @@ export class PreferenceRegistryMainImpl implements PreferenceRegistryMain { } // tslint:disable-next-line:no-any - $updateConfigurationOption(target: boolean | ConfigurationTarget | undefined, key: string, value: any): PromiseLike { + $updateConfigurationOption(target: boolean | ConfigurationTarget | undefined, key: string, value: any, resource?: string): PromiseLike { const scope = this.parseConfigurationTarget(target); - return this.preferenceService.set(key, value, scope); + return this.preferenceService.set(key, value, scope, resource); } - $removeConfigurationOption(target: boolean | ConfigurationTarget | undefined, key: string): PromiseLike { + $removeConfigurationOption(target: boolean | ConfigurationTarget | undefined, key: string, resource?: string): PromiseLike { const scope = this.parseConfigurationTarget(target); - return this.preferenceService.set(key, undefined, scope); + return this.preferenceService.set(key, undefined, scope, resource); } private parseConfigurationTarget(arg?: boolean | ConfigurationTarget): PreferenceScope { diff --git a/packages/plugin-ext/src/plugin/preference-registry.ts b/packages/plugin-ext/src/plugin/preference-registry.ts index 0e4385f51ef7f..0ea9312ff4735 100644 --- a/packages/plugin-ext/src/plugin/preference-registry.ts +++ b/packages/plugin-ext/src/plugin/preference-registry.ts @@ -41,6 +41,7 @@ enum PreferenceScope { Default, User, Workspace, + Folder, } interface ConfigurationInspect { @@ -90,9 +91,10 @@ export class PreferenceRegistryExtImpl implements PreferenceRegistryExt { } getConfiguration(section?: string, resource?: theia.Uri | null, extensionId?: string): theia.WorkspaceConfiguration { + resource = resource === null ? undefined : resource; const preferences = this.toReadonlyValue(section - ? lookUp(this._preferences.getValue(), section) - : this._preferences.getValue()); + ? lookUp(this._preferences.getValue(undefined, this.workspace, resource), section) + : this._preferences.getValue(undefined, this.workspace, resource)); const configuration: theia.WorkspaceConfiguration = { has(key: string): boolean { @@ -152,16 +154,17 @@ export class PreferenceRegistryExtImpl implements PreferenceRegistryExt { }, update: (key: string, value: any, arg?: ConfigurationTarget | boolean): PromiseLike => { key = section ? `${section}.${key}` : key; + const resourceStr: string | undefined = resource ? resource.toString() : undefined; if (typeof value !== 'undefined') { - return this.proxy.$updateConfigurationOption(arg, key, value, resource); + return this.proxy.$updateConfigurationOption(arg, key, value, resourceStr); } else { - return this.proxy.$removeConfigurationOption(arg, key, resource); + return this.proxy.$removeConfigurationOption(arg, key, resourceStr); } }, inspect: (key: string): ConfigurationInspect => { key = section ? `${section}.${key}` : key; resource = resource === null ? undefined : resource; - const result = cloneDeep(this._preferences.inspect(key, this.workspace)); + const result = cloneDeep(this._preferences.inspect(key, this.workspace, resource)); if (!result) { return undefined!; @@ -177,6 +180,9 @@ export class PreferenceRegistryExtImpl implements PreferenceRegistryExt { if (result.workspace) { configInspect.workspaceValue = result.workspace; } + if (result.workspaceFolder) { + configInspect.workspaceFolderValue = result.workspaceFolder; + } return configInspect; } }; @@ -215,7 +221,11 @@ export class PreferenceRegistryExtImpl implements PreferenceRegistryExt { const defaultConfiguration = this.getConfigurationModel(data[PreferenceScope.Default]); const userConfiguration = this.getConfigurationModel(data[PreferenceScope.User]); const workspaceConfiguration = this.getConfigurationModel(data[PreferenceScope.Workspace]); - return new Configuration(defaultConfiguration, userConfiguration, workspaceConfiguration); + const folderConfigurations = {} as { [resource: string]: ConfigurationModel }; + Object.keys(data[PreferenceScope.Folder]).forEach(resource => { + folderConfigurations[resource] = this.getConfigurationModel(data[PreferenceScope.Folder][resource]); + }); + return new Configuration(defaultConfiguration, userConfiguration, workspaceConfiguration, folderConfigurations); } private getConfigurationModel(data: { [key: string]: any }): ConfigurationModel { diff --git a/packages/plugin-ext/src/plugin/preferences/configuration.spec.ts b/packages/plugin-ext/src/plugin/preferences/configuration.spec.ts new file mode 100644 index 0000000000000..4b5d3763bf92b --- /dev/null +++ b/packages/plugin-ext/src/plugin/preferences/configuration.spec.ts @@ -0,0 +1,250 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as chai from 'chai'; +import { Configuration, ConfigurationModel } from './configuration'; +import { PreferenceData } from '../../common'; +import { PreferenceScope } from '@theia/preferences/lib/browser'; +import { WorkspaceExtImpl } from '../workspace'; +import URI from 'vscode-uri'; + +const expect = chai.expect; + +interface Inspect { + default: C; + user: C; + workspace?: C; + workspaceFolder?: C; + value: C; +} +let inspect: Inspect; + +const projects = ['/projects/workspace/project1', '/projects/workspace/project2']; + +const propertyName = 'tabSize'; +const preferences: PreferenceData = { + [PreferenceScope.Default]: { + [propertyName]: 6, + }, + [PreferenceScope.User]: { + [propertyName]: 5 + }, + [PreferenceScope.Workspace]: { + [propertyName]: 4 + }, + [PreferenceScope.Folder]: { + [projects[0]]: { + [propertyName]: 3 + }, + [projects[1]]: { + [propertyName]: 2 + } + } +}; + +const workspace: WorkspaceExtImpl = {} as WorkspaceExtImpl; +let configuration: Configuration; +let defaultConfiguration: ConfigurationModel; +let userConfiguration: ConfigurationModel; +let workspaceConfiguration: ConfigurationModel; +let folderConfigurations: { [key: string]: ConfigurationModel }; +before(() => { + workspace.getWorkspaceFolder = (uri => { + const name = uri.toString().replace(/[^\/]+$/, '$1'); + const index = projects.indexOf(uri.toString()); + return { uri, name, index }; + }); + + defaultConfiguration = new ConfigurationModel( + preferences[PreferenceScope.Default], + Object.keys(preferences[PreferenceScope.Default]) + ); + userConfiguration = new ConfigurationModel( + preferences[PreferenceScope.User], + Object.keys(preferences[PreferenceScope.User]) + ); + workspaceConfiguration = new ConfigurationModel( + preferences[PreferenceScope.Workspace], + Object.keys(preferences[PreferenceScope.Workspace]) + ); + folderConfigurations = projects.reduce((configurations: { [key: string]: ConfigurationModel }, project: string) => { + const folderPrefs = preferences[PreferenceScope.Folder][project]; + configurations[project] = new ConfigurationModel(folderPrefs, Object.keys(folderPrefs)); + return configurations; + }, {}); +}); + +describe('Configuration:', () => { + + describe('Default scope preferences:', () => { + + beforeEach(() => { + configuration = new Configuration( + defaultConfiguration, new ConfigurationModel({}, []), undefined, undefined + ); + inspect = configuration.inspect(propertyName, workspace, undefined); + }); + + it('should have correct value of \'default\' property', () => { + expect(inspect).to.have.property( + 'default', + preferences[PreferenceScope.Default][propertyName] + ); + expect(inspect.default).to.equal(preferences[PreferenceScope.Default][propertyName]); + }); + + it('should have correct value of \'value\' property', () => { + expect(inspect).to.have.property( + 'value', + preferences[PreferenceScope.Default][propertyName] + ); + expect(inspect.value).to.equal(preferences[PreferenceScope.Default][propertyName]); + }); + + }); + + describe('User scope preferences:', () => { + + beforeEach(() => { + configuration = new Configuration( + defaultConfiguration, userConfiguration, undefined, undefined + ); + inspect = configuration.inspect(propertyName, workspace, undefined); + }); + + it('should have correct value of \'default\' property', () => { + expect(inspect).to.have.property( + 'default', + preferences[PreferenceScope.Default][propertyName] + ); + expect(inspect.default).to.equal(preferences[PreferenceScope.Default][propertyName]); + }); + + it('should have correct value of \'user\' property', () => { + expect(inspect).to.have.property( + 'user', + preferences[PreferenceScope.User][propertyName] + ); + expect(inspect.user).to.equal(preferences[PreferenceScope.User][propertyName]); + }); + + it('should have correct value of \'value\' property', () => { + expect(inspect).to.have.property( + 'value', + preferences[PreferenceScope.User][propertyName] + ); + expect(inspect.value).to.equal(preferences[PreferenceScope.User][propertyName]); + }); + + }); + + describe('Workspace scope preferences:', () => { + + beforeEach(() => { + configuration = new Configuration( + defaultConfiguration, userConfiguration, workspaceConfiguration, undefined + ); + inspect = configuration.inspect(propertyName, workspace, undefined); + }); + + it('should have correct value of \'default\' property', () => { + expect(inspect).to.have.property( + 'default', + preferences[PreferenceScope.Default][propertyName] + ); + expect(inspect.default).to.equal(preferences[PreferenceScope.Default][propertyName]); + }); + + it('should have correct value of \'user\' property', () => { + expect(inspect).to.have.property( + 'user', + preferences[PreferenceScope.User][propertyName] + ); + expect(inspect.user).to.equal(preferences[PreferenceScope.User][propertyName]); + }); + + it('should have correct value of \'workspace\' property', () => { + expect(inspect).to.have.property( + 'workspace', + preferences[PreferenceScope.Workspace][propertyName] + ); + expect(inspect.workspace).to.equal(preferences[PreferenceScope.Workspace][propertyName]); + }); + + it('should have correct value of \'value\' property', () => { + expect(inspect).to.have.property( + 'value', + preferences[PreferenceScope.Workspace][propertyName] + ); + expect(inspect.value).to.equal(preferences[PreferenceScope.Workspace][propertyName]); + }); + + }); + + describe('Folder scope preferences:', () => { + const project = projects[0]; + + beforeEach(() => { + configuration = new Configuration( + defaultConfiguration, userConfiguration, workspaceConfiguration, folderConfigurations + ); + const resource = URI.revive({ path: project }); + inspect = configuration.inspect(propertyName, workspace, resource); + }); + + it('should have correct value of \'default\' property', () => { + expect(inspect).to.have.property( + 'default', + preferences[PreferenceScope.Default][propertyName] + ); + expect(inspect.default).to.equal(preferences[PreferenceScope.Default][propertyName]); + }); + + it('should have correct value of \'user\' property', () => { + expect(inspect).to.have.property( + 'user', + preferences[PreferenceScope.User][propertyName] + ); + expect(inspect.user).to.equal(preferences[PreferenceScope.User][propertyName]); + }); + + it('should have correct value of \'workspace\' property', () => { + expect(inspect).to.have.property( + 'workspace', + preferences[PreferenceScope.Workspace][propertyName] + ); + expect(inspect.workspace).to.equal(preferences[PreferenceScope.Workspace][propertyName]); + }); + + it('should have correct value of \'workspaceFolder\' property', () => { + expect(inspect).to.have.property( + 'workspaceFolder', + preferences[PreferenceScope.Folder][project][propertyName] + ); + expect(inspect.workspaceFolder).to.equal(preferences[PreferenceScope.Folder][project][propertyName]); + }); + + it('should have correct value of \'value\' property', () => { + expect(inspect).to.have.property( + 'value', + preferences[PreferenceScope.Folder][project][propertyName] + ); + expect(inspect.value).to.equal(preferences[PreferenceScope.Folder][project][propertyName]); + }); + + }); + +}); diff --git a/packages/plugin-ext/src/plugin/preferences/configuration.ts b/packages/plugin-ext/src/plugin/preferences/configuration.ts index 4030349426454..55a44b84ca0d0 100644 --- a/packages/plugin-ext/src/plugin/preferences/configuration.ts +++ b/packages/plugin-ext/src/plugin/preferences/configuration.ts @@ -17,43 +17,92 @@ import { WorkspaceExtImpl } from '../workspace'; import { isObject } from '../../common/types'; import cloneDeep = require('lodash.clonedeep'); +import URI from 'vscode-uri'; /* tslint:disable:no-any */ export class Configuration { - private configuration: ConfigurationModel | undefined; + private combinedConfig: ConfigurationModel | undefined; + private folderCombinedConfigs: { [resource: string]: ConfigurationModel } = {}; constructor( private defaultConfiguration: ConfigurationModel, private userConfiguration: ConfigurationModel, private workspaceConfiguration: ConfigurationModel = new ConfigurationModel(), + private folderConfigurations: { [resource: string]: ConfigurationModel } = {}, ) { } - getValue(section?: string): any { - return this.getCombined().getValue(section); + getValue(section: string | undefined, workspace: WorkspaceExtImpl, resource?: URI): any { + return this.getCombinedResourceConfig(workspace, resource).getValue(section); } - inspect(key: string, workspace: WorkspaceExtImpl): { + inspect(key: string, workspace: WorkspaceExtImpl, resource?: URI): { default: C, user: C, workspace: C | undefined, + workspaceFolder: C | undefined, value: C, } { - const combinedConfiguration = this.getCombined(); + const combinedConfiguration = this.getCombinedResourceConfig(workspace, resource); + const folderConfiguration = this.getFolderResourceConfig(workspace, resource); return { default: this.defaultConfiguration.getValue(key), user: this.userConfiguration.getValue(key), workspace: workspace ? this.workspaceConfiguration.getValue(key) : void 0, + workspaceFolder: folderConfiguration ? folderConfiguration.getValue(key) : void 0, value: combinedConfiguration.getValue(key) }; } - private getCombined(): ConfigurationModel { - if (!this.configuration) { - this.configuration = this.defaultConfiguration.merge(this.userConfiguration, this.workspaceConfiguration); + private getCombinedResourceConfig(workspace: WorkspaceExtImpl, resource?: URI): ConfigurationModel { + const combinedConfig = this.getCombinedConfig(); + if (!workspace || !resource) { + return combinedConfig; + } + + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (!workspaceFolder) { + return combinedConfig; + } + + return this.getFolderCombinedConfig(workspaceFolder.uri.toString()) || combinedConfig; + } + + private getCombinedConfig(): ConfigurationModel { + if (!this.combinedConfig) { + this.combinedConfig = this.defaultConfiguration.merge(this.userConfiguration, this.workspaceConfiguration); + } + return this.combinedConfig; + } + + private getFolderCombinedConfig(folder: string): ConfigurationModel | undefined { + if (this.folderCombinedConfigs[folder]) { + return this.folderCombinedConfigs[folder]; + } + + const combinedConfig = this.getCombinedConfig(); + const folderConfig = this.folderConfigurations[folder]; + if (!folderConfig) { + return combinedConfig; + } + + const folderCombinedConfig = combinedConfig.merge(folderConfig); + this.folderCombinedConfigs[folder] = folderCombinedConfig; + + return folderCombinedConfig; + } + + private getFolderResourceConfig(workspace: WorkspaceExtImpl, resource?: URI): ConfigurationModel | undefined { + if (!workspace || !resource) { + return; + } + + const workspaceFolder = workspace.getWorkspaceFolder(resource); + if (!workspaceFolder) { + return; } - return this.configuration; + return this.folderConfigurations[workspaceFolder.uri.toString()]; } } From 4dd3b91be5fdad104c1e8b28ba453d8681ce98b0 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 25 Mar 2019 16:32:07 +0200 Subject: [PATCH 2/5] Handle properties that are not valid at the time of reading. This allows to re-validate such properties when a corresponding schema has been set. Signed-off-by: Oleksii Kurinnyi --- .../preferences/preference-provider.ts | 3 + .../abstract-resource-preference-provider.ts | 54 ++++++-- .../src/browser/preference-service.spec.ts | 119 +++++++++++++++++- 3 files changed, 165 insertions(+), 11 deletions(-) diff --git a/packages/core/src/browser/preferences/preference-provider.ts b/packages/core/src/browser/preferences/preference-provider.ts index d5ac504153a32..427e0383441a2 100644 --- a/packages/core/src/browser/preferences/preference-provider.ts +++ b/packages/core/src/browser/preferences/preference-provider.ts @@ -47,6 +47,9 @@ export abstract class PreferenceProvider implements Disposable { protected readonly onDidPreferencesChangedEmitter = new Emitter(); readonly onDidPreferencesChanged: Event = this.onDidPreferencesChangedEmitter.event; + protected readonly onDidNotValidPreferencesReadEmitter = new Emitter(); + readonly onDidNotValidPreferencesRead: Event = this.onDidNotValidPreferencesReadEmitter.event; + protected readonly toDispose = new DisposableCollection(); /** diff --git a/packages/preferences/src/browser/abstract-resource-preference-provider.ts b/packages/preferences/src/browser/abstract-resource-preference-provider.ts index aff488ced08bb..79268ffa3f061 100644 --- a/packages/preferences/src/browser/abstract-resource-preference-provider.ts +++ b/packages/preferences/src/browser/abstract-resource-preference-provider.ts @@ -26,6 +26,8 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi // tslint:disable-next-line:no-any protected preferences: { [key: string]: any } = {}; + // tslint:disable-next-line:no-any + protected notValidPreferences: { [key: string]: any } = {}; protected resource: Promise; protected toDisposeOnWorkspaceLocationChanged: DisposableCollection = new DisposableCollection(); @@ -59,6 +61,10 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi this.toDisposeOnWorkspaceLocationChanged.pushAll([onDidResourceChanged, (await this.resource)]); this.toDispose.push(onDidResourceChanged); } + + this.schemaProvider.onDidPreferenceSchemaChanged(() => { + this.handleNotValidPreferences(); + }); } abstract getUri(root?: URI): MaybePromise; @@ -103,8 +109,13 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi protected async readPreferences(): Promise { const newContent = await this.readContents(); - const newPrefs = await this.getParsedContent(newContent); - await this.handlePreferenceChanges(newPrefs); + const parsedData = this.parse(newContent); + const newPrefs = await this.getValidatedPreferences(parsedData); + await this.handlePreferenceChanges(newPrefs.valid); + this.notValidPreferences = newPrefs.notValid; + if (this.notValidPreferences && Object.keys(this.notValidPreferences).length > 0) { + this.onDidNotValidPreferencesReadEmitter.fire(this.notValidPreferences); + } } protected async readContents(): Promise { @@ -117,29 +128,33 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi } // tslint:disable-next-line:no-any - protected async getParsedContent(content: string): Promise<{ [key: string]: any }> { - const jsonData = this.parse(content); + protected async getValidatedPreferences(jsonData: { [key: string]: any }): Promise<{ valid: any, notValid: any }> { // tslint:disable-next-line:no-any - const preferences: { [key: string]: any } = {}; + const preferences: { valid: any, notValid: any } = { + valid: {}, + notValid: {} + }; if (typeof jsonData !== 'object') { return preferences; } const uri = (await this.resource).uri.toString(); + this.notValidPreferences = {}; // tslint:disable-next-line:forin for (const preferenceName in jsonData) { const preferenceValue = jsonData[preferenceName]; - if (preferenceValue !== undefined && !this.schemaProvider.validate(preferenceName, preferenceValue)) { + if (this.isNotValid(preferenceName, preferenceValue)) { console.warn(`Preference ${preferenceName} in ${uri} is invalid.`); + preferences.notValid[preferenceName] = preferenceValue; continue; } if (this.schemaProvider.testOverrideValue(preferenceName, preferenceValue)) { // tslint:disable-next-line:forin for (const overriddenPreferenceName in preferenceValue) { const overriddeValue = preferenceValue[overriddenPreferenceName]; - preferences[`${preferenceName}.${overriddenPreferenceName}`] = overriddeValue; + preferences.valid[`${preferenceName}.${overriddenPreferenceName}`] = overriddeValue; } } else { - preferences[preferenceName] = preferenceValue; + preferences.valid[preferenceName] = preferenceValue; } } return preferences; @@ -151,6 +166,11 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi return jsoncparser.parse(strippedContent); } + // tslint:disable-next-line:no-any + protected isNotValid(preferenceName: string, preferenceValue: any): boolean { + return (preferenceValue !== undefined && !this.schemaProvider.validate(preferenceName, preferenceValue)); + } + // tslint:disable-next-line:no-any protected async handlePreferenceChanges(newPrefs: { [key: string]: any }): Promise { const oldPrefs = Object.assign({}, this.preferences); @@ -184,6 +204,24 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi } } + protected async handleNotValidPreferences(): Promise { + const notValidPreferencesKeys = Object.keys(this.notValidPreferences); + if (notValidPreferencesKeys.length === 0) { + return; + } + + const validPrefs = Object.assign({}, this.preferences); + const newPrefs = notValidPreferencesKeys + .filter(prefName => !this.isNotValid(prefName, this.notValidPreferences[prefName])) + .reduce((prefs, prefName) => { + prefs[prefName] = this.notValidPreferences[prefName]; + delete this.notValidPreferences[prefName]; + return prefs; + }, validPrefs); + + return this.handlePreferenceChanges(newPrefs); + } + dispose(): void { const prefChanges: PreferenceProviderDataChange[] = []; for (const prefName of Object.keys(this.preferences)) { diff --git a/packages/preferences/src/browser/preference-service.spec.ts b/packages/preferences/src/browser/preference-service.spec.ts index c85cfbfc49983..49a58ff15c603 100644 --- a/packages/preferences/src/browser/preference-service.spec.ts +++ b/packages/preferences/src/browser/preference-service.spec.ts @@ -28,8 +28,15 @@ import * as fs from 'fs-extra'; import * as temp from 'temp'; import { Emitter } from '@theia/core/lib/common'; import { - PreferenceService, PreferenceScope, PreferenceProviderDataChanges, - PreferenceSchemaProvider, PreferenceProviderProvider, PreferenceServiceImpl, bindPreferenceSchemaProvider, PreferenceChange, PreferenceSchema + PreferenceService, + PreferenceScope, + PreferenceProviderDataChanges, + PreferenceSchemaProvider, + PreferenceProviderProvider, + PreferenceServiceImpl, + bindPreferenceSchemaProvider, + PreferenceChange, + PreferenceSchema } from '@theia/core/lib/browser/preferences'; import { FileSystem, FileShouldOverwrite, FileStat } from '@theia/filesystem/lib/common/'; import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; @@ -65,6 +72,7 @@ const tempPath = temp.track().openSync().path; const mockUserPreferenceEmitter = new Emitter(); const mockWorkspacePreferenceEmitter = new Emitter(); const mockFolderPreferenceEmitter = new Emitter(); +let mockOnDidUserPreferencesChanged: sinon.SinonStub; function testContainerSetup() { testContainer = new Container(); @@ -89,7 +97,7 @@ function testContainerSetup() { switch (scope) { case PreferenceScope.User: const userProvider = ctx.container.get(UserPreferenceProvider); - sinon.stub(userProvider, 'onDidPreferencesChanged').get(() => + mockOnDidUserPreferencesChanged = sinon.stub(userProvider, 'onDidPreferencesChanged').get(() => mockUserPreferenceEmitter.event ); return userProvider; @@ -693,4 +701,109 @@ describe('Preference Service', () => { }); + describe('user preference provider', () => { + const userConfigStr = `{ + "myProp": "property value", + "launch": { + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "User scope: Debug (Attach)", + "processId": "" + } + ] + } +} +`; + const userConfig = JSON.parse(userConfigStr); + + let userProvider: UserPreferenceProvider; + beforeEach(async () => { + userProvider = testContainer.get(UserPreferenceProvider); + await userProvider.ready; + }); + + afterEach(() => { + testContainer.rebind(UserPreferenceProvider).toSelf().inSingletonScope(); + }); + + describe('when schema for `launch` property has not been set yet', () => { + + beforeEach(() => { + stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); + stubs.push(sinon.stub(prefSchema, 'validate').callsFake(prefName => { + if (prefName === 'myProp') { + return true; + } + return false; + })); + }); + + it('should fire "onDidLaunchChanged" event with correct argument', async () => { + const spy = sinon.spy(); + userProvider.onDidNotValidPreferencesRead(spy); + + fs.writeFileSync(tempPath, userConfigStr); + await (userProvider).readPreferences(); + + expect(spy.calledWith({ launch: userConfig.launch })).to.be.true; + }); + + it('should fire "onDidPreferencesChanged" with correct argument', async () => { + + const spy = sinon.spy(); + mockOnDidUserPreferencesChanged.restore(); + userProvider.onDidPreferencesChanged(spy); + + fs.writeFileSync(tempPath, userConfigStr); + await (userProvider).readPreferences(); + + expect(spy.called, 'spy should be called').to.be.true; + + const firstCallArgs = spy.args[0]; + expect(firstCallArgs[0], 'argument should have property "myProp"').to.have.property('myProp'); + expect(firstCallArgs[0], 'argument shouldn\'t have property "launch"').not.to.have.property('launch'); + }); + + }); + + describe('when schema for `launch` property has been already set', () => { + + beforeEach(() => { + stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); + stubs.push(sinon.stub(prefSchema, 'validate').returns(true)); + }); + + it('should not fire "onDidLaunchChanged"', async () => { + const spy = sinon.spy(); + userProvider.onDidNotValidPreferencesRead(spy); + + fs.writeFileSync(tempPath, userConfigStr); + await (userProvider).readPreferences(); + + expect(spy.notCalled).to.be.true; + }); + + it('should fire "onDidPreferencesChanged" with correct argument', async () => { + const spy = sinon.spy(); + mockOnDidUserPreferencesChanged.restore(); + userProvider.onDidPreferencesChanged(spy); + + fs.writeFileSync(tempPath, userConfigStr); + await (userProvider).readPreferences(); + + expect(spy.called, 'spy should be called').to.be.true; + + const firstCallArgs = spy.args[0]; + expect(firstCallArgs[0], 'argument should have property "myProp"').to.have.property('myProp'); + expect(firstCallArgs[0], 'argument should have property "launch"').to.have.property('launch'); + expect(firstCallArgs[0].launch.newValue, 'property "launch" should have correct "newValue"').to.deep.equal(userConfig.launch); + }); + + }); + + }); + }); From f9634e8e2e4bde11963af2d8bf7a60fdba0658e0 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 25 Mar 2019 16:58:19 +0200 Subject: [PATCH 3/5] Added preference provider to handle 'launch.json' Signed-off-by: Oleksii Kurinnyi --- .../folder-launch-preference-provider.ts | 40 +++++++ .../src/browser/folder-preference-provider.ts | 3 +- .../browser/folders-preferences-provider.ts | 104 +++++++++++------- .../src/browser/preference-frontend-module.ts | 10 +- 4 files changed, 116 insertions(+), 41 deletions(-) create mode 100644 packages/preferences/src/browser/folder-launch-preference-provider.ts diff --git a/packages/preferences/src/browser/folder-launch-preference-provider.ts b/packages/preferences/src/browser/folder-launch-preference-provider.ts new file mode 100644 index 0000000000000..34f2ca4161c73 --- /dev/null +++ b/packages/preferences/src/browser/folder-launch-preference-provider.ts @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { FolderPreferenceProvider } from './folder-preference-provider'; + +@injectable() +export class FolderLaunchPreferenceProvider extends FolderPreferenceProvider { + + async getUri(): Promise { + this.folderUri = new URI(this.options.folder.uri); + if (await this.fileSystem.exists(this.folderUri.toString())) { + const uri = this.folderUri.resolve('.theia').resolve('launch.json'); + return uri; + } + } + + // tslint:disable-next-line:no-any + protected parse(content: string): any { + const parsedData = super.parse(content); + if (Object.keys(parsedData).length > 0) { + return { launch: parsedData }; + } + return parsedData; + } +} diff --git a/packages/preferences/src/browser/folder-preference-provider.ts b/packages/preferences/src/browser/folder-preference-provider.ts index 4f70f55af4ae2..60b9db202a946 100644 --- a/packages/preferences/src/browser/folder-preference-provider.ts +++ b/packages/preferences/src/browser/folder-preference-provider.ts @@ -28,12 +28,13 @@ export interface FolderPreferenceProviderFactory { export const FolderPreferenceProviderOptions = Symbol('FolderPreferenceProviderOptions'); export interface FolderPreferenceProviderOptions { folder: FileStat; + fileName: string; } @injectable() export class FolderPreferenceProvider extends AbstractResourcePreferenceProvider { - private folderUri: URI | undefined; + protected folderUri: URI | undefined; constructor( @inject(FolderPreferenceProviderOptions) protected readonly options: FolderPreferenceProviderOptions, diff --git a/packages/preferences/src/browser/folders-preferences-provider.ts b/packages/preferences/src/browser/folders-preferences-provider.ts index 16ed7eed08ea3..d40ea3a51da1d 100644 --- a/packages/preferences/src/browser/folders-preferences-provider.ts +++ b/packages/preferences/src/browser/folders-preferences-provider.ts @@ -21,6 +21,10 @@ import { FolderPreferenceProvider, FolderPreferenceProviderFactory } from './fol import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import URI from '@theia/core/lib/common/uri'; +export const SETTINGS_FILE_NAME = 'settings.json'; +export const LAUNCH_FILE_NAME = 'launch.json'; +export const LAUNCH_PROPERTY_NAME = 'launch'; + @injectable() export class FoldersPreferencesProvider extends PreferenceProvider { @@ -28,69 +32,86 @@ export class FoldersPreferencesProvider extends PreferenceProvider { @inject(FileSystem) protected readonly fileSystem: FileSystem; @inject(FolderPreferenceProviderFactory) protected readonly folderPreferenceProviderFactory: FolderPreferenceProviderFactory; - private providers: FolderPreferenceProvider[] = []; + private providers: { [fileName: string]: FolderPreferenceProvider[] } = {}; @postConstruct() protected async init(): Promise { + [SETTINGS_FILE_NAME, LAUNCH_FILE_NAME].forEach(fileName => this.providers[fileName] = []); + await this.workspaceService.roots; - if (this.workspaceService.saved) { - for (const root of this.workspaceService.tryGetRoots()) { - if (await this.fileSystem.exists(root.uri)) { - const provider = this.createFolderPreferenceProvider(root); - this.providers.push(provider); - } + const readyPromises: Promise[] = []; + for (const root of this.workspaceService.tryGetRoots()) { + if (await this.fileSystem.exists(root.uri)) { + [SETTINGS_FILE_NAME, LAUNCH_FILE_NAME].forEach(fileName => { + const provider = this.createFolderPreferenceProvider(root, fileName); + this.providers[fileName].push(provider); + readyPromises.push(provider.ready); + }); } } // Try to read the initial content of the preferences. The provider // becomes ready even if we fail reading the preferences, so we don't // hang the preference service. - Promise.all(this.providers.map(p => p.ready)) + Promise.all(readyPromises) .then(() => this._ready.resolve()) .catch(() => this._ready.resolve()); this.workspaceService.onWorkspaceChanged(roots => { for (const root of roots) { - if (!this.existsProvider(root.uri)) { - const provider = this.createFolderPreferenceProvider(root); - if (!this.existsProvider(root.uri)) { - this.providers.push(provider); - } else { - provider.dispose(); + [SETTINGS_FILE_NAME, LAUNCH_FILE_NAME].forEach(fileName => { + if (!this.existsProvider(root.uri, fileName)) { + const provider = this.createFolderPreferenceProvider(root, fileName); + this.providers[fileName].push(provider); } - } + }); } - const numProviders = this.providers.length; - for (let ind = numProviders - 1; ind >= 0; ind--) { - const provider = this.providers[ind]; - if (roots.findIndex(r => !!provider.uri && r.uri === provider.uri.toString()) < 0) { - this.providers.splice(ind, 1); - provider.dispose(); + [SETTINGS_FILE_NAME, LAUNCH_FILE_NAME].forEach(fileName => { + if (!this.providers[fileName]) { + return; } - } + const numProviders = this.providers[fileName].length; + for (let i = numProviders - 1; i >= 0; i--) { + const provider = this.providers[fileName][i]; + if (!this.existsRoot(roots, provider)) { + this.providers[fileName].splice(i, 1); + provider.dispose(); + } + } + }); }); } - private existsProvider(folderUri: string): boolean { - return this.providers.findIndex(p => !!p.uri && p.uri.toString() === folderUri) >= 0; + private existsProvider(folderUri: string, fileName: string): boolean { + return this.providers[fileName] && this.providers[fileName].some(p => !!p.uri && p.uri.toString() === folderUri); + } + + private existsRoot(roots: FileStat[], provider: FolderPreferenceProvider): boolean { + return roots.some(r => !!provider.uri && r.uri === provider.uri.toString()); } // tslint:disable-next-line:no-any getPreferences(resourceUri?: string): { [p: string]: any } { - const numProviders = this.providers.length; - if (resourceUri && numProviders > 0) { - const provider = this.getProvider(resourceUri); - if (provider) { - return provider.getPreferences(); - } + if (!resourceUri) { + return {}; } - return {}; + + const prefProvider = this.getProvider(resourceUri, SETTINGS_FILE_NAME); + const prefs = prefProvider ? prefProvider.getPreferences() : {}; + + const launchProvider = this.getProvider(resourceUri, LAUNCH_FILE_NAME); + const launch = launchProvider ? launchProvider.getPreferences() : {}; + + const result = Object.assign({}, prefs, launch); + + return result; } canProvide(preferenceName: string, resourceUri?: string): { priority: number, provider: PreferenceProvider } { - if (resourceUri && this.providers.length > 0) { - const provider = this.getProvider(resourceUri); + if (resourceUri) { + const resourceName = preferenceName === LAUNCH_PROPERTY_NAME ? LAUNCH_FILE_NAME : SETTINGS_FILE_NAME; + const provider = this.getProvider(resourceUri, resourceName); if (provider) { return { priority: provider.canProvide(preferenceName, resourceUri).priority, provider }; } @@ -98,10 +119,15 @@ export class FoldersPreferencesProvider extends PreferenceProvider { return super.canProvide(preferenceName, resourceUri); } - protected getProvider(resourceUri: string): PreferenceProvider | undefined { + protected getProvider(resourceUri: string, fileName: string): PreferenceProvider | undefined { + const providers = this.providers[fileName]; + if (providers.length === 0) { + return; + } + let provider: PreferenceProvider | undefined; let relativity = Number.MAX_SAFE_INTEGER; - for (const p of this.providers) { + for (const p of providers) { if (p.uri) { const providerRelativity = p.uri.path.relativity(new URI(resourceUri).path); if (providerRelativity >= 0 && providerRelativity <= relativity) { @@ -113,17 +139,19 @@ export class FoldersPreferencesProvider extends PreferenceProvider { return provider; } - protected createFolderPreferenceProvider(folder: FileStat): FolderPreferenceProvider { - const provider = this.folderPreferenceProviderFactory({ folder }); + protected createFolderPreferenceProvider(folder: FileStat, fileName: string): FolderPreferenceProvider { + const provider = this.folderPreferenceProviderFactory({ folder, fileName }); this.toDispose.push(provider); this.toDispose.push(provider.onDidPreferencesChanged(change => this.onDidPreferencesChangedEmitter.fire(change))); + this.toDispose.push(provider.onDidNotValidPreferencesRead(prefs => this.onDidNotValidPreferencesReadEmitter.fire(prefs))); return provider; } // tslint:disable-next-line:no-any async setPreference(key: string, value: any, resourceUri?: string): Promise { if (resourceUri) { - for (const provider of this.providers) { + const resourceName = key === LAUNCH_PROPERTY_NAME ? LAUNCH_FILE_NAME : SETTINGS_FILE_NAME; + for (const provider of this.providers[resourceName]) { const providerResourceUri = await provider.getUri(); if (providerResourceUri && providerResourceUri.toString() === resourceUri) { return provider.setPreference(key, value); diff --git a/packages/preferences/src/browser/preference-frontend-module.ts b/packages/preferences/src/browser/preference-frontend-module.ts index ffc69bf4bb1f8..e4ba2c74e150d 100644 --- a/packages/preferences/src/browser/preference-frontend-module.ts +++ b/packages/preferences/src/browser/preference-frontend-module.ts @@ -24,8 +24,9 @@ import { createPreferencesTreeWidget } from './preference-tree-container'; import { PreferencesMenuFactory } from './preferences-menu-factory'; import { PreferencesFrontendApplicationContribution } from './preferences-frontend-application-contribution'; import { PreferencesContainer, PreferencesTreeWidget, PreferencesEditorsContainer } from './preferences-tree-widget'; -import { FoldersPreferencesProvider } from './folders-preferences-provider'; +import { FoldersPreferencesProvider, SETTINGS_FILE_NAME } from './folders-preferences-provider'; import { FolderPreferenceProvider, FolderPreferenceProviderFactory, FolderPreferenceProviderOptions } from './folder-preference-provider'; +import { FolderLaunchPreferenceProvider } from './folder-launch-preference-provider'; import './preferences-monaco-contribution'; @@ -36,12 +37,17 @@ export function bindPreferences(bind: interfaces.Bind, unbind: interfaces.Unbind bind(PreferenceProvider).to(WorkspacePreferenceProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Workspace); bind(PreferenceProvider).to(FoldersPreferencesProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Folder); bind(FolderPreferenceProvider).toSelf().inTransientScope(); + bind(FolderLaunchPreferenceProvider).toSelf().inTransientScope(); bind(FolderPreferenceProviderFactory).toFactory(ctx => (options: FolderPreferenceProviderOptions) => { const child = new Container({ defaultScope: 'Transient' }); child.parent = ctx.container; child.bind(FolderPreferenceProviderOptions).toConstantValue(options); - return child.get(FolderPreferenceProvider); + if (options.fileName === SETTINGS_FILE_NAME) { + return child.get(FolderPreferenceProvider); + } else { + return child.get(FolderLaunchPreferenceProvider); + } } ); From 73f89c9d9605a4ad083c4d5b96087878658376ff Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Tue, 26 Mar 2019 13:59:12 +0200 Subject: [PATCH 4/5] Handle changing of 'launch' property in order to re-build preference schema `UserLaunchProvider`, `WorkspaceLaunchProvider` and `FoldersLaunchProvider` handle changing of 'launch' section of preferences without paying attention whether configuration is valid or not. Once it happens a new preference schema for launch configurations builds. Signed-off-by: Oleksii Kurinnyi --- .../preferences/preference-contribution.ts | 24 ++- .../abstract-launch-preference-provider.ts | 157 ++++++++++++++++++ .../src/browser/debug-frontend-module.ts | 12 +- .../debug/src/browser/debug-preferences.ts | 4 +- .../debug/src/browser/debug-schema-updater.ts | 96 ++++++++++- .../src/browser/folders-launch-provider.ts | 128 ++++++++++++++ .../debug/src/browser/user-launch-provider.ts | 27 +++ .../src/browser/workspace-launch-provider.ts | 27 +++ 8 files changed, 463 insertions(+), 12 deletions(-) create mode 100644 packages/debug/src/browser/abstract-launch-preference-provider.ts create mode 100644 packages/debug/src/browser/folders-launch-provider.ts create mode 100644 packages/debug/src/browser/user-launch-provider.ts create mode 100644 packages/debug/src/browser/workspace-launch-provider.ts diff --git a/packages/core/src/browser/preferences/preference-contribution.ts b/packages/core/src/browser/preferences/preference-contribution.ts index bb2d587b3193f..3626847a75ed4 100644 --- a/packages/core/src/browser/preferences/preference-contribution.ts +++ b/packages/core/src/browser/preferences/preference-contribution.ts @@ -19,6 +19,7 @@ import { inject, injectable, interfaces, named, postConstruct } from 'inversify' import { ContributionProvider, bindContributionProvider, escapeRegExpCharacters, Emitter, Event } from '../../common'; import { PreferenceScope } from './preference-scope'; import { PreferenceProvider, PreferenceProviderPriority, PreferenceProviderDataChange } from './preference-provider'; +import { IJSONSchema } from '../../common/json-schema'; import { PreferenceSchema, PreferenceSchemaProperties, PreferenceDataSchema, PreferenceItem, PreferenceSchemaProperty, PreferenceDataProperty, JsonType @@ -53,6 +54,7 @@ export class PreferenceSchemaProvider extends PreferenceProvider { protected readonly preferences: { [name: string]: any } = {}; protected readonly combinedSchema: PreferenceDataSchema = { properties: {}, patternProperties: {} }; + private remoteSchemas: IJSONSchema[] = []; @inject(ContributionProvider) @named(PreferenceContribution) protected readonly preferenceContributions: ContributionProvider; @@ -182,7 +184,7 @@ export class PreferenceSchemaProvider extends PreferenceProvider { } protected updateValidate(): void { - this.validateFunction = new Ajv().compile(this.combinedSchema); + this.validateFunction = new Ajv({ schemas: this.remoteSchemas }).compile(this.combinedSchema); } validate(name: string, value: any): boolean { @@ -193,12 +195,30 @@ export class PreferenceSchemaProvider extends PreferenceProvider { return this.combinedSchema; } - setSchema(schema: PreferenceSchema): void { + setSchema(schema: PreferenceSchema, remoteSchema?: IJSONSchema): void { const changes = this.doSetSchema(schema); + if (remoteSchema) { + this.doSetRemoteSchema(remoteSchema); + } this.fireDidPreferenceSchemaChanged(); this.emitPreferencesChangedEvent(changes); } + protected doSetRemoteSchema(schema: IJSONSchema): void { + // remove existing remote schema if any + const existingSchemaIndex = this.remoteSchemas.findIndex(s => !!s.$id && !!s.$id && s.$id !== s.$id); + if (existingSchemaIndex) { + this.remoteSchemas.splice(existingSchemaIndex, 1); + } + + this.remoteSchemas.push(schema); + } + + setRemoteSchema(schema: IJSONSchema): void { + this.doSetRemoteSchema(schema); + this.fireDidPreferenceSchemaChanged(); + } + getPreferences(): { [name: string]: any } { return this.preferences; } diff --git a/packages/debug/src/browser/abstract-launch-preference-provider.ts b/packages/debug/src/browser/abstract-launch-preference-provider.ts new file mode 100644 index 0000000000000..d3c5ec0a79ef4 --- /dev/null +++ b/packages/debug/src/browser/abstract-launch-preference-provider.ts @@ -0,0 +1,157 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, postConstruct } from 'inversify'; +import { PreferenceScope, PreferenceProvider } from '@theia/core/lib/browser'; +import { Emitter, Event } from '@theia/core'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { DisposableCollection } from '@theia/core'; +import { Disposable } from '@theia/core'; + +export interface GlobalLaunchConfig { + version: string; + compounds?: LaunchCompound[]; + configurations: LaunchConfig[]; +} + +export namespace GlobalLaunchConfig { + /* tslint:disable-next-line:no-any */ + export function is(data: any): data is GlobalLaunchConfig { + return !data || (!!data.version && (!data.compounds || Array.isArray(data.compounds)) && Array.isArray(data.configurations)); + } +} + +export interface LaunchConfig { + type: string; + request: string; + name: string; + + /* tslint:disable-next-line:no-any */ + [field: string]: any; +} + +export interface LaunchCompound { + name: string; + configurations: (string | { name: string, folder: string })[]; +} + +export const LaunchPreferenceProvider = Symbol('LaunchConfigurationProvider'); +export interface LaunchPreferenceProvider { + + readonly onDidLaunchChanged: Event; + + ready: Promise; + + getConfigurationNames(withCompounds: boolean, resourceUri?: string): string[]; + +} + +export const FolderLaunchProviderOptions = Symbol('FolderLaunchProviderOptions'); +export interface FolderLaunchProviderOptions { + folderUri: string; +} + +export const LaunchProviderProvider = Symbol('LaunchProviderProvider'); +export type LaunchProviderProvider = (scope: PreferenceScope) => LaunchPreferenceProvider; + +@injectable() +export abstract class AbstractLaunchPreferenceProvider implements LaunchPreferenceProvider, Disposable { + + protected readonly onDidLaunchChangedEmitter = new Emitter(); + readonly onDidLaunchChanged: Event = this.onDidLaunchChangedEmitter.event; + + protected preferences: GlobalLaunchConfig | undefined; + + protected _ready: Deferred = new Deferred(); + + protected readonly toDispose = new DisposableCollection(); + + protected readonly preferenceProvider: PreferenceProvider; + + @postConstruct() + protected init(): void { + this.preferenceProvider.ready + .then(() => this._ready.resolve()) + .catch(() => this._ready.resolve()); + + this.updatePreferences(); + if (this.preferences !== undefined) { + this.emitLaunchChangedEvent(); + } + + this.toDispose.push(this.onDidLaunchChangedEmitter); + this.toDispose.push( + this.preferenceProvider.onDidNotValidPreferencesRead(prefs => { + if (!prefs || !GlobalLaunchConfig.is(prefs.launch)) { + return; + } + if (!prefs.launch && !this.preferences) { + return; + } + this.preferences = prefs.launch; + this.emitLaunchChangedEvent(); + }) + ); + this.toDispose.push( + this.preferenceProvider.onDidPreferencesChanged(prefs => { + if (!prefs || !prefs.launch) { + return; + } + this.updatePreferences(); + this.emitLaunchChangedEvent(); + }) + ); + } + + protected updatePreferences(): void { + const prefs = this.preferenceProvider.getPreferences(); + if (GlobalLaunchConfig.is(prefs.launch)) { + this.preferences = prefs.launch; + } + } + + protected emitLaunchChangedEvent(): void { + this.onDidLaunchChangedEmitter.fire(undefined); + } + + get ready(): Promise { + return this._ready.promise; + } + + dispose(): void { + this.toDispose.dispose(); + } + + getConfigurationNames(withCompounds = true, resourceUri?: string): string[] { + const config = this.preferences; + if (!config) { + return []; + } + + const names = config.configurations + .filter(launchConfig => launchConfig && typeof launchConfig.name === 'string') + .map(launchConfig => launchConfig.name); + if (withCompounds && config.compounds) { + const compoundNames = config.compounds + .filter(compoundConfig => typeof compoundConfig.name === 'string' && compoundConfig.configurations && compoundConfig.configurations.length) + .map(compoundConfig => compoundConfig.name); + names.push(...compoundNames); + } + + return names; + } + +} diff --git a/packages/debug/src/browser/debug-frontend-module.ts b/packages/debug/src/browser/debug-frontend-module.ts index dadb05d52bd72..45ed214a6b355 100644 --- a/packages/debug/src/browser/debug-frontend-module.ts +++ b/packages/debug/src/browser/debug-frontend-module.ts @@ -20,7 +20,7 @@ import { ContainerModule, interfaces } from 'inversify'; import { DebugConfigurationManager } from './debug-configuration-manager'; import { DebugWidget } from './view/debug-widget'; import { DebugPath, DebugService } from '../common/debug-service'; -import { WidgetFactory, WebSocketConnectionProvider, FrontendApplicationContribution, bindViewContribution, KeybindingContext } from '@theia/core/lib/browser'; +import { WidgetFactory, WebSocketConnectionProvider, FrontendApplicationContribution, bindViewContribution, KeybindingContext, PreferenceScope } from '@theia/core/lib/browser'; import { DebugSessionManager } from './debug-session-manager'; import { DebugResourceResolver } from './debug-resource'; import { @@ -44,6 +44,10 @@ import './debug-monaco-contribution'; import { bindDebugPreferences } from './debug-preferences'; import { DebugSchemaUpdater } from './debug-schema-updater'; import { DebugCallStackItemTypeKey } from './debug-call-stack-item-type-key'; +import { LaunchProviderProvider, LaunchPreferenceProvider } from './abstract-launch-preference-provider'; +import { WorkspaceLaunchProvider } from './workspace-launch-provider'; +import { UserLaunchProvider } from './user-launch-provider'; +import { FoldersLaunchProvider } from './folders-launch-provider'; export default new ContainerModule((bind: interfaces.Bind) => { bind(DebugCallStackItemTypeKey).toDynamicValue(({ container }) => @@ -85,5 +89,11 @@ export default new ContainerModule((bind: interfaces.Bind) => { bind(DebugSessionContributionRegistryImpl).toSelf().inSingletonScope(); bind(DebugSessionContributionRegistry).toService(DebugSessionContributionRegistryImpl); + bind(LaunchPreferenceProvider).to(UserLaunchProvider).inSingletonScope().whenTargetNamed(PreferenceScope.User); + bind(LaunchPreferenceProvider).to(WorkspaceLaunchProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Workspace); + bind(LaunchPreferenceProvider).to(FoldersLaunchProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Folder); + bind(LaunchProviderProvider).toFactory(ctx => (scope: PreferenceScope) => + ctx.container.getNamed(LaunchPreferenceProvider, scope)); + bindDebugPreferences(bind); }); diff --git a/packages/debug/src/browser/debug-preferences.ts b/packages/debug/src/browser/debug-preferences.ts index 94e40814c5125..13c7ccf712ad4 100644 --- a/packages/debug/src/browser/debug-preferences.ts +++ b/packages/debug/src/browser/debug-preferences.ts @@ -53,14 +53,14 @@ export class DebugConfiguration { export const DebugPreferences = Symbol('DebugPreferences'); export type DebugPreferences = PreferenceProxy; -export function createDebugreferences(preferences: PreferenceService): DebugPreferences { +export function createDebugPreferences(preferences: PreferenceService): DebugPreferences { return createPreferenceProxy(preferences, debugPreferencesSchema); } export function bindDebugPreferences(bind: interfaces.Bind): void { bind(DebugPreferences).toDynamicValue(ctx => { const preferences = ctx.container.get(PreferenceService); - return createDebugreferences(preferences); + return createDebugPreferences(preferences); }).inSingletonScope(); bind(PreferenceContribution).toConstantValue({ schema: debugPreferencesSchema }); diff --git a/packages/debug/src/browser/debug-schema-updater.ts b/packages/debug/src/browser/debug-schema-updater.ts index 03e4417718173..5969891801cba 100644 --- a/packages/debug/src/browser/debug-schema-updater.ts +++ b/packages/debug/src/browser/debug-schema-updater.ts @@ -14,13 +14,16 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; +import { injectable, inject, postConstruct } from 'inversify'; import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store'; import { InMemoryResources, deepClone } from '@theia/core/lib/common'; import { IJSONSchema } from '@theia/core/lib/common/json-schema'; import URI from '@theia/core/lib/common/uri'; import { DebugService } from '../common/debug-service'; import { debugPreferencesSchema } from './debug-preferences'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { LaunchPreferenceProvider, LaunchProviderProvider } from './abstract-launch-preference-provider'; +import { PreferenceSchema, PreferenceSchemaProvider, PreferenceScope } from '@theia/core/lib/browser'; @injectable() export class DebugSchemaUpdater { @@ -28,10 +31,67 @@ export class DebugSchemaUpdater { @inject(JsonSchemaStore) protected readonly jsonSchemaStore: JsonSchemaStore; @inject(InMemoryResources) protected readonly inmemoryResources: InMemoryResources; @inject(DebugService) protected readonly debug: DebugService; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(PreferenceSchemaProvider) protected readonly preferenceSchemaProvider: PreferenceSchemaProvider; + @inject(LaunchProviderProvider) protected readonly launchProviderProvider: LaunchProviderProvider; - async update(): Promise { + private launchProviders: LaunchPreferenceProvider[] = []; + + private debugLaunchSchemaId = 'vscode://debug/launch.json'; + + private schemaIsSet = false; + + @postConstruct() + protected init(): void { + this.initializeLaunchProviders(); + } + + protected initializeLaunchProviders(): void { + PreferenceScope.getScopes().forEach(scope => { + if (scope === PreferenceScope.Default) { + return; + } + const provider = this.launchProviderProvider(scope); + this.launchProviders.push(provider); + }); + this.launchProviders.map(p => + p.onDidLaunchChanged(() => { + this.updateDebugLaunchSchema(); + }) + ); + } + + protected async updateDebugLaunchSchema(): Promise { + const schema = await this.update(); + this.setDebugLaunchSchema(schema); + } + + protected setDebugLaunchSchema(remoteSchema: IJSONSchema) { + if (this.schemaIsSet) { + this.preferenceSchemaProvider.setRemoteSchema(remoteSchema); + return; + } + + this.schemaIsSet = true; + + const debugLaunchPreferencesSchema: PreferenceSchema = { + type: 'object', + scope: 'resource', + properties: { + 'launch': { + type: 'object', + description: "Global debug launch configuration. Should be used as an alternative to 'launch.json' that is shared across workspaces", + default: { configurations: [], compounds: [] }, + $ref: launchSchemaId + } + } + }; + this.preferenceSchemaProvider.setSchema(debugLaunchPreferencesSchema, remoteSchema); + } + + async update(): Promise { const types = await this.debug.debugTypes(); - const launchSchemaUrl = new URI('vscode://debug/launch.json'); + const launchSchemaUrl = new URI(this.debugLaunchSchemaId); const schema = { ...deepClone(launchSchema) }; const items = (schema!.properties!['configurations'].items); @@ -48,6 +108,26 @@ export class DebugSchemaUpdater { } items.defaultSnippets!.push(...await this.debug.getConfigurationSnippets()); + await Promise.all(this.launchProviders.map(l => l.ready)); + + const compoundConfigurationSchema = (schema.properties!.compounds.items as IJSONSchema).properties!.configurations; + const launchNames = this.launchProviders + .map(launch => launch.getConfigurationNames(false)) + .reduce((allNames: string[], names: string[]) => { + names.forEach(name => { + if (allNames.indexOf(name) === -1) { + allNames.push(name); + } + }); + return allNames; + }, []); + (compoundConfigurationSchema.items as IJSONSchema).oneOf![0].enum = launchNames; + (compoundConfigurationSchema.items as IJSONSchema).oneOf![1].properties!.name.enum = launchNames; + + const roots = await this.workspaceService.roots; + const folderNames = roots.map(root => root.uri); + (compoundConfigurationSchema.items as IJSONSchema).oneOf![1].properties!.folder.enum = folderNames; + const contents = JSON.stringify(schema); try { await this.inmemoryResources.update(launchSchemaUrl, contents); @@ -58,15 +138,17 @@ export class DebugSchemaUpdater { url: launchSchemaUrl.toString() }); } + + return schema; } } // debug general schema -const defaultCompound = { name: 'Compound', configurations: [] }; +export const defaultCompound = { name: 'Compound', configurations: [] }; -const launchSchemaId = 'vscode://schemas/launch'; -const launchSchema: IJSONSchema = { - id: launchSchemaId, +export const launchSchemaId = 'vscode://schemas/launch'; +export const launchSchema: IJSONSchema = { + $id: launchSchemaId, type: 'object', title: 'Launch', required: [], diff --git a/packages/debug/src/browser/folders-launch-provider.ts b/packages/debug/src/browser/folders-launch-provider.ts new file mode 100644 index 0000000000000..c602a2fad6c15 --- /dev/null +++ b/packages/debug/src/browser/folders-launch-provider.ts @@ -0,0 +1,128 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, postConstruct, named } from 'inversify'; +import { Disposable, DisposableCollection } from '@theia/core'; +import { Emitter, Event } from '@theia/core'; +import { WorkspaceService } from '@theia/workspace/lib/browser'; +import { LaunchPreferenceProvider, GlobalLaunchConfig } from './abstract-launch-preference-provider'; +import { PreferenceScope, PreferenceProvider } from '@theia/core/lib/browser'; +import { Deferred } from '@theia/core/lib/common/promise-util'; + +@injectable() +export class FoldersLaunchProvider implements LaunchPreferenceProvider, Disposable { + + @inject(WorkspaceService) + protected readonly workspaceService: WorkspaceService; + @inject(PreferenceProvider) @named(PreferenceScope.Folder) + protected readonly preferenceProvider: PreferenceProvider; + + protected readonly onDidLaunchChangedEmitter = new Emitter(); + readonly onDidLaunchChanged: Event = this.onDidLaunchChangedEmitter.event; + + protected preferencesNotValid: GlobalLaunchConfig | undefined; + protected preferencesByFolder: Map = new Map(); + + protected _ready: Deferred = new Deferred(); + + protected readonly toDispose = new DisposableCollection(); + + @postConstruct() + protected init(): void { + this.preferenceProvider.ready + .then(() => this._ready.resolve()) + .catch(() => this._ready.resolve()); + + this.updatePreferences(); + if (this.preferencesByFolder.size !== 0) { + this.emitLaunchChangedEvent(); + } + + this.toDispose.push(this.onDidLaunchChangedEmitter); + this.toDispose.push( + this.preferenceProvider.onDidNotValidPreferencesRead(prefs => { + if (!prefs || !GlobalLaunchConfig.is(prefs.launch)) { + return; + } + if (!prefs.launch && !this.preferencesNotValid) { + return; + } + this.preferencesNotValid = prefs.launch; + this.emitLaunchChangedEvent(); + }) + ); + this.toDispose.push( + this.preferenceProvider.onDidPreferencesChanged(prefs => { + if (!prefs || !prefs.launch) { + return; + } + this.updatePreferences(); + this.emitLaunchChangedEvent(); + }) + ); + } + + protected updatePreferences(): void { + this.preferencesByFolder.clear(); + this.preferencesNotValid = undefined; + for (const root of this.workspaceService.tryGetRoots()) { + const preferences = this.preferenceProvider.getPreferences(root.uri); + if (GlobalLaunchConfig.is(preferences.launch)) { + this.preferencesByFolder.set(root.uri, preferences.launch); + } + } + } + + protected emitLaunchChangedEvent(): void { + this.onDidLaunchChangedEmitter.fire(undefined); + } + + get ready(): Promise { + return this._ready.promise; + } + + dispose(): void { + this.toDispose.dispose(); + } + + getConfigurationNames(withCompounds: boolean, resourceUri: string): string[] { + let names: string[] = []; + + const launchConfigurations = Array.from(this.preferencesByFolder.values()); + launchConfigurations.push(this.preferencesNotValid); + + for (const config of launchConfigurations) { + if (!config) { + continue; + } + + const configNames = config.configurations + .filter(launchConfig => launchConfig && typeof launchConfig.name === 'string') + .map(launchConfig => launchConfig.name); + if (withCompounds && config.compounds) { + const compoundNames = config.compounds + .filter(compoundConfig => typeof compoundConfig.name === 'string' && compoundConfig.configurations && compoundConfig.configurations.length) + .map(compoundConfig => compoundConfig.name); + configNames.push(...compoundNames); + } + + names = names.concat(configNames); + } + + return names; + } + +} diff --git a/packages/debug/src/browser/user-launch-provider.ts b/packages/debug/src/browser/user-launch-provider.ts new file mode 100644 index 0000000000000..8beeabfb75cc9 --- /dev/null +++ b/packages/debug/src/browser/user-launch-provider.ts @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, named } from 'inversify'; +import { PreferenceProvider, PreferenceScope } from '@theia/core/lib/browser'; +import { AbstractLaunchPreferenceProvider } from './abstract-launch-preference-provider'; + +@injectable() +export class UserLaunchProvider extends AbstractLaunchPreferenceProvider { + + @inject(PreferenceProvider) @named(PreferenceScope.User) + protected readonly preferenceProvider: PreferenceProvider; + +} diff --git a/packages/debug/src/browser/workspace-launch-provider.ts b/packages/debug/src/browser/workspace-launch-provider.ts new file mode 100644 index 0000000000000..312ffbc7f633e --- /dev/null +++ b/packages/debug/src/browser/workspace-launch-provider.ts @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, named } from 'inversify'; +import { PreferenceProvider, PreferenceScope } from '@theia/core/lib/browser'; +import { AbstractLaunchPreferenceProvider } from './abstract-launch-preference-provider'; + +@injectable() +export class WorkspaceLaunchProvider extends AbstractLaunchPreferenceProvider { + + @inject(PreferenceProvider) @named(PreferenceScope.Workspace) + protected readonly preferenceProvider: PreferenceProvider; + +} From 397f360422dae3dc590e58665fd526d04d7810c7 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Fri, 29 Mar 2019 17:32:11 +0200 Subject: [PATCH 5/5] fixes Signed-off-by: Oleksii Kurinnyi --- CHANGELOG.md | 1 + .../preferences/preference-provider.ts | 4 +- .../abstract-launch-preference-provider.ts | 2 +- .../src/browser/folders-launch-provider.ts | 2 +- .../abstract-resource-preference-provider.ts | 64 +++++---------- .../folder-launch-preference-provider.ts | 2 +- .../src/browser/folder-preference-provider.ts | 3 +- .../browser/folders-preferences-provider.ts | 80 +++++++++++-------- .../src/browser/preference-frontend-module.ts | 4 +- .../src/browser/preference-service.spec.ts | 4 +- 10 files changed, 77 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6309d2d6439c1..e0d907467ac9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v0.6.0 - [filesystem] added the menu item `Upload Files...` to easily upload files into a workspace +- [preferences] changed signature for methods `getProvider`, `setProvider` and `createFolderPreferenceProvider` of `FoldersPreferenceProvider`. ## v0.5.0 diff --git a/packages/core/src/browser/preferences/preference-provider.ts b/packages/core/src/browser/preferences/preference-provider.ts index 427e0383441a2..cec9ead420226 100644 --- a/packages/core/src/browser/preferences/preference-provider.ts +++ b/packages/core/src/browser/preferences/preference-provider.ts @@ -47,8 +47,8 @@ export abstract class PreferenceProvider implements Disposable { protected readonly onDidPreferencesChangedEmitter = new Emitter(); readonly onDidPreferencesChanged: Event = this.onDidPreferencesChangedEmitter.event; - protected readonly onDidNotValidPreferencesReadEmitter = new Emitter(); - readonly onDidNotValidPreferencesRead: Event = this.onDidNotValidPreferencesReadEmitter.event; + protected readonly onDidInvalidPreferencesReadEmitter = new Emitter<{ [key: string]: any }>(); + readonly onDidInvalidPreferencesRead: Event<{ [key: string]: any }> = this.onDidInvalidPreferencesReadEmitter.event; protected readonly toDispose = new DisposableCollection(); diff --git a/packages/debug/src/browser/abstract-launch-preference-provider.ts b/packages/debug/src/browser/abstract-launch-preference-provider.ts index d3c5ec0a79ef4..ea2e3e28f22fb 100644 --- a/packages/debug/src/browser/abstract-launch-preference-provider.ts +++ b/packages/debug/src/browser/abstract-launch-preference-provider.ts @@ -94,7 +94,7 @@ export abstract class AbstractLaunchPreferenceProvider implements LaunchPreferen this.toDispose.push(this.onDidLaunchChangedEmitter); this.toDispose.push( - this.preferenceProvider.onDidNotValidPreferencesRead(prefs => { + this.preferenceProvider.onDidInvalidPreferencesRead(prefs => { if (!prefs || !GlobalLaunchConfig.is(prefs.launch)) { return; } diff --git a/packages/debug/src/browser/folders-launch-provider.ts b/packages/debug/src/browser/folders-launch-provider.ts index c602a2fad6c15..f161ba98f742b 100644 --- a/packages/debug/src/browser/folders-launch-provider.ts +++ b/packages/debug/src/browser/folders-launch-provider.ts @@ -53,7 +53,7 @@ export class FoldersLaunchProvider implements LaunchPreferenceProvider, Disposab this.toDispose.push(this.onDidLaunchChangedEmitter); this.toDispose.push( - this.preferenceProvider.onDidNotValidPreferencesRead(prefs => { + this.preferenceProvider.onDidInvalidPreferencesRead(prefs => { if (!prefs || !GlobalLaunchConfig.is(prefs.launch)) { return; } diff --git a/packages/preferences/src/browser/abstract-resource-preference-provider.ts b/packages/preferences/src/browser/abstract-resource-preference-provider.ts index 79268ffa3f061..92306dae842d4 100644 --- a/packages/preferences/src/browser/abstract-resource-preference-provider.ts +++ b/packages/preferences/src/browser/abstract-resource-preference-provider.ts @@ -26,8 +26,6 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi // tslint:disable-next-line:no-any protected preferences: { [key: string]: any } = {}; - // tslint:disable-next-line:no-any - protected notValidPreferences: { [key: string]: any } = {}; protected resource: Promise; protected toDisposeOnWorkspaceLocationChanged: DisposableCollection = new DisposableCollection(); @@ -62,9 +60,11 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi this.toDispose.push(onDidResourceChanged); } - this.schemaProvider.onDidPreferenceSchemaChanged(() => { - this.handleNotValidPreferences(); - }); + this.toDispose.push( + this.schemaProvider.onDidPreferenceSchemaChanged(() => { + this.readPreferences(); + }) + ); } abstract getUri(root?: URI): MaybePromise; @@ -109,13 +109,8 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi protected async readPreferences(): Promise { const newContent = await this.readContents(); - const parsedData = this.parse(newContent); - const newPrefs = await this.getValidatedPreferences(parsedData); - await this.handlePreferenceChanges(newPrefs.valid); - this.notValidPreferences = newPrefs.notValid; - if (this.notValidPreferences && Object.keys(this.notValidPreferences).length > 0) { - this.onDidNotValidPreferencesReadEmitter.fire(this.notValidPreferences); - } + const newPrefs = await this.getParsedContent(newContent); + await this.handlePreferenceChanges(newPrefs); } protected async readContents(): Promise { @@ -128,35 +123,37 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi } // tslint:disable-next-line:no-any - protected async getValidatedPreferences(jsonData: { [key: string]: any }): Promise<{ valid: any, notValid: any }> { + protected async getParsedContent(content: string): Promise<{ [key: string]: any }> { + const jsonData = this.parse(content); // tslint:disable-next-line:no-any - const preferences: { valid: any, notValid: any } = { - valid: {}, - notValid: {} - }; + const preferences: { [key: string]: any } = {}; + // tslint:disable-next-line:no-any + const notValidPreferences: { [key: string]: any } = {}; if (typeof jsonData !== 'object') { return preferences; } const uri = (await this.resource).uri.toString(); - this.notValidPreferences = {}; // tslint:disable-next-line:forin for (const preferenceName in jsonData) { const preferenceValue = jsonData[preferenceName]; - if (this.isNotValid(preferenceName, preferenceValue)) { + if (preferenceValue !== undefined && !this.schemaProvider.validate(preferenceName, preferenceValue)) { console.warn(`Preference ${preferenceName} in ${uri} is invalid.`); - preferences.notValid[preferenceName] = preferenceValue; + notValidPreferences[preferenceName] = preferenceValue; continue; } if (this.schemaProvider.testOverrideValue(preferenceName, preferenceValue)) { // tslint:disable-next-line:forin for (const overriddenPreferenceName in preferenceValue) { const overriddeValue = preferenceValue[overriddenPreferenceName]; - preferences.valid[`${preferenceName}.${overriddenPreferenceName}`] = overriddeValue; + preferences[`${preferenceName}.${overriddenPreferenceName}`] = overriddeValue; } } else { - preferences.valid[preferenceName] = preferenceValue; + preferences[preferenceName] = preferenceValue; } } + if (Object.keys(notValidPreferences).length > 0) { + this.onDidInvalidPreferencesReadEmitter.fire(notValidPreferences); + } return preferences; } @@ -166,11 +163,6 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi return jsoncparser.parse(strippedContent); } - // tslint:disable-next-line:no-any - protected isNotValid(preferenceName: string, preferenceValue: any): boolean { - return (preferenceValue !== undefined && !this.schemaProvider.validate(preferenceName, preferenceValue)); - } - // tslint:disable-next-line:no-any protected async handlePreferenceChanges(newPrefs: { [key: string]: any }): Promise { const oldPrefs = Object.assign({}, this.preferences); @@ -204,24 +196,6 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi } } - protected async handleNotValidPreferences(): Promise { - const notValidPreferencesKeys = Object.keys(this.notValidPreferences); - if (notValidPreferencesKeys.length === 0) { - return; - } - - const validPrefs = Object.assign({}, this.preferences); - const newPrefs = notValidPreferencesKeys - .filter(prefName => !this.isNotValid(prefName, this.notValidPreferences[prefName])) - .reduce((prefs, prefName) => { - prefs[prefName] = this.notValidPreferences[prefName]; - delete this.notValidPreferences[prefName]; - return prefs; - }, validPrefs); - - return this.handlePreferenceChanges(newPrefs); - } - dispose(): void { const prefChanges: PreferenceProviderDataChange[] = []; for (const prefName of Object.keys(this.preferences)) { diff --git a/packages/preferences/src/browser/folder-launch-preference-provider.ts b/packages/preferences/src/browser/folder-launch-preference-provider.ts index 34f2ca4161c73..04d0054ed675c 100644 --- a/packages/preferences/src/browser/folder-launch-preference-provider.ts +++ b/packages/preferences/src/browser/folder-launch-preference-provider.ts @@ -32,7 +32,7 @@ export class FolderLaunchPreferenceProvider extends FolderPreferenceProvider { // tslint:disable-next-line:no-any protected parse(content: string): any { const parsedData = super.parse(content); - if (Object.keys(parsedData).length > 0) { + if (!!parsedData && Object.keys(parsedData).length > 0) { return { launch: parsedData }; } return parsedData; diff --git a/packages/preferences/src/browser/folder-preference-provider.ts b/packages/preferences/src/browser/folder-preference-provider.ts index 60b9db202a946..30699b9192cc9 100644 --- a/packages/preferences/src/browser/folder-preference-provider.ts +++ b/packages/preferences/src/browser/folder-preference-provider.ts @@ -19,6 +19,7 @@ import URI from '@theia/core/lib/common/uri'; import { PreferenceScope, PreferenceProvider, PreferenceProviderPriority } from '@theia/core/lib/browser'; import { AbstractResourcePreferenceProvider } from './abstract-resource-preference-provider'; import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; +import { ResourceKind } from './folders-preferences-provider'; export const FolderPreferenceProviderFactory = Symbol('FolderPreferenceProviderFactory'); export interface FolderPreferenceProviderFactory { @@ -28,7 +29,7 @@ export interface FolderPreferenceProviderFactory { export const FolderPreferenceProviderOptions = Symbol('FolderPreferenceProviderOptions'); export interface FolderPreferenceProviderOptions { folder: FileStat; - fileName: string; + kind: ResourceKind; } @injectable() diff --git a/packages/preferences/src/browser/folders-preferences-provider.ts b/packages/preferences/src/browser/folders-preferences-provider.ts index d40ea3a51da1d..71ab0c8d2778d 100644 --- a/packages/preferences/src/browser/folders-preferences-provider.ts +++ b/packages/preferences/src/browser/folders-preferences-provider.ts @@ -21,9 +21,8 @@ import { FolderPreferenceProvider, FolderPreferenceProviderFactory } from './fol import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import URI from '@theia/core/lib/common/uri'; -export const SETTINGS_FILE_NAME = 'settings.json'; -export const LAUNCH_FILE_NAME = 'launch.json'; export const LAUNCH_PROPERTY_NAME = 'launch'; +export type ResourceKind = 'settings' | 'launch'; @injectable() export class FoldersPreferencesProvider extends PreferenceProvider { @@ -32,19 +31,18 @@ export class FoldersPreferencesProvider extends PreferenceProvider { @inject(FileSystem) protected readonly fileSystem: FileSystem; @inject(FolderPreferenceProviderFactory) protected readonly folderPreferenceProviderFactory: FolderPreferenceProviderFactory; - private providers: { [fileName: string]: FolderPreferenceProvider[] } = {}; + private providersByKind: Map = new Map(); + private resourceKinds: ResourceKind[] = ['launch', 'settings']; @postConstruct() protected async init(): Promise { - [SETTINGS_FILE_NAME, LAUNCH_FILE_NAME].forEach(fileName => this.providers[fileName] = []); - await this.workspaceService.roots; const readyPromises: Promise[] = []; for (const root of this.workspaceService.tryGetRoots()) { if (await this.fileSystem.exists(root.uri)) { - [SETTINGS_FILE_NAME, LAUNCH_FILE_NAME].forEach(fileName => { - const provider = this.createFolderPreferenceProvider(root, fileName); - this.providers[fileName].push(provider); + this.resourceKinds.forEach(kind => { + const provider = this.createFolderPreferenceProvider(root, kind); + this.setProvider(provider, kind); readyPromises.push(provider.ready); }); } @@ -59,23 +57,24 @@ export class FoldersPreferencesProvider extends PreferenceProvider { this.workspaceService.onWorkspaceChanged(roots => { for (const root of roots) { - [SETTINGS_FILE_NAME, LAUNCH_FILE_NAME].forEach(fileName => { - if (!this.existsProvider(root.uri, fileName)) { - const provider = this.createFolderPreferenceProvider(root, fileName); - this.providers[fileName].push(provider); + this.resourceKinds.forEach(kind => { + if (!this.existsProvider(root.uri, kind)) { + const provider = this.createFolderPreferenceProvider(root, kind); + this.setProvider(provider, kind); } }); } - [SETTINGS_FILE_NAME, LAUNCH_FILE_NAME].forEach(fileName => { - if (!this.providers[fileName]) { + this.resourceKinds.forEach(kind => { + const providers = this.providersByKind.get(kind); + if (!providers || providers.length === 0) { return; } - const numProviders = this.providers[fileName].length; + const numProviders = providers.length; for (let i = numProviders - 1; i >= 0; i--) { - const provider = this.providers[fileName][i]; + const provider = providers[i]; if (!this.existsRoot(roots, provider)) { - this.providers[fileName].splice(i, 1); + providers.splice(i, 1); provider.dispose(); } } @@ -83,8 +82,9 @@ export class FoldersPreferencesProvider extends PreferenceProvider { }); } - private existsProvider(folderUri: string, fileName: string): boolean { - return this.providers[fileName] && this.providers[fileName].some(p => !!p.uri && p.uri.toString() === folderUri); + private existsProvider(folderUri: string, kind: ResourceKind): boolean { + const providers = this.providersByKind.get(kind); + return !!providers && providers.some(p => !!p.uri && p.uri.toString() === folderUri); } private existsRoot(roots: FileStat[], provider: FolderPreferenceProvider): boolean { @@ -97,10 +97,10 @@ export class FoldersPreferencesProvider extends PreferenceProvider { return {}; } - const prefProvider = this.getProvider(resourceUri, SETTINGS_FILE_NAME); + const prefProvider = this.getProvider(resourceUri, 'settings'); const prefs = prefProvider ? prefProvider.getPreferences() : {}; - const launchProvider = this.getProvider(resourceUri, LAUNCH_FILE_NAME); + const launchProvider = this.getProvider(resourceUri, 'launch'); const launch = launchProvider ? launchProvider.getPreferences() : {}; const result = Object.assign({}, prefs, launch); @@ -110,8 +110,8 @@ export class FoldersPreferencesProvider extends PreferenceProvider { canProvide(preferenceName: string, resourceUri?: string): { priority: number, provider: PreferenceProvider } { if (resourceUri) { - const resourceName = preferenceName === LAUNCH_PROPERTY_NAME ? LAUNCH_FILE_NAME : SETTINGS_FILE_NAME; - const provider = this.getProvider(resourceUri, resourceName); + const resourceKind = preferenceName === LAUNCH_PROPERTY_NAME ? 'launch' : 'settings'; + const provider = this.getProvider(resourceUri, resourceKind); if (provider) { return { priority: provider.canProvide(preferenceName, resourceUri).priority, provider }; } @@ -119,9 +119,9 @@ export class FoldersPreferencesProvider extends PreferenceProvider { return super.canProvide(preferenceName, resourceUri); } - protected getProvider(resourceUri: string, fileName: string): PreferenceProvider | undefined { - const providers = this.providers[fileName]; - if (providers.length === 0) { + protected getProvider(resourceUri: string, kind: ResourceKind): PreferenceProvider | undefined { + const providers = this.providersByKind.get(kind); + if (!providers || providers.length === 0) { return; } @@ -139,22 +139,34 @@ export class FoldersPreferencesProvider extends PreferenceProvider { return provider; } - protected createFolderPreferenceProvider(folder: FileStat, fileName: string): FolderPreferenceProvider { - const provider = this.folderPreferenceProviderFactory({ folder, fileName }); + protected setProvider(provider: FolderPreferenceProvider, kind: ResourceKind): void { + const providers = this.providersByKind.get(kind); + if (providers && Array.isArray(providers)) { + providers.push(provider); + } else { + this.providersByKind.set(kind, [provider]); + } + } + + protected createFolderPreferenceProvider(folder: FileStat, kind: ResourceKind): FolderPreferenceProvider { + const provider = this.folderPreferenceProviderFactory({ folder, kind }); this.toDispose.push(provider); this.toDispose.push(provider.onDidPreferencesChanged(change => this.onDidPreferencesChangedEmitter.fire(change))); - this.toDispose.push(provider.onDidNotValidPreferencesRead(prefs => this.onDidNotValidPreferencesReadEmitter.fire(prefs))); + this.toDispose.push(provider.onDidInvalidPreferencesRead(prefs => this.onDidInvalidPreferencesReadEmitter.fire(prefs))); return provider; } // tslint:disable-next-line:no-any async setPreference(key: string, value: any, resourceUri?: string): Promise { if (resourceUri) { - const resourceName = key === LAUNCH_PROPERTY_NAME ? LAUNCH_FILE_NAME : SETTINGS_FILE_NAME; - for (const provider of this.providers[resourceName]) { - const providerResourceUri = await provider.getUri(); - if (providerResourceUri && providerResourceUri.toString() === resourceUri) { - return provider.setPreference(key, value); + const resourceKind = key === LAUNCH_PROPERTY_NAME ? 'launch' : 'settings'; + const providers = this.providersByKind.get(resourceKind); + if (providers && providers.length) { + for (const provider of providers) { + const providerResourceUri = await provider.getUri(); + if (providerResourceUri && providerResourceUri.toString() === resourceUri) { + return provider.setPreference(key, value); + } } } console.error(`FoldersPreferencesProvider did not find the provider for ${resourceUri} to update the preference ${key}`); diff --git a/packages/preferences/src/browser/preference-frontend-module.ts b/packages/preferences/src/browser/preference-frontend-module.ts index e4ba2c74e150d..f8839b9217a5b 100644 --- a/packages/preferences/src/browser/preference-frontend-module.ts +++ b/packages/preferences/src/browser/preference-frontend-module.ts @@ -24,7 +24,7 @@ import { createPreferencesTreeWidget } from './preference-tree-container'; import { PreferencesMenuFactory } from './preferences-menu-factory'; import { PreferencesFrontendApplicationContribution } from './preferences-frontend-application-contribution'; import { PreferencesContainer, PreferencesTreeWidget, PreferencesEditorsContainer } from './preferences-tree-widget'; -import { FoldersPreferencesProvider, SETTINGS_FILE_NAME } from './folders-preferences-provider'; +import { FoldersPreferencesProvider } from './folders-preferences-provider'; import { FolderPreferenceProvider, FolderPreferenceProviderFactory, FolderPreferenceProviderOptions } from './folder-preference-provider'; import { FolderLaunchPreferenceProvider } from './folder-launch-preference-provider'; @@ -43,7 +43,7 @@ export function bindPreferences(bind: interfaces.Bind, unbind: interfaces.Unbind const child = new Container({ defaultScope: 'Transient' }); child.parent = ctx.container; child.bind(FolderPreferenceProviderOptions).toConstantValue(options); - if (options.fileName === SETTINGS_FILE_NAME) { + if (options.kind === 'settings') { return child.get(FolderPreferenceProvider); } else { return child.get(FolderLaunchPreferenceProvider); diff --git a/packages/preferences/src/browser/preference-service.spec.ts b/packages/preferences/src/browser/preference-service.spec.ts index 49a58ff15c603..75f3b01748b3b 100644 --- a/packages/preferences/src/browser/preference-service.spec.ts +++ b/packages/preferences/src/browser/preference-service.spec.ts @@ -743,7 +743,7 @@ describe('Preference Service', () => { it('should fire "onDidLaunchChanged" event with correct argument', async () => { const spy = sinon.spy(); - userProvider.onDidNotValidPreferencesRead(spy); + userProvider.onDidInvalidPreferencesRead(spy); fs.writeFileSync(tempPath, userConfigStr); await (userProvider).readPreferences(); @@ -778,7 +778,7 @@ describe('Preference Service', () => { it('should not fire "onDidLaunchChanged"', async () => { const spy = sinon.spy(); - userProvider.onDidNotValidPreferencesRead(spy); + userProvider.onDidInvalidPreferencesRead(spy); fs.writeFileSync(tempPath, userConfigStr); await (userProvider).readPreferences();