Skip to content

Commit

Permalink
Implement updateValue method for PreferenceService
Browse files Browse the repository at this point in the history
Signed-off-by: Colin Grant <[email protected]>
  • Loading branch information
colin-grant-work committed Mar 23, 2021
1 parent 224edb4 commit ce50a61
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 54 deletions.
23 changes: 11 additions & 12 deletions examples/api-tests/src/launch-preferences.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,15 @@ describe('Launch Preferences', function () {
});
}

/**
* @typedef {Partial<import('@theia/core/src/browser/preferences/preference-service').PreferenceInspection<any>>} PreferenceInspection
*/

/**
* @typedef {Object} SuiteOptions
* @property {string} name
* @property {any} expectation
* @property {any} [inspectExpectation]
* @property {PreferenceInspection} [inspectExpectation]
* @property {any} [launch]
* @property {any} [settings]
* @property {boolean} [only]
Expand Down Expand Up @@ -518,6 +522,7 @@ describe('Launch Preferences', function () {

testIt('inspect in undefined', () => {
const inspect = preferences.inspect('launch');
/** @type {PreferenceInspection} */
let expected = inspectExpectation;
if (!expected) {
expected = {
Expand All @@ -529,11 +534,13 @@ describe('Launch Preferences', function () {
Object.assign(expected, { workspaceValue });
}
}
assert.deepStrictEqual(JSON.parse(JSON.stringify(inspect)), expected);
const expectedValue = expected.workspaceFolderValue || expected.workspaceValue || expected.globalValue || expected.defaultValue;
assert.deepStrictEqual(JSON.parse(JSON.stringify(inspect)), { ...expected, value: expectedValue });
});

testIt('inspect in rootUri', () => {
const inspect = preferences.inspect('launch', rootUri.toString());
/** @type {PreferenceInspection} */
const expected = {
preferenceName: 'launch',
defaultValue: defaultLaunch
Expand All @@ -552,7 +559,8 @@ describe('Launch Preferences', function () {
});
}
}
assert.deepStrictEqual(JSON.parse(JSON.stringify(inspect)), expected);
const expectedValue = expected.workspaceFolderValue || expected.workspaceValue || expected.globalValue || expected.defaultValue;
assert.deepStrictEqual(JSON.parse(JSON.stringify(inspect)), { ...expected, value: expectedValue });
});

