Path: blob/main/src/vs/editor/common/model/tokens/annotations.ts
4797 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { binarySearch2 } from '../../../../base/common/arrays.js';6import { StringEdit } from '../../core/edits/stringEdit.js';7import { OffsetRange } from '../../core/ranges/offsetRange.js';89export interface IAnnotation<T> {10range: OffsetRange;11annotation: T;12}1314export interface IAnnotatedString<T> {15/**16* Set annotations for a specific line.17* Annotations should be sorted and non-overlapping.18*/19setAnnotations(annotations: AnnotationsUpdate<T>): void;20/**21* Return annotations intersecting with the given offset range.22*/23getAnnotationsIntersecting(range: OffsetRange): IAnnotation<T>[];24/**25* Get all the annotations. Method is used for testing.26*/27getAllAnnotations(): IAnnotation<T>[];28/**29* Apply a string edit to the annotated string.30* @returns The annotations that were deleted (became empty) as a result of the edit.31*/32applyEdit(edit: StringEdit): IAnnotation<T>[];33/**34* Clone the annotated string.35*/36clone(): IAnnotatedString<T>;37}3839export class AnnotatedString<T> implements IAnnotatedString<T> {4041/**42* Annotations are non intersecting and contiguous in the array.43*/44private _annotations: IAnnotation<T>[] = [];4546constructor(annotations: IAnnotation<T>[] = []) {47this._annotations = annotations;48}4950/**51* Set annotations for a specific range.52* Annotations should be sorted and non-overlapping.53* If the annotation value is undefined, the annotation is removed.54*/55public setAnnotations(annotations: AnnotationsUpdate<T>): void {56for (const annotation of annotations.annotations) {57const startIndex = this._getStartIndexOfIntersectingAnnotation(annotation.range.start);58const endIndexExclusive = this._getEndIndexOfIntersectingAnnotation(annotation.range.endExclusive);59if (annotation.annotation !== undefined) {60this._annotations.splice(startIndex, endIndexExclusive - startIndex, { range: annotation.range, annotation: annotation.annotation });61} else {62this._annotations.splice(startIndex, endIndexExclusive - startIndex);63}64}65}6667/**68* Returns all annotations that intersect with the given offset range.69*/70public getAnnotationsIntersecting(range: OffsetRange): IAnnotation<T>[] {71const startIndex = this._getStartIndexOfIntersectingAnnotation(range.start);72const endIndexExclusive = this._getEndIndexOfIntersectingAnnotation(range.endExclusive);73return this._annotations.slice(startIndex, endIndexExclusive);74}7576private _getStartIndexOfIntersectingAnnotation(offset: number): number {77// Find index to the left of the offset78const startIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => {79return this._annotations[index].range.start - offset;80});81let startIndex: number;82if (startIndexWhereToReplace >= 0) {83startIndex = startIndexWhereToReplace;84} else {85const candidate = this._annotations[- (startIndexWhereToReplace + 2)]?.range;86if (candidate && offset >= candidate.start && offset <= candidate.endExclusive) {87startIndex = - (startIndexWhereToReplace + 2);88} else {89startIndex = - (startIndexWhereToReplace + 1);90}91}92return startIndex;93}9495private _getEndIndexOfIntersectingAnnotation(offset: number): number {96// Find index to the right of the offset97const endIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => {98return this._annotations[index].range.endExclusive - offset;99});100let endIndexExclusive: number;101if (endIndexWhereToReplace >= 0) {102endIndexExclusive = endIndexWhereToReplace + 1;103} else {104const candidate = this._annotations[-(endIndexWhereToReplace + 1)]?.range;105if (candidate && offset >= candidate.start && offset <= candidate.endExclusive) {106endIndexExclusive = - endIndexWhereToReplace;107} else {108endIndexExclusive = - (endIndexWhereToReplace + 1);109}110}111return endIndexExclusive;112}113114/**115* Returns a copy of all annotations.116*/117public getAllAnnotations(): IAnnotation<T>[] {118return this._annotations.slice();119}120121/**122* Applies a string edit to the annotated string, updating annotation ranges accordingly.123* @param edit The string edit to apply.124* @returns The annotations that were deleted (became empty) as a result of the edit.125*/126public applyEdit(edit: StringEdit): IAnnotation<T>[] {127const annotations = this._annotations.slice();128129// treat edits as deletion of the replace range and then as insertion that extends the first range130const finalAnnotations: IAnnotation<T>[] = [];131const deletedAnnotations: IAnnotation<T>[] = [];132133let offset = 0;134135for (const e of edit.replacements) {136while (true) {137// ranges before the current edit138const annotation = annotations[0];139if (!annotation) {140break;141}142const range = annotation.range;143if (range.endExclusive >= e.replaceRange.start) {144break;145}146annotations.shift();147const newAnnotation = { range: range.delta(offset), annotation: annotation.annotation };148if (!newAnnotation.range.isEmpty) {149finalAnnotations.push(newAnnotation);150} else {151deletedAnnotations.push(newAnnotation);152}153}154155const intersecting: IAnnotation<T>[] = [];156while (true) {157const annotation = annotations[0];158if (!annotation) {159break;160}161const range = annotation.range;162if (!range.intersectsOrTouches(e.replaceRange)) {163break;164}165annotations.shift();166intersecting.push(annotation);167}168169for (let i = intersecting.length - 1; i >= 0; i--) {170const annotation = intersecting[i];171let r = annotation.range;172173// Inserted text will extend the first intersecting annotation, if the edit truly overlaps it174const shouldExtend = i === 0 && (e.replaceRange.endExclusive > r.start) && (e.replaceRange.start < r.endExclusive);175// Annotation shrinks by the overlap then grows with the new text length176const overlap = r.intersect(e.replaceRange)!.length;177r = r.deltaEnd(-overlap + (shouldExtend ? e.newText.length : 0));178179// If the annotation starts after the edit start, shift left to the edit start position180const rangeAheadOfReplaceRange = r.start - e.replaceRange.start;181if (rangeAheadOfReplaceRange > 0) {182r = r.delta(-rangeAheadOfReplaceRange);183}184185// If annotation shouldn't be extended AND it is after or on edit start, move it after the newly inserted text186if (!shouldExtend && rangeAheadOfReplaceRange >= 0) {187r = r.delta(e.newText.length);188}189190// We already took our offset into account.191// Because we add r back to the queue (which then adds offset again),192// we have to remove it here so as to not double count it.193r = r.delta(-(e.newText.length - e.replaceRange.length));194195annotations.unshift({ annotation: annotation.annotation, range: r });196}197198offset += e.newText.length - e.replaceRange.length;199}200201while (true) {202const annotation = annotations[0];203if (!annotation) {204break;205}206annotations.shift();207const newAnnotation = { annotation: annotation.annotation, range: annotation.range.delta(offset) };208if (!newAnnotation.range.isEmpty) {209finalAnnotations.push(newAnnotation);210} else {211deletedAnnotations.push(newAnnotation);212}213}214this._annotations = finalAnnotations;215return deletedAnnotations;216}217218/**219* Creates a shallow clone of this annotated string.220*/221public clone(): IAnnotatedString<T> {222return new AnnotatedString<T>(this._annotations.slice());223}224}225226export interface IAnnotationUpdate<T> {227range: OffsetRange;228annotation: T | undefined;229}230231type DefinedValue = object | string | number | boolean;232233export type ISerializedAnnotation<TSerializedProperty extends DefinedValue> = {234range: { start: number; endExclusive: number };235annotation: TSerializedProperty | undefined;236};237238export class AnnotationsUpdate<T> {239240public static create<T>(annotations: IAnnotationUpdate<T>[]): AnnotationsUpdate<T> {241return new AnnotationsUpdate(annotations);242}243244private _annotations: IAnnotationUpdate<T>[];245246private constructor(annotations: IAnnotationUpdate<T>[]) {247this._annotations = annotations;248}249250get annotations(): IAnnotationUpdate<T>[] {251return this._annotations;252}253254public rebase(edit: StringEdit): void {255const annotatedString = new AnnotatedString<T | undefined>(this._annotations);256annotatedString.applyEdit(edit);257this._annotations = annotatedString.getAllAnnotations();258}259260public serialize<TSerializedProperty extends DefinedValue>(serializingFunc: (annotation: T) => TSerializedProperty): ISerializedAnnotation<TSerializedProperty>[] {261return this._annotations.map(annotation => {262const range = { start: annotation.range.start, endExclusive: annotation.range.endExclusive };263if (!annotation.annotation) {264return { range, annotation: undefined };265}266return { range, annotation: serializingFunc(annotation.annotation) };267});268}269270static deserialize<T, TSerializedProperty extends DefinedValue>(serializedAnnotations: ISerializedAnnotation<TSerializedProperty>[], deserializingFunc: (annotation: TSerializedProperty) => T): AnnotationsUpdate<T> {271const annotations: IAnnotationUpdate<T>[] = serializedAnnotations.map(serializedAnnotation => {272const range = new OffsetRange(serializedAnnotation.range.start, serializedAnnotation.range.endExclusive);273if (!serializedAnnotation.annotation) {274return { range, annotation: undefined };275}276return { range, annotation: deserializingFunc(serializedAnnotation.annotation) };277});278return new AnnotationsUpdate(annotations);279}280}281282283