From 50e32d3c0dfeab80a927afa2b361982759c6e7cf Mon Sep 17 00:00:00 2001 From: Anton Kosyakov Date: Sun, 9 Feb 2020 08:37:01 +0000 Subject: [PATCH] [preferences] fix #6845: use text models to update content It resolve following issues: - dirty editors are respected - edits are applied in thread safe fashion Signed-off-by: Anton Kosyakov --- .../api-tests/src/launch-preferences.spec.js | 198 +++++++----------- .../monaco/src/browser/monaco-editor-model.ts | 2 +- .../monaco/src/browser/monaco-workspace.ts | 31 +++ .../plugin-ext/src/common/plugin-api-rpc.ts | 2 +- .../src/main/browser/documents-main.ts | 23 +- .../src/main/browser/text-editor-main.ts | 23 +- .../abstract-resource-preference-provider.ts | 149 +++++++------ 7 files changed, 226 insertions(+), 202 deletions(-) rename packages/debug/src/browser/preferences/launch-preferences.spec.ts => examples/api-tests/src/launch-preferences.spec.js (75%) diff --git a/packages/debug/src/browser/preferences/launch-preferences.spec.ts b/examples/api-tests/src/launch-preferences.spec.js similarity index 75% rename from packages/debug/src/browser/preferences/launch-preferences.spec.ts rename to examples/api-tests/src/launch-preferences.spec.js index 72d37dbccb8dc..e184afff16c83 100644 --- a/packages/debug/src/browser/preferences/launch-preferences.spec.ts +++ b/examples/api-tests/src/launch-preferences.spec.js @@ -16,50 +16,31 @@ /* eslint-disable no-unused-expressions, @typescript-eslint/no-explicit-any */ -import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; -const disableJSDOM = enableJSDOM(); - -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as assert from 'assert'; -import { Container } from 'inversify'; -import { FileUri } from '@theia/core/lib/node/file-uri'; -import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; -import { PreferenceService, PreferenceServiceImpl, PreferenceScope } from '@theia/core/lib/browser/preferences/preference-service'; -import { bindPreferenceService, bindMessageService, bindResourceProvider } from '@theia/core/lib/browser/frontend-application-bindings'; -import { bindFileSystem } from '@theia/filesystem/lib/node/filesystem-backend-module'; -import { bindFileResource } from '@theia/filesystem/lib/browser/filesystem-frontend-module'; -import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; -import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; -import { FileSystemWatcher } from '@theia/filesystem/lib/browser/filesystem-watcher'; -import { bindFileSystemPreferences } from '@theia/filesystem/lib/browser/filesystem-preferences'; -import { FileShouldOverwrite } from '@theia/filesystem/lib/common/filesystem'; -import { bindLogger } from '@theia/core/lib/node/logger-backend-module'; -import { bindWorkspacePreferences } from '@theia/workspace/lib/browser'; -import { WindowService } from '@theia/core/lib/browser/window/window-service'; -import { MockWindowService } from '@theia/core/lib/browser/window/test/mock-window-service'; -import { MockWorkspaceServer } from '@theia/workspace/lib/common/test/mock-workspace-server'; -import { WorkspaceServer } from '@theia/workspace/lib/common/workspace-protocol'; -import { bindPreferenceProviders } from '@theia/preferences/lib/browser/preference-bindings'; -import { bindUserStorage } from '@theia/userstorage/lib/browser/user-storage-frontend-module'; -import { FileSystemWatcherServer } from '@theia/filesystem/lib/common/filesystem-watcher-protocol'; -import { MockFilesystemWatcherServer } from '@theia/filesystem/lib/common/test/mock-filesystem-watcher-server'; -import { bindLaunchPreferences } from './launch-preferences'; - -disableJSDOM(); - -process.on('unhandledRejection', (reason, promise) => { - console.error(reason); - throw reason; -}); +/** + * @typedef {'.vscode' | '.theia' | ['.theia', '.vscode']} ConfigMode + */ /** * Expectations should be tested and aligned against VS Code. * See https://github.com/akosyakov/vscode-launch/blob/master/src/test/extension.test.ts */ -describe('Launch Preferences', () => { +describe('Launch Preferences', function () { + + const { assert } = chai; - type ConfigMode = '.vscode' | '.theia' | ['.theia', '.vscode']; + import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable'; + import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser/preferences/preference-service'; + + const Uri = require('@theia/core/lib/common/uri'); + const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service'); + const { FileSystem } = require('@theia/filesystem/lib/common/filesystem'); + + /** @type {import('inversify').Container} */ + const container = window['theia'].container; + const preferences = container.get(PreferenceService); + const workspaceService = container.get(WorkspaceService); + /** @type {import('@theia/filesystem/lib/common/filesystem').FileSystem} */ + const fileSystem = container.get(FileSystem); const defaultLaunch = { 'configurations': [], @@ -107,7 +88,8 @@ describe('Launch Preferences', () => { testSuite({ name: 'No Preferences', - expectation: defaultLaunch + expectation: defaultLaunch, + only: true }); testLaunchAndSettingsSuite({ @@ -329,15 +311,20 @@ describe('Launch Preferences', () => { } }); + /** + * @typedef {Object} LaunchAndSettingsSuiteOptions + * @property {string} name + * @property {any} expectation + * @property {any} [launch] + * @property {boolean} [only] + * @property {ConfigMode} [configMode] + */ + /** + * @type {(options: LaunchAndSettingsSuiteOptions) => void} + */ function testLaunchAndSettingsSuite({ name, expectation, launch, only, configMode - }: { - name: string, - expectation: any, - launch?: any, - only?: boolean, - configMode?: ConfigMode - }): void { + }) { testSuite({ name: name + ' Launch Configuration', launch, @@ -356,20 +343,24 @@ describe('Launch Preferences', () => { }); } - function testSuite(options: { - name: string, - expectation: any, - inspectExpectation?: any, - launch?: any, - settings?: any, - only?: boolean, - configMode?: ConfigMode - }): void { - + /** + * @typedef {Object} SuiteOptions + * @property {string} name + * @property {any} expectation + * @property {any} [inspectExpectation] + * @property {any} [launch] + * @property {any} [settings] + * @property {boolean} [only] + * @property {ConfigMode} [configMode] + */ + /** + * @type {(options: SuiteOptions) => void} + */ + function testSuite(options) { describe(options.name, () => { if (options.configMode) { - testConfigSuite(options as any); + testConfigSuite(options); } else { testConfigSuite({ @@ -394,87 +385,50 @@ describe('Launch Preferences', () => { } + /** + * @typedef {Object} ConfigSuiteOptions + * @property {any} expectation + * @property {any} [inspectExpectation] + * @property {any} [launch] + * @property {any} [settings] + * @property {boolean} [only] + * @property {ConfigMode} [configMode] + */ + /** + * @type {(options: ConfigSuiteOptions) => void} + */ function testConfigSuite({ configMode, expectation, inspectExpectation, settings, launch, only - }: { - configMode: ConfigMode - expectation: any, - inspectExpectation?: any, - launch?: any, - settings?: any, - only?: boolean - }): void { + }) { describe(JSON.stringify(configMode, undefined, 2), () => { const configPaths = Array.isArray(configMode) ? configMode : [configMode]; - const rootPath = path.resolve(__dirname, '..', '..', '..', 'launch-preference-test-temp'); - const rootUri = FileUri.create(rootPath).toString(); - - let preferences: PreferenceService; - const toTearDown = new DisposableCollection(); - beforeEach(async function (): Promise { - toTearDown.push(Disposable.create(enableJSDOM())); - FrontendApplicationConfigProvider.set({ - 'applicationName': 'test', - }); - - fs.removeSync(rootPath); - fs.ensureDirSync(rootPath); - toTearDown.push(Disposable.create(() => fs.removeSync(rootPath))); + beforeEach(async () => { + const rootUri = new Uri.default(workspaceService.tryGetRoots()[0].uri); if (settings) { for (const configPath of configPaths) { - const settingsPath = path.resolve(rootPath, configPath, 'settings.json'); - fs.ensureFileSync(settingsPath); - fs.writeFileSync(settingsPath, JSON.stringify(settings), 'utf-8'); + const uri = rootUri.resolve(configPath + '/settings.json'); + await fileSystem.createFile(rootUri.resolve(configPath + '/settings.json'), { + content: JSON.stringify(settings), + encoding: 'utf-8' + }); + this.toTearDown.push(Disposable.create(() => fileSystem.delete(uri))); } } if (launch) { for (const configPath of configPaths) { - const launchPath = path.resolve(rootPath, configPath, 'launch.json'); - fs.ensureFileSync(launchPath); - fs.writeFileSync(launchPath, JSON.stringify(launch), 'utf-8'); + const uri = rootUri.resolve(configPath + '/launch.json'); + await fileSystem.createFile(rootUri.resolve(configPath + '/settings.json'), { + content: JSON.stringify(launch), + encoding: 'utf-8' + }); + this.toTearDown.push(Disposable.create(() => fileSystem.delete(uri))); } } - - const container = new Container(); - const bind = container.bind.bind(container); - const unbind = container.unbind.bind(container); - bindLogger(bind); - bindMessageService(bind); - bindResourceProvider(bind); - bindFileResource(bind); - bindUserStorage(bind); - bindPreferenceService(bind); - bindFileSystem(bind); - bind(FileSystemWatcherServer).toConstantValue(new MockFilesystemWatcherServer()); - bindFileSystemPreferences(bind); - container.bind(FileShouldOverwrite).toConstantValue(async () => true); - bind(FileSystemWatcher).toSelf().inSingletonScope(); - bindPreferenceProviders(bind, unbind); - bindWorkspacePreferences(bind); - container.bind(WorkspaceService).toSelf().inSingletonScope(); - container.bind(WindowService).toConstantValue(new MockWindowService()); - - const workspaceServer = new MockWorkspaceServer(); - workspaceServer['getMostRecentlyUsedWorkspace'] = async () => rootUri; - container.bind(WorkspaceServer).toConstantValue(workspaceServer); - - bindLaunchPreferences(bind); - - toTearDown.push(container.get(FileSystemWatcher)); - - const impl = container.get(PreferenceServiceImpl); - toTearDown.push(impl); - - preferences = impl; - toTearDown.push(Disposable.create(() => preferences = undefined!)); - - await preferences.ready; - await container.get(WorkspaceService).roots; }); afterEach(() => toTearDown.dispose()); @@ -587,7 +541,7 @@ describe('Launch Preferences', () => { await preferences.set('launch.configurations', [validConfiguration, validConfiguration2]); const inspect = preferences.inspect('launch'); - const actual = inspect && inspect.workspaceValue && (inspect.workspaceValue).configurations; + const actual = inspect && inspect.workspaceValue && inspect.workspaceValue.configurations; assert.deepStrictEqual(actual, [validConfiguration, validConfiguration2]); }); } diff --git a/packages/monaco/src/browser/monaco-editor-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts index 4f418cd13a6ed..f6d240543ae5d 100644 --- a/packages/monaco/src/browser/monaco-editor-model.ts +++ b/packages/monaco/src/browser/monaco-editor-model.ts @@ -75,7 +75,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { this.toDispose.push(this.onDidSaveModelEmitter); this.toDispose.push(this.onWillSaveModelEmitter); this.toDispose.push(this.onDirtyChangedEmitter); - this.resolveModel = resource.readContents(options).then(content => this.initialize(content)); + this.resolveModel = resource.readContents(options).then(content => this.initialize(content), () => this.initialize('')); this.defaultEncoding = options && options.encoding ? options.encoding : undefined; } diff --git a/packages/monaco/src/browser/monaco-workspace.ts b/packages/monaco/src/browser/monaco-workspace.ts index ef552149c166f..2da1ff9441359 100644 --- a/packages/monaco/src/browser/monaco-workspace.ts +++ b/packages/monaco/src/browser/monaco-workspace.ts @@ -32,6 +32,7 @@ import { WillSaveMonacoModelEvent, MonacoEditorModel, MonacoModelContentChangedE import { MonacoEditor } from './monaco-editor'; import { MonacoConfigurations } from './monaco-configurations'; import { ProblemManager } from '@theia/markers/lib/browser'; +import { MaybePromise } from '@theia/core/lib/common/types'; export interface MonacoDidChangeTextDocumentParams extends lang.DidChangeTextDocumentParams { readonly textDocument: MonacoEditorModel; @@ -276,7 +277,12 @@ export class MonacoWorkspace implements lang.Workspace { this.onDidSaveTextDocumentEmitter.fire(model); } + protected suppressedOpenIfDirty: MonacoEditorModel[] = []; + protected openEditorIfDirty(model: MonacoEditorModel): void { + if (this.suppressedOpenIfDirty.indexOf(model) !== -1) { + return; + } if (model.dirty && MonacoEditor.findByDocument(this.editorManager, model).length === 0) { // create a new reference to make sure the model is not disposed before it is // acquired by the editor, thus losing the changes that made it dirty. @@ -288,6 +294,18 @@ export class MonacoWorkspace implements lang.Workspace { } } + protected async suppressOpenIfDirty(model: MonacoEditorModel, cb: () => MaybePromise): Promise { + this.suppressedOpenIfDirty.push(model); + try { + await cb(); + } finally { + const i = this.suppressedOpenIfDirty.indexOf(model); + if (i !== -1) { + this.suppressedOpenIfDirty.splice(i, 1); + } + } + } + createFileSystemWatcher(globPattern: string, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): lang.FileSystemWatcher { const disposables = new DisposableCollection(); const onDidCreateEmitter = new lang.Emitter(); @@ -331,6 +349,19 @@ export class MonacoWorkspace implements lang.Workspace { }; } + applyBackgroundEdit(model: MonacoEditorModel, editOperations: monaco.editor.IIdentifiedSingleEditOperation[]): Promise { + return this.suppressOpenIfDirty(model, async () => { + const editor = MonacoEditor.findByDocument(this.editorManager, model)[0]; + const cursorState = editor && editor.getControl().getSelections() || []; + model.textEditorModel.pushStackElement(); + model.textEditorModel.pushEditOperations(cursorState, editOperations, () => cursorState); + model.textEditorModel.pushStackElement(); + if (!editor) { + await model.save(); + } + }); + } + async applyEdit(changes: lang.WorkspaceEdit, options?: EditorOpenerOptions): Promise { const workspaceEdit = this.p2m.asWorkspaceEdit(changes); await this.applyBulkEdit(workspaceEdit, options); diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index f7de7f6820513..88bcc62843dfc 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -810,7 +810,7 @@ export interface TextEditorsExt { } export interface SingleEditOperation { - range: Range; + range?: Range; text?: string; forceMoveMarkers?: boolean; } diff --git a/packages/plugin-ext/src/main/browser/documents-main.ts b/packages/plugin-ext/src/main/browser/documents-main.ts index cf94a6cbe8aeb..1753f7e4f4921 100644 --- a/packages/plugin-ext/src/main/browser/documents-main.ts +++ b/packages/plugin-ext/src/main/browser/documents-main.ts @@ -108,13 +108,24 @@ export class DocumentsMainImpl implements DocumentsMain, Disposable { onWillSaveModelEvent.waitUntil(new Promise(async (resolve, reject) => { setTimeout(() => reject(new Error(`Aborted onWillSaveTextDocument-event after ${this.saveTimeout}ms`)), this.saveTimeout); const edits = await this.proxy.$acceptModelWillSave(onWillSaveModelEvent.model.textEditorModel.uri, onWillSaveModelEvent.reason, this.saveTimeout); - const transformedEdits = edits.map((edit): monaco.editor.IIdentifiedSingleEditOperation => - ({ - range: monaco.Range.lift(edit.range), - text: edit.text!, + const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = []; + for (const edit of edits) { + const { range, text } = edit; + if (!range && !text) { + continue; + } + if (range && range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn && !edit.text) { + continue; + } + + editOperations.push({ + range: range ? monaco.Range.lift(range) : onWillSaveModelEvent.model.textEditorModel.getFullModelRange(), + /* eslint-disable-next-line no-null/no-null */ + text: text || null, forceMoveMarkers: edit.forceMoveMarkers - })); - resolve(transformedEdits); + }); + } + resolve(editOperations); })); })); this.toDispose.push(modelService.onModelDirtyChanged(m => { diff --git a/packages/plugin-ext/src/main/browser/text-editor-main.ts b/packages/plugin-ext/src/main/browser/text-editor-main.ts index 8e94c8c0a2e0b..83882d0f5c583 100644 --- a/packages/plugin-ext/src/main/browser/text-editor-main.ts +++ b/packages/plugin-ext/src/main/browser/text-editor-main.ts @@ -234,17 +234,28 @@ export class TextEditorMain implements Disposable { this.model.setEOL(monaco.editor.EndOfLineSequence.LF); } - const transformedEdits = edits.map((edit): monaco.editor.IIdentifiedSingleEditOperation => - ({ - range: monaco.Range.lift(edit.range), - text: edit.text!, + const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = []; + for (const edit of edits) { + const { range, text } = edit; + if (!range && !text) { + continue; + } + if (range && range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn && !edit.text) { + continue; + } + + editOperations.push({ + range: range ? monaco.Range.lift(range) : this.editor.getControl().getModel()!.getFullModelRange(), + /* eslint-disable-next-line no-null/no-null */ + text: text || null, forceMoveMarkers: edit.forceMoveMarkers - })); + }); + } if (opts.undoStopBefore) { this.editor.getControl().pushUndoStop(); } - this.editor.getControl().executeEdits('MainThreadTextEditor', transformedEdits); + this.editor.getControl().executeEdits('MainThreadTextEditor', editOperations); if (opts.undoStopAfter) { this.editor.getControl().pushUndoStop(); } diff --git a/packages/preferences/src/browser/abstract-resource-preference-provider.ts b/packages/preferences/src/browser/abstract-resource-preference-provider.ts index 81ef6222cf3ce..2e5881523b217 100644 --- a/packages/preferences/src/browser/abstract-resource-preference-provider.ts +++ b/packages/preferences/src/browser/abstract-resource-preference-provider.ts @@ -15,20 +15,26 @@ ********************************************************************************/ /* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-null/no-null */ import * as jsoncparser from 'jsonc-parser'; import { JSONExt } from '@phosphor/coreutils/lib/json'; import { inject, injectable, postConstruct } from 'inversify'; -import { MessageService, Resource, ResourceProvider, Disposable } from '@theia/core'; +import { ResourceProvider } from '@theia/core/lib/common/resource'; +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 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'; @injectable() export abstract class AbstractResourcePreferenceProvider extends PreferenceProvider { protected preferences: { [key: string]: any } = {}; - protected resource: Promise; + protected reference: Promise>; @inject(PreferenceService) protected readonly preferenceService: PreferenceService; @inject(ResourceProvider) protected readonly resourceProvider: ResourceProvider; @@ -38,23 +44,43 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi @inject(PreferenceConfigurations) protected readonly configurations: PreferenceConfigurations; + @inject(MonacoTextModelService) + protected readonly textModelService: MonacoTextModelService; + + @inject(MonacoWorkspace) + protected readonly workspace: MonacoWorkspace; + @postConstruct() protected async init(): Promise { const uri = this.getUri(); - this.resource = this.resourceProvider(uri); + + // it is blocking till the preference service is initialized, + // so first try to load from the underlying resource + this.reference = this.textModelService.createModelReference(uri); // Try to read the initial content of the preferences. The provider // becomes ready even if we fail reading the preferences, so we don't // hang the preference service. - this.readPreferences() - .then(() => this._ready.resolve()) - .catch(() => this._ready.resolve()); + try { + const resource = await this.resourceProvider(uri); + try { + const content = await resource.readContents(); + this.loadPreferences(content); + } finally { + resource.dispose(); + } + } catch { + /* no-op */ + } finally { + this._ready.resolve(); + } - const resource = await this.resource; - this.toDispose.push(resource); - if (resource.onDidChangeContents) { - this.toDispose.push(resource.onDidChangeContents(() => this.readPreferences())); + const reference = await this.reference; + if (this.toDispose.disposed) { + reference.dispose(); } + this.toDispose.push(reference); + this.toDispose.push(reference.object.onDidChangeContent(() => this.readPreferences())); this.toDispose.push(Disposable.create(() => this.reset())); } @@ -94,53 +120,69 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi if (!path) { return false; } - const resource = await this.resource; - if (!resource.saveContents) { - return false; - } - const content = ((await this.readContents()) || '').trim(); - if (!content && value === undefined) { - return true; - } try { - let newContent = ''; + const reference = await this.reference; + const content = reference.object.getText().trim(); + if (!content && value === undefined) { + return true; + } + const textModel = reference.object.textEditorModel; + const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = []; if (path.length || value !== undefined) { - const formattingOptions = this.getFormattingOptions(resourceUri); - const edits = jsoncparser.modify(content, path, value, { formattingOptions }); - newContent = jsoncparser.applyEdits(content, edits); + const { insertSpaces, tabSize, defaultEOL } = textModel.getOptions(); + for (const edit of jsoncparser.modify(content, path, value, { + formattingOptions: { + insertSpaces, + tabSize, + eol: defaultEOL === monaco.editor.DefaultEndOfLine.LF ? '\n' : '\r\n' + } + })) { + const start = textModel.getPositionAt(edit.offset); + const end = textModel.getPositionAt(edit.offset + edit.length); + editOperations.push({ + range: monaco.Range.fromPositions(start, end), + text: edit.content || null, + forceMoveMarkers: false + }); + } + } else { + editOperations.push({ + range: textModel.getFullModelRange(), + text: null, + forceMoveMarkers: false + }); } - await resource.saveContents(newContent); + await this.workspace.applyBackgroundEdit(reference.object, editOperations); + return true; } catch (e) { - const message = `Failed to update the value of ${key}.`; - this.messageService.error(`${message} Please check if ${resource.uri.toString()} is corrupted.`); - console.error(`${message} ${e.toString()}`); + const message = `Failed to update the value of '${key}' in '${this.getUri()}'.`; + this.messageService.error(`${message} Please check if it is corrupted.`); + console.error(`${message}`, e); return false; } - await this.readPreferences(); - return true; } protected getPath(preferenceName: string): string[] | undefined { return [preferenceName]; } - protected loaded = false; protected async readPreferences(): Promise { - const newContent = await this.readContents(); - this.loaded = newContent !== undefined; - const newPrefs = newContent ? this.getParsedContent(newContent) : {}; - this.handlePreferenceChanges(newPrefs); - } - - protected async readContents(): Promise { try { - const resource = await this.resource; - return await resource.readContents(); - } catch { - return undefined; + const reference = await this.reference; + const newContent = reference.object.getText(); + this.loadPreferences(newContent); + } catch (e) { + console.error(`Failed to load preferences from '${this.getUri()}'.`, e); } } + protected loaded = false; + protected loadPreferences(content: string | undefined): void { + this.loaded = content !== undefined; + const newPrefs = content ? this.getParsedContent(content) : {}; + this.handlePreferenceChanges(newPrefs); + } + protected getParsedContent(content: string): { [key: string]: any } { const jsonData = this.parse(content); @@ -222,30 +264,5 @@ export abstract class AbstractResourcePreferenceProvider extends PreferenceProvi } } - /** - * Get the formatting options to be used when calling `jsoncparser`. - * The formatting options are based on the corresponding preference values. - * - * The formatting options should attempt to obtain the preference values from JSONC, - * and if necessary fallback to JSON and the global values. - * @param uri the preference settings URI. - * - * @returns a tuple representing the tab indentation size, and if it is spaces. - */ - protected getFormattingOptions(uri?: string): jsoncparser.FormattingOptions { - // Get the global formatting options for both `tabSize` and `insertSpaces`. - const globalTabSize = this.preferenceService.get('editor.tabSize', 2, uri); - const globalInsertSpaces = this.preferenceService.get('editor.insertSpaces', true, uri); - - // Get the superset JSON formatting options for both `tabSize` and `insertSpaces`. - const jsonTabSize = this.preferenceService.get('[json].editor.tabSize', globalTabSize, uri); - const jsonInsertSpaces = this.preferenceService.get('[json].editor.insertSpaces', globalInsertSpaces, uri); - - return { - tabSize: this.preferenceService.get('[jsonc].editor.tabSize', jsonTabSize, uri), - insertSpaces: this.preferenceService.get('[jsonc].editor.insertSpaces', jsonInsertSpaces, uri), - eol: '' - }; - } - } +