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-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/core/src/browser/preferences/preference-provider.ts b/packages/core/src/browser/preferences/preference-provider.ts index d5ac504153a32..cec9ead420226 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 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 new file mode 100644 index 0000000000000..ea2e3e28f22fb --- /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.onDidInvalidPreferencesRead(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..f161ba98f742b --- /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.onDidInvalidPreferencesRead(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; + +} 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()]; } } diff --git a/packages/preferences/src/browser/abstract-resource-preference-provider.ts b/packages/preferences/src/browser/abstract-resource-preference-provider.ts index aff488ced08bb..92306dae842d4 100644 --- a/packages/preferences/src/browser/abstract-resource-preference-provider.ts +++ b/packages/preferences/src/browser/abstract-resource-preference-provider.ts @@ -59,6 +59,12 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi this.toDisposeOnWorkspaceLocationChanged.pushAll([onDidResourceChanged, (await this.resource)]); this.toDispose.push(onDidResourceChanged); } + + this.toDispose.push( + this.schemaProvider.onDidPreferenceSchemaChanged(() => { + this.readPreferences(); + }) + ); } abstract getUri(root?: URI): MaybePromise; @@ -121,6 +127,8 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi const jsonData = this.parse(content); // tslint:disable-next-line:no-any const preferences: { [key: string]: any } = {}; + // tslint:disable-next-line:no-any + const notValidPreferences: { [key: string]: any } = {}; if (typeof jsonData !== 'object') { return preferences; } @@ -130,6 +138,7 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi const preferenceValue = jsonData[preferenceName]; if (preferenceValue !== undefined && !this.schemaProvider.validate(preferenceName, preferenceValue)) { console.warn(`Preference ${preferenceName} in ${uri} is invalid.`); + notValidPreferences[preferenceName] = preferenceValue; continue; } if (this.schemaProvider.testOverrideValue(preferenceName, preferenceValue)) { @@ -142,6 +151,9 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi preferences[preferenceName] = preferenceValue; } } + if (Object.keys(notValidPreferences).length > 0) { + this.onDidInvalidPreferencesReadEmitter.fire(notValidPreferences); + } return preferences; } 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..04d0054ed675c --- /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 (!!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 4f70f55af4ae2..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,12 +29,13 @@ export interface FolderPreferenceProviderFactory { export const FolderPreferenceProviderOptions = Symbol('FolderPreferenceProviderOptions'); export interface FolderPreferenceProviderOptions { folder: FileStat; + kind: ResourceKind; } @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..71ab0c8d2778d 100644 --- a/packages/preferences/src/browser/folders-preferences-provider.ts +++ b/packages/preferences/src/browser/folders-preferences-provider.ts @@ -21,6 +21,9 @@ import { FolderPreferenceProvider, FolderPreferenceProviderFactory } from './fol import { FileSystem, FileStat } from '@theia/filesystem/lib/common'; import URI from '@theia/core/lib/common/uri'; +export const LAUNCH_PROPERTY_NAME = 'launch'; +export type ResourceKind = 'settings' | 'launch'; + @injectable() export class FoldersPreferencesProvider extends PreferenceProvider { @@ -28,69 +31,87 @@ export class FoldersPreferencesProvider extends PreferenceProvider { @inject(FileSystem) protected readonly fileSystem: FileSystem; @inject(FolderPreferenceProviderFactory) protected readonly folderPreferenceProviderFactory: FolderPreferenceProviderFactory; - private providers: FolderPreferenceProvider[] = []; + private providersByKind: Map = new Map(); + private resourceKinds: ResourceKind[] = ['launch', 'settings']; @postConstruct() protected async init(): Promise { 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)) { + this.resourceKinds.forEach(kind => { + const provider = this.createFolderPreferenceProvider(root, kind); + this.setProvider(provider, kind); + 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(); + this.resourceKinds.forEach(kind => { + if (!this.existsProvider(root.uri, kind)) { + const provider = this.createFolderPreferenceProvider(root, kind); + this.setProvider(provider, kind); } - } + }); } - 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(); + this.resourceKinds.forEach(kind => { + const providers = this.providersByKind.get(kind); + if (!providers || providers.length === 0) { + return; } - } + const numProviders = providers.length; + for (let i = numProviders - 1; i >= 0; i--) { + const provider = providers[i]; + if (!this.existsRoot(roots, provider)) { + providers.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, 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 { + 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'); + const prefs = prefProvider ? prefProvider.getPreferences() : {}; + + const launchProvider = this.getProvider(resourceUri, 'launch'); + 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 resourceKind = preferenceName === LAUNCH_PROPERTY_NAME ? 'launch' : 'settings'; + const provider = this.getProvider(resourceUri, resourceKind); 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, kind: ResourceKind): PreferenceProvider | undefined { + const providers = this.providersByKind.get(kind); + if (!providers || 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,20 +139,34 @@ export class FoldersPreferencesProvider extends PreferenceProvider { return provider; } - protected createFolderPreferenceProvider(folder: FileStat): FolderPreferenceProvider { - const provider = this.folderPreferenceProviderFactory({ folder }); + 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.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) { - for (const provider of this.providers) { - 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 ffc69bf4bb1f8..f8839b9217a5b 100644 --- a/packages/preferences/src/browser/preference-frontend-module.ts +++ b/packages/preferences/src/browser/preference-frontend-module.ts @@ -26,6 +26,7 @@ import { PreferencesFrontendApplicationContribution } from './preferences-fronte import { PreferencesContainer, PreferencesTreeWidget, PreferencesEditorsContainer } from './preferences-tree-widget'; import { FoldersPreferencesProvider } 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.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 c85cfbfc49983..75f3b01748b3b 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.onDidInvalidPreferencesRead(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.onDidInvalidPreferencesRead(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); + }); + + }); + + }); + });