testIt('update launch', async () => {
Expand All @@ -564,15 +572,6 @@ describe('Launch Preferences', function () {
assert.deepStrictEqual(actual, expected);
});

testIt('update launch Global', async () => {
try {
await preferences.set('launch', validLaunch, PreferenceScope.User);
assert.fail('should not be possible to update User Settings');
} catch (e) {
assert.deepStrictEqual(e.message, 'Unable to write to User Settings because launch does not support for global scope.');
}
});

testIt('update launch Workspace', async () => {
await preferences.set('launch', validLaunch, PreferenceScope.Workspace);

Expand Down
126 changes: 106 additions & 20 deletions packages/core/src/browser/preferences/preference-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,107 @@ describe('Preference Service', () => {
assert.strictEqual(prefService.get('[go].editor.insertSpaces'), undefined, 'get after overridden');
});

function prepareServices(options?: { schema: PreferenceSchema }): {
preferences: PreferenceServiceImpl;
schema: PreferenceSchemaProvider;
} {
prefSchema.setSchema(options && options.schema || {
properties: {
'editor.tabSize': {
type: 'number',
description: '',
overridable: true,
default: 4
}
}
});

return { preferences: prefService, schema: prefSchema };
}

describe('PreferenceService.updateValues()', () => {
const TAB_SIZE = 'editor.tabSize';
const DUMMY_URI = 'dummy_uri';
async function generateAndCheckValues(
preferences: PreferenceService,
globalValue: number | undefined,
workspaceValue: number | undefined,
workspaceFolderValue: number | undefined
): Promise<void> {
await preferences.set(TAB_SIZE, globalValue, PreferenceScope.User);
await preferences.set(TAB_SIZE, workspaceValue, PreferenceScope.Workspace);
await preferences.set(TAB_SIZE, workspaceFolderValue, PreferenceScope.Folder, DUMMY_URI);
const expectedValue = workspaceFolderValue ?? workspaceValue ?? globalValue ?? 4;
checkValues(preferences, globalValue, workspaceValue, workspaceFolderValue, expectedValue);
}

function checkValues(
preferences: PreferenceService,
globalValue: number | undefined,
workspaceValue: number | undefined,
workspaceFolderValue: number | undefined,
value: number = 4,
): void {
const expected = {
preferenceName: 'editor.tabSize',
defaultValue: 4,
globalValue,
workspaceValue,
workspaceFolderValue,
value,
};
const inspection = preferences.inspect(TAB_SIZE, DUMMY_URI);
assert.deepStrictEqual(inspection, expected);
}

it('should modify the narrowest scope.', async () => {
const { preferences } = prepareServices();

await generateAndCheckValues(preferences, 1, 2, 3);
await preferences.updateValue(TAB_SIZE, 8, DUMMY_URI);
checkValues(preferences, 1, 2, 8, 8);

await generateAndCheckValues(preferences, 1, 2, undefined);
await preferences.updateValue(TAB_SIZE, 8, DUMMY_URI);
checkValues(preferences, 1, 8, undefined, 8);

await generateAndCheckValues(preferences, 1, undefined, undefined);
await preferences.updateValue(TAB_SIZE, 8, DUMMY_URI);
checkValues(preferences, 8, undefined, undefined, 8);
});

it('defaults to user scope.', async () => {
const { preferences } = prepareServices();
checkValues(preferences, undefined, undefined, undefined);
await preferences.updateValue(TAB_SIZE, 8, DUMMY_URI);
checkValues(preferences, 8, undefined, undefined, 8);
});

it('clears all settings when input is undefined.', async () => {
const { preferences } = prepareServices();

await generateAndCheckValues(preferences, 1, 2, 3);
await preferences.updateValue(TAB_SIZE, undefined, DUMMY_URI);
checkValues(preferences, undefined, undefined, undefined);
});

it('deletes user setting if user is only defined scope and target is default value', async () => {
const { preferences } = prepareServices();

await generateAndCheckValues(preferences, 8, undefined, undefined);
await preferences.updateValue(TAB_SIZE, 4, DUMMY_URI);
checkValues(preferences, undefined, undefined, undefined);
});

it('does not delete setting in lower scopes, even if target is default', async () => {
const { preferences } = prepareServices();

await generateAndCheckValues(preferences, undefined, 2, undefined);
await preferences.updateValue(TAB_SIZE, 4, DUMMY_URI);
checkValues(preferences, undefined, 4, undefined);
});
});

describe('overridden preferences', () => {

it('get #0', () => {
Expand Down Expand Up @@ -320,6 +421,7 @@ describe('Preference Service', () => {
globalValue: undefined,
workspaceValue: undefined,
workspaceFolderValue: undefined,
value: 4,
};
assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize'));
assert.ok(!preferences.has('[json].editor.tabSize'));
Expand All @@ -342,6 +444,7 @@ describe('Preference Service', () => {
globalValue: 2,
workspaceValue: undefined,
workspaceFolderValue: undefined,
value: 2
};
preferences.set('editor.tabSize', 2, PreferenceScope.User);

Expand All @@ -366,6 +469,7 @@ describe('Preference Service', () => {
globalValue: undefined,
workspaceValue: undefined,
workspaceFolderValue: undefined,
value: 4
};
assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize'));
assert.ok(!preferences.has('[json].editor.tabSize'));
Expand All @@ -377,7 +481,8 @@ describe('Preference Service', () => {
assert.deepStrictEqual({
...expected,
preferenceName: '[json].editor.tabSize',
globalValue: 2
globalValue: 2,
value: 2,
}, preferences.inspect('[json].editor.tabSize'));
});

Expand Down Expand Up @@ -511,25 +616,6 @@ describe('Preference Service', () => {
assert.strictEqual(false, preferences.get('editor.formatOnSave'));
assert.strictEqual(true, preferences.get('[go].editor.formatOnSave'));
});

function prepareServices(options?: { schema: PreferenceSchema }): {
preferences: PreferenceServiceImpl;
schema: PreferenceSchemaProvider;
} {
prefSchema.setSchema(options && options.schema || {
properties: {
'editor.tabSize': {
type: 'number',
description: '',
overridable: true,
default: 4
}
}
});

return { preferences: prefService, schema: prefSchema };
}

});

});
81 changes: 68 additions & 13 deletions packages/core/src/browser/preferences/preference-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { PreferenceSchemaProvider, OverridePreferenceName } from './preference-c
import URI from '../../common/uri';
import { PreferenceScope } from './preference-scope';
import { PreferenceConfigurations } from './preference-configurations';
import { JSONExt } from '@phosphor/coreutils/lib/json';

export { PreferenceScope };

Expand Down Expand Up @@ -156,6 +157,18 @@ export interface PreferenceService extends Disposable {
* with an error.
*/
set(preferenceName: string, value: any, scope?: PreferenceScope, resourceUri?: string): Promise<void>;

/**
* Determines and applies the changes necessary to apply `value` to either the `resourceUri` supplied or the active session.
* If there is no setting for the `preferenceName`, the change will be applied in user scope.
* If there is a setting conflicting with the specified `value`, the change will be applied in the most specific scope with a conflicting value.
*
* @param preferenceName the identifier of the preference to modify.
* @param value the value to which to set the preference. `undefined` will reset the preference to its default value.
* @param resourceUri the uri of the resource to which the change is to apply. If none is provided, folder scope will be ignored.
*/
updateValue(preferenceName: string, value: any, resourceUri?: string): Promise<void>

/**
* Registers a callback which will be called whenever a preference is changed.
*/
Expand Down Expand Up @@ -244,7 +257,11 @@ export interface PreferenceInspection<T> {
/**
* Value in folder scope.
*/
workspaceFolderValue: T | undefined
workspaceFolderValue: T | undefined,
/**
* The value that is active, i.e. the value set in the lowest scope available.
*/
value: T | undefined;
}

/**
Expand Down Expand Up @@ -401,10 +418,7 @@ export class PreferenceServiceImpl implements PreferenceService {
}

async set(preferenceName: string, value: any, scope: PreferenceScope | undefined, resourceUri?: string): Promise<void> {
const resolvedScope = scope !== undefined ? scope : (!resourceUri ? PreferenceScope.Workspace : PreferenceScope.Folder);
if (resolvedScope === PreferenceScope.User && this.configurations.isSectionName(preferenceName.split('.', 1)[0])) {
throw new Error(`Unable to write to User Settings because ${preferenceName} does not support for global scope.`);
}
const resolvedScope = scope ?? (!resourceUri ? PreferenceScope.Workspace : PreferenceScope.Folder);
if (resolvedScope === PreferenceScope.Folder && !resourceUri) {
throw new Error('Unable to write to Folder Settings because no resource is provided.');
}
Expand Down Expand Up @@ -451,20 +465,17 @@ export class PreferenceServiceImpl implements PreferenceService {
return Number(value);
}

inspect<T>(preferenceName: string, resourceUri?: string): {
preferenceName: string,
defaultValue: T | undefined,
globalValue: T | undefined, // User Preference
workspaceValue: T | undefined, // Workspace Preference
workspaceFolderValue: T | undefined // Folder Preference
} | undefined {
inspect<T>(preferenceName: string, resourceUri?: string): PreferenceInspection<T> | undefined {
const defaultValue = this.inspectInScope<T>(preferenceName, PreferenceScope.Default, resourceUri);
const globalValue = this.inspectInScope<T>(preferenceName, PreferenceScope.User, resourceUri);
const workspaceValue = this.inspectInScope<T>(preferenceName, PreferenceScope.Workspace, resourceUri);
const workspaceFolderValue = this.inspectInScope<T>(preferenceName, PreferenceScope.Folder, resourceUri);

return { preferenceName, defaultValue, globalValue, workspaceValue, workspaceFolderValue };
const valueApplied = workspaceFolderValue ?? workspaceValue ?? globalValue ?? defaultValue;

return { preferenceName, defaultValue, globalValue, workspaceValue, workspaceFolderValue, value: valueApplied };
}

protected inspectInScope<T>(preferenceName: string, scope: PreferenceScope, resourceUri?: string): T | undefined {
const value = this.doInspectInScope<T>(preferenceName, scope, resourceUri);
if (value === undefined) {
Expand All @@ -476,6 +487,50 @@ export class PreferenceServiceImpl implements PreferenceService {
return value;
}

protected getScopedValueFromInspection<T>(inspection: PreferenceInspection<T>, scope: PreferenceScope): T | undefined {
switch (scope) {
case PreferenceScope.Default:
return inspection.defaultValue;
case PreferenceScope.User:
return inspection.globalValue;
case PreferenceScope.Workspace:
return inspection.workspaceValue;
case PreferenceScope.Folder:
return inspection.workspaceFolderValue;
}
return ((unhandledScope: never): never => { throw new Error('Must handle all enum values!'); })(scope);
}

async updateValue(preferenceName: string, value: any, resourceUri?: string): Promise<void> {
const inspection = this.inspect<any>(preferenceName, resourceUri);
if (inspection) {
const scopesToChange = this.getScopesToChange(inspection, value);
const isDeletion = value === undefined
|| (scopesToChange.length === 1 && scopesToChange[0] === PreferenceScope.User && JSONExt.deepEqual(value, inspection.defaultValue));
const effectiveValue = isDeletion ? undefined : value;
await Promise.all(scopesToChange.map(scope => this.set(preferenceName, effectiveValue, scope, resourceUri)));
}
}

protected getScopesToChange(inspection: PreferenceInspection<any>, intendedValue: any): PreferenceScope[] {
if (JSONExt.deepEqual(inspection.value, intendedValue)) {
return [];
}

// Scopes in ascending order of scope breadth.
const allScopes = PreferenceScope.getReversedScopes();
// Get rid of Default scope. We can't set anything there.
allScopes.pop();

const isScopeDefined = (scope: PreferenceScope) => this.getScopedValueFromInspection(inspection, scope) !== undefined;

if (intendedValue === undefined) {
return allScopes.filter(isScopeDefined);
}

return [allScopes.find(isScopeDefined) ?? PreferenceScope.User];
}

overridePreferenceName(options: OverridePreferenceName): string {
return this.schema.overridePreferenceName(options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { PreferenceService, PreferenceChange } from '../';
import { Emitter, Event } from '../../../common';
import { OverridePreferenceName } from '../preference-contribution';
import URI from '../../../common/uri';
import { PreferenceChanges } from '../preference-service';
import { PreferenceChanges, PreferenceInspection } from '../preference-service';
import { PreferenceScope } from '../preference-scope';

@injectable()
Expand All @@ -38,17 +38,12 @@ export class MockPreferenceService implements PreferenceService {
} {
return {};
}
inspect<T>(preferenceName: string, resourceUri?: string): {
preferenceName: string,
defaultValue: T | undefined,
globalValue: T | undefined, // User Preference
workspaceValue: T | undefined, // Workspace Preference
workspaceFolderValue: T | undefined // Folder Preference
} | undefined {
inspect<T>(preferenceName: string, resourceUri?: string): PreferenceInspection<T> | undefined {
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
set(preferenceName: string, value: any): Promise<void> { return Promise.resolve(); }
updateValue(): Promise<void> { return Promise.resolve(); }
ready: Promise<void> = Promise.resolve();
readonly onPreferenceChanged: Event<PreferenceChange> = new Emitter<PreferenceChange>().event;
readonly onPreferencesChanged: Event<PreferenceChanges> = new Emitter<PreferenceChanges>().event;
Expand Down
Loading

0 comments on commit ce50a61

Please sign in to comment.