Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/configuration/common/jsonEditingService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as nls from '../../../../nls.js';
7
import { URI } from '../../../../base/common/uri.js';
8
import * as json from '../../../../base/common/json.js';
9
import { setProperty } from '../../../../base/common/jsonEdit.js';
10
import { Queue } from '../../../../base/common/async.js';
11
import { Edit } from '../../../../base/common/jsonFormatter.js';
12
import { IDisposable, IReference } from '../../../../base/common/lifecycle.js';
13
import { EditOperation } from '../../../../editor/common/core/editOperation.js';
14
import { Range } from '../../../../editor/common/core/range.js';
15
import { Selection } from '../../../../editor/common/core/selection.js';
16
import { ITextFileService } from '../../textfile/common/textfiles.js';
17
import { IFileService } from '../../../../platform/files/common/files.js';
18
import { ITextModelService, IResolvedTextEditorModel } from '../../../../editor/common/services/resolverService.js';
19
import { IJSONEditingService, IJSONValue, JSONEditingError, JSONEditingErrorCode } from './jsonEditing.js';
20
import { ITextModel } from '../../../../editor/common/model.js';
21
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
22
import { IFilesConfigurationService } from '../../filesConfiguration/common/filesConfigurationService.js';
23
24
export class JSONEditingService implements IJSONEditingService {
25
26
public _serviceBrand: undefined;
27
28
private queue: Queue<void>;
29
30
constructor(
31
@IFileService private readonly fileService: IFileService,
32
@ITextModelService private readonly textModelResolverService: ITextModelService,
33
@ITextFileService private readonly textFileService: ITextFileService,
34
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService
35
) {
36
this.queue = new Queue<void>();
37
}
38
39
write(resource: URI, values: IJSONValue[]): Promise<void> {
40
return Promise.resolve(this.queue.queue(() => this.doWriteConfiguration(resource, values))); // queue up writes to prevent race conditions
41
}
42
43
private async doWriteConfiguration(resource: URI, values: IJSONValue[]): Promise<void> {
44
const reference = await this.resolveAndValidate(resource, true);
45
try {
46
await this.writeToBuffer(reference.object.textEditorModel, values);
47
} finally {
48
reference.dispose();
49
}
50
}
51
52
private async writeToBuffer(model: ITextModel, values: IJSONValue[]): Promise<any> {
53
let disposable: IDisposable | undefined;
54
try {
55
// Optimization: we apply edits to a text model and save it
56
// right after. Use the files config service to signal this
57
// to the workbench to optimise the UI during this operation.
58
// For example, avoids to briefly show dirty indicators.
59
disposable = this.filesConfigurationService.enableAutoSaveAfterShortDelay(model.uri);
60
61
let hasEdits: boolean = false;
62
for (const value of values) {
63
const edit = this.getEdits(model, value)[0];
64
hasEdits = (!!edit && this.applyEditsToBuffer(edit, model)) || hasEdits;
65
}
66
if (hasEdits) {
67
return this.textFileService.save(model.uri);
68
}
69
} finally {
70
disposable?.dispose();
71
}
72
}
73
74
private applyEditsToBuffer(edit: Edit, model: ITextModel): boolean {
75
const startPosition = model.getPositionAt(edit.offset);
76
const endPosition = model.getPositionAt(edit.offset + edit.length);
77
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
78
const currentText = model.getValueInRange(range);
79
if (edit.content !== currentText) {
80
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
81
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
82
return true;
83
}
84
return false;
85
}
86
87
private getEdits(model: ITextModel, configurationValue: IJSONValue): Edit[] {
88
const { tabSize, insertSpaces } = model.getOptions();
89
const eol = model.getEOL();
90
const { path, value } = configurationValue;
91
92
// With empty path the entire file is being replaced, so we just use JSON.stringify
93
if (!path.length) {
94
const content = JSON.stringify(value, null, insertSpaces ? ' '.repeat(tabSize) : '\t');
95
return [{
96
content,
97
length: content.length,
98
offset: 0
99
}];
100
}
101
102
return setProperty(model.getValue(), path, value, { tabSize, insertSpaces, eol });
103
}
104
105
private async resolveModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
106
const exists = await this.fileService.exists(resource);
107
if (!exists) {
108
await this.textFileService.write(resource, '{}', { encoding: 'utf8' });
109
}
110
return this.textModelResolverService.createModelReference(resource);
111
}
112
113
private hasParseErrors(model: ITextModel): boolean {
114
const parseErrors: json.ParseError[] = [];
115
json.parse(model.getValue(), parseErrors, { allowTrailingComma: true, allowEmptyContent: true });
116
return parseErrors.length > 0;
117
}
118
119
private async resolveAndValidate(resource: URI, checkDirty: boolean): Promise<IReference<IResolvedTextEditorModel>> {
120
const reference = await this.resolveModelReference(resource);
121
122
const model = reference.object.textEditorModel;
123
124
if (this.hasParseErrors(model)) {
125
reference.dispose();
126
return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_INVALID_FILE);
127
}
128
129
return reference;
130
}
131
132
private reject<T>(code: JSONEditingErrorCode): Promise<T> {
133
const message = this.toErrorMessage(code);
134
return Promise.reject(new JSONEditingError(message, code));
135
}
136
137
private toErrorMessage(error: JSONEditingErrorCode): string {
138
switch (error) {
139
// User issues
140
case JSONEditingErrorCode.ERROR_INVALID_FILE: {
141
return nls.localize('errorInvalidFile', "Unable to write into the file. Please open the file to correct errors/warnings in the file and try again.");
142
}
143
}
144
}
145
}
146
147
registerSingleton(IJSONEditingService, JSONEditingService, InstantiationType.Delayed);
148
149