Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use file service in preference provider initialization #9362

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/********************************************************************************
* Copyright (C) 2021 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
********************************************************************************/

/* eslint-disable @typescript-eslint/no-explicit-any,no-unused-expressions */

import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
const disableJSDOM = enableJSDOM();

import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { ApplicationProps } from '@theia/application-package/lib/application-props';
FrontendApplicationConfigProvider.set({
...ApplicationProps.DEFAULT.frontend.config
});

import { expect } from 'chai';
import { Container } from '@theia/core/shared/inversify';
import { AbstractResourcePreferenceProvider } from './abstract-resource-preference-provider';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { bindPreferenceService } from '@theia/core/lib/browser/frontend-application-bindings';
import { bindMockPreferenceProviders } from '@theia/core/lib/browser/preferences/test';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { Disposable, MessageService } from '@theia/core/lib/common';
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
import { PreferenceSchemaProvider } from '@theia/core/lib/browser';

disableJSDOM();

class MockFileService {
releaseContent = new Deferred();
async read(): Promise<{ value: string }> {
await this.releaseContent.promise;
return { value: JSON.stringify({ 'editor.fontSize': 20 }) };
}
}

const DO_NOTHING = () => { };
const RETURN_DISPOSABLE = () => Disposable.NULL;

class MockTextModelService {
createModelReference(): any {
return {
dispose: DO_NOTHING,
object: {
onDidChangeContent: RETURN_DISPOSABLE,
onDirtyChanged: RETURN_DISPOSABLE,
onDidChangeValid: RETURN_DISPOSABLE,
}
};
}
}

const mockSchemaProvider = { getCombinedSchema: () => ({ properties: {} }) };

class LessAbstractPreferenceProvider extends AbstractResourcePreferenceProvider {
getUri(): any { }
getScope(): any { }
}

describe('AbstractResourcePreferenceProvider', () => {
let provider: AbstractResourcePreferenceProvider;
let fileService: MockFileService;

beforeEach(() => {
fileService = new MockFileService();
const testContainer = new Container();
bindPreferenceService(testContainer.bind.bind(testContainer));
bindMockPreferenceProviders(testContainer.bind.bind(testContainer), testContainer.unbind.bind(testContainer));
testContainer.rebind(<any>PreferenceSchemaProvider).toConstantValue(mockSchemaProvider);
testContainer.bind(<any>FileService).toConstantValue(fileService);
testContainer.bind(<any>MonacoTextModelService).toConstantValue(new MockTextModelService);
testContainer.bind(<any>MessageService).toConstantValue(undefined);
testContainer.bind(<any>MonacoWorkspace).toConstantValue(undefined);
provider = testContainer.resolve(<any>LessAbstractPreferenceProvider);
});

it('should not store any preferences before it is ready.', async () => {
const resolveWhenFinished = new Deferred();
const errorIfReadyFirst = provider.ready.then(() => Promise.reject());

expect(provider.get('editor.fontSize')).to.be.undefined;

resolveWhenFinished.resolve();
fileService.releaseContent.resolve(); // Allow the initialization to run

// This promise would reject if the provider had declared itself ready before we resolve `resolveWhenFinished`
await Promise.race([resolveWhenFinished.promise, errorIfReadyFirst]);
});

it('should report values in file when `ready` resolves.', async () => {
fileService.releaseContent.resolve();
await provider.ready;
expect(provider.get('editor.fontSize')).to.equal(20); // The value provided by the mock FileService implementation.
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,26 @@ import { JSONExt } from '@theia/core/shared/@phosphor/coreutils';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { MessageService } from '@theia/core/lib/common/message-service';
import { Disposable } from '@theia/core/lib/common/disposable';
import { PreferenceProvider, PreferenceSchemaProvider, PreferenceScope, PreferenceProviderDataChange, PreferenceService } from '@theia/core/lib/browser';
import { PreferenceProvider, PreferenceSchemaProvider, PreferenceScope, PreferenceProviderDataChange } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations';
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { FileService } from '@theia/filesystem/lib/browser/file-service';

@injectable()
export abstract class AbstractResourcePreferenceProvider extends PreferenceProvider {

protected preferences: { [key: string]: any } = {};
protected model: MonacoEditorModel | undefined;
protected readonly loading = new Deferred();
protected modelInitialized = false;

@inject(PreferenceService) protected readonly preferenceService: PreferenceService;
@inject(MessageService) protected readonly messageService: MessageService;
@inject(PreferenceSchemaProvider) protected readonly schemaProvider: PreferenceSchemaProvider;
@inject(FileService) protected readonly fileService: FileService;

@inject(PreferenceConfigurations)
protected readonly configurations: PreferenceConfigurations;
Expand All @@ -54,6 +56,7 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi
protected async init(): Promise<void> {
const uri = this.getUri();
this.toDispose.push(Disposable.create(() => this.loading.reject(new Error(`preference provider for '${uri}' was disposed`))));
await this.readPreferencesFromFile();
colin-grant-work marked this conversation as resolved.
Show resolved Hide resolved
this._ready.resolve();

const reference = await this.textModelService.createModelReference(uri);
Expand All @@ -64,11 +67,11 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi

this.model = reference.object;
this.loading.resolve();
this.modelInitialized = true;

this.toDispose.push(reference);
this.toDispose.push(Disposable.create(() => this.model = undefined));

this.readPreferences();
this.toDispose.push(this.model.onDidChangeContent(() => this.readPreferences()));
this.toDispose.push(this.model.onDirtyChanged(() => this.readPreferences()));
this.toDispose.push(this.model.onDidChangeValid(() => this.readPreferences()));
Expand All @@ -80,7 +83,7 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi
protected abstract getScope(): PreferenceScope;

protected get valid(): boolean {
return this.model && this.model.valid || false;
return this.modelInitialized ? !!this.model?.valid : Object.keys(this.preferences).length > 0;
}

getConfigUri(): URI;
Expand Down Expand Up @@ -165,6 +168,11 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi
return [preferenceName];
}

protected async readPreferencesFromFile(): Promise<void> {
const content = await this.fileService.read(this.getUri()).catch(() => ({ value: '' }));
this.readPreferencesFromContent(content.value);
}

/**
* It HAS to be sync to ensure that `setPreference` returns only when values are updated
* or any other operation modifying the monaco model content.
Expand All @@ -175,20 +183,24 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi
return;
}
try {
let preferences;
if (model.valid) {
const content = model.getText();
const jsonContent = this.parse(content);
preferences = this.getParsedContent(jsonContent);
} else {
preferences = {};
}
this.handlePreferenceChanges(preferences);
const content = model.valid ? model.getText() : '';
this.readPreferencesFromContent(content);
} catch (e) {
console.error(`Failed to load preferences from '${this.getUri()}'.`, e);
}
}

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) {
Expand Down