Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

preferences: fix duplicated resolution #12165

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/api-tests/src/launch-preferences.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('Launch Preferences', function () {
const { FileResourceResolver } = require('@theia/filesystem/lib/browser/file-resource');
const { AbstractResourcePreferenceProvider } = require('@theia/preferences/lib/browser/abstract-resource-preference-provider');
const { waitForEvent } = require('@theia/core/lib/common/promise-util');
const { FoldersPreferencesProvider } = require('@theia/preferences/lib/browser/folders-preferences-provider');

const container = window.theia.container;
/** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */
Expand All @@ -46,7 +47,7 @@ describe('Launch Preferences', function () {
/** @type {import('@theia/preferences/lib/browser/workspace-preference-provider').WorkspacePreferenceProvider} */
const workspacePreferences = container.getNamed(PreferenceProvider, PreferenceScope.Workspace);
/** @type {import('@theia/preferences/lib/browser/folders-preferences-provider').FoldersPreferencesProvider} */
const folderPreferences = container.getNamed(PreferenceProvider, PreferenceScope.Folder);
const folderPreferences = container.get(FoldersPreferencesProvider);
const workspaceService = container.get(WorkspaceService);
const fileService = container.get(FileService);
const fileResourceResolver = container.get(FileResourceResolver);
Expand Down
4 changes: 2 additions & 2 deletions examples/api-tests/src/preferences.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
describe('Preferences', function () {
this.timeout(5_000);
const { assert } = chai;
const { PreferenceProvider } = require('@theia/core/lib/browser/preferences/preference-provider');
const { PreferenceService, PreferenceScope } = require('@theia/core/lib/browser/preferences/preference-service');
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
const { PreferenceLanguageOverrideService } = require('@theia/core/lib/browser/preferences/preference-language-override-service');
const { MonacoTextModelService } = require('@theia/monaco/lib/browser/monaco-text-model-service');
const { PreferenceSchemaProvider } = require('@theia/core/lib/browser/preferences/preference-contribution')
const { FoldersPreferencesProvider } = require('@theia/preferences/lib/browser/folders-preferences-provider');
const { container } = window.theia;
/** @type {import ('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */
const preferenceService = container.get(PreferenceService);
Expand All @@ -34,7 +34,7 @@ describe('Preferences', function () {
/** @type {import ('@theia/core/lib/common/uri').default} */
const uri = preferenceService.getConfigUri(PreferenceScope.Workspace);
/** @type {import('@theia/preferences/lib/browser/folders-preferences-provider').FoldersPreferencesProvider} */
const folderPreferences = container.getNamed(PreferenceProvider, PreferenceScope.Folder);
const folderPreferences = container.get(FoldersPreferencesProvider);
/** @type PreferenceSchemaProvider */
const schemaProvider = container.get(PreferenceSchemaProvider);
const modelService = container.get(MonacoTextModelService);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/browser/preferences/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from './preference-provider';
export * from './preference-scope';
export * from './preference-language-override-service';
export * from './preference-validation-service';
export { TogglePreferenceProvider } from './toggle-preference-provider';
27 changes: 20 additions & 7 deletions packages/core/src/browser/preferences/preference-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,16 +429,29 @@ export class PreferenceServiceImpl implements PreferenceService {
return { value, configUri };
}

async set(preferenceName: string, value: any, scope: PreferenceScope | undefined, resourceUri?: string): Promise<void> {
const resolvedScope = scope ?? (!resourceUri ? PreferenceScope.Workspace : PreferenceScope.Folder);
if (resolvedScope === PreferenceScope.Folder && !resourceUri) {
async set(preferenceName: string, value: any, scope?: PreferenceScope, resourceUri?: string): Promise<void> {
if (scope === PreferenceScope.Folder && !resourceUri) {
throw new Error('Unable to write to Folder Settings because no resource is provided.');
}
const provider = this.getProvider(resolvedScope);
if (provider && await provider.setPreference(preferenceName, value, resourceUri)) {
return;
let scopes: PreferenceScope[];
if (scope === undefined) {
// Pick default scopes to query:
if (resourceUri) {
// If we don't specify an explicit scope, then try both Folder
// and Workspace scopes with resourceUri:
scopes = [PreferenceScope.Folder, PreferenceScope.Workspace];
} else {
scopes = [PreferenceScope.Workspace];
}
} else {
scopes = [scope];
}
for (scope of scopes) {
if (await this.getProvider(scope)?.setPreference(preferenceName, value, resourceUri)) {
return;
}
}
throw new Error(`Unable to write to ${PreferenceScope[resolvedScope]} Settings.`);
throw new Error(`Unable to write to ${PreferenceScope[scope!]} Settings.`);
}

getBoolean(preferenceName: string): boolean | undefined;
Expand Down
105 changes: 105 additions & 0 deletions packages/core/src/browser/preferences/toggle-preference-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// *****************************************************************************
// Copyright (C) 2023 Ericsson 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
// *****************************************************************************

/* eslint-disable @typescript-eslint/no-explicit-any */

import { URI } from '../../common';
import { PreferenceProvider, PreferenceProviderDataChanges, PreferenceResolveResult } from './preference-provider';

/**
* Allows enabling/disabling a {@link PreferenceProvider} by wrapping it.
*/
export class TogglePreferenceProvider extends PreferenceProvider {

#enabled: boolean;

constructor(
enabled: boolean,
protected provider: PreferenceProvider
) {
super();
this.#enabled = enabled;
this.provider.ready.then(() => this._ready.resolve());
this.provider.onDidPreferencesChanged(this.handleDidPreferencesChanged, this, this.toDispose);
}

get enabled(): boolean {
return this.#enabled;
}

set enabled(value: boolean) {
if (this.#enabled !== value) {
this.#enabled = value;
this.onDidPreferencesChangedEmitter.fire({});
}
}

getPreferences(resourceUri?: string): { [p: string]: any; } {
if (this.enabled) {
return this.provider.getPreferences(resourceUri);
}
return {};
}

async setPreference(key: string, value: any, resourceUri?: string): Promise<boolean> {
if (this.enabled) {
return this.provider.setPreference(key, value, resourceUri);
}
return false;
}

override get<T>(preferenceName: string, resourceUri?: string): T | undefined {
if (this.enabled) {
return this.provider.get<T>(preferenceName, resourceUri);
}
}

override resolve<T>(preferenceName: string, resourceUri?: string): PreferenceResolveResult<T> {
if (this.enabled) {
return this.provider.resolve<T>(preferenceName, resourceUri);
}
return {};
}

override getDomain(): string[] | undefined {
if (this.enabled) {
return this.provider.getDomain();
}
}

override getConfigUri(resourceUri?: string, sectionName?: string): URI | undefined {
if (this.enabled) {
return this.provider.getConfigUri(resourceUri, sectionName);
}
}

override getContainingConfigUri?(resourceUri?: string, sectionName?: string): URI | undefined {
if (this.enabled) {
return this.provider.getContainingConfigUri?.(resourceUri, sectionName);
}
}

override dispose(): void {
super.dispose();
this.provider.dispose();
}

protected handleDidPreferencesChanged(event: PreferenceProviderDataChanges): void {
if (this.enabled) {
this.onDidPreferencesChangedEmitter.fire(event);
}
}
}
23 changes: 8 additions & 15 deletions packages/preferences/src/browser/folders-preferences-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,16 @@ export class FoldersPreferencesProvider extends PreferenceProvider {
protected readonly providers = new Map<string, FolderPreferenceProvider>();

@postConstruct()
protected async init(): Promise<void> {
await this.workspaceService.roots;

this.updateProviders();
this.workspaceService.onWorkspaceChanged(() => this.updateProviders());

const readyPromises: Promise<void>[] = [];
for (const provider of this.providers.values()) {
readyPromises.push(provider.ready.catch(e => console.error(e)));
}
Promise.all(readyPromises).then(() => this._ready.resolve());
protected init(): void {
this.workspaceService.roots.then(roots => {
this.updateProviders(roots);
this.workspaceService.onWorkspaceChanged(newRoots => this.updateProviders(newRoots));
const allReady = Array.from(this.providers.values(), provider => provider.ready);
Promise.allSettled(allReady).then(() => this._ready.resolve());
});
}

protected updateProviders(): void {
const roots = this.workspaceService.tryGetRoots();
protected updateProviders(roots: FileStat[]): void {
const toDelete = new Set(this.providers.keys());
for (const folder of roots) {
for (const configPath of this.configurations.getPaths()) {
Expand Down Expand Up @@ -94,7 +89,6 @@ export class FoldersPreferencesProvider extends PreferenceProvider {
return configUri;
}
}
return undefined;
}

override getDomain(): string[] {
Expand Down Expand Up @@ -232,5 +226,4 @@ export class FoldersPreferencesProvider extends PreferenceProvider {
this.toDispose.push(provider.onDidPreferencesChanged(change => this.onDidPreferencesChangedEmitter.fire(change)));
return provider;
}

}
89 changes: 65 additions & 24 deletions packages/preferences/src/browser/preference-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,92 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************

import { Container, interfaces } from '@theia/core/shared/inversify';
import { PreferenceProvider, PreferenceScope } from '@theia/core/lib/browser/preferences';
import { interfaces } from '@theia/core/shared/inversify';
import { PreferenceProvider, PreferenceScope, TogglePreferenceProvider } from '@theia/core/lib/browser/preferences';
import { UserPreferenceProvider, UserPreferenceProviderFactory } from './user-preference-provider';
import { WorkspacePreferenceProvider } from './workspace-preference-provider';
import { WorkspaceFilePreferenceProvider, WorkspaceFilePreferenceProviderFactory, WorkspaceFilePreferenceProviderOptions } from './workspace-file-preference-provider';
import { FoldersPreferencesProvider } from './folders-preferences-provider';
import { FolderPreferenceProvider, FolderPreferenceProviderFactory, FolderPreferenceProviderFolder } from './folder-preference-provider';
import { UserConfigsPreferenceProvider } from './user-configs-preference-provider';
import { SectionPreferenceProviderUri, SectionPreferenceProviderSection } from './section-preference-provider';
import { WorkspaceService } from '@theia/workspace/lib/browser';

export function bindWorkspaceFilePreferenceProvider(bind: interfaces.Bind): void {
bind(WorkspaceFilePreferenceProviderFactory).toFactory(ctx => (options: WorkspaceFilePreferenceProviderOptions) => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = ctx.container;
child.bind(WorkspaceFilePreferenceProvider).toSelf();
const child = ctx.container.createChild();
child.bind(WorkspaceFilePreferenceProvider).toSelf().inSingletonScope();
child.bind(WorkspaceFilePreferenceProviderOptions).toConstantValue(options);
return child.get(WorkspaceFilePreferenceProvider);
});
}

export function bindFactory<F, C>(bind: interfaces.Bind,
export function bindFactory<F, C>(
bind: interfaces.Bind,
factoryId: interfaces.ServiceIdentifier<F>,
constructor: interfaces.Newable<C>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...parameterBindings: interfaces.ServiceIdentifier<any>[]): void {
bind(factoryId).toFactory(ctx =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(...args: any[]) => {
const child = new Container({ defaultScope: 'Singleton' });
child.parent = ctx.container;
for (let i = 0; i < parameterBindings.length; i++) {
child.bind(parameterBindings[i]).toConstantValue(args[i]);
}
child.bind(constructor).to(constructor);
return child.get(constructor);
}
);
...parameterBindings: interfaces.ServiceIdentifier<unknown>[]
): void {
bind(factoryId).toFactory(ctx => (...args: unknown[]) => {
const child = ctx.container.createChild();
parameterBindings.forEach((parameterBinding, i) => {
child.bind(parameterBinding).toConstantValue(args[i]);
});
child.bind(constructor).to(constructor).inSingletonScope();
return child.get(constructor);
});
}

export function bindPreferenceProviders(bind: interfaces.Bind, unbind: interfaces.Unbind): void {
unbind(PreferenceProvider);

bind(PreferenceProvider).to(UserConfigsPreferenceProvider).inSingletonScope().whenTargetNamed(PreferenceScope.User);
bind(PreferenceProvider).to(WorkspacePreferenceProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Workspace);
bind(PreferenceProvider).to(FoldersPreferencesProvider).inSingletonScope().whenTargetNamed(PreferenceScope.Folder);
// #region bind FoldersPreferencesProvider based on the status of the workspace:
bind(FoldersPreferencesProvider)
.toSelf()
.inSingletonScope()
.whenTargetIsDefault();
// Bind a FoldersPreferencesProvider that's only enabled if the workspace
// is a single root workspace:
bind<PreferenceProvider>(FoldersPreferencesProvider)
.toDynamicValue(ctx => {
const workspaceService = ctx.container.get(WorkspaceService);
const foldersPreferencesProvider = ctx.container.get(FoldersPreferencesProvider);
const preferenceProvider = new TogglePreferenceProvider(!workspaceService.isMultiRootWorkspaceOpened, foldersPreferencesProvider);
workspaceService.onWorkspaceChanged(() => {
preferenceProvider.enabled = !workspaceService.isMultiRootWorkspaceOpened;
});
return preferenceProvider;
})
.inSingletonScope()
.whenTargetNamed(PreferenceScope.Workspace);
// Bind a FoldersPreferencesProvider that's only enabled if the workspace
// is a multi root workspace:
bind<PreferenceProvider>(FoldersPreferencesProvider)
.toDynamicValue(ctx => {
const workspaceService = ctx.container.get(WorkspaceService);
const foldersPreferencesProvider = ctx.container.get(FoldersPreferencesProvider);
const preferenceProvider = new TogglePreferenceProvider(workspaceService.isMultiRootWorkspaceOpened, foldersPreferencesProvider);
workspaceService.onWorkspaceChanged(() => {
preferenceProvider.enabled = workspaceService.isMultiRootWorkspaceOpened;
});
return preferenceProvider;
})
.inSingletonScope()
.whenTargetNamed(PreferenceScope.Folder);
// #endregion
// #region bind PreferenceProvider by PreferenceScope:
bind(PreferenceProvider)
.to(UserConfigsPreferenceProvider)
.inSingletonScope()
.whenTargetNamed(PreferenceScope.User);
bind(PreferenceProvider)
.to(WorkspacePreferenceProvider)
.inSingletonScope()
.whenTargetNamed(PreferenceScope.Workspace);
bind(PreferenceProvider)
.toDynamicValue(ctx => ctx.container.getNamed(FoldersPreferencesProvider, PreferenceScope.Folder))
.inSingletonScope()
.whenTargetNamed(PreferenceScope.Folder);
// #endregion
bindWorkspaceFilePreferenceProvider(bind);
bindFactory(bind, UserPreferenceProviderFactory, UserPreferenceProvider, SectionPreferenceProviderUri, SectionPreferenceProviderSection);
bindFactory(bind, FolderPreferenceProviderFactory, FolderPreferenceProvider, SectionPreferenceProviderUri, SectionPreferenceProviderSection, FolderPreferenceProviderFolder);
Expand Down
Loading