src/vs/editor/common/model/tokens/annotations.ts

291 LOC · 272 covered · 19 uncovered · 73 ranges · 1756 concepts · 31 introducers · 884 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- annotations.ts ×15
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; annotations.ts ×1
49 > }
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) { annotations.ts ×6
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 }); annotations.ts ×1
62 > } else { annotations.ts ×6
63 > this._annotations.splice(startIndex, endIndexExclusive - startIndex); annotations.ts ×1
64 > }
66 > }
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); annotations.ts ×1
73 > const endIndexExclusive = this._getEndIndexOfIntersectingAnnotation(range.endExclusive);
74 > return this._annotations.slice(startIndex, endIndexExclusive);
75 > }
77 > private _getStartIndexOfIntersectingAnnotation(offset: number): number {
78 > // Find index to the left of the offset annotations.ts ×8
79 > const startIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => {
80 > return this._annotations[index].range.start - offset; annotations.ts ×2
82 > let startIndex: number;
83 > if (startIndexWhereToReplace >= 0) {
84 > startIndex = startIndexWhereToReplace; annotations.ts ×1
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 { annotations.ts ×8
91 > const candidate = this._annotations[- (startIndexWhereToReplace + 2)]?.range; annotations.ts ×3
92 > if (candidate && offset >= candidate.start && offset < candidate.endExclusive) {
93 > startIndex = - (startIndexWhereToReplace + 2); annotations.ts ×1
94 > } else { annotations.ts ×3
95 > startIndex = - (startIndexWhereToReplace + 1); annotations.ts ×1
96 > }
98 > return startIndex; annotations.ts ×8
99 > }
101 > private _getEndIndexOfIntersectingAnnotation(offset: number): number {
102 > // Find index to the right of the offset annotations.ts ×8
103 > const endIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => {
104 > return this._annotations[index].range.endExclusive - offset; annotations.ts ×2
105 > }); annotations.ts ×8
106 > let endIndexExclusive: number;
107 > if (endIndexWhereToReplace >= 0) {
108 > endIndexExclusive = endIndexWhereToReplace + 1; annotations.ts ×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++; annotations.ts ×1
113 > }
114 > } else { annotations.ts ×8
115 > const candidate = this._annotations[-(endIndexWhereToReplace + 1)]?.range; annotations.ts ×3
116 > if (candidate && offset >= candidate.start && offset <= candidate.endExclusive) {
117 > endIndexExclusive = - endIndexWhereToReplace; annotations.ts ×1
118 > } else { annotations.ts ×3
119 > endIndexExclusive = - (endIndexWhereToReplace + 1); annotations.ts ×1
120 > }
122 > return endIndexExclusive; annotations.ts ×8
123 > }
125 > /**
126 > * Returns a copy of all annotations.
127 > */
128 > public getAllAnnotations(): IAnnotation<T>[] {
129 > return this._annotations.slice(); annotations.ts ×1
130 > }
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(); annotations.ts ×5
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; textModel.ts ×9
152 > }
153 > const range = annotation.range; annotations.ts ×3
154 > if (range.endExclusive >= e.replaceRange.start) {
155 > break;
156 > }
157 > annotations.shift(); annotations.ts ×2
158 > const newAnnotation = { range: range.delta(offset), annotation: annotation.annotation };
159 > if (!newAnnotation.range.isEmpty) {
160 > finalAnnotations.push(newAnnotation); annotations.ts ×1
161 > } else { annotations.ts ×2
162 > deletedAnnotations.push(newAnnotation); annotations.ts ×1
163 > }
165 >
166 > const intersecting: IAnnotation<T>[] = [];
167 > while (true) {
168 > const annotation = annotations[0];
169 > if (!annotation) {
170 > break; annotations.ts ×1
171 > }
172 > const range = annotation.range; annotations.ts ×3
173 > if (!range.intersectsOrTouches(e.replaceRange)) {
174 > break; annotations.ts ×1
175 > }
176 > annotations.shift(); annotations.ts ×4
177 > intersecting.push(annotation);
178 > }
180 > for (let i = intersecting.length - 1; i >= 0; i--) {
181 > const annotation = intersecting[i]; annotations.ts ×4
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); annotations.ts ×1
194 > }
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); annotations.ts ×1
199 > }
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 > }
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(); annotations.ts ×3
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); annotations.ts ×1
223 > }
225 > this._annotations = finalAnnotations;
226 > return deletedAnnotations;
227 > }
229 > /**
230 > * Creates a shallow clone of this annotated string.
231 > */
232 > public clone(): IAnnotatedString<T> {
233 > return new AnnotatedString<T>(this._annotations.slice()); annotations.ts ×2
234 > }
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); annotations.ts ×6
253 > }
255 > private _annotations: IAnnotationUpdate<T>[];
256 >
257 > private constructor(annotations: IAnnotationUpdate<T>[]) {
258 > this._annotations = annotations; annotations.ts ×6
259 > }
261 > get annotations(): IAnnotationUpdate<T>[] {
262 > return this._annotations; annotations.ts ×6
263 > }
265 > public rebase(edit: StringEdit): void {
266 > const annotatedString = new AnnotatedString<T | undefined>(this._annotations); annotations.ts ×2
267 > annotatedString.applyEdit(edit);
268 > this._annotations = annotatedString.getAllAnnotations();
269 > }
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 }
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 }