Path: blob/main/src/vs/workbench/services/configuration/common/jsonEditingService.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as nls from '../../../../nls.js';6import { URI } from '../../../../base/common/uri.js';7import * as json from '../../../../base/common/json.js';8import { setProperty } from '../../../../base/common/jsonEdit.js';9import { Queue } from '../../../../base/common/async.js';10import { Edit } from '../../../../base/common/jsonFormatter.js';11import { IDisposable, IReference } from '../../../../base/common/lifecycle.js';12import { EditOperation } from '../../../../editor/common/core/editOperation.js';13import { Range } from '../../../../editor/common/core/range.js';14import { Selection } from '../../../../editor/common/core/selection.js';15import { ITextFileService } from '../../textfile/common/textfiles.js';16import { IFileService } from '../../../../platform/files/common/files.js';17import { ITextModelService, IResolvedTextEditorModel } from '../../../../editor/common/services/resolverService.js';18import { IJSONEditingService, IJSONValue, JSONEditingError, JSONEditingErrorCode } from './jsonEditing.js';19import { ITextModel } from '../../../../editor/common/model.js';20import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';21import { IFilesConfigurationService } from '../../filesConfiguration/common/filesConfigurationService.js';2223export class JSONEditingService implements IJSONEditingService {2425public _serviceBrand: undefined;2627private queue: Queue<void>;2829constructor(30@IFileService private readonly fileService: IFileService,31@ITextModelService private readonly textModelResolverService: ITextModelService,32@ITextFileService private readonly textFileService: ITextFileService,33@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService34) {35this.queue = new Queue<void>();36}3738write(resource: URI, values: IJSONValue[]): Promise<void> {39return Promise.resolve(this.queue.queue(() => this.doWriteConfiguration(resource, values))); // queue up writes to prevent race conditions40}4142private async doWriteConfiguration(resource: URI, values: IJSONValue[]): Promise<void> {43const reference = await this.resolveAndValidate(resource, true);44try {45await this.writeToBuffer(reference.object.textEditorModel, values);46} finally {47reference.dispose();48}49}5051private async writeToBuffer(model: ITextModel, values: IJSONValue[]): Promise<URI | undefined> {52let disposable: IDisposable | undefined;53try {54// Optimization: we apply edits to a text model and save it55// right after. Use the files config service to signal this56// to the workbench to optimise the UI during this operation.57// For example, avoids to briefly show dirty indicators.58disposable = this.filesConfigurationService.enableAutoSaveAfterShortDelay(model.uri);5960let hasEdits: boolean = false;61for (const value of values) {62const edit = this.getEdits(model, value)[0];63hasEdits = (!!edit && this.applyEditsToBuffer(edit, model)) || hasEdits;64}65if (hasEdits) {66return this.textFileService.save(model.uri);67}68} finally {69disposable?.dispose();70}7172return undefined;73}7475private applyEditsToBuffer(edit: Edit, model: ITextModel): boolean {76const startPosition = model.getPositionAt(edit.offset);77const endPosition = model.getPositionAt(edit.offset + edit.length);78const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);79const currentText = model.getValueInRange(range);80if (edit.content !== currentText) {81const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);82model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);83return true;84}85return false;86}8788private getEdits(model: ITextModel, configurationValue: IJSONValue): Edit[] {89const { tabSize, insertSpaces } = model.getOptions();90const eol = model.getEOL();91const { path, value } = configurationValue;9293// With empty path the entire file is being replaced, so we just use JSON.stringify94if (!path.length) {95const content = JSON.stringify(value, null, insertSpaces ? ' '.repeat(tabSize) : '\t');96return [{97content,98length: content.length,99offset: 0100}];101}102103return setProperty(model.getValue(), path, value, { tabSize, insertSpaces, eol });104}105106private async resolveModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {107const exists = await this.fileService.exists(resource);108if (!exists) {109await this.textFileService.write(resource, '{}', { encoding: 'utf8' });110}111return this.textModelResolverService.createModelReference(resource);112}113114private hasParseErrors(model: ITextModel): boolean {115const parseErrors: json.ParseError[] = [];116json.parse(model.getValue(), parseErrors, { allowTrailingComma: true, allowEmptyContent: true });117return parseErrors.length > 0;118}119120private async resolveAndValidate(resource: URI, checkDirty: boolean): Promise<IReference<IResolvedTextEditorModel>> {121const reference = await this.resolveModelReference(resource);122123const model = reference.object.textEditorModel;124125if (this.hasParseErrors(model)) {126reference.dispose();127return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_INVALID_FILE);128}129130return reference;131}132133private reject<T>(code: JSONEditingErrorCode): Promise<T> {134const message = this.toErrorMessage(code);135return Promise.reject(new JSONEditingError(message, code));136}137138private toErrorMessage(error: JSONEditingErrorCode): string {139switch (error) {140// User issues141case JSONEditingErrorCode.ERROR_INVALID_FILE: {142return nls.localize('errorInvalidFile', "Unable to write into the file. Please open the file to correct errors/warnings in the file and try again.");143}144}145}146}147148registerSingleton(IJSONEditingService, JSONEditingService, InstantiationType.Delayed);149150151