/********************************************************************************
 * 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
 ********************************************************************************/ import * as Ajv from 'ajv' ; import { inject , injectable , interfaces , named , postConstruct } from 'inversify' ; import { ContributionProvider , bindContributionProvider , Emitter , Event , Disposable } from '../../common' ; import { PreferenceScope } from './preference-scope' ; import { PreferenceProvider , PreferenceProviderDataChange } from './preference-provider' ; import { PreferenceSchema , PreferenceSchemaProperties , PreferenceDataSchema , PreferenceItem , PreferenceSchemaProperty , PreferenceDataProperty , JsonType } from '../../common/preferences/preference-schema' ; import { FrontendApplicationConfigProvider } from '../frontend-application-config-provider' ; import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props' ; import { bindPreferenceConfigurations , PreferenceConfigurations } from './preference-configurations' ; export { PreferenceSchema , PreferenceSchemaProperties , PreferenceDataSchema , PreferenceItem , PreferenceSchemaProperty , PreferenceDataProperty , JsonType } ; import { Mutable } from '../../common/types' ; import { OverridePreferenceName , PreferenceLanguageOverrideService } from './preference-language-override-service' ; deprecated theia export { OVERRIDE_PROPERTY_PATTERN } from './preference-language-override-service' ; export const PreferenceContribution = Symbol ( 'PreferenceContribution' ) ; /**
 * A  PreferenceContribution allows adding additional custom preferences.
 * For this, the  PreferenceContribution has to provide a valid JSON Schema specifying which preferences
 * are available including their types and description.
 *
 * ### Example usage
 * ```typescript
 * const MyPreferencesSchema: PreferenceSchema = {
 *     'type': 'object',
 *     'properties': {
 *         'myext.decorations.enabled': {
 *             'type': 'boolean',
 *             'description': 'Show file status',
 *             'default': true
 *         },
 *         // [...]
 *     }
 * }
 *  injectable export interface PreferenceContribution { readonly schema : PreferenceSchema ; } export function bindPreferenceSchemaProvider ( bind ) { bindPreferenceConfigurations ( bind ) ; bind ( PreferenceSchemaProvider ) . toSelf ( ) . inSingletonScope ( ) ; bind ( PreferenceLanguageOverrideService ) . toSelf ( ) . inSingletonScope ( ) ; bindContributionProvider ( bind , PreferenceContribution ) ; } /**
 * Specialized  FrontendApplicationConfig to configure default
 * preference values for the  PreferenceSchemaProvider .
  export interface FrontendApplicationPreferenceConfig extends FrontendApplicationConfig { preferences : { [ preferenceName ] : any ; } ; } export namespace FrontendApplicationPreferenceConfig { export function is ( config ) { return 'preferences' in config && typeof config [ 'preferences' ] === 'object' ; } } /**
 * The  PreferenceSchemaProvider collects all  PreferenceContribution s and combines
 * the preference schema provided by these contributions into one collective schema. The preferences which
 * are provided by this  PreferenceProvider are derived from this combined schema.
  @ injectable ( ) export class PreferenceSchemaProvider extends PreferenceProvider { protected readonly preferences : { [ name ] : any ; } = { } ; protected readonly combinedSchema : PreferenceDataSchema = { properties : { } , patternProperties : { } , allowComments : true , allowTrailingCommas : true , } ; protected readonly workspaceSchema : PreferenceDataSchema = { properties : { } , patternProperties : { } , allowComments : true , allowTrailingCommas : true , } ; protected readonly folderSchema : PreferenceDataSchema = { properties : { } , patternProperties : { } , allowComments : true , allowTrailingCommas : true , } ; @ inject ( ContributionProvider ) @ named ( PreferenceContribution ) protected readonly preferenceContributions : ContributionProvider < PreferenceContribution > ; @ inject ( PreferenceConfigurations ) protected readonly configurations : PreferenceConfigurations ; protected readonly onDidPreferenceSchemaChangedEmitter = new Emitter < void > ( ) ; readonly onDidPreferenceSchemaChanged : Event < void > = this . onDidPreferenceSchemaChangedEmitter . event ; protected fireDidPreferenceSchemaChanged ( ) { this . onDidPreferenceSchemaChangedEmitter . fire ( undefined ) ; } @ postConstruct ( ) protected init ( ) { this . readConfiguredPreferences ( ) ; this . preferenceContributions . getContributions ( ) . forEach ( contrib => { this . doSetSchema ( contrib . schema ) ; } ) ; this . combinedSchema . additionalProperties = false ; this . _ready . resolve ( ) ; } param overrideIdentifier registerOverrideIdentifier ( overrideIdentifier ) { if ( this . preferenceOverrideService . addOverrideIdentifier ( overrideIdentifier ) ) { this . updateOverridePatternPropertiesKey ( ) ; } } protected readonly overridePatternProperties : Required < Pick < PreferenceDataProperty , 'properties' | 'additionalProperties' > > & PreferenceDataProperty = { type : 'object' , description : 'Configure editor settings to be overridden for a language.' , errorMessage : 'Unknown Identifier. Use language identifiers' , properties : { } , additionalProperties : false } ; protected overridePatternPropertiesKey : string | undefined ; protected updateOverridePatternPropertiesKey ( ) { const oldKey = this . overridePatternPropertiesKey ; const newKey = this . preferenceOverrideService . computeOverridePatternPropertiesKey ( ) ; if ( oldKey === newKey ) { return ; } if ( oldKey ) { delete this . combinedSchema . patternProperties [ oldKey ] ; } this . overridePatternPropertiesKey = newKey ; if ( newKey ) { this . combinedSchema . patternProperties [ newKey ] = this . overridePatternProperties ; } this . fireDidPreferenceSchemaChanged ( ) ; } protected doUnsetSchema ( changes ) { const inverseChanges = [ ] ; for ( const change of changes ) { const preferenceName = change . preferenceName ; const overridden = this . preferenceOverrideService . overriddenPreferenceName ( preferenceName ) ; if ( overridden ) { delete this . overridePatternProperties . properties [ `[${ overridden . overrideIdentifier }]` ] ; this . removePropFromSchemas ( `[${ overridden . overrideIdentifier }]` ) ; } else { this . removePropFromSchemas ( preferenceName ) ; } const newValue = change . oldValue ; const oldValue = change . newValue ; const { scope , domain } = change ; const inverseChange = { preferenceName , oldValue , scope , domain } ; if ( typeof newValue === undefined ) { delete this . preferences [ preferenceName ] ; } else { inverseChange . newValue = newValue ; this . preferences [ preferenceName ] = newValue ; } inverseChanges . push ( inverseChange ) ; } return inverseChanges ; } protected doSetSchema ( schema ) { const ajv = new Ajv ( ) ; const valid = ajv . validateSchema ( schema ) ; if ( ! valid ) { const errors = ! ! ajv . errors ? ajv . errorsText ( ajv . errors ) : 'unknown validation error' ; console . warn ( 'A contributed preference schema has validation issues : ' + errors ) ; } const scope = PreferenceScope . Default ; const domain = this . getDomain ( ) ; const changes = [ ] ; const defaultScope = PreferenceSchema . getDefaultScope ( schema ) ; const overridable = schema . overridable || false ; for ( const preferenceName of Object . keys ( schema . properties ) ) { if ( this . combinedSchema . properties [ preferenceName ] ) { console . error ( 'Preference name collision detected in the schema for property: ' + preferenceName ) ; } else { const schemaProps = PreferenceDataProperty . fromPreferenceSchemaProperty ( schema . properties [ preferenceName ] , defaultScope ) ; if ( typeof schemaProps . overridable !== 'boolean' && overridable ) { schemaProps . overridable = true ; } if ( schemaProps . overridable ) { this . overridePatternProperties . properties [ preferenceName ] = schemaProps ; } this . updateSchemaProps ( preferenceName , schemaProps ) ; const schemaDefault = this . getDefaultValue ( schemaProps ) ; const configuredDefault = this . getConfiguredDefault ( preferenceName ) ; if ( this . preferenceOverrideService . testOverrideValue ( preferenceName , schemaDefault ) ) { schemaProps . defaultValue = PreferenceSchemaProperties . is ( configuredDefault ) ? PreferenceProvider . merge ( schemaDefault , configuredDefault ) : schemaDefault ; for ( const overriddenPreferenceName in schemaProps . defaultValue ) { const overrideValue = schemaDefault [ overriddenPreferenceName ] ; const overridePreferenceName = `${ preferenceName }.${ overriddenPreferenceName }` ; changes . push ( this . doSetPreferenceValue ( overridePreferenceName , overrideValue , { scope , domain } ) ) ; } } else { schemaProps . defaultValue = configuredDefault === undefined ? schemaDefault : configuredDefault ; changes . push ( this . doSetPreferenceValue ( preferenceName , schemaProps . defaultValue , { scope , domain } ) ) ; } } } return changes ; } protected doSetPreferenceValue ( preferenceName , newValue , { scope , domain } ) { const oldValue = this . preferences [ preferenceName ] ; this . preferences [ preferenceName ] = newValue ; return { preferenceName , oldValue , newValue , scope , domain } ; } protected getDefaultValue ( property ) { if ( property . defaultValue !== undefined ) { return property . defaultValue ; } if ( property . default !== undefined ) { return property . default ; } const type = Array . isArray ( property . type ) ? property . type [ 0 ] : property . type ; switch ( type ) { case 'boolean' : return false ; case 'integer' : case 'number' : return 0 ; case 'string' : return '' ; case 'array' : return [ ] ; case 'object' : return { } ; } return null ; } protected getConfiguredDefault ( preferenceName ) { const config = FrontendApplicationConfigProvider . get ( ) ; if ( preferenceName && FrontendApplicationPreferenceConfig . is ( config ) && preferenceName in config . preferences ) { return config . preferences [ preferenceName ] ; } } getCombinedSchema ( ) { return this . combinedSchema ; } getSchema ( scope ) { switch ( scope ) { case PreferenceScope . Default : case PreferenceScope . User : return this . combinedSchema ; case PreferenceScope . Workspace : return this . workspaceSchema ; case PreferenceScope . Folder : return this . folderSchema ; } } setSchema ( schema ) { const changes = this . doSetSchema ( schema ) ; if ( ! changes . length ) { return Disposable . NULL ; } this . fireDidPreferenceSchemaChanged ( ) ; this . emitPreferencesChangedEvent ( changes ) ; return Disposable . create ( ( ) => { const inverseChanges = this . doUnsetSchema ( changes ) ; if ( ! inverseChanges . length ) { return ; } this . fireDidPreferenceSchemaChanged ( ) ; this . emitPreferencesChangedEvent ( inverseChanges ) ; } ) ; } getPreferences ( ) { return this . preferences ; } async setPreference ( ) { return false ; } isValidInScope ( preferenceName , scope ) { let property ; const overridden = this . preferenceOverrideService . overriddenPreferenceName ( preferenceName ) ; if ( overridden ) { property = this . overridePatternProperties [ `[${ overridden . overrideIdentifier }]` ] ; property = property && property [ overridden . preferenceName ] ; if ( ! property ) { property = this . overridePatternProperties [ overridden . preferenceName ] ; } if ( ! property ) { property = this . combinedSchema . properties [ overridden . preferenceName ] ; } } else { property = this . combinedSchema . properties [ preferenceName ] ; } return property && property . scope ! >= scope ; } * getPreferenceNames ( ) { for ( const preferenceName in this . combinedSchema . properties ) { yield preferenceName ; for ( const overridePreferenceName of this . getOverridePreferenceNames ( preferenceName ) ) { yield overridePreferenceName ; } } } getOverridePreferenceNames ( preferenceName ) { const preference = this . combinedSchema . properties [ preferenceName ] ; if ( preference && preference . overridable ) { return this . preferenceOverrideService . getOverridePreferenceNames ( preferenceName ) ; } return [ ] [ Symbol . iterator ] ( ) ; } protected updateSchemaProps ( key , property ) { this . combinedSchema . properties [ key ] = property ; switch ( property . scope ) { case PreferenceScope . Folder : this . folderSchema . properties [ key ] = property ; case PreferenceScope . Workspace : this . workspaceSchema . properties [ key ] = property ; break ; } } protected removePropFromSchemas ( key ) { delete this . combinedSchema . properties [ key ] ; delete this . workspaceSchema . properties [ key ] ; delete this . folderSchema . properties [ key ] ; } protected readConfiguredPreferences ( ) { const config = FrontendApplicationConfigProvider . get ( ) ; if ( FrontendApplicationPreferenceConfig . is ( config ) ) { try { const configuredDefaults = config . preferences ; const parsedDefaults = this . getParsedContent ( configuredDefaults ) ; Object . assign ( this . preferences , parsedDefaults ) ; const scope = PreferenceScope . Default ; const domain = this . getDomain ( ) ; const changes = Object . keys ( this . preferences ) . map ( ( key ) : PreferenceProviderDataChange => ( { preferenceName : key , oldValue : undefined , newValue : this . preferences [ key ] , scope , domain } ) ) ; this . emitPreferencesChangedEvent ( changes ) ; } catch ( e ) { console . error ( 'Failed to load preferences from frontend configuration.' , e ) ; } } } deprecated overridePreferenceName ( override ) { return this . preferenceOverrideService . overridePreferenceName ( override ) ; } deprecated testOverrideValue ( name , value ) { return this . preferenceOverrideService . testOverrideValue ( name , value ) ; } deprecated overriddenPreferenceName ( name ) { return this . preferenceOverrideService . overriddenPreferenceName ( name ) ; } }