// ***************************************************************************** // 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-only WITH Classpath-exception-2.0 // ***************************************************************************** /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-null/no-null */ import * as jsoncparser from 'jsonc-parser'; import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { Disposable } from '@theia/core/lib/common/disposable'; import { PreferenceProviderImpl, PreferenceScope, PreferenceProviderDataChange, PreferenceSchemaService, PreferenceConfigurations, PreferenceUtils, PreferenceLanguageOverrideService, Listener } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { Emitter, Event } from '@theia/core'; import { JSONValue } from '@theia/core/shared/@lumino/coreutils'; export interface FileContentStatus { content: string; fileOK: boolean } /** * Abtracts the way to read and write preferences to a given resource */ export interface PreferenceStorage extends Disposable { /** * Write a value to the underlying preference store * @param key the preference key * @param path the path to the JSON object to change * @param value the new preference value * @returns a promise that will resolve when all "onStored" listeners have finished */ writeValue(key: string, path: string[], value: JSONValue): Promise; /** * List of listeners that will get a string with the newly stored resource content and should return a promise that resolves when * they are done with their processing */ onDidChangeFileContent: Listener.Registration>; /** * Reds the content of the underlying resource */ read(): Promise; }; export const PreferenceStorageFactory = Symbol('PreferenceStorageFactory'); export type PreferenceStorageFactory = (uri: URI, scope: PreferenceScope) => PreferenceStorage; @injectable() export abstract class AbstractResourcePreferenceProvider extends PreferenceProviderImpl { protected preferenceStorage: PreferenceStorage; protected preferences: Record = {}; protected _fileExists = false; protected readonly loading = new Deferred(); protected readonly onDidChangeValidityEmitter = new Emitter(); set fileExists(exists: boolean) { if (exists !== this._fileExists) { this._fileExists = exists; this.onDidChangeValidityEmitter.fire(exists); } } get onDidChangeValidity(): Event { return this.onDidChangeValidityEmitter.event; } @inject(PreferenceSchemaService) protected readonly schemaProvider: PreferenceSchemaService; @inject(PreferenceConfigurations) protected readonly configurations: PreferenceConfigurations; @inject(PreferenceLanguageOverrideService) protected readonly preferenceOverrideService: PreferenceLanguageOverrideService; @inject(PreferenceStorageFactory) protected readonly preferenceStorageFactory: PreferenceStorageFactory; @postConstruct() protected init(): void { this.doInit(); } protected async doInit(): Promise { const uri = this.getUri(); this.toDispose.push(Disposable.create(() => this.loading.reject(new Error(`Preference provider for '${uri}' was disposed.`)))); this.preferenceStorage = this.preferenceStorageFactory(uri, this.getScope()); this.toDispose.push(this.preferenceStorage); this.preferenceStorage.onDidChangeFileContent(async ({ content, fileOK }) => { this.fileExists = fileOK; this.readPreferencesFromContent(content); await this.fireDidPreferencesChanged(); // Ensure all consumers of the event have received it.ยจ return true; }); await this.readPreferencesFromFile(); this._ready.resolve(); this.loading.resolve(); this.toDispose.pushAll([ Disposable.create(() => this.reset()), ]); } protected abstract getUri(): URI; abstract getScope(): PreferenceScope; get valid(): boolean { return this._fileExists; } override getConfigUri(): URI; override getConfigUri(resourceUri: string | undefined): URI | undefined; override getConfigUri(resourceUri?: string): URI | undefined { if (!resourceUri) { return this.getUri(); } return this.valid && this.contains(resourceUri) ? this.getUri() : undefined; } contains(resourceUri: string | undefined): boolean { if (!resourceUri) { return true; } const domain = this.getDomain(); if (!domain) { return true; } const resourcePath = new URI(resourceUri).path; return domain.some(uri => new URI(uri).path.relativity(resourcePath) >= 0); } getPreferences(resourceUri?: string): { [key: string]: any } { return this.valid && this.contains(resourceUri) ? this.preferences : {}; } async setPreference(key: string, value: any, resourceUri?: string): Promise { let path: string[] | undefined; if (this.toDispose.disposed || !(path = this.getPath(key)) || !this.contains(resourceUri)) { return false; } return this.doSetPreference(key, path, value); } protected doSetPreference(key: string, path: string[], value: JSONValue): Promise { return this.preferenceStorage.writeValue(key, path, value); } protected getPath(preferenceName: string): string[] | undefined { const asOverride = this.preferenceOverrideService.overriddenPreferenceName(preferenceName); if (asOverride?.overrideIdentifier) { return [this.preferenceOverrideService.markLanguageOverride(asOverride.overrideIdentifier), asOverride.preferenceName]; } return [preferenceName]; } protected readPreferencesFromFile(): Promise { return this.preferenceStorage.read().then(value => { this.fileExists = true; this.readPreferencesFromContent(value); }).catch(() => { this.fileExists = false; this.readPreferencesFromContent(''); }); } protected readPreferencesFromContent(content: string): void { let preferencesInJson; try { preferencesInJson = this.parse(content); } catch { preferencesInJson = {}; } const parsedPreferences = this.getParsedContent(preferencesInJson); this.handlePreferenceChanges(parsedPreferences); } protected parse(content: string): any { content = content.trim(); if (!content) { return undefined; } const strippedContent = jsoncparser.stripComments(content); return jsoncparser.parse(strippedContent); } protected handlePreferenceChanges(newPrefs: { [key: string]: any }): void { const oldPrefs = Object.assign({}, this.preferences); this.preferences = newPrefs; const prefNames = new Set([...Object.keys(oldPrefs), ...Object.keys(newPrefs)]); const prefChanges: PreferenceProviderDataChange[] = []; const uri = this.getUri(); for (const prefName of prefNames.values()) { const oldValue = oldPrefs[prefName]; const newValue = newPrefs[prefName]; const schemaProperty = this.schemaProvider.getSchemaProperty(prefName); if (schemaProperty && schemaProperty.included) { const scope = schemaProperty.scope; // do not emit the change event if the change is made out of the defined preference scope if (!this.schemaProvider.isValidInScope(prefName, this.getScope())) { console.warn(`Preference ${prefName} in ${uri} can only be defined in scopes: ${PreferenceScope.getScopeNames(scope).join(', ')}.`); continue; } } if (!PreferenceUtils.deepEqual(newValue, oldValue)) { prefChanges.push({ preferenceName: prefName, newValue, oldValue, scope: this.getScope(), domain: this.getDomain() }); } } if (prefChanges.length > 0) { this.emitPreferencesChangedEvent(prefChanges); } } protected reset(): void { const preferences = this.preferences; this.preferences = {}; const changes: PreferenceProviderDataChange[] = []; for (const prefName of Object.keys(preferences)) { const value = preferences[prefName]; if (value !== undefined) { changes.push({ preferenceName: prefName, newValue: undefined, oldValue: value, scope: this.getScope(), domain: this.getDomain() }); } } if (changes.length > 0) { this.emitPreferencesChangedEvent(changes); } } }