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
4797 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
} else {
86
const candidate = this._annotations[- (startIndexWhereToReplace + 2)]?.range;
87
if (candidate && offset >= candidate.start && offset <= candidate.endExclusive) {
88
startIndex = - (startIndexWhereToReplace + 2);
89
} else {
90
startIndex = - (startIndexWhereToReplace + 1);
91
}
92
}
93
return startIndex;
94
}
95
96
private _getEndIndexOfIntersectingAnnotation(offset: number): number {
97
// Find index to the right of the offset
98
const endIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => {
99
return this._annotations[index].range.endExclusive - offset;
100
});
101
let endIndexExclusive: number;
102
if (endIndexWhereToReplace >= 0) {
103
endIndexExclusive = endIndexWhereToReplace + 1;
104
} else {
105
const candidate = this._annotations[-(endIndexWhereToReplace + 1)]?.range;
106
if (candidate && offset >= candidate.start && offset <= candidate.endExclusive) {
107
endIndexExclusive = - endIndexWhereToReplace;
108
} else {
109
endIndexExclusive = - (endIndexWhereToReplace + 1);
110
}
111
}
112
return endIndexExclusive;
113
}
114
115
/**
116
* Returns a copy of all annotations.
117
*/
118
public getAllAnnotations(): IAnnotation<T>[] {
119
return this._annotations.slice();
120
}
121
122
/**
123
* Applies a string edit to the annotated string, updating annotation ranges accordingly.
124
* @param edit The string edit to apply.
125
* @returns The annotations that were deleted (became empty) as a result of the edit.
126
*/
127
public applyEdit(edit: StringEdit): IAnnotation<T>[] {
128
const annotations = this._annotations.slice();
129
130
// treat edits as deletion of the replace range and then as insertion that extends the first range
131
const finalAnnotations: IAnnotation<T>[] = [];
132
const deletedAnnotations: IAnnotation<T>[] = [];
133
134
let offset = 0;
135
136
for (const e of edit.replacements) {
137
while (true) {
138
// ranges before the current edit
139
const annotation = annotations[0];
140
if (!annotation) {
141
break;
142
}
143
const range = annotation.range;
144
if (range.endExclusive >= e.replaceRange.start) {
145
break;
146
}
147
annotations.shift();
148
const newAnnotation = { range: range.delta(offset), annotation: annotation.annotation };
149
if (!newAnnotation.range.isEmpty) {
150
finalAnnotations.push(newAnnotation);
151
} else {
152
deletedAnnotations.push(newAnnotation);
153
}
154
}
155
156
const intersecting: IAnnotation<T>[] = [];
157
while (true) {
158
const annotation = annotations[0];
159
if (!annotation) {
160
break;
161
}
162
const range = annotation.range;
163
if (!range.intersectsOrTouches(e.replaceRange)) {
164
break;
165
}
166
annotations.shift();
167
intersecting.push(annotation);
168
}
169
170
for (let i = intersecting.length - 1; i >= 0; i--) {
171
const annotation = intersecting[i];
172
let r = annotation.range;
173
174
// Inserted text will extend the first intersecting annotation, if the edit truly overlaps it
175
const shouldExtend = i === 0 && (e.replaceRange.endExclusive > r.start) && (e.replaceRange.start < r.endExclusive);
176
// Annotation shrinks by the overlap then grows with the new text length
177
const overlap = r.intersect(e.replaceRange)!.length;
178
r = r.deltaEnd(-overlap + (shouldExtend ? e.newText.length : 0));
179
180
// If the annotation starts after the edit start, shift left to the edit start position
181
const rangeAheadOfReplaceRange = r.start - e.replaceRange.start;
182
if (rangeAheadOfReplaceRange > 0) {
183
r = r.delta(-rangeAheadOfReplaceRange);
184
}
185
186
// If annotation shouldn't be extended AND it is after or on edit start, move it after the newly inserted text
187
if (!shouldExtend && rangeAheadOfReplaceRange >= 0) {
188
r = r.delta(e.newText.length);
189
}
190
191
// We already took our offset into account.
192
// Because we add r back to the queue (which then adds offset again),
193
// we have to remove it here so as to not double count it.
194
r = r.delta(-(e.newText.length - e.replaceRange.length));
195
196
annotations.unshift({ annotation: annotation.annotation, range: r });
197
}
198
199
offset += e.newText.length - e.replaceRange.length;
200
}
201
202
while (true) {
203
const annotation = annotations[0];
204
if (!annotation) {
205
break;
206
}
207
annotations.shift();
208
const newAnnotation = { annotation: annotation.annotation, range: annotation.range.delta(offset) };
209
if (!newAnnotation.range.isEmpty) {
210
finalAnnotations.push(newAnnotation);
211
} else {
212
deletedAnnotations.push(newAnnotation);
213
}
214
}
215
this._annotations = finalAnnotations;
216
return deletedAnnotations;
217
}
218
219
/**
220
* Creates a shallow clone of this annotated string.
221
*/
222
public clone(): IAnnotatedString<T> {
223
return new AnnotatedString<T>(this._annotations.slice());
224
}
225
}
226
227
export interface IAnnotationUpdate<T> {
228
range: OffsetRange;
229
annotation: T | undefined;
230
}
231
232
type DefinedValue = object | string | number | boolean;
233
234
export type ISerializedAnnotation<TSerializedProperty extends DefinedValue> = {
235
range: { start: number; endExclusive: number };
236
annotation: TSerializedProperty | undefined;
237
};
238
239
export class AnnotationsUpdate<T> {
240
241
public static create<T>(annotations: IAnnotationUpdate<T>[]): AnnotationsUpdate<T> {
242
return new AnnotationsUpdate(annotations);
243
}
244
245
private _annotations: IAnnotationUpdate<T>[];
246
247
private constructor(annotations: IAnnotationUpdate<T>[]) {
248
this._annotations = annotations;
249
}
250
251
get annotations(): IAnnotationUpdate<T>[] {
252
return this._annotations;
253
}
254
255
public rebase(edit: StringEdit): void {
256
const annotatedString = new AnnotatedString<T | undefined>(this._annotations);
257
annotatedString.applyEdit(edit);
258
this._annotations = annotatedString.getAllAnnotations();
259
}
260
261
public serialize<TSerializedProperty extends DefinedValue>(serializingFunc: (annotation: T) => TSerializedProperty): ISerializedAnnotation<TSerializedProperty>[] {
262
return this._annotations.map(annotation => {
263
const range = { start: annotation.range.start, endExclusive: annotation.range.endExclusive };
264
if (!annotation.annotation) {
265
return { range, annotation: undefined };
266
}
267
return { range, annotation: serializingFunc(annotation.annotation) };
268
});
269
}
270
271
static deserialize<T, TSerializedProperty extends DefinedValue>(serializedAnnotations: ISerializedAnnotation<TSerializedProperty>[], deserializingFunc: (annotation: TSerializedProperty) => T): AnnotationsUpdate<T> {
272
const annotations: IAnnotationUpdate<T>[] = serializedAnnotations.map(serializedAnnotation => {
273
const range = new OffsetRange(serializedAnnotation.range.start, serializedAnnotation.range.endExclusive);
274
if (!serializedAnnotation.annotation) {
275
return { range, annotation: undefined };
276
}
277
return { range, annotation: deserializingFunc(serializedAnnotation.annotation) };
278
});
279
return new AnnotationsUpdate(annotations);
280
}
281
}
282
283