/********************************************************************************
 * 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 { injectable , inject , postConstruct } from 'inversify' ; import { Event , Emitter , DisposableCollection , Disposable , deepFreeze , unreachable } from '../../common' ; import { Deferred } from '../../common/promise-util' ; import { PreferenceProvider , PreferenceProviderDataChange , PreferenceProviderDataChanges , PreferenceResolveResult } from './preference-provider' ; import { PreferenceSchemaProvider } from './preference-contribution' ; import URI from '../../common/uri' ; import { PreferenceScope } from './preference-scope' ; import { PreferenceConfigurations } from './preference-configurations' ; import { JSONExt , JSONValue } from '@phosphor/coreutils/lib/json' ; import { OverridePreferenceName , PreferenceLanguageOverrideService } from './preference-language-override-service' ; export { PreferenceScope } ; /**
 * Representation of a preference change. A preference value can be set to `undefined` for a specific scope.
 * This means that the value from a more general scope will be used.
 */ export interface PreferenceChange { /**
     * The name of the changed preference.
     */ readonly preferenceName : string ; /**
     * The new value of the changed preference.
     */ readonly newValue ? : any ; /**
     * The old value of the changed preference.
     */ readonly oldValue ? : any ; /**
     * The  PreferenceScope of the changed preference.
      readonly scope : PreferenceScope ; param resourceUri affects ( resourceUri ? ) : boolean ; } export class PreferenceChangeImpl implements PreferenceChange { constructor ( private change ) { } get preferenceName ( ) : string { return this . change . preferenceName ; } get newValue ( ) : string { return this . change . newValue ; } get oldValue ( ) : string { return this . change . oldValue ; } get scope ( ) : PreferenceScope { return this . change . scope ; } affects ( resourceUri ? ) { const resourcePath = resourceUri && new URI ( resourceUri ) . path ; const domain = this . change . domain ; return ! resourcePath || ! domain || domain . some ( uri => new URI ( uri ) . path . relativity ( resourcePath ) >= 0 ) ; } } /**
 * A key-value storage for  PreferenceChange s. Used to aggregate multiple simultaneous preference changes.
  export interface PreferenceChanges { [ preferenceName ] : PreferenceChange ; } export const PreferenceService = Symbol ( 'PreferenceService' ) ; /**
 * Service to manage preferences including, among others, getting and setting preference values as well
 * as listening to preference changes.
 *
 * Depending on your use case you might also want to look at  createPreferenceProxy with which
 * you can easily create a typesafe schema-based interface for your preferences. Internally the proxy
 * uses the PreferenceService so both approaches are compatible.
  export interface PreferenceService extends Disposable { /**
     * Promise indicating whether the service successfully initialized.
     */ readonly ready : Promise < void > ; param preferenceName returns get < T > ( preferenceName ) : T | undefined ; param preferenceName param defaultValue returns get < T > ( preferenceName , defaultValue ) : T ; param preferenceName param defaultValue param resourceUri returns get < T > ( preferenceName , defaultValue , resourceUri ? ) : T ; param preferenceName param defaultValue param resourceUri returns get < T > ( preferenceName , defaultValue ? , resourceUri ? ) : T | undefined ; param preferenceName param value param scope param resourceUri returns set ( preferenceName , value , scope ? , resourceUri ? ) : Promise < void > ; param preferenceName param value param resourceUri updateValue ( preferenceName , value , resourceUri ? ) : Promise < void > ; /**
     * Registers a callback which will be called whenever a preference is changed.
     */ onPreferenceChanged : Event < PreferenceChange > ; /**
     * Registers a callback which will be called whenever one or more preferences are changed.
     */ onPreferencesChanged : Event < PreferenceChanges > ; param preferenceName param resourceUri return inspect < T extends JSONValue > ( preferenceName , resourceUri ? ) : PreferenceInspection < T > | undefined ; param options returns overridePreferenceName ( options ) : string ; param preferenceName the preferenceName which might have been created via  PreferenceService . overridePreferenceName .
     *
     *  returns overriddenPreferenceName ( preferenceName ) : OverridePreferenceName | undefined ; param preferenceName param defaultValue param resourceUri returns resolve < T > ( preferenceName , defaultValue ? , resourceUri ? ) : PreferenceResolveResult < T > ; param scope param resourceUri param sectionName returns getConfigUri ( scope , resourceUri ? , sectionName ? ) : URI | undefined ; } /**
 * Return type of the  PreferenceService . inspect call.
  export interface PreferenceInspection < T = JSONValue > { /**
     * The preference identifier.
     */ preferenceName : string ; /**
     * Value in default scope.
     */ defaultValue : T | undefined ; /**
     * Value in user scope.
     */ globalValue : T | undefined ; /**
     * Value in workspace scope.
     */ workspaceValue : T | undefined ; /**
     * Value in folder scope.
     */ workspaceFolderValue : T | undefined ; /**
     * The value that is active, i.e. the value set in the lowest scope available.
     */ value : T | undefined ; } export type PreferenceInspectionScope = keyof Omit < PreferenceInspection < unknown > , 'preferenceName' > ; /**
 * We cannot load providers directly in the case if they depend on `PreferenceService` somehow.
 * It allows to load them lazily after DI is configured.
 */ export const PreferenceProviderProvider = Symbol ( 'PreferenceProviderProvider' ) ; export type PreferenceProviderProvider = ( scope : PreferenceScope , uri ? : URI ) => PreferenceProvider ; @ injectable ( ) export class PreferenceServiceImpl implements PreferenceService { protected readonly onPreferenceChangedEmitter = new Emitter < PreferenceChange > ( ) ; readonly onPreferenceChanged = this . onPreferenceChangedEmitter . event ; protected readonly onPreferencesChangedEmitter = new Emitter < PreferenceChanges > ( ) ; readonly onPreferencesChanged = this . onPreferencesChangedEmitter . event ; protected readonly toDispose = new DisposableCollection ( this . onPreferenceChangedEmitter , this . onPreferencesChangedEmitter ) ; @ inject ( PreferenceSchemaProvider ) protected readonly schema : PreferenceSchemaProvider ; @ inject ( PreferenceProviderProvider ) protected readonly providerProvider : PreferenceProviderProvider ; @ inject ( PreferenceConfigurations ) protected readonly configurations : PreferenceConfigurations ; @ inject ( PreferenceLanguageOverrideService ) protected readonly preferenceOverrideService : PreferenceLanguageOverrideService ; protected readonly preferenceProviders = new Map < PreferenceScope , PreferenceProvider > ( ) ; protected async initializeProviders ( ) { 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 ( ) { this . toDispose . push ( Disposable . create ( ( ) => this . _ready . reject ( new Error ( 'preference service is disposed' ) ) ) ) ; this . initializeProviders ( ) ; } dispose ( ) { this . toDispose . dispose ( ) ; } protected readonly _ready = new Deferred < void > ( ) ; get ready ( ) : Promise < void > { return this . _ready . promise ; } protected reconcilePreferences ( changes ) { const changesToEmit = { } ; const acceptChange = ( change ) => this . getAffectedPreferenceNames ( change , preferenceName => changesToEmit [ preferenceName ] = new PreferenceChangeImpl ( { ... change , preferenceName } ) ) ; 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 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 ) { break ; } else if ( scope === change . scope && change . newValue !== undefined ) { acceptChange ( change ) ; } else if ( scope < change . scope && change . newValue === undefined && value !== undefined ) { change = { ... change , newValue : value , scope } ; acceptChange ( change ) ; } } } else if ( change . newValue === undefined && change . scope === PreferenceScope . Default ) { acceptChange ( change ) ; break ; } } } const changedPreferenceNames = Object . keys ( changesToEmit ) ; if ( changedPreferenceNames . length > 0 ) { this . onPreferencesChangedEmitter . fire ( changesToEmit ) ; } changedPreferenceNames . forEach ( preferenceName => this . onPreferenceChangedEmitter . fire ( changesToEmit [ preferenceName ] ) ) ; } protected getAffectedPreferenceNames ( change , accept ) { accept ( change . preferenceName ) ; for ( const overridePreferenceName of this . schema . getOverridePreferenceNames ( change . preferenceName ) ) { if ( ! this . doHas ( overridePreferenceName ) ) { accept ( overridePreferenceName ) ; } } } protected getProvider ( scope ) { return this . preferenceProviders . get ( scope ) ; } has ( preferenceName , resourceUri ? ) { return this . get ( preferenceName , undefined , resourceUri ) !== undefined ; } get < T > ( preferenceName ) ; get < T > ( preferenceName , defaultValue ) ; get < T > ( preferenceName , defaultValue , resourceUri ) ; get < T > ( preferenceName , defaultValue ? , resourceUri ? ) ; get < T > ( preferenceName , defaultValue ? , resourceUri ? ) { return this . resolve < T > ( preferenceName , defaultValue , resourceUri ) . value ; } resolve < T > ( preferenceName , defaultValue ? , resourceUri ? ) { const { value , configUri } = this . doResolve ( preferenceName , defaultValue , resourceUri ) ; if ( value === undefined ) { const overridden = this . overriddenPreferenceName ( preferenceName ) ; if ( overridden ) { return this . doResolve ( overridden . preferenceName , defaultValue , resourceUri ) ; } } return { value , configUri } ; } async set ( preferenceName , value , scope , resourceUri ? ) { const resolvedScope = scope ?? ( ! resourceUri ? PreferenceScope . Workspace : PreferenceScope . Folder ) ; if ( resolvedScope === PreferenceScope . Folder && ! resourceUri ) { throw new Error ( 'Unable to write to Folder Settings because no resource is provided.' ) ; } const provider = this . getProvider ( resolvedScope ) ; if ( provider && await provider . setPreference ( preferenceName , value , resourceUri ) ) { return ; } throw new Error ( `Unable to write to ${ PreferenceScope [ resolvedScope ] } Settings.` ) ; } getBoolean ( preferenceName ) ; getBoolean ( preferenceName , defaultValue ) ; getBoolean ( preferenceName , defaultValue , resourceUri ) ; getBoolean ( preferenceName , defaultValue ? , resourceUri ? ) { const value = resourceUri ? this . get ( preferenceName , defaultValue , resourceUri ) : this . get ( preferenceName , defaultValue ) ; return value !== null && value !== undefined ? ! ! value : defaultValue ; } getString ( preferenceName ) ; getString ( preferenceName , defaultValue ) ; getString ( preferenceName , defaultValue , resourceUri ) ; getString ( preferenceName , defaultValue ? , resourceUri ? ) { const value = resourceUri ? this . get ( preferenceName , defaultValue , resourceUri ) : this . get ( preferenceName , defaultValue ) ; if ( value === null || value === undefined ) { return defaultValue ; } return value . toString ( ) ; } getNumber ( preferenceName ) ; getNumber ( preferenceName , defaultValue ) ; getNumber ( preferenceName , defaultValue , resourceUri ) ; getNumber ( preferenceName , defaultValue ? , resourceUri ? ) { const value = resourceUri ? this . get ( preferenceName , defaultValue , resourceUri ) : this . get ( preferenceName , defaultValue ) ; if ( value === null || value === undefined ) { return defaultValue ; } if ( typeof value === 'number' ) { return value ; } return Number ( value ) ; } inspect < T > ( preferenceName , resourceUri ? ) { const defaultValue = this . inspectInScope < T > ( preferenceName , PreferenceScope . Default , resourceUri ) ; const globalValue = this . inspectInScope < T > ( preferenceName , PreferenceScope . User , resourceUri ) ; const workspaceValue = this . inspectInScope < T > ( preferenceName , PreferenceScope . Workspace , resourceUri ) ; const workspaceFolderValue = this . inspectInScope < T > ( preferenceName , PreferenceScope . Folder , resourceUri ) ; const valueApplied = workspaceFolderValue ?? workspaceValue ?? globalValue ?? defaultValue ; return { preferenceName , defaultValue , globalValue , workspaceValue , workspaceFolderValue , value : valueApplied } ; } protected inspectInScope < T > ( preferenceName , scope , resourceUri ? ) { const value = this . doInspectInScope < T > ( preferenceName , scope , resourceUri ) ; if ( value === undefined ) { const overridden = this . overriddenPreferenceName ( preferenceName ) ; if ( overridden ) { return this . doInspectInScope ( overridden . preferenceName , scope , resourceUri ) ; } } return value ; } protected getScopedValueFromInspection < T > ( inspection , scope ) { switch ( scope ) { case PreferenceScope . Default : return inspection . defaultValue ; case PreferenceScope . User : return inspection . globalValue ; case PreferenceScope . Workspace : return inspection . workspaceValue ; case PreferenceScope . Folder : return inspection . workspaceFolderValue ; } unreachable ( scope , 'Not all PreferenceScope enum variants handled.' ) ; } async updateValue ( preferenceName , value , resourceUri ? ) { const inspection = this . inspect < any > ( preferenceName , resourceUri ) ; if ( inspection ) { const scopesToChange = this . getScopesToChange ( inspection , value ) ; const isDeletion = value === undefined || ( scopesToChange . length === 1 && scopesToChange [ 0 ] === PreferenceScope . User && JSONExt . deepEqual ( value , inspection . defaultValue ) ) ; const effectiveValue = isDeletion ? undefined : value ; await Promise . all ( scopesToChange . map ( scope => this . set ( preferenceName , effectiveValue , scope , resourceUri ) ) ) ; } } protected getScopesToChange ( inspection , intendedValue ) { if ( JSONExt . deepEqual ( inspection . value , intendedValue ) ) { return [ ] ; } const allScopes = PreferenceScope . getReversedScopes ( ) ; allScopes . pop ( ) ; const isScopeDefined = ( scope ) => this . getScopedValueFromInspection ( inspection , scope ) !== undefined ; if ( intendedValue === undefined ) { return allScopes . filter ( isScopeDefined ) ; } return [ allScopes . find ( isScopeDefined ) ?? PreferenceScope . User ] ; } overridePreferenceName ( options ) { return this . preferenceOverrideService . overridePreferenceName ( options ) ; } overriddenPreferenceName ( preferenceName ) { return this . preferenceOverrideService . overriddenPreferenceName ( preferenceName ) ; } protected doHas ( preferenceName , resourceUri ? ) { return this . doGet ( preferenceName , undefined , resourceUri ) !== undefined ; } protected doInspectInScope < T > ( preferenceName , scope , resourceUri ? ) { const provider = this . getProvider ( scope ) ; return provider && provider . get < T > ( preferenceName , resourceUri ) ; } protected doGet < T > ( preferenceName , defaultValue ? , resourceUri ? ) { return this . doResolve ( preferenceName , defaultValue , resourceUri ) . value ; } protected doResolve < T > ( preferenceName , defaultValue ? , resourceUri ? ) { const result = { } ; for ( const scope of PreferenceScope . getScopes ( ) ) { if ( this . schema . isValidInScope ( preferenceName , scope ) ) { const provider = this . getProvider ( scope ) ; if ( provider ) { const { configUri , value } = provider . resolve < T > ( preferenceName , resourceUri ) ; if ( value !== undefined ) { result . configUri = configUri ; result . value = PreferenceProvider . merge ( result . value as any , value as any ) as any ; } } } } return { configUri : result . configUri , value : result . value !== undefined ? deepFreeze ( result . value ) : defaultValue } ; } getConfigUri ( scope , resourceUri ? , sectionName = this . configurations . getConfigName ( ) ) { const provider = this . getProvider ( scope ) ; if ( ! provider || ! this . configurations . isAnyConfig ( sectionName ) ) { return undefined ; } const configUri = provider . getConfigUri ( resourceUri , sectionName ) ; if ( configUri ) { return configUri ; } return provider . getContainingConfigUri && provider . getContainingConfigUri ( resourceUri , sectionName ) ; } }