diff --git a/packages/core/src/browser/preferences/preference-provider.ts b/packages/core/src/browser/preferences/preference-provider.ts index d331a8058de6e..581e4defc97af 100644 --- a/packages/core/src/browser/preferences/preference-provider.ts +++ b/packages/core/src/browser/preferences/preference-provider.ts @@ -43,8 +43,8 @@ export interface PreferenceResolveResult { @injectable() export abstract class PreferenceProvider implements Disposable { - protected readonly onDidPreferencesChangedEmitter = new Emitter(); - readonly onDidPreferencesChanged: Event = this.onDidPreferencesChangedEmitter.event; + protected readonly onDidPreferencesChangedEmitter = new Emitter(); + readonly onDidPreferencesChanged: Event = this.onDidPreferencesChangedEmitter.event; protected readonly toDispose = new DisposableCollection(); @@ -74,14 +74,6 @@ export abstract class PreferenceProvider implements Disposable { } } - /** - * Informs the listeners that one or more preferences of this provider are changed. - * @deprecated Use emitPreferencesChangedEvent instead. - */ - protected fireOnDidPreferencesChanged(): void { - this.onDidPreferencesChangedEmitter.fire(undefined); - } - get(preferenceName: string, resourceUri?: string): T | undefined { return this.resolve(preferenceName, resourceUri).value; } diff --git a/packages/core/src/browser/preferences/preference-proxy.spec.ts b/packages/core/src/browser/preferences/preference-proxy.spec.ts index c74b1ebf68979..561cb18bb43d5 100644 --- a/packages/core/src/browser/preferences/preference-proxy.spec.ts +++ b/packages/core/src/browser/preferences/preference-proxy.spec.ts @@ -14,18 +14,166 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { createPreferenceProxy } from './preference-proxy'; -import { MockPreferenceService } from './test/mock-preference-service'; -import { expect } from 'chai'; +// tslint:disable:no-any +// tslint:disable:no-unused-expression -describe('preference proxy', function (): void { - /** Verify the return type of the ready property. */ - it('.ready should return a promise', function (): void { +import { enableJSDOM } from '../test/jsdom'; - const proxy = createPreferenceProxy(new MockPreferenceService(), { - properties: {} +let disableJSDOM = enableJSDOM(); + +import * as assert from 'assert'; +import { Container } from 'inversify'; +import { bindPreferenceService } from '../frontend-application-bindings'; +import { bindMockPreferenceProviders, MockPreferenceProvider } from './test'; +import { PreferenceService, PreferenceServiceImpl } from './preference-service'; +import { PreferenceSchemaProvider, PreferenceSchema } from './preference-contribution'; +import { PreferenceScope } from './preference-scope'; +import { PreferenceProvider } from './preference-provider'; +import { FrontendApplicationConfigProvider } from '../frontend-application-config-provider'; +import { createPreferenceProxy, PreferenceProxyOptions, PreferenceProxy, PreferenceChangeEvent } from './preference-proxy'; + +disableJSDOM(); + +process.on('unhandledRejection', (reason, promise) => { + console.error(reason); + throw reason; +}); + +const { expect } = require('chai'); +let testContainer: Container; + +function createTestContainer(): Container { + const result = new Container(); + bindPreferenceService(result.bind.bind(result)); + bindMockPreferenceProviders(result.bind.bind(result), result.unbind.bind(result)); + return result; +} + +describe('Preference Proxy', () => { + let prefService: PreferenceServiceImpl; + let prefSchema: PreferenceSchemaProvider; + + before(() => { + disableJSDOM = enableJSDOM(); + FrontendApplicationConfigProvider.set({ + 'applicationName': 'test', + }); + }); + + after(() => { + disableJSDOM(); + }); + + beforeEach(async () => { + testContainer = createTestContainer(); + prefSchema = testContainer.get(PreferenceSchemaProvider); + prefService = testContainer.get(PreferenceService) as PreferenceServiceImpl; + getProvider(PreferenceScope.User).markReady(); + getProvider(PreferenceScope.Workspace).markReady(); + getProvider(PreferenceScope.Folder).markReady(); + console.log('before ready'); + try { + await prefService.ready; + } catch (e) { + console.error(e); + } + console.log('done'); + }); + + afterEach(() => { + }); + + function getProvider(scope: PreferenceScope): MockPreferenceProvider { + return testContainer.getNamed(PreferenceProvider, scope) as MockPreferenceProvider; + } + + function getProxy(schema?: PreferenceSchema, options?: PreferenceProxyOptions): PreferenceProxy<{ [key: string]: any }> { + const s: PreferenceSchema = schema || { + properties: { + 'my.pref': { + type: 'string', + defaultValue: 'foo' + } + } + }; + prefSchema.setSchema(s); + return createPreferenceProxy(prefService, s, options); + } + + it('by default, it should get provide access in flat style but not deep', () => { + const proxy = getProxy(); + expect(proxy['my.pref']).to.equal('foo'); + expect(proxy.my).to.equal(undefined); + expect(Object.keys(proxy).join()).to.equal(['my.pref'].join()); + }); + + it('it should get provide access in deep style but not flat', () => { + const proxy = getProxy(undefined, { style: 'deep' }); + expect(proxy['my.pref']).to.equal(undefined); + expect(proxy.my.pref).to.equal('foo'); + expect(Object.keys(proxy).join()).to.equal(['my'].join()); + }); + + it('it should get provide access in to both styles', () => { + const proxy = getProxy(undefined, { style: 'both' }); + expect(proxy['my.pref']).to.equal('foo'); + expect(proxy.my.pref).to.equal('foo'); + expect(Object.keys(proxy).join()).to.equal(['my', 'my.pref'].join()); + }); + + it('it should forward change events', () => { + const proxy = getProxy(undefined, { style: 'both' }); + let theChange: PreferenceChangeEvent<{ [key: string]: any }>; + proxy.onPreferenceChanged(change => { + expect(theChange).to.equal(undefined); + theChange = change; }); - const proto = Object.getPrototypeOf(proxy.ready); - expect(proto).to.equal(Promise.prototype); + let theSecondChange: PreferenceChangeEvent<{ [key: string]: any }>; + (proxy.my as PreferenceProxy<{ [key: string]: any }>).onPreferenceChanged(change => { + expect(theSecondChange).to.equal(undefined); + theSecondChange = change; + }); + + getProvider(PreferenceScope.User).setPreference('my.pref', 'bar'); + + expect(theChange!.newValue).to.equal('bar'); + expect(theChange!.oldValue).to.equal(undefined); + expect(theChange!.preferenceName).to.equal('my.pref'); + expect(theSecondChange!.newValue).to.equal('bar'); + expect(theSecondChange!.oldValue).to.equal(undefined); + expect(theSecondChange!.preferenceName).to.equal('my.pref'); + }); + + it('toJSON with deep', () => { + const proxy = getProxy({ + properties: { + 'foo.baz': { + type: 'number', + default: 4 + }, + 'foo.bar.x': { + type: 'boolean', + default: true + }, + 'foo.bar.y': { + type: 'boolean', + default: false + }, + 'a': { + type: 'string', + default: 'a' + } + } + }, { style: 'deep' }); + assert.deepStrictEqual(JSON.stringify(proxy, undefined, 2), JSON.stringify({ + foo: { + baz: 4, + bar: { + x: true, + y: false + } + }, + a: 'a' + }, undefined, 2), 'there should not be foo.bar.x to avoid sending excessive data to remote clients'); }); }); diff --git a/packages/core/src/browser/preferences/preference-proxy.ts b/packages/core/src/browser/preferences/preference-proxy.ts index 61d104e2dab16..795912a4d6881 100644 --- a/packages/core/src/browser/preferences/preference-proxy.ts +++ b/packages/core/src/browser/preferences/preference-proxy.ts @@ -16,9 +16,10 @@ // tslint:disable:no-any -import { Disposable, DisposableCollection, Event, Emitter } from '../../common'; +import { Disposable, Event } from '../../common'; import { PreferenceService } from './preference-service'; import { PreferenceSchema, OverridePreferenceName } from './preference-contribution'; +import { PreferenceScope } from './preference-scope'; export interface PreferenceChangeEvent { readonly preferenceName: keyof T; @@ -40,39 +41,53 @@ export interface PreferenceRetrieval { } export type PreferenceProxy = Readonly & Disposable & PreferenceEventEmitter & PreferenceRetrieval; +export interface PreferenceProxyOptions { + prefix?: string; + resourceUri?: string; + overrideIdentifier?: string; + style?: 'flat' | 'deep' | 'both'; +} -export function createPreferenceProxy(preferences: PreferenceService, schema: PreferenceSchema): PreferenceProxy { - const toDispose = new DisposableCollection(); - const onPreferenceChangedEmitter = new Emitter>(); - toDispose.push(onPreferenceChangedEmitter); - toDispose.push(preferences.onPreferenceChanged(e => { - const overridden = preferences.overriddenPreferenceName(e.preferenceName); - const preferenceName: any = overridden ? overridden.preferenceName : e.preferenceName; - if (schema.properties[preferenceName]) { - const { newValue, oldValue } = e; - onPreferenceChangedEmitter.fire({ - newValue, oldValue, preferenceName, - affects: (resourceUri, overrideIdentifier) => { - if (overrideIdentifier !== undefined) { - if (overridden && overridden.overrideIdentifier !== overrideIdentifier) { - return false; +export function createPreferenceProxy(preferences: PreferenceService, schema: PreferenceSchema, options?: PreferenceProxyOptions): PreferenceProxy { + const opts = options || {}; + const prefix = opts.prefix || ''; + const style = opts.style || 'flat'; + const isDeep = style === 'deep' || style === 'both'; + const isFlat = style === 'both' || style === 'flat'; + const onPreferenceChanged = (listener: (e: PreferenceChangeEvent) => any, thisArgs?: any, disposables?: Disposable[]) => preferences.onPreferencesChanged(changes => { + for (const key of Object.keys(changes)) { + const e = changes[key]; + const overridden = preferences.overriddenPreferenceName(e.preferenceName); + const preferenceName: any = overridden ? overridden.preferenceName : e.preferenceName; + if (preferenceName.startsWith(prefix) && (!overridden || !opts.overrideIdentifier || overridden.overrideIdentifier === opts.overrideIdentifier)) { + if (schema.properties[preferenceName]) { + const { newValue, oldValue } = e; + listener({ + newValue, oldValue, preferenceName, + affects: (resourceUri, overrideIdentifier) => { + if (overrideIdentifier !== undefined) { + if (overridden && overridden.overrideIdentifier !== overrideIdentifier) { + return false; + } + } + return e.affects(resourceUri); } - } - return e.affects(resourceUri); + }); } - }); + } } - })); + }, thisArgs, disposables); const unsupportedOperation = (_: any, __: string) => { throw new Error('Unsupported operation'); }; + const getValue: PreferenceRetrieval['get'] = (arg, defaultValue, resourceUri) => { const isArgOverridePreferenceName = typeof arg === 'object' && arg.overrideIdentifier; const preferenceName = isArgOverridePreferenceName ? preferences.overridePreferenceName(arg) : arg; - const value = preferences.get(preferenceName, defaultValue, resourceUri); + const value = preferences.get(preferenceName, defaultValue, resourceUri || opts.resourceUri); if (preferences.validate(isArgOverridePreferenceName ? (arg).preferenceName : preferenceName, value)) { return value; } @@ -82,33 +97,119 @@ export function createPreferenceProxy(preferences: PreferenceService, schema: const values = preferences.inspect(preferenceName, resourceUri); return values && values.defaultValue; }; - return new Proxy({}, { - get: (_, property: string) => { - if (schema.properties[property]) { - const value = preferences.get(property); - if (preferences.validate(property, value)) { - return value; + + const ownKeys: () => string[] = () => { + const properties = []; + for (const p of Object.keys(schema.properties)) { + if (p.startsWith(prefix)) { + const idx = p.indexOf('.', prefix.length); + if (idx !== -1 && isDeep) { + const pre = p.substr(prefix.length, idx - prefix.length); + if (properties.indexOf(pre) === -1) { + properties.push(pre); + } + } + const prop = p.substr(prefix.length); + if (isFlat || prop.indexOf('.') === -1) { + properties.push(prop); } - const values = preferences.inspect(property); - return values && values.defaultValue; - } - if (property === 'onPreferenceChanged') { - return onPreferenceChangedEmitter.event; } - if (property === 'dispose') { - return () => toDispose.dispose(); + } + return properties; + }; + + const set: (target: any, prop: string, value: any, receiver: any) => boolean = (_, property: string | symbol | number, value: any) => { + if (typeof property !== 'string') { + throw new Error(`unexpected property: ${String(property)}`); + } + if (style === 'deep' && property.indexOf('.') !== -1) { + return false; + } + const fullProperty = prefix ? prefix + property : property; + if (schema.properties[fullProperty]) { + preferences.set(fullProperty, value, PreferenceScope.Default); + return true; + } + const newPrefix = fullProperty + '.'; + for (const p of Object.keys(schema.properties)) { + if (p.startsWith(newPrefix)) { + const subProxy: { [k: string]: any } = createPreferenceProxy(preferences, schema, { + prefix: newPrefix, + resourceUri: opts.resourceUri, + overrideIdentifier: opts.overrideIdentifier, + style + }); + for (const k of Object.keys(value)) { + subProxy[k] = value[k]; + } } - if (property === 'ready') { - return preferences.ready; + } + return false; + }; + + const get: (target: any, prop: string) => any = (_, property: string | symbol | number) => { + if (typeof property !== 'string') { + throw new Error(`unexpected property: ${String(property)}`); + } + const fullProperty = prefix ? prefix + property : property; + if (isFlat || property.indexOf('.') === -1) { + if (schema.properties[fullProperty]) { + let value; + if (opts.overrideIdentifier) { + value = preferences.get(preferences.overridePreferenceName({ + overrideIdentifier: opts.overrideIdentifier, + preferenceName: fullProperty + }), undefined, opts.resourceUri); + } + if (value === undefined) { + value = preferences.get(fullProperty, undefined, opts.resourceUri); + } + if (preferences.validate(fullProperty, value)) { + return value; + } + const values = preferences.inspect(fullProperty, opts.resourceUri); + return values && values.defaultValue; } - if (property === 'get') { - return getValue; + } + if (property === 'onPreferenceChanged') { + return onPreferenceChanged; + } + if (property === 'dispose') { + return () => { /* do nothing */ }; + } + if (property === 'ready') { + return preferences.ready; + } + if (property === 'get') { + return getValue; + } + if (property === 'toJSON') { + return toJSON(); + } + if (isDeep) { + const newPrefix = fullProperty + '.'; + for (const p of Object.keys(schema.properties)) { + if (p.startsWith(newPrefix)) { + return createPreferenceProxy(preferences, schema, { prefix: newPrefix, resourceUri: opts.resourceUri, overrideIdentifier: opts.overrideIdentifier, style }); + } } - throw new Error(`unexpected property: ${property}`); - }, - ownKeys: () => Object.keys(schema.properties), + } + return undefined; + }; + + const toJSON = () => { + const result: any = {}; + for (const k of ownKeys()) { + result[k] = get(undefined, k); + } + return result; + }; + + return new Proxy({}, { + get, + ownKeys, getOwnPropertyDescriptor: (_, property: string) => { - if (schema.properties[property]) { + if (ownKeys().indexOf(property) !== -1) { return { enumerable: true, configurable: true @@ -116,7 +217,7 @@ export function createPreferenceProxy(preferences: PreferenceService, schema: } return {}; }, - set: unsupportedOperation, + set, deleteProperty: unsupportedOperation, defineProperty: unsupportedOperation }); diff --git a/packages/core/src/browser/preferences/preference-service.spec.ts b/packages/core/src/browser/preferences/preference-service.spec.ts new file mode 100644 index 0000000000000..a0c41e19d6f9d --- /dev/null +++ b/packages/core/src/browser/preferences/preference-service.spec.ts @@ -0,0 +1,459 @@ +/******************************************************************************** + * Copyright (C) 2018 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 + ********************************************************************************/ + +// tslint:disable:no-any +// tslint:disable:no-unused-expression + +import { enableJSDOM } from '../test/jsdom'; + +let disableJSDOM = enableJSDOM(); + +import * as assert from 'assert'; +import { Container } from 'inversify'; +import { bindPreferenceService } from '../frontend-application-bindings'; +import { bindMockPreferenceProviders, MockPreferenceProvider } from './test'; +import { PreferenceService, PreferenceServiceImpl, PreferenceChange } from './preference-service'; +import { PreferenceSchemaProvider, PreferenceSchema } from './preference-contribution'; +import { PreferenceScope } from './preference-scope'; +import { PreferenceProvider } from './preference-provider'; +import { FrontendApplicationConfigProvider } from '../frontend-application-config-provider'; +import { createPreferenceProxy, PreferenceChangeEvent } from './preference-proxy'; + +disableJSDOM(); + +process.on('unhandledRejection', (reason, promise) => { + console.error(reason); + throw reason; +}); + +const { expect } = require('chai'); +let testContainer: Container; + +function createTestContainer(): Container { + const result = new Container(); + bindPreferenceService(result.bind.bind(result)); + bindMockPreferenceProviders(result.bind.bind(result), result.unbind.bind(result)); + return result; +} + +describe('Preference Service', () => { + let prefService: PreferenceServiceImpl; + let prefSchema: PreferenceSchemaProvider; + + before(() => { + disableJSDOM = enableJSDOM(); + FrontendApplicationConfigProvider.set({ + 'applicationName': 'test', + }); + }); + + after(() => { + disableJSDOM(); + }); + + beforeEach(async () => { + testContainer = createTestContainer(); + prefSchema = testContainer.get(PreferenceSchemaProvider); + prefService = testContainer.get(PreferenceService) as PreferenceServiceImpl; + getProvider(PreferenceScope.User).markReady(); + getProvider(PreferenceScope.Workspace).markReady(); + getProvider(PreferenceScope.Folder).markReady(); + console.log('before ready'); + try { + await prefService.ready; + } catch (e) { + console.error(e); + } + console.log('done'); + }); + + afterEach(() => { + }); + + function getProvider(scope: PreferenceScope): MockPreferenceProvider { + return testContainer.getNamed(PreferenceProvider, scope) as MockPreferenceProvider; + } + + it('should get notified if a provider emits a change', done => { + prefSchema.setSchema({ + properties: { + 'testPref': { + type: 'string' + } + } + }); + const userProvider = getProvider(PreferenceScope.User); + userProvider.setPreference('testPref', 'oldVal'); + prefService.onPreferenceChanged(pref => { + if (pref) { + expect(pref.preferenceName).eq('testPref'); + expect(pref.newValue).eq('newVal'); + return done(); + } + return done(new Error('onPreferenceChanged() fails to return any preference change infomation')); + }); + + userProvider.emitPreferencesChangedEvent({ + testPref: { + preferenceName: 'testPref', + newValue: 'newVal', + oldValue: 'oldVal', + scope: PreferenceScope.User, + domain: [] + } + }); + }).timeout(2000); + + it('should return the preference from the more specific scope (user > workspace)', () => { + prefSchema.setSchema({ + properties: { + 'test.number': { + type: 'number', + scope: 'resource' + } + } + }); + const userProvider = getProvider(PreferenceScope.User); + const workspaceProvider = getProvider(PreferenceScope.Workspace); + const folderProvider = getProvider(PreferenceScope.Folder); + userProvider.setPreference('test.number', 1); + expect(prefService.get('test.number')).equals(1); + workspaceProvider.setPreference('test.number', 0); + expect(prefService.get('test.number')).equals(0); + folderProvider.setPreference('test.number', 2); + expect(prefService.get('test.number')).equals(2); + + // remove property on lower scope + folderProvider.setPreference('test.number', undefined); + expect(prefService.get('test.number')).equals(0); + }); + + it('should throw a TypeError if the preference (reference object) is modified', () => { + prefSchema.setSchema({ + properties: { + 'test.immutable': { + type: 'array', + items: { + type: 'string' + }, + scope: 'resource' + } + } + }); + const userProvider = getProvider(PreferenceScope.User); + userProvider.setPreference('test.immutable', [ + 'test', 'test', 'test' + ]); + const immutablePref: string[] | undefined = prefService.get('test.immutable'); + expect(immutablePref).to.not.be.undefined; + if (immutablePref !== undefined) { + expect(() => immutablePref.push('fails')).to.throw(TypeError); + } + }); + + it('should still report the more specific preference even though the less specific one changed', () => { + prefSchema.setSchema({ + properties: { + 'test.number': { + type: 'number', + scope: 'resource' + } + } + }); + const userProvider = getProvider(PreferenceScope.User); + const workspaceProvider = getProvider(PreferenceScope.Workspace); + userProvider.setPreference('test.number', 1); + workspaceProvider.setPreference('test.number', 0); + expect(prefService.get('test.number')).equals(0); + + userProvider.setPreference('test.number', 4); + expect(prefService.get('test.number')).equals(0); + }); + + describe('overridden preferences', () => { + + it('get #0', () => { + const { preferences, schema } = prepareServices(); + + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + + expect(preferences.get('editor.tabSize')).to.equal(4); + expect(preferences.get('[json].editor.tabSize')).to.equal(undefined); + + schema.registerOverrideIdentifier('json'); + + expect(preferences.get('editor.tabSize')).to.equal(4); + expect(preferences.get('[json].editor.tabSize')).to.equal(2); + }); + + it('get #1', () => { + const { preferences, schema } = prepareServices(); + schema.registerOverrideIdentifier('json'); + + expect(preferences.get('editor.tabSize')).to.equal(4); + expect(preferences.get('[json].editor.tabSize')).to.equal(4); + + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + + expect(preferences.get('editor.tabSize')).to.equal(4); + expect(preferences.get('[json].editor.tabSize')).to.equal(2); + }); + + it('get #2', () => { + const { preferences, schema } = prepareServices(); + schema.registerOverrideIdentifier('json'); + + expect(preferences.get('editor.tabSize')).to.equal(4); + expect(preferences.get('[json].editor.tabSize')).to.equal(4); + + preferences.set('editor.tabSize', 2, PreferenceScope.User); + + expect(preferences.get('editor.tabSize')).to.equal(2); + expect(preferences.get('[json].editor.tabSize')).to.equal(2); + }); + + it('has', () => { + const { preferences, schema } = prepareServices(); + + expect(preferences.has('editor.tabSize')).to.be.true; + expect(preferences.has('[json].editor.tabSize')).to.be.false; + + schema.registerOverrideIdentifier('json'); + + expect(preferences.has('editor.tabSize')).to.be.true; + expect(preferences.has('[json].editor.tabSize')).to.be.true; + }); + + it('inspect #0', () => { + const { preferences, schema } = prepareServices(); + + const expected = { + preferenceName: 'editor.tabSize', + defaultValue: 4, + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); + assert.ok(!preferences.has('[json].editor.tabSize')); + + schema.registerOverrideIdentifier('json'); + + assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); + assert.deepStrictEqual({ + ...expected, + preferenceName: '[json].editor.tabSize' + }, preferences.inspect('[json].editor.tabSize')); + }); + + it('inspect #1', () => { + const { preferences, schema } = prepareServices(); + + const expected = { + preferenceName: 'editor.tabSize', + defaultValue: 4, + globalValue: 2, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + preferences.set('editor.tabSize', 2, PreferenceScope.User); + + assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); + assert.ok(!preferences.has('[json].editor.tabSize')); + + schema.registerOverrideIdentifier('json'); + + assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); + assert.deepStrictEqual({ + ...expected, + preferenceName: '[json].editor.tabSize' + }, preferences.inspect('[json].editor.tabSize')); + }); + + it('inspect #2', () => { + const { preferences, schema } = prepareServices(); + + const expected = { + preferenceName: 'editor.tabSize', + defaultValue: 4, + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); + assert.ok(!preferences.has('[json].editor.tabSize')); + + schema.registerOverrideIdentifier('json'); + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + + assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); + assert.deepStrictEqual({ + ...expected, + preferenceName: '[json].editor.tabSize', + globalValue: 2 + }, preferences.inspect('[json].editor.tabSize')); + }); + + it('onPreferenceChanged #0', () => { + const { preferences, schema } = prepareServices(); + + const events: PreferenceChange[] = []; + preferences.onPreferenceChanged(event => events.push(event)); + + schema.registerOverrideIdentifier('json'); + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + preferences.set('editor.tabSize', 3, PreferenceScope.User); + + assert.deepStrictEqual([{ + preferenceName: '[json].editor.tabSize', + newValue: 2 + }, { + preferenceName: 'editor.tabSize', + newValue: 3 + }], events.map(e => ({ + preferenceName: e.preferenceName, + newValue: e.newValue + }))); + }); + + it('onPreferenceChanged #1', () => { + const { preferences, schema } = prepareServices(); + + const events: PreferenceChange[] = []; + preferences.onPreferenceChanged(event => events.push(event)); + + schema.registerOverrideIdentifier('json'); + preferences.set('editor.tabSize', 2, PreferenceScope.User); + + assert.deepStrictEqual([{ + preferenceName: 'editor.tabSize', + newValue: 2 + }, { + preferenceName: '[json].editor.tabSize', + newValue: 2 + }], events.map(e => ({ + preferenceName: e.preferenceName, + newValue: e.newValue + }))); + }); + + it('onPreferenceChanged #2', () => { + const { preferences, schema } = prepareServices(); + + schema.registerOverrideIdentifier('json'); + schema.registerOverrideIdentifier('javascript'); + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + preferences.set('editor.tabSize', 3, PreferenceScope.User); + + const events: PreferenceChangeEvent<{ [key: string]: any }>[] = []; + const proxy = createPreferenceProxy<{ [key: string]: any }>(preferences, schema.getCombinedSchema(), { overrideIdentifier: 'json' }); + proxy.onPreferenceChanged(event => events.push(event)); + + preferences.set('[javascript].editor.tabSize', 4, PreferenceScope.User); + + assert.deepStrictEqual([], events.map(e => ({ + preferenceName: e.preferenceName, + newValue: e.newValue + })), 'changes not relevant to json override should be ignored'); + }); + + it('onPreferenceChanged #3', () => { + const { preferences, schema } = prepareServices(); + + schema.registerOverrideIdentifier('json'); + preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); + preferences.set('editor.tabSize', 3, PreferenceScope.User); + + const events: PreferenceChange[] = []; + preferences.onPreferenceChanged(event => events.push(event)); + + preferences.set('[json].editor.tabSize', undefined, PreferenceScope.User); + + assert.deepStrictEqual([{ + preferenceName: '[json].editor.tabSize', + newValue: 3 + }], events.map(e => ({ + preferenceName: e.preferenceName, + newValue: e.newValue + }))); + }); + + it('defaultOverrides [go].editor.formatOnSave', () => { + const { preferences, schema } = prepareServices({ + schema: { + properties: { + 'editor.insertSpaces': { + type: 'boolean', + default: true, + overridable: true + }, + 'editor.formatOnSave': { + type: 'boolean', + default: false, + overridable: true + } + } + } + }); + + assert.strictEqual(true, preferences.get('editor.insertSpaces')); + assert.strictEqual(undefined, preferences.get('[go].editor.insertSpaces')); + assert.strictEqual(false, preferences.get('editor.formatOnSave')); + assert.strictEqual(undefined, preferences.get('[go].editor.formatOnSave')); + + schema.registerOverrideIdentifier('go'); + schema.setSchema({ + id: 'defaultOverrides', + title: 'Default Configuration Overrides', + properties: { + '[go]': { + type: 'object', + default: { + 'editor.insertSpaces': false, + 'editor.formatOnSave': true + }, + description: 'Configure editor settings to be overridden for go language.' + } + } + }); + + assert.strictEqual(true, preferences.get('editor.insertSpaces')); + assert.strictEqual(false, preferences.get('[go].editor.insertSpaces')); + 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 }; + } + + }); + +}); diff --git a/packages/core/src/browser/preferences/preference-service.ts b/packages/core/src/browser/preferences/preference-service.ts index eb305912b1302..2b56901f15489 100644 --- a/packages/core/src/browser/preferences/preference-service.ts +++ b/packages/core/src/browser/preferences/preference-service.ts @@ -17,8 +17,6 @@ // tslint:disable:no-any import { injectable, inject, postConstruct } from 'inversify'; -import { JSONExt } from '@phosphor/coreutils/lib/json'; -import { FrontendApplicationContribution } from '../../browser'; import { Event, Emitter, DisposableCollection, Disposable, deepFreeze } from '../../common'; import { Deferred } from '../../common/promise-util'; import { PreferenceProvider, PreferenceProviderDataChange, PreferenceProviderDataChanges, PreferenceResolveResult } from './preference-provider'; @@ -76,6 +74,7 @@ export interface PreferenceService extends Disposable { get(preferenceName: string, defaultValue?: T, resourceUri?: string): T | undefined; set(preferenceName: string, value: any, scope?: PreferenceScope, resourceUri?: string): Promise; onPreferenceChanged: Event; + onPreferencesChanged: Event; inspect(preferenceName: string, resourceUri?: string): { preferenceName: string, @@ -100,7 +99,7 @@ export const PreferenceProviderProvider = Symbol('PreferenceProviderProvider'); export type PreferenceProviderProvider = (scope: PreferenceScope, uri?: URI) => PreferenceProvider; @injectable() -export class PreferenceServiceImpl implements PreferenceService, FrontendApplicationContribution { +export class PreferenceServiceImpl implements PreferenceService { protected readonly onPreferenceChangedEmitter = new Emitter(); readonly onPreferenceChanged = this.onPreferenceChangedEmitter.event; @@ -119,18 +118,30 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica @inject(PreferenceConfigurations) protected readonly configurations: PreferenceConfigurations; - protected readonly providers: PreferenceProvider[] = []; - protected providersMap: Map = new Map(); + protected readonly preferenceProviders = new Map(); - /** - * @deprecated Use getPreferences() instead - */ - protected preferences: { [key: string]: any } = {}; + protected async initializeProviders(): Promise { + try { + for (const scope of PreferenceScope.getScopes()) { + const provider = this.providerProvider(scope); + this.preferenceProviders.set(scope, provider); + this.toDispose.push(provider.onDidPreferencesChanged(changes => + this.reconcilePreferences(changes) + )); + await provider.ready; + } + this._ready.resolve(); + } catch (e) { + this._ready.reject(e); + } + } @postConstruct() protected init(): void { this.toDispose.push(Disposable.create(() => this._ready.reject(new Error('preference service is disposed')))); - this.doSetProvider(PreferenceScope.Default, this.schema); + this.onPreferenceChanged.maxListeners = 64; + this.onPreferencesChanged.maxListeners = 64; + this.initializeProviders(); } dispose(): void { @@ -142,96 +153,50 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica return this._ready.promise; } - initialize(): void { - this.initializeProviders(); - } - - protected initializeProviders(): void { - try { - this.createProviders(); - if (this.toDispose.disposed) { - return; - } - for (const provider of this.providersMap.values()) { - this.toDispose.push(provider.onDidPreferencesChanged(changes => - this.reconcilePreferences(changes) - )); - } - Promise.all(this.providers.map(p => p.ready)).then(() => this._ready.resolve()); - } catch (e) { - this._ready.reject(e); - } - } - - protected createProviders(): PreferenceProvider[] { - const providers: PreferenceProvider[] = []; - PreferenceScope.getScopes().forEach(scope => { - const p = this.doCreateProvider(scope); - if (p) { - providers.push(p); - } - }); - return providers; - } - - protected reconcilePreferences(changes?: PreferenceProviderDataChanges): void { + protected reconcilePreferences(changes: PreferenceProviderDataChanges): void { const changesToEmit: PreferenceChanges = {}; const acceptChange = (change: PreferenceProviderDataChange) => this.getAffectedPreferenceNames(change, preferenceName => changesToEmit[preferenceName] = new PreferenceChangeImpl({ ...change, preferenceName }) ); - if (changes) { - for (const preferenceName of Object.keys(changes)) { - let change = changes[preferenceName]; - if (change.newValue === undefined) { - const overridden = this.overriddenPreferenceName(change.preferenceName); - if (overridden) { - change = { - ...change, newValue: this.doGet(overridden.preferenceName) - }; - } - } - if (this.schema.isValidInScope(preferenceName, PreferenceScope.Folder)) { - acceptChange(change); - continue; + + for (const preferenceName of Object.keys(changes)) { + let change = changes[preferenceName]; + if (change.newValue === undefined) { + const overridden = this.overriddenPreferenceName(change.preferenceName); + if (overridden) { + change = { + ...change, newValue: this.doGet(overridden.preferenceName) + }; } - for (const scope of PreferenceScope.getReversedScopes()) { - if (this.schema.isValidInScope(preferenceName, scope)) { - const provider = this.getProvider(scope); - if (provider) { - const value = provider.get(preferenceName); - if (scope > change.scope && value !== undefined) { - // preference defined in a more specific scope - break; - } else if (scope === change.scope && change.newValue !== undefined) { - // preference is changed into something other than `undefined` - acceptChange(change); - } else if (scope < change.scope && change.newValue === undefined && value !== undefined) { - // preference is changed to `undefined`, use the value from a more general scope - change = { - ...change, - newValue: value, - scope - }; - acceptChange(change); - } + } + if (this.schema.isValidInScope(preferenceName, PreferenceScope.Folder)) { + acceptChange(change); + continue; + } + for (const scope of PreferenceScope.getReversedScopes()) { + if (this.schema.isValidInScope(preferenceName, scope)) { + const provider = this.getProvider(scope); + if (provider) { + const value = provider.get(preferenceName); + if (scope > change.scope && value !== undefined) { + // preference defined in a more specific scope + break; + } else if (scope === change.scope && change.newValue !== undefined) { + // preference is changed into something other than `undefined` + acceptChange(change); + } else if (scope < change.scope && change.newValue === undefined && value !== undefined) { + // preference is changed to `undefined`, use the value from a more general scope + change = { + ...change, + newValue: value, + scope + }; + acceptChange(change); } } } } - } else { // go through providers for the Default, User, and Workspace Scopes to find delta - const newPrefs = this.getPreferences(); - const oldPrefs = this.preferences; - for (const preferenceName of Object.keys(newPrefs)) { - const newValue = newPrefs[preferenceName]; - const oldValue = oldPrefs[preferenceName]; - if (newValue === undefined && oldValue !== newValue - || oldValue === undefined && newValue !== oldValue // JSONExt.deepEqual() does not support handling `undefined` - || !JSONExt.deepEqual(oldValue, newValue)) { - acceptChange({ newValue, oldValue, preferenceName, scope: PreferenceScope.Workspace, domain: [] }); - } - } - this.preferences = newPrefs; } // emit the changes @@ -250,31 +215,8 @@ export class PreferenceServiceImpl implements PreferenceService, FrontendApplica } } - protected doCreateProvider(scope: PreferenceScope): PreferenceProvider | undefined { - if (!this.providersMap.has(scope)) { - const provider = this.providerProvider(scope); - this.doSetProvider(scope, provider); - return provider; - } - return this.providersMap.get(scope); - } - - private doSetProvider(scope: PreferenceScope, provider: PreferenceProvider): void { - this.providersMap.set(scope, provider); - this.providers.push(provider); - this.toDispose.push(provider); - } - protected getProvider(scope: PreferenceScope): PreferenceProvider | undefined { - return this.providersMap.get(scope); - } - - getPreferences(resourceUri?: string): { [key: string]: any } { - const preferences: { [key: string]: any } = {}; - for (const preferenceName of this.schema.getPreferenceNames()) { - preferences[preferenceName] = this.get(preferenceName, undefined, resourceUri); - } - return preferences; + return this.preferenceProviders.get(scope); } has(preferenceName: string, resourceUri?: string): boolean { diff --git a/packages/core/src/browser/preferences/test/mock-preference-provider.ts b/packages/core/src/browser/preferences/test/mock-preference-provider.ts index 9c6fc9155ea48..ee9819b900190 100644 --- a/packages/core/src/browser/preferences/test/mock-preference-provider.ts +++ b/packages/core/src/browser/preferences/test/mock-preference-provider.ts @@ -16,21 +16,41 @@ // tslint:disable:no-any -import { injectable } from 'inversify'; +import { interfaces } from 'inversify'; import { PreferenceProvider } from '../'; import { PreferenceScope } from '../preference-scope'; +import { PreferenceProviderDataChanges, PreferenceProviderDataChange } from '../preference-provider'; -@injectable() export class MockPreferenceProvider extends PreferenceProvider { readonly prefs: { [p: string]: any } = {}; + constructor(protected scope: PreferenceScope) { + super(); + } + + public emitPreferencesChangedEvent(changes: PreferenceProviderDataChanges | PreferenceProviderDataChange[]): void { + super.emitPreferencesChangedEvent(changes); + } + + public markReady(): void { + this._ready.resolve(); + } + getPreferences(): { [p: string]: any } { return this.prefs; } async setPreference(preferenceName: string, newValue: any, resourceUri?: string): Promise { const oldValue = this.prefs[preferenceName]; this.prefs[preferenceName] = newValue; - this.emitPreferencesChangedEvent([{ preferenceName, oldValue, newValue, scope: PreferenceScope.User, domain: [] }]); + this.emitPreferencesChangedEvent([{ preferenceName, oldValue, newValue, scope: this.scope, domain: [] }]); return true; } } + +export function bindMockPreferenceProviders(bind: interfaces.Bind, unbind: interfaces.Unbind): void { + unbind(PreferenceProvider); + + bind(PreferenceProvider).toDynamicValue(ctx => new MockPreferenceProvider(PreferenceScope.User)).inSingletonScope().whenTargetNamed(PreferenceScope.User); + bind(PreferenceProvider).toDynamicValue(ctx => new MockPreferenceProvider(PreferenceScope.Workspace)).inSingletonScope().whenTargetNamed(PreferenceScope.Workspace); + bind(PreferenceProvider).toDynamicValue(ctx => new MockPreferenceProvider(PreferenceScope.Folder)).inSingletonScope().whenTargetNamed(PreferenceScope.Folder); +} diff --git a/packages/core/src/browser/preferences/test/mock-preference-service.ts b/packages/core/src/browser/preferences/test/mock-preference-service.ts index b88c5eb1c75c1..d3c0439924e76 100644 --- a/packages/core/src/browser/preferences/test/mock-preference-service.ts +++ b/packages/core/src/browser/preferences/test/mock-preference-service.ts @@ -19,6 +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'; @injectable() export class MockPreferenceService implements PreferenceService { @@ -49,6 +50,7 @@ export class MockPreferenceService implements PreferenceService { set(preferenceName: string, value: any): Promise { return Promise.resolve(); } ready: Promise = Promise.resolve(); readonly onPreferenceChanged: Event = new Emitter().event; + readonly onPreferencesChanged: Event = new Emitter().event; overridePreferenceName(options: OverridePreferenceName): string { return options.preferenceName; } diff --git a/packages/cpp/src/browser/cpp-language-client-contribution.ts b/packages/cpp/src/browser/cpp-language-client-contribution.ts index a6bd5c5ad28d8..8323eaadd79dd 100644 --- a/packages/cpp/src/browser/cpp-language-client-contribution.ts +++ b/packages/cpp/src/browser/cpp-language-client-contribution.ts @@ -82,11 +82,7 @@ export class CppLanguageClientContribution extends BaseLanguageClientContributio @postConstruct() protected init(): void { this.cppBuildConfigurations.onActiveConfigChange2(() => this.onActiveBuildConfigChanged()); - this.cppPreferences.onPreferenceChanged(e => { - if (this.running) { - this.restart(); - } - }); + this.cppPreferences.onPreferenceChanged(() => this.restart()); } protected onReady(languageClient: ILanguageClient): void { @@ -147,9 +143,7 @@ export class CppLanguageClientContribution extends BaseLanguageClientContributio } protected onActiveBuildConfigChanged(): void { - if (this.running) { - this.restart(); - } + this.restart(); } protected get documentSelector(): string[] { diff --git a/packages/debug/src/browser/preferences/launch-preferences.spec.ts b/packages/debug/src/browser/preferences/launch-preferences.spec.ts index 4fcc101186ef2..5b884904bdb50 100644 --- a/packages/debug/src/browser/preferences/launch-preferences.spec.ts +++ b/packages/debug/src/browser/preferences/launch-preferences.spec.ts @@ -469,7 +469,6 @@ describe('Launch Preferences', () => { toTearDown.push(container.get(FileSystemWatcher)); const impl = container.get(PreferenceServiceImpl); - impl.initialize(); toTearDown.push(impl); preferences = impl; diff --git a/packages/editor-preview/src/browser/editor-preview-manager.spec.ts b/packages/editor-preview/src/browser/editor-preview-manager.spec.ts index 1b8038ae4d15b..715bd9f9cae26 100644 --- a/packages/editor-preview/src/browser/editor-preview-manager.spec.ts +++ b/packages/editor-preview/src/browser/editor-preview-manager.spec.ts @@ -53,7 +53,7 @@ mockWidgetManager.onDidCreateWidget = sinon.stub().callsFake((fn: Function) => o const mockShell = sinon.createStubInstance(ApplicationShell) as ApplicationShell; const mockPreference = sinon.createStubInstance(PreferenceServiceImpl); -mockPreference.onPreferenceChanged = sinon.stub().returns({ dispose: () => { } }); +mockPreference.onPreferencesChanged = sinon.stub().returns({ dispose: () => { } }); let testContainer: Container; diff --git a/packages/json/src/browser/json-client-contribution.ts b/packages/json/src/browser/json-client-contribution.ts index 1bac6a76b9136..a017d3b57493d 100644 --- a/packages/json/src/browser/json-client-contribution.ts +++ b/packages/json/src/browser/json-client-contribution.ts @@ -24,7 +24,7 @@ import { DocumentSelector } from '@theia/languages/lib/browser'; import { JSON_LANGUAGE_ID, JSON_LANGUAGE_NAME, JSONC_LANGUAGE_ID } from '../common'; -import { ResourceProvider } from '@theia/core'; +import { ResourceProvider, DisposableCollection } from '@theia/core'; import URI from '@theia/core/lib/common/uri'; import { JsonPreferences } from './json-preferences'; import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store'; @@ -46,18 +46,9 @@ export class JsonClientContribution extends BaseLanguageClientContribution { ) { super(workspace, languages, languageClientFactory); this.initializeJsonSchemaAssociations(); - preferences.onPreferenceChanged(e => { - if (e.preferenceName === 'json.schemas') { - this.updateSchemas(); - } - }); - jsonSchemaStore.onSchemasChanged(() => { - this.updateSchemas(); - }); - this.updateSchemas(); } - protected async updateSchemas(): Promise { + protected updateSchemas(client: ILanguageClient): void { const allConfigs = [...this.jsonSchemaStore.getJsonSchemaConfigurations()]; const config = this.preferences['json.schemas']; if (config instanceof Array) { @@ -74,8 +65,6 @@ export class JsonClientContribution extends BaseLanguageClientContribution { } } } - const client = await this.languageClient; - await client.onReady(); client.sendNotification('json/schemaAssociations', registry); } @@ -94,7 +83,8 @@ export class JsonClientContribution extends BaseLanguageClientContribution { return [this.id]; } - protected onReady(languageClient: ILanguageClient): void { + protected onReady(languageClient: ILanguageClient, toStop: DisposableCollection): void { + super.onReady(languageClient, toStop); // handle content request languageClient.onRequest('vscode/content', async (uriPath: string) => { const uri = new URI(uriPath); @@ -102,8 +92,13 @@ export class JsonClientContribution extends BaseLanguageClientContribution { const text = await resource.readContents(); return text; }); - super.onReady(languageClient); - setTimeout(() => this.initializeJsonSchemaAssociations()); + toStop.push(this.preferences.onPreferenceChanged(e => { + if (e.preferenceName === 'json.schemas') { + this.updateSchemas(languageClient); + } + })); + toStop.push(this.jsonSchemaStore.onSchemasChanged(() => this.updateSchemas(languageClient))); + this.updateSchemas(languageClient); } protected async initializeJsonSchemaAssociations(): Promise { diff --git a/packages/languages/src/browser/language-client-contribution.ts b/packages/languages/src/browser/language-client-contribution.ts index 7b2535e176107..2d78963f12143 100644 --- a/packages/languages/src/browser/language-client-contribution.ts +++ b/packages/languages/src/browser/language-client-contribution.ts @@ -159,15 +159,16 @@ export abstract class BaseLanguageClientContribution implements LanguageClientCo } catch { /* no-op */ } } })())); - toStop.push(messageConnection.onClose(() => this.restart())); - this.onWillStart(this._languageClient!); + toStop.push(messageConnection.onClose(() => this.forceRestart())); this._languageClient!.start(); + // it should be called after `start` that `onReady` promise gets reinitialized + this.onWillStart(this._languageClient!, toStop); } }, { reconnecting: false }); } catch (e) { console.error(e); if (!toStop.disposed) { - this.restart(); + this.forceRestart(); } } } @@ -181,15 +182,19 @@ export abstract class BaseLanguageClientContribution implements LanguageClientCo if (!this.running) { return; } + this.forceRestart(); + } + + protected forceRestart(): void { this.deactivate(); this.activate(); } - protected onWillStart(languageClient: ILanguageClient): void { - languageClient.onReady().then(() => this.onReady(languageClient)); + protected onWillStart(languageClient: ILanguageClient, toStop?: DisposableCollection): void { + languageClient.onReady().then(() => this.onReady(languageClient, toStop)); } - protected onReady(languageClient: ILanguageClient): void { + protected onReady(languageClient: ILanguageClient, toStop?: DisposableCollection): void { this._languageClient = languageClient; this.resolveReady(this._languageClient); this.waitForReady(); diff --git a/packages/monaco/src/browser/monaco-configurations.ts b/packages/monaco/src/browser/monaco-configurations.ts index f2fd1e0db68d3..ed8157bed0378 100644 --- a/packages/monaco/src/browser/monaco-configurations.ts +++ b/packages/monaco/src/browser/monaco-configurations.ts @@ -17,10 +17,10 @@ // tslint:disable:no-any import { injectable, inject, postConstruct } from 'inversify'; -import { JSONExt, JSONObject, JSONValue } from '@phosphor/coreutils'; +import { JSONValue } from '@phosphor/coreutils'; import { Configurations, ConfigurationChangeEvent, WorkspaceConfiguration } from 'monaco-languageclient'; import { Event, Emitter } from '@theia/core/lib/common'; -import { PreferenceServiceImpl, PreferenceChanges } from '@theia/core/lib/browser'; +import { PreferenceService, PreferenceChanges, PreferenceSchemaProvider, createPreferenceProxy } from '@theia/core/lib/browser'; export interface MonacoConfigurationChangeEvent extends ConfigurationChangeEvent { affectedSections?: string[] @@ -32,10 +32,11 @@ export class MonacoConfigurations implements Configurations { protected readonly onDidChangeConfigurationEmitter = new Emitter(); readonly onDidChangeConfiguration: Event = this.onDidChangeConfigurationEmitter.event; - @inject(PreferenceServiceImpl) - protected readonly preferences: PreferenceServiceImpl; + @inject(PreferenceService) + protected readonly preferences: PreferenceService; - protected tree: JSONObject = {}; + @inject(PreferenceSchemaProvider) + protected readonly preferenceSchemaProvider: PreferenceSchemaProvider; @postConstruct() protected init(): void { @@ -44,12 +45,12 @@ export class MonacoConfigurations implements Configurations { } protected reconcileData(changes?: PreferenceChanges): void { - this.tree = MonacoConfigurations.parse(this.preferences.getPreferences()); this.onDidChangeConfigurationEmitter.fire({ affectedSections: MonacoConfigurations.parseSections(changes), affectsConfiguration: section => this.affectsConfiguration(section, changes) }); } + protected affectsConfiguration(section: string, changes?: PreferenceChanges): boolean { if (!changes) { return true; @@ -63,12 +64,11 @@ export class MonacoConfigurations implements Configurations { } getConfiguration(section?: string, resource?: string): WorkspaceConfiguration { - const tree = section ? MonacoConfigurations.lookUp(this.tree, section) : this.tree; - // TODO take resource into the account when the multi-root is supported by preferences - return new MonacoWorkspaceConfiguration(tree); + return new MonacoWorkspaceConfiguration(this.preferences, this.preferenceSchemaProvider, section, resource); } } + export namespace MonacoConfigurations { export function parseSections(changes?: PreferenceChanges): string[] | undefined { if (!changes) { @@ -76,75 +76,56 @@ export namespace MonacoConfigurations { } const sections = []; for (let key of Object.keys(changes)) { + const hasOverride = key.startsWith('['); while (key) { sections.push(key); + if (hasOverride && key.indexOf('.') !== -1) { + sections.push(key.substr(key.indexOf('.'))); + } const index = key.lastIndexOf('.'); key = key.substring(0, index); } } return sections; } - export function parse(raw: { [section: string]: Object | undefined }): JSONObject { - const tree = {}; - for (const section of Object.keys(raw)) { - const value = raw[section]; - if (value !== undefined) { - assign(tree, section, value); - } - } - return tree; - } - export function assign(data: JSONObject, section: string, value: JSONValue): void { - let node: JSONValue = data; - const parts = section.split('.'); - while (JSONExt.isObject(node) && parts.length > 1) { - const part = parts.shift()!; - node = node[part] || (node[part] = {}); - } - if (JSONExt.isObject(node) && parts.length === 1) { - node[parts[0]] = value; - } - } - export function lookUp(tree: JSONValue | undefined, section: string | undefined): JSONValue | undefined { - if (!section) { - return undefined; - } - let node: JSONValue | undefined = tree; - const parts = section.split('.'); - while (node && JSONExt.isObject(node) && parts.length > 0) { - node = node[parts.shift()!]; - } - return !parts.length ? node : undefined; - } } export class MonacoWorkspaceConfiguration implements WorkspaceConfiguration { constructor( - protected readonly tree: JSONValue | undefined + protected readonly preferences: PreferenceService, + protected readonly preferenceSchemaProvider: PreferenceSchemaProvider, + protected readonly section?: string, + protected readonly resource?: string ) { - if (tree && JSONExt.isObject(tree)) { - Object.assign(this, tree); - } } readonly [key: string]: any; + protected getSection(section: string): string { + if (this.section) { + return this.section + '.' + section; + } + return section; + } + has(section: string): boolean { - return typeof MonacoConfigurations.lookUp(this.tree, section) !== 'undefined'; + return this.preferences.inspect(this.getSection(section), this.resource) !== undefined; } get(section: string, defaultValue?: T): T | undefined { - const value = MonacoConfigurations.lookUp(this.tree, section); - if (typeof value === 'undefined') { - return defaultValue; - } - const result = JSONExt.isObject(value) ? JSONExt.deepCopy(value) : value; - return result as any; + return this.preferences.get(this.getSection(section), defaultValue, this.resource); } toJSON(): JSONValue | undefined { - return this.tree && JSONExt.isObject(this.tree) ? JSONExt.deepCopy(this.tree) : this.tree; + const proxy = createPreferenceProxy<{ [key: string]: any }>(this.preferences, this.preferenceSchemaProvider.getCombinedSchema(), { + resourceUri: this.resource, + style: 'deep' + }); + if (this.section) { + return proxy[this.section]; + } + return proxy; } } diff --git a/packages/monaco/src/browser/monaco-editor-provider.ts b/packages/monaco/src/browser/monaco-editor-provider.ts index 5188c4bca0338..5acb6f70fed8b 100644 --- a/packages/monaco/src/browser/monaco-editor-provider.ts +++ b/packages/monaco/src/browser/monaco-editor-provider.ts @@ -16,7 +16,7 @@ // tslint:disable:no-any import URI from '@theia/core/lib/common/uri'; -import { EditorPreferenceChange, EditorPreferences, TextEditor, DiffNavigator, EndOfLinePreference } from '@theia/editor/lib/browser'; +import { EditorPreferenceChange, EditorPreferences, TextEditor, DiffNavigator } from '@theia/editor/lib/browser'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; import { inject, injectable } from 'inversify'; import { DisposableCollection } from '@theia/core/lib/common'; @@ -46,8 +46,7 @@ export class MonacoEditorProvider { @inject(MonacoEditorServices) protected readonly services: MonacoEditorServices; - private isWindowsBackend = false; - private hookedConfigService: any = undefined; + private isWindowsBackend: boolean = false; constructor( @inject(MonacoEditorService) protected readonly codeEditorService: MonacoEditorService, @@ -63,41 +62,32 @@ export class MonacoEditorProvider { @inject(ApplicationServer) protected readonly applicationServer: ApplicationServer, @inject(monaco.contextKeyService.ContextKeyService) protected readonly contextKeyService: monaco.contextKeyService.ContextKeyService ) { - const init = monaco.services.StaticServices.init.bind(monaco.services.StaticServices); + const staticServices = monaco.services.StaticServices; + const init = staticServices.init.bind(monaco.services.StaticServices); this.applicationServer.getBackendOS().then(os => { this.isWindowsBackend = os === OS.Type.Windows; }); + + if (staticServices.resourcePropertiesService) { + // tslint:disable-next-line:no-any + const original = staticServices.resourcePropertiesService.get() as any; + original.getEOL = () => { + const eol = this.editorPreferences['files.eol']; + if (eol) { + if (eol !== 'auto') { + return eol; + } + } + return this.isWindowsBackend ? '\r\n' : '\n'; + }; + } monaco.services.StaticServices.init = o => { const result = init(o); result[0].set(monaco.services.ICodeEditorService, codeEditorService); - if (!this.hookedConfigService) { - this.hookedConfigService = result[0].get(monaco.services.IConfigurationService); - const originalGetValue = this.hookedConfigService.getValue.bind(this.hookedConfigService); - this.hookedConfigService.getValue = (arg1: any, arg2: any) => { - const creationOptions = originalGetValue(arg1, arg2); - if (typeof creationOptions === 'object') { - const eol = this.getEOL(); - creationOptions.files = { - eol - }; - } - return creationOptions; - }; - } return result; }; } - protected getEOL(): EndOfLinePreference { - const eol = this.editorPreferences['files.eol']; - if (eol) { - if (eol !== 'auto') { - return eol; - } - } - return this.isWindowsBackend ? '\r\n' : '\n'; - } - protected async getModel(uri: URI, toDispose: DisposableCollection): Promise { const reference = await this.textModelService.createModelReference(uri); toDispose.push(reference); diff --git a/packages/monaco/src/browser/monaco-frontend-module.ts b/packages/monaco/src/browser/monaco-frontend-module.ts index eb331df910a17..8d205dace3dee 100644 --- a/packages/monaco/src/browser/monaco-frontend-module.ts +++ b/packages/monaco/src/browser/monaco-frontend-module.ts @@ -21,7 +21,10 @@ import '../../src/browser/style/symbol-icons.css'; import { ContainerModule, decorate, injectable, interfaces } from 'inversify'; import { MenuContribution, CommandContribution } from '@theia/core/lib/common'; import { PreferenceScope } from '@theia/core/lib/common/preferences/preference-scope'; -import { QuickOpenService, FrontendApplicationContribution, KeybindingContribution, PreferenceServiceImpl } from '@theia/core/lib/browser'; +import { + QuickOpenService, FrontendApplicationContribution, KeybindingContribution, + PreferenceService, PreferenceSchemaProvider, createPreferenceProxy +} from '@theia/core/lib/browser'; import { Languages, Workspace } from '@theia/languages/lib/browser'; import { TextEditorProvider, DiffNavigatorProvider } from '@theia/editor/lib/browser'; import { StrictEditorTextFocusContext } from '@theia/editor/lib/browser/editor-keybinding-contexts'; @@ -59,8 +62,6 @@ import { MimeService } from '@theia/core/lib/browser/mime-service'; import debounce = require('lodash.debounce'); import { MonacoEditorServices } from './monaco-editor'; -const deepmerge: (args: object[]) => object = require('deepmerge').default.all; - decorate(injectable(), MonacoToProtocolConverter); decorate(injectable(), ProtocolToMonacoConverter); decorate(injectable(), monaco.contextKeyService.ContextKeyService); @@ -134,22 +135,22 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { export const MonacoConfigurationService = Symbol('MonacoConfigurationService'); export function createMonacoConfigurationService(container: interfaces.Container): monaco.services.IConfigurationService { const configurations = container.get(MonacoConfigurations); - const preferences = container.get(PreferenceServiceImpl); + const preferences = container.get(PreferenceService); + const preferenceSchemaProvider = container.get(PreferenceSchemaProvider); const service = monaco.services.StaticServices.configurationService.get(); const _configuration = service._configuration; - const getValue = _configuration.getValue.bind(_configuration); _configuration.getValue = (section, overrides, workspace) => { - const preferenceConfig = configurations.getConfiguration(); + const overrideIdentifier = overrides && 'overrideIdentifier' in overrides && overrides['overrideIdentifier'] as string || undefined; + const resourceUri = overrides && 'resource' in overrides && overrides['resource'].toString(); + // tslint:disable-next-line:no-any + const proxy = createPreferenceProxy<{ [key: string]: any }>(preferences, preferenceSchemaProvider.getCombinedSchema(), { + resourceUri, overrideIdentifier, style: 'both' + }); if (section) { - const value = preferenceConfig.get(section); - return value !== undefined ? value : getValue(section, overrides, workspace); - } - const simpleConfig = getValue(section, overrides, workspace); - if (typeof simpleConfig === 'object') { - return deepmerge([{}, simpleConfig, preferenceConfig.toJSON()]); + return proxy[section]; } - return preferenceConfig.toJSON(); + return proxy; }; const initFromConfiguration = debounce(() => { diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index b67cd0cef3851..9de50f9529a9c 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -442,6 +442,9 @@ declare module monaco.services { _configuration: Configuration; } + export interface ITextResourcePropertiesService { + } + export abstract class CodeEditorServiceImpl implements monaco.editor.ICodeEditorService { constructor(themeService: IStandaloneThemeService); abstract getActiveCodeEditor(): monaco.editor.ICodeEditor | undefined; @@ -510,6 +513,7 @@ declare module monaco.services { export const modeService: LazyStaticService; export const codeEditorService: LazyStaticService; export const configurationService: LazyStaticService; + export const resourcePropertiesService: LazyStaticService; } } diff --git a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index 9d4e59a0e7122..ac32bb1229876 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -153,7 +153,11 @@ export interface SingleEditOperation { } export interface Command { + /** + * @deprecated use command instead + */ id: string; + command?: string; title: string; tooltip?: string; // tslint:disable-next-line:no-any diff --git a/packages/plugin-ext/src/hosted/node/plugin-service.ts b/packages/plugin-ext/src/hosted/node/plugin-service.ts index 16645de07191c..e39cdfac9f9c0 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-service.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-service.ts @@ -16,7 +16,7 @@ import { injectable, inject, named, postConstruct } from 'inversify'; import { HostedPluginServer, HostedPluginClient, PluginMetadata, PluginDeployer } from '../../common/plugin-protocol'; import { HostedPluginSupport } from './hosted-plugin'; -import { ILogger } from '@theia/core'; +import { ILogger, Disposable } from '@theia/core'; import { ContributionProvider } from '@theia/core'; import { ExtPluginApiProvider, ExtPluginApi } from '../../common/plugin-ext-api-contribution'; import { HostedPluginDeployerHandler } from './hosted-plugin-deployer-handler'; @@ -39,13 +39,15 @@ export class HostedPluginServerImpl implements HostedPluginServer { protected client: HostedPluginClient | undefined; + protected deployedListener: Disposable; + constructor( @inject(HostedPluginSupport) private readonly hostedPlugin: HostedPluginSupport) { } @postConstruct() protected init(): void { - this.pluginDeployer.onDidDeploy(() => { + this.deployedListener = this.pluginDeployer.onDidDeploy(() => { if (this.client) { this.client.onDidDeploy(); } @@ -54,6 +56,7 @@ export class HostedPluginServerImpl implements HostedPluginServer { dispose(): void { this.hostedPlugin.clientClosed(); + this.deployedListener.dispose(); } setClient(client: HostedPluginClient): void { this.client = client; diff --git a/packages/plugin-ext/src/plugin/command-registry.ts b/packages/plugin-ext/src/plugin/command-registry.ts index 60b801dbe05f5..57415a246e4e2 100644 --- a/packages/plugin-ext/src/plugin/command-registry.ts +++ b/packages/plugin-ext/src/plugin/command-registry.ts @@ -175,7 +175,7 @@ export class CommandsConverter { const id = this.handle++; this.commandsMap.set(id, command); disposables.push(new Disposable(() => this.commandsMap.delete(id))); - result.id = this.safeCommandId; + result.command = this.safeCommandId; result.arguments = [id]; } diff --git a/packages/preferences/src/browser/folder-preference-provider.ts b/packages/preferences/src/browser/folder-preference-provider.ts index 353b442135c6f..fbb5c337fae63 100644 --- a/packages/preferences/src/browser/folder-preference-provider.ts +++ b/packages/preferences/src/browser/folder-preference-provider.ts @@ -28,8 +28,8 @@ export interface FolderPreferenceProviderFactory { export const FolderPreferenceProviderOptions = Symbol('FolderPreferenceProviderOptions'); export interface FolderPreferenceProviderOptions { - folder: FileStat; - configUri: URI; + readonly folder: FileStat; + readonly configUri: URI; } @injectable() @@ -38,8 +38,13 @@ export class FolderPreferenceProvider extends AbstractResourcePreferenceProvider @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @inject(FolderPreferenceProviderOptions) protected readonly options: FolderPreferenceProviderOptions; + private _folderUri: URI; + get folderUri(): URI { - return new URI(this.options.folder.uri); + if (!this._folderUri) { + this._folderUri = new URI(this.options.folder.uri); + } + return this._folderUri; } protected getUri(): URI { diff --git a/packages/preferences/src/browser/preference-service.spec.ts b/packages/preferences/src/browser/preference-service.spec.ts deleted file mode 100644 index 32819f63dd313..0000000000000 --- a/packages/preferences/src/browser/preference-service.spec.ts +++ /dev/null @@ -1,689 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 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 - ********************************************************************************/ - -// tslint:disable:no-any -// tslint:disable:no-unused-expression - -import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; - -let disableJSDOM = enableJSDOM(); - -import { Container } from 'inversify'; -import * as assert from 'assert'; -import * as chai from 'chai'; -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, PreferenceProvider -} from '@theia/core/lib/browser/preferences'; -import { FileSystem, FileShouldOverwrite, FileStat } from '@theia/filesystem/lib/common/'; -import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; -import { FileSystemWatcherServer } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; -import { FileSystemPreferences, createFileSystemPreferences } from '@theia/filesystem/lib/browser/filesystem-preferences'; -import { ILogger, MessageService, MessageClient } from '@theia/core'; -import { UserPreferenceProvider } from './user-preference-provider'; -import { WorkspacePreferenceProvider } from './workspace-preference-provider'; -import { FoldersPreferencesProvider, } from './folders-preferences-provider'; -import { FolderPreferenceProviderOptions } from './folder-preference-provider'; -import { ResourceProvider } from '@theia/core/lib/common/resource'; -import { WorkspaceServer } from '@theia/workspace/lib/common/'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { MockPreferenceProvider } from '@theia/core/lib/browser/preferences/test'; -import { MockFilesystem, MockFilesystemWatcherServer } from '@theia/filesystem/lib/common/test'; -import { MockLogger } from '@theia/core/lib/common/test/mock-logger'; -import { MockResourceProvider } from '@theia/core/lib/common/test/mock-resource-provider'; -import { MockWorkspaceServer } from '@theia/workspace/lib/common/test/mock-workspace-server'; -import { MockWindowService } from '@theia/core/lib/browser/window/test/mock-window-service'; -import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; -import { WorkspacePreferences, createWorkspacePreferences } from '@theia/workspace/lib/browser/workspace-preferences'; -import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; -import * as sinon from 'sinon'; -import URI from '@theia/core/lib/common/uri'; -import { bindWorkspaceFilePreferenceProvider, bindFolderPreferenceProvider } from './preference-bindings'; - -disableJSDOM(); - -process.on('unhandledRejection', (reason, promise) => { - console.error(reason); - throw reason; -}); - -const expect = chai.expect; -let testContainer: Container; - -const tempPath = temp.track().openSync().path; - -const mockUserPreferenceEmitter = new Emitter(); -const mockWorkspacePreferenceEmitter = new Emitter(); -const mockFolderPreferenceEmitter = new Emitter(); - -function testContainerSetup(): void { - testContainer = new Container(); - bindPreferenceSchemaProvider(testContainer.bind.bind(testContainer)); - - testContainer.bind(UserPreferenceProvider).toSelf().inSingletonScope(); - testContainer.bind(WorkspacePreferenceProvider).toSelf().inSingletonScope(); - testContainer.bind(FoldersPreferencesProvider).toSelf().inSingletonScope(); - - testContainer.bind(PreferenceProvider).toDynamicValue(ctx => - ctx.container.get(FoldersPreferencesProvider) - ).inSingletonScope().whenTargetNamed(PreferenceScope.Folder); - bindWorkspaceFilePreferenceProvider(testContainer.bind.bind(testContainer)); - - testContainer.bind(FolderPreferenceProviderOptions).toConstantValue({ folder: { uri: 'file:///home/oneFile', isDirectory: true, lastModification: 0 } }); - bindFolderPreferenceProvider(testContainer.bind.bind(testContainer)); - - testContainer.bind(PreferenceProviderProvider).toFactory(ctx => (scope: PreferenceScope) => { - switch (scope) { - case PreferenceScope.User: - const userProvider = ctx.container.get(UserPreferenceProvider); - sinon.stub(userProvider, 'onDidPreferencesChanged').get(() => - mockUserPreferenceEmitter.event - ); - return userProvider; - case PreferenceScope.Workspace: - const workspaceProvider = ctx.container.get(WorkspacePreferenceProvider); - sinon.stub(workspaceProvider, 'onDidPreferencesChanged').get(() => - mockWorkspacePreferenceEmitter.event - ); - return workspaceProvider; - case PreferenceScope.Folder: - const folderProvider = ctx.container.get(FoldersPreferencesProvider); - sinon.stub(folderProvider, 'onDidPreferencesChanged').get(() => - mockFolderPreferenceEmitter.event - ); - return folderProvider; - default: - return ctx.container.get(PreferenceSchemaProvider); - } - }); - testContainer.bind(PreferenceServiceImpl).toSelf().inSingletonScope(); - - testContainer.bind(PreferenceService).toService(PreferenceServiceImpl); - - testContainer.bind(FileSystemPreferences).toDynamicValue(ctx => { - const preferences = ctx.container.get(PreferenceService); - return createFileSystemPreferences(preferences); - }).inSingletonScope(); - - /* Workspace mocks and bindings */ - testContainer.bind(WorkspaceServer).to(MockWorkspaceServer); - testContainer.bind(WorkspaceService).toSelf(); - testContainer.bind(WorkspacePreferences).toDynamicValue(ctx => { - const preferences = ctx.container.get(PreferenceService); - return createWorkspacePreferences(preferences); - }).inSingletonScope(); - - /* Window mocks and bindings*/ - testContainer.bind(WindowService).to(MockWindowService); - - /* Resource mocks and bindings */ - testContainer.bind(MockResourceProvider).toDynamicValue(ctx => { - const resourceProvider = new MockResourceProvider(); - sinon.stub(resourceProvider, 'get').callsFake(() => Promise.resolve({ - uri: new URI(''), - dispose(): void { }, - readContents(): Promise { - return fs.readFile(tempPath, 'utf-8'); - }, - saveContents(content: string, options?: { encoding?: string }): Promise { - return fs.writeFile(tempPath, content); - } - })); - return resourceProvider; - }); - testContainer.bind(ResourceProvider).toProvider(context => - uri => context.container.get(MockResourceProvider).get(uri) - ); - - /* FS mocks and bindings */ - testContainer.bind(FileSystemWatcherServer).to(MockFilesystemWatcherServer); - testContainer.bind(FileSystemWatcher).toSelf().onActivation((_, watcher) => - watcher - ); - testContainer.bind(FileShouldOverwrite).toFunction( - async (originalStat: FileStat, currentStat: FileStat): Promise => true); - - testContainer.bind(FileSystem).to(MockFilesystem); - - /* Logger mock */ - testContainer.bind(ILogger).to(MockLogger); - - /* Message Service mocks */ - testContainer.bind(MessageService).toSelf().inSingletonScope(); - testContainer.bind(MessageClient).toSelf().inSingletonScope(); -} - -describe('Preference Service', () => { - let prefService: PreferenceService; - let prefSchema: PreferenceSchemaProvider; - const stubs: sinon.SinonStub[] = []; - - before(() => { - disableJSDOM = enableJSDOM(); - FrontendApplicationConfigProvider.set({ - 'applicationName': 'test', - }); - testContainerSetup(); - }); - - after(() => { - disableJSDOM(); - }); - - beforeEach(async () => { - prefSchema = testContainer.get(PreferenceSchemaProvider); - prefService = testContainer.get(PreferenceService); - const impl = testContainer.get(PreferenceServiceImpl); - impl.initialize(); - impl['_ready'].resolve(); - }); - - afterEach(() => { - prefService.dispose(); - stubs.forEach(s => s.restore()); - stubs.length = 0; - }); - - it('should get notified if a provider emits a change', done => { - const userProvider = testContainer.get(UserPreferenceProvider); - stubs.push(sinon.stub(userProvider, 'getPreferences').returns({ - testPref: 'oldVal' - })); - prefService.onPreferenceChanged(pref => { - if (pref) { - expect(pref.preferenceName).eq('testPref'); - expect(pref.newValue).eq('newVal'); - return done(); - } - return done(new Error('onPreferenceChanged() fails to return any preference change infomation')); - }); - stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); - mockUserPreferenceEmitter.fire({ - testPref: { - preferenceName: 'testPref', - newValue: 'newVal', - oldValue: 'oldVal', - scope: PreferenceScope.User, - domain: [] - } - }); - }).timeout(2000); - - it('should return the preference from the more specific scope (user > workspace)', () => { - const userProvider = testContainer.get(UserPreferenceProvider); - const workspaceProvider = testContainer.get(WorkspacePreferenceProvider); - stubs.push(sinon.stub(userProvider, 'getPreferences').returns({ - 'test.number': 1 - })); - stubs.push(sinon.stub(workspaceProvider, 'resolve').returns({ - value: 0 - })); - stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); - expect(prefService.get('test.number')).equals(0); - }); - - it('should return the preference from the more specific scope (folders > workspace)', () => { - const userProvider = testContainer.get(UserPreferenceProvider); - const workspaceProvider = testContainer.get(WorkspacePreferenceProvider); - const foldersProvider = testContainer.get(FoldersPreferencesProvider); - stubs.push(sinon.stub(userProvider, 'getPreferences').returns({ - 'test.number': 1 - })); - stubs.push(sinon.stub(workspaceProvider, 'getPreferences').returns({ - 'test.number': 0 - })); - stubs.push(sinon.stub(foldersProvider, 'resolve').returns({ - value: 20 - })); - stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); - expect(prefService.get('test.number')).equals(20); - }); - - it('should return the preference from the less specific scope if the value is removed from the more specific one', () => { - const userProvider = testContainer.get(UserPreferenceProvider); - const workspaceProvider = testContainer.get(WorkspacePreferenceProvider); - stubs.push(sinon.stub(userProvider, 'getPreferences').returns({ - 'test.number': 1 - })); - const stubWorkspace = sinon.stub(workspaceProvider, 'resolve').returns({ - value: 0 - }); - stubs.push(stubWorkspace); - stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); - expect(prefService.get('test.number')).equals(0); - - stubWorkspace.restore(); - expect(prefService.get('test.number')).equals(1); - }); - - it('should throw a TypeError if the preference (reference object) is modified', () => { - const userProvider = testContainer.get(UserPreferenceProvider); - stubs.push(sinon.stub(userProvider, 'getPreferences').returns({ - 'test.immutable': [ - 'test', 'test', 'test' - ] - })); - stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); - const immutablePref: string[] | undefined = prefService.get('test.immutable'); - expect(immutablePref).to.not.be.undefined; - if (immutablePref !== undefined) { - expect(() => immutablePref.push('fails')).to.throw(TypeError); - } - }); - - it('should still report the more specific preference even though the less specific one changed', () => { - const userProvider = testContainer.get(UserPreferenceProvider); - const workspaceProvider = testContainer.get(WorkspacePreferenceProvider); - const stubUser = sinon.stub(userProvider, 'getPreferences').returns({ - 'test.number': 1 - }); - stubs.push(sinon.stub(workspaceProvider, 'resolve').returns({ - value: 0 - })); - mockUserPreferenceEmitter.fire({ - 'test.number': { - preferenceName: 'test.number', - newValue: 2, - scope: PreferenceScope.User, - domain: [] - } - }); - stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); - expect(prefService.get('test.number')).equals(0); - - stubUser.restore(); - stubs.push(sinon.stub(userProvider, 'getPreferences').returns({ - 'test.number': 4 - })); - mockUserPreferenceEmitter.fire({ - 'test.number': { - preferenceName: 'test.number', - newValue: 4, - scope: PreferenceScope.User, - domain: [] - } - }); - expect(prefService.get('test.number')).equals(0); - }); - - it('should store preference when settings file is empty', async () => { - const settings = '{\n "key": "value"\n}'; - await prefService.set('key', 'value', PreferenceScope.User); - expect(fs.readFileSync(tempPath).toString()).equals(settings); - }); - - it('should store preference when settings file is not empty', async () => { - const settings = '{\n "key": "value",\n "newKey": "newValue"\n}'; - fs.writeFileSync(tempPath, '{\n "key": "value"\n}'); - await prefService.set('newKey', 'newValue', PreferenceScope.User); - expect(fs.readFileSync(tempPath).toString()).equals(settings); - }); - - it('should override existing preference', async () => { - const settings = '{\n "key": "newValue"\n}'; - fs.writeFileSync(tempPath, '{\n "key": "oldValue"\n}'); - await prefService.set('key', 'newValue', PreferenceScope.User); - expect(fs.readFileSync(tempPath).toString()).equals(settings); - }); - - /** - * A slow provider that becomes ready after 1 second. - */ - class SlowProvider extends MockPreferenceProvider { - constructor() { - super(); - setTimeout(() => { - this.prefs['mypref'] = 2; - this._ready.resolve(); - }, 1000); - } - } - /** - * Default provider that becomes ready after constructor gets called - */ - class MockDefaultProvider extends MockPreferenceProvider { - constructor() { - super(); - this.prefs['mypref'] = 5; - this._ready.resolve(); - } - } - - /** - * Make sure that the preference service is ready only once the providers - * are ready to provide preferences. - */ - it('should be ready only when all providers are ready', async () => { - const container = new Container(); - bindPreferenceSchemaProvider(container.bind.bind(container)); - container.bind(ILogger).to(MockLogger); - container.bind(PreferenceProviderProvider).toFactory(ctx => (scope: PreferenceScope) => { - if (scope === PreferenceScope.User) { - return new MockDefaultProvider(); - } - return new SlowProvider(); - }); - container.bind(PreferenceServiceImpl).toSelf().inSingletonScope(); - - const service = container.get(PreferenceServiceImpl); - service.initialize(); - prefSchema = container.get(PreferenceSchemaProvider); - await service.ready; - stubs.push(sinon.stub(PreferenceServiceImpl, 'doSetProvider').callsFake(() => { })); - stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); - expect(service.get('mypref')).to.equal(2); - }); - - it('should answer queries before all providers are ready', async () => { - const container = new Container(); - bindPreferenceSchemaProvider(container.bind.bind(container)); - container.bind(ILogger).to(MockLogger); - container.bind(PreferenceProviderProvider).toFactory(ctx => (scope: PreferenceScope) => { - if (scope === PreferenceScope.User) { - return new MockDefaultProvider(); - } - return new SlowProvider(); - }); - container.bind(PreferenceServiceImpl).toSelf().inSingletonScope(); - - const service = container.get(PreferenceServiceImpl); - service.initialize(); - prefSchema = container.get(PreferenceSchemaProvider); - stubs.push(sinon.stub(PreferenceServiceImpl, 'doSetProvider').callsFake(() => { })); - stubs.push(sinon.stub(prefSchema, 'isValidInScope').returns(true)); - expect(service.get('mypref')).to.equal(5); - }); - - describe('overridden preferences', () => { - - it('getPreferences', () => { - const { preferences, schema } = prepareServices(); - preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); - - assert.deepStrictEqual({ - 'editor.tabSize': 4 - }, preferences.getPreferences()); - - schema.registerOverrideIdentifier('json'); - - assert.deepStrictEqual({ - 'editor.tabSize': 4, - '[json].editor.tabSize': 2 - }, preferences.getPreferences()); - }); - - it('get #0', () => { - const { preferences, schema } = prepareServices(); - preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); - - assert.strictEqual(4, preferences.get('editor.tabSize')); - assert.strictEqual(undefined, preferences.get('[json].editor.tabSize')); - - schema.registerOverrideIdentifier('json'); - - assert.strictEqual(4, preferences.get('editor.tabSize')); - assert.strictEqual(2, preferences.get('[json].editor.tabSize')); - }); - - it('get #1', () => { - const { preferences, schema } = prepareServices(); - schema.registerOverrideIdentifier('json'); - - assert.strictEqual(4, preferences.get('editor.tabSize')); - assert.strictEqual(4, preferences.get('[json].editor.tabSize')); - - preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); - - assert.strictEqual(4, preferences.get('editor.tabSize')); - assert.strictEqual(2, preferences.get('[json].editor.tabSize')); - }); - - it('get #2', () => { - const { preferences, schema } = prepareServices(); - schema.registerOverrideIdentifier('json'); - - assert.strictEqual(4, preferences.get('editor.tabSize')); - assert.strictEqual(4, preferences.get('[json].editor.tabSize')); - - preferences.set('editor.tabSize', 2, PreferenceScope.User); - - assert.strictEqual(2, preferences.get('editor.tabSize')); - assert.strictEqual(2, preferences.get('[json].editor.tabSize')); - }); - - it('has', () => { - const { preferences, schema } = prepareServices(); - - assert.ok(preferences.has('editor.tabSize')); - assert.ok(!preferences.has('[json].editor.tabSize')); - - schema.registerOverrideIdentifier('json'); - - assert.ok(preferences.has('editor.tabSize')); - assert.ok(preferences.has('[json].editor.tabSize')); - }); - - it('inspect #0', () => { - const { preferences, schema } = prepareServices(); - - const expected = { - preferenceName: 'editor.tabSize', - defaultValue: 4, - globalValue: undefined, - workspaceValue: undefined, - workspaceFolderValue: undefined, - }; - assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); - assert.ok(!preferences.has('[json].editor.tabSize')); - - schema.registerOverrideIdentifier('json'); - - assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); - assert.deepStrictEqual({ - ...expected, - preferenceName: '[json].editor.tabSize' - }, preferences.inspect('[json].editor.tabSize')); - }); - - it('inspect #1', () => { - const { preferences, schema } = prepareServices(); - - const expected = { - preferenceName: 'editor.tabSize', - defaultValue: 4, - globalValue: 2, - workspaceValue: undefined, - workspaceFolderValue: undefined, - }; - preferences.set('editor.tabSize', 2, PreferenceScope.User); - - assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); - assert.ok(!preferences.has('[json].editor.tabSize')); - - schema.registerOverrideIdentifier('json'); - - assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); - assert.deepStrictEqual({ - ...expected, - preferenceName: '[json].editor.tabSize' - }, preferences.inspect('[json].editor.tabSize')); - }); - - it('inspect #2', () => { - const { preferences, schema } = prepareServices(); - - const expected = { - preferenceName: 'editor.tabSize', - defaultValue: 4, - globalValue: undefined, - workspaceValue: undefined, - workspaceFolderValue: undefined, - }; - assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); - assert.ok(!preferences.has('[json].editor.tabSize')); - - schema.registerOverrideIdentifier('json'); - preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); - - assert.deepStrictEqual(expected, preferences.inspect('editor.tabSize')); - assert.deepStrictEqual({ - ...expected, - preferenceName: '[json].editor.tabSize', - globalValue: 2 - }, preferences.inspect('[json].editor.tabSize')); - }); - - it('onPreferenceChanged #0', () => { - const { preferences, schema } = prepareServices(); - - const events: PreferenceChange[] = []; - preferences.onPreferenceChanged(event => events.push(event)); - - schema.registerOverrideIdentifier('json'); - preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); - preferences.set('editor.tabSize', 3, PreferenceScope.User); - - assert.deepStrictEqual([{ - preferenceName: '[json].editor.tabSize', - newValue: 2 - }, { - preferenceName: 'editor.tabSize', - newValue: 3 - }], events.map(e => ({ - preferenceName: e.preferenceName, - newValue: e.newValue - }))); - }); - - it('onPreferenceChanged #1', () => { - const { preferences, schema } = prepareServices(); - - const events: PreferenceChange[] = []; - preferences.onPreferenceChanged(event => events.push(event)); - - schema.registerOverrideIdentifier('json'); - preferences.set('editor.tabSize', 2, PreferenceScope.User); - - assert.deepStrictEqual([{ - preferenceName: 'editor.tabSize', - newValue: 2 - }, { - preferenceName: '[json].editor.tabSize', - newValue: 2 - }], events.map(e => ({ - preferenceName: e.preferenceName, - newValue: e.newValue - }))); - }); - - it('onPreferenceChanged #3', () => { - const { preferences, schema } = prepareServices(); - - schema.registerOverrideIdentifier('json'); - preferences.set('[json].editor.tabSize', 2, PreferenceScope.User); - preferences.set('editor.tabSize', 3, PreferenceScope.User); - - const events: PreferenceChange[] = []; - preferences.onPreferenceChanged(event => events.push(event)); - - preferences.set('[json].editor.tabSize', undefined, PreferenceScope.User); - - assert.deepStrictEqual([{ - preferenceName: '[json].editor.tabSize', - newValue: 3 - }], events.map(e => ({ - preferenceName: e.preferenceName, - newValue: e.newValue - }))); - }); - - it('defaultOverrides [go].editor.formatOnSave', () => { - const { preferences, schema } = prepareServices({ - schema: { - properties: { - 'editor.insertSpaces': { - type: 'boolean', - default: true, - overridable: true - }, - 'editor.formatOnSave': { - type: 'boolean', - default: false, - overridable: true - } - } - } - }); - - assert.strictEqual(true, preferences.get('editor.insertSpaces')); - assert.strictEqual(undefined, preferences.get('[go].editor.insertSpaces')); - assert.strictEqual(false, preferences.get('editor.formatOnSave')); - assert.strictEqual(undefined, preferences.get('[go].editor.formatOnSave')); - - schema.registerOverrideIdentifier('go'); - schema.setSchema({ - id: 'defaultOverrides', - title: 'Default Configuration Overrides', - properties: { - '[go]': { - type: 'object', - default: { - 'editor.insertSpaces': false, - 'editor.formatOnSave': true - }, - description: 'Configure editor settings to be overridden for go language.' - } - } - }); - - assert.strictEqual(true, preferences.get('editor.insertSpaces')); - assert.strictEqual(false, preferences.get('[go].editor.insertSpaces')); - assert.strictEqual(false, preferences.get('editor.formatOnSave')); - assert.strictEqual(true, preferences.get('[go].editor.formatOnSave')); - }); - - function prepareServices(options?: { schema: PreferenceSchema }): { - preferences: PreferenceServiceImpl; - schema: PreferenceSchemaProvider; - } { - const container = new Container(); - bindPreferenceSchemaProvider(container.bind.bind(container)); - container.bind(PreferenceProviderProvider).toFactory(() => () => new MockPreferenceProvider()); - container.bind(PreferenceServiceImpl).toSelf().inSingletonScope(); - - const schema = container.get(PreferenceSchemaProvider); - schema.setSchema(options && options.schema || { - properties: { - 'editor.tabSize': { - type: 'number', - description: '', - overridable: true, - default: 4 - } - } - }); - - const preferences = container.get(PreferenceServiceImpl); - preferences.initialize(); - return { preferences, schema }; - } - - }); - -}); diff --git a/packages/preferences/src/browser/workspace-preference-provider.ts b/packages/preferences/src/browser/workspace-preference-provider.ts index 99838e0ed76e3..d3cdd9fd58033 100644 --- a/packages/preferences/src/browser/workspace-preference-provider.ts +++ b/packages/preferences/src/browser/workspace-preference-provider.ts @@ -69,7 +69,6 @@ export class WorkspacePreferenceProvider extends PreferenceProvider { delegate.onDidPreferencesChanged(changes => this.onDidPreferencesChangedEmitter.fire(changes)) ]); } - this.onDidPreferencesChangedEmitter.fire(undefined); } } protected createDelegate(): PreferenceProvider | undefined { diff --git a/packages/scm/src/browser/scm-contribution.ts b/packages/scm/src/browser/scm-contribution.ts index 1028dd45a835f..49a5bf57c2e92 100644 --- a/packages/scm/src/browser/scm-contribution.ts +++ b/packages/scm/src/browser/scm-contribution.ts @@ -168,7 +168,7 @@ export class ScmContribution extends AbstractViewContribution impleme this.scmService.statusBarCommands.forEach((value, index) => this.setStatusBarEntry(`scm.status.${index}`, { text: value.title, tooltip: label + (value.tooltip ? ` - ${value.tooltip}` : ''), - command: value.command || value.id, + command: value.command, arguments: value.arguments, alignment: StatusBarAlignment.LEFT, priority: 100