Path: blob/main/src/vs/editor/common/model/tokens/annotations.ts
5251 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// Also include the next annotation if it ends exactly at offset (touching boundary)85const nextCandidate = this._annotations[startIndex]?.range;86if (nextCandidate && nextCandidate.endExclusive === offset) {87startIndex--;88}89} else {90const candidate = this._annotations[- (startIndexWhereToReplace + 2)]?.range;91if (candidate && offset >= candidate.start && offset < candidate.endExclusive) {92startIndex = - (startIndexWhereToReplace + 2);93} else {94startIndex = - (startIndexWhereToReplace + 1);95}96}97return startIndex;98}99100private _getEndIndexOfIntersectingAnnotation(offset: number): number {101// Find index to the right of the offset102const endIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => {103return this._annotations[index].range.endExclusive - offset;104});105let endIndexExclusive: number;106if (endIndexWhereToReplace >= 0) {107endIndexExclusive = endIndexWhereToReplace + 1;108// Also include the next annotation if it starts exactly at offset (touching boundary)109const nextCandidate = this._annotations[endIndexExclusive]?.range;110if (nextCandidate && nextCandidate.start === offset) {111endIndexExclusive++;112}113} else {114const candidate = this._annotations[-(endIndexWhereToReplace + 1)]?.range;115if (candidate && offset >= candidate.start && offset <= candidate.endExclusive) {116endIndexExclusive = - endIndexWhereToReplace;117} else {118endIndexExclusive = - (endIndexWhereToReplace + 1);119}120}121return endIndexExclusive;122}123124/**125* Returns a copy of all annotations.126*/127public getAllAnnotations(): IAnnotation<T>[] {128return this._annotations.slice();129}130131/**132* Applies a string edit to the annotated string, updating annotation ranges accordingly.133* @param edit The string edit to apply.134* @returns The annotations that were deleted (became empty) as a result of the edit.135*/136public applyEdit(edit: StringEdit): IAnnotation<T>[] {137const annotations = this._annotations.slice();138139// treat edits as deletion of the replace range and then as insertion that extends the first range140const finalAnnotations: IAnnotation<T>[] = [];141const deletedAnnotations: IAnnotation<T>[] = [];142143let offset = 0;144145for (const e of edit.replacements) {146while (true) {147// ranges before the current edit148const annotation = annotations[0];149if (!annotation) {150break;151}152const range = annotation.range;153if (range.endExclusive >= e.replaceRange.start) {154break;155}156annotations.shift();157const newAnnotation = { range: range.delta(offset), annotation: annotation.annotation };158if (!newAnnotation.range.isEmpty) {159finalAnnotations.push(newAnnotation);160} else {161deletedAnnotations.push(newAnnotation);162}163}164165const intersecting: IAnnotation<T>[] = [];166while (true) {167const annotation = annotations[0];168if (!annotation) {169break;170}171const range = annotation.range;172if (!range.intersectsOrTouches(e.replaceRange)) {173break;174}175annotations.shift();176intersecting.push(annotation);177}178179for (let i = intersecting.length - 1; i >= 0; i--) {180const annotation = intersecting[i];181let r = annotation.range;182183// Inserted text will extend the first intersecting annotation, if the edit truly overlaps it184const shouldExtend = i === 0 && (e.replaceRange.endExclusive > r.start) && (e.replaceRange.start < r.endExclusive);185// Annotation shrinks by the overlap then grows with the new text length186const overlap = r.intersect(e.replaceRange)!.length;187r = r.deltaEnd(-overlap + (shouldExtend ? e.newText.length : 0));188189// If the annotation starts after the edit start, shift left to the edit start position190const rangeAheadOfReplaceRange = r.start - e.replaceRange.start;191if (rangeAheadOfReplaceRange > 0) {192r = r.delta(-rangeAheadOfReplaceRange);193}194195// If annotation shouldn't be extended AND it is after or on edit start, move it after the newly inserted text196if (!shouldExtend && rangeAheadOfReplaceRange >= 0) {197r = r.delta(e.newText.length);198}199200// We already took our offset into account.201// Because we add r back to the queue (which then adds offset again),202// we have to remove it here so as to not double count it.203r = r.delta(-(e.newText.length - e.replaceRange.length));204205annotations.unshift({ annotation: annotation.annotation, range: r });206}207208offset += e.newText.length - e.replaceRange.length;209}210211while (true) {212const annotation = annotations[0];213if (!annotation) {214break;215}216annotations.shift();217const newAnnotation = { annotation: annotation.annotation, range: annotation.range.delta(offset) };218if (!newAnnotation.range.isEmpty) {219finalAnnotations.push(newAnnotation);220} else {221deletedAnnotations.push(newAnnotation);222}223}224this._annotations = finalAnnotations;225return deletedAnnotations;226}227228/**229* Creates a shallow clone of this annotated string.230*/231public clone(): IAnnotatedString<T> {232return new AnnotatedString<T>(this._annotations.slice());233}234}235236export interface IAnnotationUpdate<T> {237range: OffsetRange;238annotation: T | undefined;239}240241type DefinedValue = object | string | number | boolean;242243export type ISerializedAnnotation<TSerializedProperty extends DefinedValue> = {244range: { start: number; endExclusive: number };245annotation: TSerializedProperty | undefined;246};247248export class AnnotationsUpdate<T> {249250public static create<T>(annotations: IAnnotationUpdate<T>[]): AnnotationsUpdate<T> {251return new AnnotationsUpdate(annotations);252}253254private _annotations: IAnnotationUpdate<T>[];255256private constructor(annotations: IAnnotationUpdate<T>[]) {257this._annotations = annotations;258}259260get annotations(): IAnnotationUpdate<T>[] {261return this._annotations;262}263264public rebase(edit: StringEdit): void {265const annotatedString = new AnnotatedString<T | undefined>(this._annotations);266annotatedString.applyEdit(edit);267this._annotations = annotatedString.getAllAnnotations();268}269270public serialize<TSerializedProperty extends DefinedValue>(serializingFunc: (annotation: T) => TSerializedProperty): ISerializedAnnotation<TSerializedProperty>[] {271return this._annotations.map(annotation => {272const range = { start: annotation.range.start, endExclusive: annotation.range.endExclusive };273if (!annotation.annotation) {274return { range, annotation: undefined };275}276return { range, annotation: serializingFunc(annotation.annotation) };277});278}279280static deserialize<T, TSerializedProperty extends DefinedValue>(serializedAnnotations: ISerializedAnnotation<TSerializedProperty>[], deserializingFunc: (annotation: TSerializedProperty) => T): AnnotationsUpdate<T> {281const annotations: IAnnotationUpdate<T>[] = serializedAnnotations.map(serializedAnnotation => {282const range = new OffsetRange(serializedAnnotation.range.start, serializedAnnotation.range.endExclusive);283if (!serializedAnnotation.annotation) {284return { range, annotation: undefined };285}286return { range, annotation: deserializingFunc(serializedAnnotation.annotation) };287});288return new AnnotationsUpdate(annotations);289}290}291292293