Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/model/tokens/annotations.ts
5251 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 { binarySearch2 } from '../../../../base/common/arrays.js';
7
import { StringEdit } from '../../core/edits/stringEdit.js';
8
import { OffsetRange } from '../../core/ranges/offsetRange.js';
9
10
export interface IAnnotation<T> {
11
range: OffsetRange;
12
annotation: T;
13
}
14
15
export interface IAnnotatedString<T> {
16
/**
17
* Set annotations for a specific line.
18
* Annotations should be sorted and non-overlapping.
19
*/
20
setAnnotations(annotations: AnnotationsUpdate<T>): void;
21
/**
22
* Return annotations intersecting with the given offset range.
23
*/
24
getAnnotationsIntersecting(range: OffsetRange): IAnnotation<T>[];
25
/**
26
* Get all the annotations. Method is used for testing.
27
*/
28
getAllAnnotations(): IAnnotation<T>[];
29
/**
30
* Apply a string edit to the annotated string.
31
* @returns The annotations that were deleted (became empty) as a result of the edit.
32
*/
33
applyEdit(edit: StringEdit): IAnnotation<T>[];
34
/**
35
* Clone the annotated string.
36
*/
37
clone(): IAnnotatedString<T>;
38
}
39
40
export class AnnotatedString<T> implements IAnnotatedString<T> {
41
42
/**
43
* Annotations are non intersecting and contiguous in the array.
44
*/
45
private _annotations: IAnnotation<T>[] = [];
46
47
constructor(annotations: IAnnotation<T>[] = []) {
48
this._annotations = annotations;
49
}
50
51
/**
52
* Set annotations for a specific range.
53
* Annotations should be sorted and non-overlapping.
54
* If the annotation value is undefined, the annotation is removed.
55
*/
56
public setAnnotations(annotations: AnnotationsUpdate<T>): void {
57
for (const annotation of annotations.annotations) {
58
const startIndex = this._getStartIndexOfIntersectingAnnotation(annotation.range.start);
59
const endIndexExclusive = this._getEndIndexOfIntersectingAnnotation(annotation.range.endExclusive);
60
if (annotation.annotation !== undefined) {
61
this._annotations.splice(startIndex, endIndexExclusive - startIndex, { range: annotation.range, annotation: annotation.annotation });
62
} else {
63
this._annotations.splice(startIndex, endIndexExclusive - startIndex);
64
}
65
}
66
}
67
68
/**
69
* Returns all annotations that intersect with the given offset range.
70
*/
71
public getAnnotationsIntersecting(range: OffsetRange): IAnnotation<T>[] {
72
const startIndex = this._getStartIndexOfIntersectingAnnotation(range.start);
73
const endIndexExclusive = this._getEndIndexOfIntersectingAnnotation(range.endExclusive);
74
return this._annotations.slice(startIndex, endIndexExclusive);
75
}
76
77
private _getStartIndexOfIntersectingAnnotation(offset: number): number {
78
// Find index to the left of the offset
79
const startIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => {
80
return this._annotations[index].range.start - offset;
81
});
82
let startIndex: number;
83
if (startIndexWhereToReplace >= 0) {
84
startIndex = startIndexWhereToReplace;
85
// Also include the next annotation if it ends exactly at offset (touching boundary)
86
const nextCandidate = this._annotations[startIndex]?.range;
87
if (nextCandidate && nextCandidate.endExclusive === offset) {
88
startIndex--;
89
}
90
} else {
91
const candidate = this._annotations[- (startIndexWhereToReplace + 2)]?.range;
92
if (candidate && offset >= candidate.start && offset < candidate.endExclusive) {
93
startIndex = - (startIndexWhereToReplace + 2);
94
} else {
95
startIndex = - (startIndexWhereToReplace + 1);
96
}
97
}
98
return startIndex;
99
}
100
101
private _getEndIndexOfIntersectingAnnotation(offset: number): number {
102
// Find index to the right of the offset
103
const endIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => {
104
return this._annotations[index].range.endExclusive - offset;
105
});
106
let endIndexExclusive: number;
107
if (endIndexWhereToReplace >= 0) {
108
endIndexExclusive = endIndexWhereToReplace + 1;
109
// Also include the next annotation if it starts exactly at offset (touching boundary)
110
const nextCandidate = this._annotations[endIndexExclusive]?.range;
111
if (nextCandidate && nextCandidate.start === offset) {
112
endIndexExclusive++;
113
}
114
} else {
115
const candidate = this._annotations[-(endIndexWhereToReplace + 1)]?.range;
116
if (candidate && offset >= candidate.start && offset <= candidate.endExclusive) {
117
endIndexExclusive = - endIndexWhereToReplace;
118
} else {
119
endIndexExclusive = - (endIndexWhereToReplace + 1);
120
}
121
}
122
return endIndexExclusive;
123
}
124
125
/**
126
* Returns a copy of all annotations.
127
*/
128
public getAllAnnotations(): IAnnotation<T>[] {
129
return this._annotations.slice();
130
}
131
132
/**
133
* Applies a string edit to the annotated string, updating annotation ranges accordingly.
134
* @param edit The string edit to apply.
135
* @returns The annotations that were deleted (became empty) as a result of the edit.
136
*/
137
public applyEdit(edit: StringEdit): IAnnotation<T>[] {
138
const annotations = this._annotations.slice();
139
140
// treat edits as deletion of the replace range and then as insertion that extends the first range
141
const finalAnnotations: IAnnotation<T>[] = [];
142
const deletedAnnotations: IAnnotation<T>[] = [];
143
144
let offset = 0;
145
146
for (const e of edit.replacements) {
147
while (true) {
148
// ranges before the current edit
149
const annotation = annotations[0];
150
if (!annotation) {
151
break;
152
}
153
const range = annotation.range;
154
if (range.endExclusive >= e.replaceRange.start) {
155
break;
156
}
157
annotations.shift();
158
const newAnnotation = { range: range.delta(offset), annotation: annotation.annotation };
159
if (!newAnnotation.range.isEmpty) {
160
finalAnnotations.push(newAnnotation);
161
} else {
162
deletedAnnotations.push(newAnnotation);
163
}
164
}
165
166
const intersecting: IAnnotation<T>[] = [];
167
while (true) {
168
const annotation = annotations[0];
169
if (!annotation) {
170
break;
171
}
172
const range = annotation.range;
173
if (!range.intersectsOrTouches(e.replaceRange)) {
174
break;
175
}
176
annotations.shift();
177
intersecting.push(annotation);
178
}
179
180
for (let i = intersecting.length - 1; i >= 0; i--) {
181
const annotation = intersecting[i];
182
let r = annotation.range;
183
184
// Inserted text will extend the first intersecting annotation, if the edit truly overlaps it
185
const shouldExtend = i === 0 && (e.replaceRange.endExclusive > r.start) && (e.replaceRange.start < r.endExclusive);
186
// Annotation shrinks by the overlap then grows with the new text length
187
const overlap = r.intersect(e.replaceRange)!.length;
188
r = r.deltaEnd(-overlap + (shouldExtend ? e.newText.length : 0));
189
190
// If the annotation starts after the edit start, shift left to the edit start position
191
const rangeAheadOfReplaceRange = r.start - e.replaceRange.start;
192
if (rangeAheadOfReplaceRange > 0) {
193
r = r.delta(-rangeAheadOfReplaceRange);
194
}
195
196
// If annotation shouldn't be extended AND it is after or on edit start, move it after the newly inserted text
197
if (!shouldExtend && rangeAheadOfReplaceRange >= 0) {
198
r = r.delta(e.newText.length);
199
}
200
201
// We already took our offset into account.
202
// Because we add r back to the queue (which then adds offset again),
203
// we have to remove it here so as to not double count it.
204
r = r.delta(-(e.newText.length - e.replaceRange.length));
205
206
annotations.unshift({ annotation: annotation.annotation, range: r });
207
}
208
209
offset += e.newText.length - e.replaceRange.length;
210
}
211
212
while (true) {
213
const annotation = annotations[0];
214
if (!annotation) {
215
break;
216
}
217
annotations.shift();
218
const newAnnotation = { annotation: annotation.annotation, range: annotation.range.delta(offset) };
219
if (!newAnnotation.range.isEmpty) {
220
finalAnnotations.push(newAnnotation);
221
} else {
222
deletedAnnotations.push(newAnnotation);
223
}
224
}
225
this._annotations = finalAnnotations;
226
return deletedAnnotations;
227
}
228
229
/**
230
* Creates a shallow clone of this annotated string.
231
*/
232
public clone(): IAnnotatedString<T> {
233
return new AnnotatedString<T>(this._annotations.slice());
234
}
235
}
236
237
export interface IAnnotationUpdate<T> {
238
range: OffsetRange;
239
annotation: T | undefined;
240
}
241
242
type DefinedValue = object | string | number | boolean;
243
244
export type ISerializedAnnotation<TSerializedProperty extends DefinedValue> = {
245
range: { start: number; endExclusive: number };
246
annotation: TSerializedProperty | undefined;
247
};
248
249
export class AnnotationsUpdate<T> {
250
251
public static create<T>(annotations: IAnnotationUpdate<T>[]): AnnotationsUpdate<T> {
252
return new AnnotationsUpdate(annotations);
253
}
254
255
private _annotations: IAnnotationUpdate<T>[];
256
257
private constructor(annotations: IAnnotationUpdate<T>[]) {
258
this._annotations = annotations;
259
}
260
261
get annotations(): IAnnotationUpdate<T>[] {
262
return this._annotations;
263
}
264
265
public rebase(edit: StringEdit): void {
266
const annotatedString = new AnnotatedString<T | undefined>(this._annotations);
267
annotatedString.applyEdit(edit);
268
this._annotations = annotatedString.getAllAnnotations();
269
}
270
271
public serialize<TSerializedProperty extends DefinedValue>(serializingFunc: (annotation: T) => TSerializedProperty): ISerializedAnnotation<TSerializedProperty>[] {
272
return this._annotations.map(annotation => {
273
const range = { start: annotation.range.start, endExclusive: annotation.range.endExclusive };
274
if (!annotation.annotation) {
275
return { range, annotation: undefined };
276
}
277
return { range, annotation: serializingFunc(annotation.annotation) };
278
});
279
}
280
281
static deserialize<T, TSerializedProperty extends DefinedValue>(serializedAnnotations: ISerializedAnnotation<TSerializedProperty>[], deserializingFunc: (annotation: TSerializedProperty) => T): AnnotationsUpdate<T> {
282
const annotations: IAnnotationUpdate<T>[] = serializedAnnotations.map(serializedAnnotation => {
283
const range = new OffsetRange(serializedAnnotation.range.start, serializedAnnotation.range.endExclusive);
284
if (!serializedAnnotation.annotation) {
285
return { range, annotation: undefined };
286
}
287
return { range, annotation: deserializingFunc(serializedAnnotation.annotation) };
288
});
289
return new AnnotationsUpdate(annotations);
290
}
291
}
292
293