-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathedit.go
More file actions
276 lines (250 loc) · 9.55 KB
/
Copy pathedit.go
File metadata and controls
276 lines (250 loc) · 9.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// This file was ported from https://github.com/Microsoft/vscode/blob/c0bc1ace7ca3ce2d6b1aeb2bde9d1bb0f4b4bae6/src/vs/base/common/jsonEdit.ts,
// which is licensed as follows:
//
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License.
package jsonx
import (
"encoding/json"
"errors"
"fmt"
)
// An Edit represents an edit to a JSON document.
//
// Source: https://github.com/Microsoft/vscode/blob/c0bc1ace7ca3ce2d6b1aeb2bde9d1bb0f4b4bae6/src/vs/base/common/jsonFormatter.ts#L24
type Edit struct {
Offset int // the character offset where the edit begins
Length int // the character length of the region to replace with the content
Content string // the content to insert into the document
}
// ComputePropertyEdit returns the edits necessary to set the value at the specified
// key path to the value. If value is nil, the property's value is set to JSON null;
// use ComputePropertyRemoval to obtain the edits necessary to remove a property. If
// value is a json.RawMessage, it is treated as an opaque value to insert (which
// means it can contain comments, trailing commas, etc.).
//
// If the insertionIndex is non-nil, it is called to determine the index at which to
// insert the value (given the existing properties, in order).
//
// Source: https://github.com/Microsoft/vscode/blob/c0bc1ace7ca3ce2d6b1aeb2bde9d1bb0f4b4bae6/src/vs/base/common/jsonEdit.ts#L14
func ComputePropertyEdit(text string, path Path, value interface{}, insertionIndex func(properties []string) int, options FormatOptions) ([]Edit, []ParseErrorCode, error) {
if value == nil {
value = json.RawMessage("null") // otherwise would remove property
}
return computePropertyEdit(text, path, value, insertionIndex, options)
}
// ComputePropertyRemoval returns the edits necessary to remove the property at the
// specified key path.
//
// Source: https://github.com/Microsoft/vscode/blob/c0bc1ace7ca3ce2d6b1aeb2bde9d1bb0f4b4bae6/src/vs/base/common/jsonEdit.ts#L10
func ComputePropertyRemoval(text string, path Path, options FormatOptions) ([]Edit, []ParseErrorCode, error) {
return computePropertyEdit(text, path, nil, nil, options)
}
func computePropertyEdit(text string, path Path, valueObj interface{}, insertionIndex func(properties []string) int, options FormatOptions) ([]Edit, []ParseErrorCode, error) {
// Tolerate errors in value if it's json.RawMessage.
var value string
if v, ok := valueObj.(json.RawMessage); ok {
value = string(v)
} else {
data, err := json.Marshal(valueObj)
if err != nil {
return nil, nil, err
}
value = string(data)
}
root, parseErrorCodes := ParseTree(text, ParseOptions{Comments: true, TrailingCommas: true})
var parent *Node
var lastSegment Segment
for len(path) > 0 {
lastSegment = path[len(path)-1]
path = path[:len(path)-1]
parent = FindNodeAtLocation(root, path)
if parent == nil && valueObj != nil {
if lastSegment.IsProperty {
key, err := json.Marshal(lastSegment.Property)
if err != nil {
return nil, nil, err
}
value = "{" + string(key) + ":" + value + "}"
} else {
value = "[" + value + "]"
}
} else {
break
}
}
if parent == nil {
// empty document
if valueObj == nil { // delete
return nil, nil, errors.New("can't delete in empty document")
}
edit := Edit{Content: value}
if root != nil {
edit.Offset = root.Offset
edit.Length = root.Length
}
edits, err := FormatEdit(text, edit, options)
return edits, parseErrorCodes, err
} else if parent.Type == Object && lastSegment.IsProperty {
indexOf := func(slice []*Node, candidateElement *Node) int {
for i, e := range slice {
if e == candidateElement {
return i
}
}
return -1
}
existing := FindNodeAtLocation(parent, Path{lastSegment})
if existing != nil {
if valueObj == nil { // delete
propertyIndex := indexOf(parent.Children, existing.Parent)
var removeBegin int
removeEnd := existing.Parent.Offset + existing.Parent.Length
if propertyIndex > 0 {
// remove the comma of the previous node
previous := parent.Children[propertyIndex-1]
removeBegin = previous.Offset + previous.Length
} else {
removeBegin = parent.Offset + 1
if len(parent.Children) > 1 {
// remove the comma of the next node
next := parent.Children[1]
removeEnd = next.Offset
} else {
// remove trailing comma after this node, if any
removeEnd = parent.Offset + parent.Length - 1
}
}
edits, err := FormatEdit(text, Edit{Offset: removeBegin, Length: removeEnd - removeBegin, Content: ""}, options)
return edits, parseErrorCodes, err
}
// set value of existing property
edits, err := FormatEdit(text, Edit{Offset: existing.Offset, Length: existing.Length, Content: value}, options)
return edits, parseErrorCodes, err
}
if valueObj == nil { // delete
return nil, parseErrorCodes, nil // property does not exist, nothing to do
}
propNameData, err := json.Marshal(lastSegment.Property)
if err != nil {
return nil, nil, err
}
newProperty := string(propNameData) + ": " + value
var index int
if insertionIndex != nil {
index = insertionIndex(ObjectPropertyNames(*parent))
} else {
index = len(parent.Children)
}
var edit Edit
if index > 0 {
previous := parent.Children[index-1]
edit = Edit{Offset: previous.Offset + previous.Length, Length: 0, Content: "," + newProperty}
} else if len(parent.Children) == 0 {
edit = Edit{Offset: parent.Offset + 1, Length: 0, Content: newProperty}
} else {
edit = Edit{Offset: parent.Offset + 1, Length: 0, Content: newProperty + ","}
}
edits, err := FormatEdit(text, edit, options)
return edits, parseErrorCodes, err
} else if parent.Type == Array && !lastSegment.IsProperty {
insertIndex := lastSegment
if insertIndex.Index == -1 {
// Insert
var edit Edit
if len(parent.Children) == 0 {
edit = Edit{Offset: parent.Offset + 1, Length: 0, Content: value}
} else {
previous := parent.Children[len(parent.Children)-1]
edit = Edit{Offset: previous.Offset + previous.Length, Length: 0, Content: "," + value}
}
edits, err := FormatEdit(text, edit, options)
return edits, parseErrorCodes, err
}
if valueObj == nil && len(parent.Children) >= 0 {
// Removal
removalIndex := lastSegment.Index
toRemove := parent.Children[removalIndex]
var edit Edit
if len(parent.Children) == 1 {
// only item
edit = Edit{Offset: parent.Offset + 1, Length: parent.Length - 2, Content: ""}
} else if len(parent.Children)-1 == removalIndex {
// last item
previous := parent.Children[removalIndex-1]
offset := previous.Offset + previous.Length
parentEndOffset := parent.Offset + parent.Length
edit = Edit{Offset: offset, Length: parentEndOffset - 2 - offset, Content: ""}
} else {
edit = Edit{Offset: toRemove.Offset, Length: parent.Children[removalIndex+1].Offset - toRemove.Offset, Content: ""}
}
edits, err := FormatEdit(text, edit, options)
return edits, parseErrorCodes, err
}
// Modify
editIndex := lastSegment.Index
toEdit := parent.Children[editIndex]
edit := Edit{Offset: toEdit.Offset, Length: toEdit.Length, Content: value}
edits, err := FormatEdit(text, edit, options)
return edits, parseErrorCodes, err
}
var noun string
if lastSegment.IsProperty {
noun = "property"
} else {
noun = "index"
}
return nil, nil, fmt.Errorf("can't add %s to parent of type %s", noun, parent.Type)
}
// ApplyEdits applies the edits to the JSON document and returns the edited
// document. The edits must be ordered and within the bounds of the document.
//
// Source: https://github.com/Microsoft/vscode/blob/c0bc1ace7ca3ce2d6b1aeb2bde9d1bb0f4b4bae6/src/vs/base/common/jsonFormatter.ts#L34
func ApplyEdits(text string, edits ...Edit) (string, error) {
chars := []rune(text)
lastEditOffset := len(chars)
for i := len(edits) - 1; i >= 0; i-- {
edit := edits[i]
if edit.Offset < 0 || edit.Length < 0 && edit.Offset+edit.Length > len(chars) {
return "", fmt.Errorf("edit out of bounds: offset %d, length %d, doc length %d", edit.Offset, edit.Length, len(chars))
}
if lastEditOffset < edit.Offset+edit.Length {
return "", fmt.Errorf("edit out of order: edit end offset %d exceeds next edit offset %d", edit.Offset+edit.Length, lastEditOffset)
}
lastEditOffset = edit.Offset
chars = []rune(string(chars[:edit.Offset]) + edit.Content + string(chars[edit.Offset+edit.Length:]))
}
return string(chars), nil
}
// FormatEdit returns the edits necessary to perform the original edit for maintaining the
// formatting of the JSON document.
//
// Source: https://github.com/Microsoft/vscode/blob/c0bc1ace7ca3ce2d6b1aeb2bde9d1bb0f4b4bae6/src/vs/base/common/jsonEdit.ts#L122
func FormatEdit(text string, edit Edit, options FormatOptions) ([]Edit, error) {
// apply the edit
newText, err := ApplyEdits(text, edit)
if err != nil {
return nil, err
}
// format the new text
begin := edit.Offset
end := edit.Offset + len([]rune(edit.Content))
edits := FormatRange(newText, begin, end-begin, options)
// apply the formatting edits and track the begin and end offsets of the changes
for i := len(edits) - 1; i >= 0; i-- {
edit := edits[i]
newText, err = ApplyEdits(newText, edit)
if err != nil {
return nil, err
}
if edit.Offset < begin {
begin = edit.Offset
}
if edit.Offset+edit.Length > end {
end = edit.Offset + edit.Length
}
end += len([]rune(edit.Content)) - edit.Length
}
// create a single edit with all changes
editLength := len([]rune(text)) - (len([]rune(newText)) - end) - begin
return []Edit{{Offset: begin, Length: editLength, Content: string(([]rune(newText))[begin:end])}}, nil
}