Skip to content

Commit

Permalink
Use file service in preference provider initialization
Browse files Browse the repository at this point in the history
Signed-off-by: Colin Grant <[email protected]>
  • Loading branch information
colin-grant-work committed May 26, 2021
1 parent 6e15b35 commit c5834ea
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 13 deletions.
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 have processed file content when it is ready.', 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();
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

0 comments on commit c5834ea

Please sign in to comment.