Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/common/jsonEdit.ts
3291 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 { findNodeAtLocation, JSONPath, Node, ParseError, parseTree, Segment } from './json.js';
7
import { Edit, format, FormattingOptions, isEOL } from './jsonFormatter.js';
8
9
10
export function removeProperty(text: string, path: JSONPath, formattingOptions: FormattingOptions): Edit[] {
11
return setProperty(text, path, undefined, formattingOptions);
12
}
13
14
export function setProperty(text: string, originalPath: JSONPath, value: unknown, formattingOptions: FormattingOptions, getInsertionIndex?: (properties: string[]) => number): Edit[] {
15
const path = originalPath.slice();
16
const errors: ParseError[] = [];
17
const root = parseTree(text, errors);
18
let parent: Node | undefined = undefined;
19
20
let lastSegment: Segment | undefined = undefined;
21
while (path.length > 0) {
22
lastSegment = path.pop();
23
parent = findNodeAtLocation(root, path);
24
if (parent === undefined && value !== undefined) {
25
if (typeof lastSegment === 'string') {
26
value = { [lastSegment]: value };
27
} else {
28
value = [value];
29
}
30
} else {
31
break;
32
}
33
}
34
35
if (!parent) {
36
// empty document
37
if (value === undefined) { // delete
38
return []; // property does not exist, nothing to do
39
}
40
return withFormatting(text, { offset: root ? root.offset : 0, length: root ? root.length : 0, content: JSON.stringify(value) }, formattingOptions);
41
} else if (parent.type === 'object' && typeof lastSegment === 'string' && Array.isArray(parent.children)) {
42
const existing = findNodeAtLocation(parent, [lastSegment]);
43
if (existing !== undefined) {
44
if (value === undefined) { // delete
45
if (!existing.parent) {
46
throw new Error('Malformed AST');
47
}
48
const propertyIndex = parent.children.indexOf(existing.parent);
49
let removeBegin: number;
50
let removeEnd = existing.parent.offset + existing.parent.length;
51
if (propertyIndex > 0) {
52
// remove the comma of the previous node
53
const previous = parent.children[propertyIndex - 1];
54
removeBegin = previous.offset + previous.length;
55
} else {
56
removeBegin = parent.offset + 1;
57
if (parent.children.length > 1) {
58
// remove the comma of the next node
59
const next = parent.children[1];
60
removeEnd = next.offset;
61
}
62
}
63
return withFormatting(text, { offset: removeBegin, length: removeEnd - removeBegin, content: '' }, formattingOptions);
64
} else {
65
// set value of existing property
66
return withFormatting(text, { offset: existing.offset, length: existing.length, content: JSON.stringify(value) }, formattingOptions);
67
}
68
} else {
69
if (value === undefined) { // delete
70
return []; // property does not exist, nothing to do
71
}
72
const newProperty = `${JSON.stringify(lastSegment)}: ${JSON.stringify(value)}`;
73
const index = getInsertionIndex ? getInsertionIndex(parent.children.map(p => p.children![0].value)) : parent.children.length;
74
let edit: Edit;
75
if (index > 0) {
76
const previous = parent.children[index - 1];
77
edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty };
78
} else if (parent.children.length === 0) {
79
edit = { offset: parent.offset + 1, length: 0, content: newProperty };
80
} else {
81
edit = { offset: parent.offset + 1, length: 0, content: newProperty + ',' };
82
}
83
return withFormatting(text, edit, formattingOptions);
84
}
85
} else if (parent.type === 'array' && typeof lastSegment === 'number' && Array.isArray(parent.children)) {
86
if (value !== undefined) {
87
// Insert
88
const newProperty = `${JSON.stringify(value)}`;
89
let edit: Edit;
90
if (parent.children.length === 0 || lastSegment === 0) {
91
edit = { offset: parent.offset + 1, length: 0, content: parent.children.length === 0 ? newProperty : newProperty + ',' };
92
} else {
93
const index = lastSegment === -1 || lastSegment > parent.children.length ? parent.children.length : lastSegment;
94
const previous = parent.children[index - 1];
95
edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty };
96
}
97
return withFormatting(text, edit, formattingOptions);
98
} else {
99
//Removal
100
const removalIndex = lastSegment;
101
const toRemove = parent.children[removalIndex];
102
let edit: Edit;
103
if (parent.children.length === 1) {
104
// only item
105
edit = { offset: parent.offset + 1, length: parent.length - 2, content: '' };
106
} else if (parent.children.length - 1 === removalIndex) {
107
// last item
108
const previous = parent.children[removalIndex - 1];
109
const offset = previous.offset + previous.length;
110
const parentEndOffset = parent.offset + parent.length;
111
edit = { offset, length: parentEndOffset - 2 - offset, content: '' };
112
} else {
113
edit = { offset: toRemove.offset, length: parent.children[removalIndex + 1].offset - toRemove.offset, content: '' };
114
}
115
return withFormatting(text, edit, formattingOptions);
116
}
117
} else {
118
throw new Error(`Can not add ${typeof lastSegment !== 'number' ? 'index' : 'property'} to parent of type ${parent.type}`);
119
}
120
}
121
122
export function withFormatting(text: string, edit: Edit, formattingOptions: FormattingOptions): Edit[] {
123
// apply the edit
124
let newText = applyEdit(text, edit);
125
126
// format the new text
127
let begin = edit.offset;
128
let end = edit.offset + edit.content.length;
129
if (edit.length === 0 || edit.content.length === 0) { // insert or remove
130
while (begin > 0 && !isEOL(newText, begin - 1)) {
131
begin--;
132
}
133
while (end < newText.length && !isEOL(newText, end)) {
134
end++;
135
}
136
}
137
138
const edits = format(newText, { offset: begin, length: end - begin }, formattingOptions);
139
140
// apply the formatting edits and track the begin and end offsets of the changes
141
for (let i = edits.length - 1; i >= 0; i--) {
142
const curr = edits[i];
143
newText = applyEdit(newText, curr);
144
begin = Math.min(begin, curr.offset);
145
end = Math.max(end, curr.offset + curr.length);
146
end += curr.content.length - curr.length;
147
}
148
// create a single edit with all changes
149
const editLength = text.length - (newText.length - end) - begin;
150
return [{ offset: begin, length: editLength, content: newText.substring(begin, end) }];
151
}
152
153
export function applyEdit(text: string, edit: Edit): string {
154
return text.substring(0, edit.offset) + edit.content + text.substring(edit.offset + edit.length);
155
}
156
157
export function applyEdits(text: string, edits: Edit[]): string {
158
const sortedEdits = edits.slice(0).sort((a, b) => {
159
const diff = a.offset - b.offset;
160
if (diff === 0) {
161
return a.length - b.length;
162
}
163
return diff;
164
});
165
let lastModifiedOffset = text.length;
166
for (let i = sortedEdits.length - 1; i >= 0; i--) {
167
const e = sortedEdits[i];
168
if (e.offset + e.length <= lastModifiedOffset) {
169
text = applyEdit(text, e);
170
} else {
171
throw new Error('Overlapping edit');
172
}
173
lastModifiedOffset = e.offset;
174
}
175
return text;
176
}
177
178