Path: blob/main/src/vs/editor/common/viewLayout/lineHeights.ts
3294 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 { intersection } from '../../../base/common/collections.js';78export class CustomLine {910public index: number;11public lineNumber: number;12public specialHeight: number;13public prefixSum: number;14public maximumSpecialHeight: number;15public decorationId: string;16public deleted: boolean;1718constructor(decorationId: string, index: number, lineNumber: number, specialHeight: number, prefixSum: number) {19this.decorationId = decorationId;20this.index = index;21this.lineNumber = lineNumber;22this.specialHeight = specialHeight;23this.prefixSum = prefixSum;24this.maximumSpecialHeight = specialHeight;25this.deleted = false;26}27}2829/**30* Manages line heights in the editor with support for custom line heights from decorations.31*32* This class maintains an ordered collection of line heights, where each line can have either33* the default height or a custom height specified by decorations. It supports efficient querying34* of individual line heights as well as accumulated heights up to a specific line.35*36* Line heights are stored in a sorted array for efficient binary search operations. Each line37* with custom height is represented by a {@link CustomLine} object which tracks its special height,38* accumulated height prefix sum, and associated decoration ID.39*40* The class optimizes performance by:41* - Using binary search to locate lines in the ordered array42* - Batching updates through a pending changes mechanism43* - Computing prefix sums for O(1) accumulated height lookup44* - Tracking maximum height for lines with multiple decorations45* - Efficiently handling document changes (line insertions and deletions)46*47* When lines are inserted or deleted, the manager updates line numbers and prefix sums48* for all affected lines. It also handles special cases like decorations that span49* the insertion/deletion points by re-applying those decorations appropriately.50*51* All query operations automatically commit pending changes to ensure consistent results.52* Clients can modify line heights by adding or removing custom line height decorations,53* which are tracked by their unique decoration IDs.54*/55export class LineHeightsManager {5657private _decorationIDToCustomLine: ArrayMap<string, CustomLine> = new ArrayMap<string, CustomLine>();58private _orderedCustomLines: CustomLine[] = [];59private _pendingSpecialLinesToInsert: CustomLine[] = [];60private _invalidIndex: number = 0;61private _defaultLineHeight: number;62private _hasPending: boolean = false;6364constructor(defaultLineHeight: number, customLineHeightData: ICustomLineHeightData[]) {65this._defaultLineHeight = defaultLineHeight;66if (customLineHeightData.length > 0) {67for (const data of customLineHeightData) {68this.insertOrChangeCustomLineHeight(data.decorationId, data.startLineNumber, data.endLineNumber, data.lineHeight);69}70this.commit();71}72}7374set defaultLineHeight(defaultLineHeight: number) {75this._defaultLineHeight = defaultLineHeight;76}7778get defaultLineHeight() {79return this._defaultLineHeight;80}8182public removeCustomLineHeight(decorationID: string): void {83const customLines = this._decorationIDToCustomLine.get(decorationID);84if (!customLines) {85return;86}87this._decorationIDToCustomLine.delete(decorationID);88for (const customLine of customLines) {89customLine.deleted = true;90this._invalidIndex = Math.min(this._invalidIndex, customLine.index);91}92this._hasPending = true;93}9495public insertOrChangeCustomLineHeight(decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number): void {96this.removeCustomLineHeight(decorationId);97for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {98const customLine = new CustomLine(decorationId, -1, lineNumber, lineHeight, 0);99this._pendingSpecialLinesToInsert.push(customLine);100}101this._hasPending = true;102}103104public heightForLineNumber(lineNumber: number): number {105const searchIndex = this._binarySearchOverOrderedCustomLinesArray(lineNumber);106if (searchIndex >= 0) {107return this._orderedCustomLines[searchIndex].maximumSpecialHeight;108}109return this._defaultLineHeight;110}111112public getAccumulatedLineHeightsIncludingLineNumber(lineNumber: number): number {113const searchIndex = this._binarySearchOverOrderedCustomLinesArray(lineNumber);114if (searchIndex >= 0) {115return this._orderedCustomLines[searchIndex].prefixSum + this._orderedCustomLines[searchIndex].maximumSpecialHeight;116}117if (searchIndex === -1) {118return this._defaultLineHeight * lineNumber;119}120const modifiedIndex = -(searchIndex + 1);121const previousSpecialLine = this._orderedCustomLines[modifiedIndex - 1];122return previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (lineNumber - previousSpecialLine.lineNumber);123}124125public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void {126const deleteCount = toLineNumber - fromLineNumber + 1;127const numberOfCustomLines = this._orderedCustomLines.length;128const candidateStartIndexOfDeletion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber);129let startIndexOfDeletion: number;130if (candidateStartIndexOfDeletion >= 0) {131startIndexOfDeletion = candidateStartIndexOfDeletion;132for (let i = candidateStartIndexOfDeletion - 1; i >= 0; i--) {133if (this._orderedCustomLines[i].lineNumber === fromLineNumber) {134startIndexOfDeletion--;135} else {136break;137}138}139} else {140startIndexOfDeletion = candidateStartIndexOfDeletion === -(numberOfCustomLines + 1) && candidateStartIndexOfDeletion !== -1 ? numberOfCustomLines - 1 : - (candidateStartIndexOfDeletion + 1);141}142const candidateEndIndexOfDeletion = this._binarySearchOverOrderedCustomLinesArray(toLineNumber);143let endIndexOfDeletion: number;144if (candidateEndIndexOfDeletion >= 0) {145endIndexOfDeletion = candidateEndIndexOfDeletion;146for (let i = candidateEndIndexOfDeletion + 1; i < numberOfCustomLines; i++) {147if (this._orderedCustomLines[i].lineNumber === toLineNumber) {148endIndexOfDeletion++;149} else {150break;151}152}153} else {154endIndexOfDeletion = candidateEndIndexOfDeletion === -(numberOfCustomLines + 1) && candidateEndIndexOfDeletion !== -1 ? numberOfCustomLines - 1 : - (candidateEndIndexOfDeletion + 1);155}156const isEndIndexBiggerThanStartIndex = endIndexOfDeletion > startIndexOfDeletion;157const isEndIndexEqualToStartIndexAndCoversCustomLine = endIndexOfDeletion === startIndexOfDeletion158&& this._orderedCustomLines[startIndexOfDeletion]159&& this._orderedCustomLines[startIndexOfDeletion].lineNumber >= fromLineNumber160&& this._orderedCustomLines[startIndexOfDeletion].lineNumber <= toLineNumber;161162if (isEndIndexBiggerThanStartIndex || isEndIndexEqualToStartIndexAndCoversCustomLine) {163let maximumSpecialHeightOnDeletedInterval = 0;164for (let i = startIndexOfDeletion; i <= endIndexOfDeletion; i++) {165maximumSpecialHeightOnDeletedInterval = Math.max(maximumSpecialHeightOnDeletedInterval, this._orderedCustomLines[i].maximumSpecialHeight);166}167let prefixSumOnDeletedInterval = 0;168if (startIndexOfDeletion > 0) {169const previousSpecialLine = this._orderedCustomLines[startIndexOfDeletion - 1];170prefixSumOnDeletedInterval = previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (fromLineNumber - previousSpecialLine.lineNumber - 1);171} else {172prefixSumOnDeletedInterval = fromLineNumber > 0 ? (fromLineNumber - 1) * this._defaultLineHeight : 0;173}174const firstSpecialLineDeleted = this._orderedCustomLines[startIndexOfDeletion];175const lastSpecialLineDeleted = this._orderedCustomLines[endIndexOfDeletion];176const firstSpecialLineAfterDeletion = this._orderedCustomLines[endIndexOfDeletion + 1];177const heightOfFirstLineAfterDeletion = firstSpecialLineAfterDeletion && firstSpecialLineAfterDeletion.lineNumber === toLineNumber + 1 ? firstSpecialLineAfterDeletion.maximumSpecialHeight : this._defaultLineHeight;178const totalHeightDeleted = lastSpecialLineDeleted.prefixSum179+ lastSpecialLineDeleted.maximumSpecialHeight180- firstSpecialLineDeleted.prefixSum181+ this._defaultLineHeight * (toLineNumber - lastSpecialLineDeleted.lineNumber)182+ this._defaultLineHeight * (firstSpecialLineDeleted.lineNumber - fromLineNumber)183+ heightOfFirstLineAfterDeletion - maximumSpecialHeightOnDeletedInterval;184185const decorationIdsSeen = new Set<string>();186const newOrderedCustomLines: CustomLine[] = [];187const newDecorationIDToSpecialLine = new ArrayMap<string, CustomLine>();188let numberOfDeletions = 0;189for (let i = 0; i < this._orderedCustomLines.length; i++) {190const customLine = this._orderedCustomLines[i];191if (i < startIndexOfDeletion) {192newOrderedCustomLines.push(customLine);193newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);194} else if (i >= startIndexOfDeletion && i <= endIndexOfDeletion) {195const decorationId = customLine.decorationId;196if (!decorationIdsSeen.has(decorationId)) {197customLine.index -= numberOfDeletions;198customLine.lineNumber = fromLineNumber;199customLine.prefixSum = prefixSumOnDeletedInterval;200customLine.maximumSpecialHeight = maximumSpecialHeightOnDeletedInterval;201newOrderedCustomLines.push(customLine);202newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);203} else {204numberOfDeletions++;205}206} else if (i > endIndexOfDeletion) {207customLine.index -= numberOfDeletions;208customLine.lineNumber -= deleteCount;209customLine.prefixSum -= totalHeightDeleted;210newOrderedCustomLines.push(customLine);211newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);212}213decorationIdsSeen.add(customLine.decorationId);214}215this._orderedCustomLines = newOrderedCustomLines;216this._decorationIDToCustomLine = newDecorationIDToSpecialLine;217} else {218const totalHeightDeleted = deleteCount * this._defaultLineHeight;219for (let i = endIndexOfDeletion; i < this._orderedCustomLines.length; i++) {220const customLine = this._orderedCustomLines[i];221if (customLine.lineNumber > toLineNumber) {222customLine.lineNumber -= deleteCount;223customLine.prefixSum -= totalHeightDeleted;224}225}226}227}228229public onLinesInserted(fromLineNumber: number, toLineNumber: number): void {230const insertCount = toLineNumber - fromLineNumber + 1;231const candidateStartIndexOfInsertion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber);232let startIndexOfInsertion: number;233if (candidateStartIndexOfInsertion >= 0) {234startIndexOfInsertion = candidateStartIndexOfInsertion;235for (let i = candidateStartIndexOfInsertion - 1; i >= 0; i--) {236if (this._orderedCustomLines[i].lineNumber === fromLineNumber) {237startIndexOfInsertion--;238} else {239break;240}241}242} else {243startIndexOfInsertion = -(candidateStartIndexOfInsertion + 1);244}245const toReAdd: ICustomLineHeightData[] = [];246const decorationsImmediatelyAfter = new Set<string>();247for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) {248if (this._orderedCustomLines[i].lineNumber === fromLineNumber) {249decorationsImmediatelyAfter.add(this._orderedCustomLines[i].decorationId);250}251}252const decorationsImmediatelyBefore = new Set<string>();253for (let i = startIndexOfInsertion - 1; i >= 0; i--) {254if (this._orderedCustomLines[i].lineNumber === fromLineNumber - 1) {255decorationsImmediatelyBefore.add(this._orderedCustomLines[i].decorationId);256}257}258const decorationsWithGaps = intersection(decorationsImmediatelyBefore, decorationsImmediatelyAfter);259for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) {260this._orderedCustomLines[i].lineNumber += insertCount;261this._orderedCustomLines[i].prefixSum += this._defaultLineHeight * insertCount;262}263264if (decorationsWithGaps.size > 0) {265for (const decorationId of decorationsWithGaps) {266const decoration = this._decorationIDToCustomLine.get(decorationId);267if (decoration) {268const startLineNumber = decoration.reduce((min, l) => Math.min(min, l.lineNumber), fromLineNumber); // min269const endLineNumber = decoration.reduce((max, l) => Math.max(max, l.lineNumber), fromLineNumber); // max270const lineHeight = decoration.reduce((max, l) => Math.max(max, l.specialHeight), 0);271toReAdd.push({272decorationId,273startLineNumber,274endLineNumber,275lineHeight276});277}278}279280for (const dec of toReAdd) {281this.insertOrChangeCustomLineHeight(dec.decorationId, dec.startLineNumber, dec.endLineNumber, dec.lineHeight);282}283this.commit();284}285}286287public commit(): void {288if (!this._hasPending) {289return;290}291for (const pendingChange of this._pendingSpecialLinesToInsert) {292const candidateInsertionIndex = this._binarySearchOverOrderedCustomLinesArray(pendingChange.lineNumber);293const insertionIndex = candidateInsertionIndex >= 0 ? candidateInsertionIndex : -(candidateInsertionIndex + 1);294this._orderedCustomLines.splice(insertionIndex, 0, pendingChange);295this._invalidIndex = Math.min(this._invalidIndex, insertionIndex);296}297this._pendingSpecialLinesToInsert = [];298const newDecorationIDToSpecialLine = new ArrayMap<string, CustomLine>();299const newOrderedSpecialLines: CustomLine[] = [];300301for (let i = 0; i < this._invalidIndex; i++) {302const customLine = this._orderedCustomLines[i];303newOrderedSpecialLines.push(customLine);304newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);305}306307let numberOfDeletions = 0;308let previousSpecialLine: CustomLine | undefined = (this._invalidIndex > 0) ? newOrderedSpecialLines[this._invalidIndex - 1] : undefined;309for (let i = this._invalidIndex; i < this._orderedCustomLines.length; i++) {310const customLine = this._orderedCustomLines[i];311if (customLine.deleted) {312numberOfDeletions++;313continue;314}315customLine.index = i - numberOfDeletions;316if (previousSpecialLine && previousSpecialLine.lineNumber === customLine.lineNumber) {317customLine.maximumSpecialHeight = previousSpecialLine.maximumSpecialHeight;318customLine.prefixSum = previousSpecialLine.prefixSum;319} else {320let maximumSpecialHeight = customLine.specialHeight;321for (let j = i; j < this._orderedCustomLines.length; j++) {322const nextSpecialLine = this._orderedCustomLines[j];323if (nextSpecialLine.deleted) {324continue;325}326if (nextSpecialLine.lineNumber !== customLine.lineNumber) {327break;328}329maximumSpecialHeight = Math.max(maximumSpecialHeight, nextSpecialLine.specialHeight);330}331customLine.maximumSpecialHeight = maximumSpecialHeight;332333let prefixSum: number;334if (previousSpecialLine) {335prefixSum = previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (customLine.lineNumber - previousSpecialLine.lineNumber - 1);336} else {337prefixSum = this._defaultLineHeight * (customLine.lineNumber - 1);338}339customLine.prefixSum = prefixSum;340}341previousSpecialLine = customLine;342newOrderedSpecialLines.push(customLine);343newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);344}345this._orderedCustomLines = newOrderedSpecialLines;346this._decorationIDToCustomLine = newDecorationIDToSpecialLine;347this._invalidIndex = Infinity;348this._hasPending = false;349}350351private _binarySearchOverOrderedCustomLinesArray(lineNumber: number): number {352return binarySearch2(this._orderedCustomLines.length, (index) => {353const line = this._orderedCustomLines[index];354if (line.lineNumber === lineNumber) {355return 0;356} else if (line.lineNumber < lineNumber) {357return -1;358} else {359return 1;360}361});362}363}364365export interface ICustomLineHeightData {366readonly decorationId: string;367readonly startLineNumber: number;368readonly endLineNumber: number;369readonly lineHeight: number;370}371372class ArrayMap<K, T> {373374private _map: Map<K, T[]> = new Map<K, T[]>();375376constructor() { }377378add(key: K, value: T) {379const array = this._map.get(key);380if (!array) {381this._map.set(key, [value]);382} else {383array.push(value);384}385}386387get(key: K): T[] | undefined {388return this._map.get(key);389}390391delete(key: K): void {392this._map.delete(key);393}394}395396397