src/vs/editor/common/viewLayout/lineHeights.ts
493 LOC · 468 covered · 25 uncovered · 150 ranges · 118 concepts · 56 introducers · 50 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.
/*---------------------------------------------------------------------------------------------
lineHeights.ts ×24
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { binarySearch2 } from '../../../base/common/arrays.js';
import { intersection } from '../../../base/common/collections.js';
import { IEditorConfiguration } from '../config/editorConfiguration.js';
import { EditorOption } from '../config/editorOptions.js';
import { ICoordinatesConverter } from '../coordinatesConverter.js';
import { IModelDecoration } from '../model.js';
const enum PendingChangeKind {
InsertOrChange,
Remove,
LinesDeleted,
LinesInserted,
}
type PendingChange =
| { readonly kind: PendingChangeKind.InsertOrChange; readonly decorationId: string; readonly startLineNumber: number; readonly endLineNumber: number; readonly lineHeight: number }
| { readonly kind: PendingChangeKind.Remove; readonly decorationId: string }
| { readonly kind: PendingChangeKind.LinesDeleted; readonly fromLineNumber: number; readonly toLineNumber: number }
| { readonly kind: PendingChangeKind.LinesInserted; readonly fromLineNumber: number; readonly toLineNumber: number };
export class CustomLine {
public index: number;
public lineNumber: number;
public specialHeight: number;
public prefixSum: number;
public maximumSpecialHeight: number;
public decorationId: string;
public deleted: boolean;
constructor(decorationId: string, index: number, lineNumber: number, specialHeight: number, prefixSum: number) {
this.index = index;
this.lineNumber = lineNumber;
this.specialHeight = specialHeight;
this.prefixSum = prefixSum;
this.maximumSpecialHeight = specialHeight;
this.deleted = false;
}
/**
* Manages line heights in the editor with support for custom line heights from decorations.
*
* This class maintains an ordered collection of line heights, where each line can have either
* the default height or a custom height specified by decorations. It supports efficient querying
* of individual line heights as well as accumulated heights up to a specific line.
*
* Line heights are stored in a sorted array for efficient binary search operations. Each line
* with custom height is represented by a {@link CustomLine} object which tracks its special height,
* accumulated height prefix sum, and associated decoration ID.
*
* The class optimizes performance by:
* - Using binary search to locate lines in the ordered array
* - Batching updates through a pending changes mechanism
* - Computing prefix sums for O(1) accumulated height lookup
* - Tracking maximum height for lines with multiple decorations
* - Efficiently handling document changes (line insertions and deletions)
*
* When lines are inserted or deleted, the manager updates line numbers and prefix sums
* for all affected lines. It also handles special cases like decorations that span
* the insertion/deletion points by re-applying those decorations appropriately.
*
* All query operations automatically commit pending changes to ensure consistent results.
* Clients can modify line heights by adding or removing custom line height decorations,
* which are tracked by their unique decoration IDs.
*/
export class LineHeightsManager {
private _decorationIDToCustomLine: ArrayMap<string, CustomLine> = new ArrayMap<string, CustomLine>();
private _orderedCustomLines: CustomLine[] = [];
private _pendingChanges: PendingChange[] = [];
private _invalidIndex: number = Infinity;
private _defaultLineHeight: number;
private _hasPending: boolean = false;
constructor(defaultLineHeight: number, customLineHeightData: CustomLineHeightData[]) {
for (const data of customLineHeightData) {
this.insertOrChangeCustomLineHeight(data.decorationId, data.startLineNumber, data.endLineNumber, data.lineHeight);
lineHeights.ts ×2
}
set defaultLineHeight(defaultLineHeight: number) {
}
get defaultLineHeight() {
}
public removeCustomLineHeight(decorationID: string): void {
this._pendingChanges.push({ kind: PendingChangeKind.Remove, decorationId: decorationID });
lineHeights.ts ×2
this._hasPending = true;
}
public insertOrChangeCustomLineHeight(decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number): void {
this._pendingChanges.push({ kind: PendingChangeKind.InsertOrChange, decorationId, startLineNumber, endLineNumber, lineHeight });
lineHeights.ts ×13
this._hasPending = true;
}
public heightForLineNumber(lineNumber: number): number {
const searchIndex = this._binarySearchOverOrderedCustomLinesArray(lineNumber);
if (searchIndex >= 0) {
}
public getAccumulatedLineHeightsIncludingLineNumber(lineNumber: number): number {
const searchIndex = this._binarySearchOverOrderedCustomLinesArray(lineNumber);
if (searchIndex >= 0) {
return this._orderedCustomLines[searchIndex].prefixSum + this._orderedCustomLines[searchIndex].maximumSpecialHeight;
lineHeights.ts ×1
}
}
const previousSpecialLine = this._orderedCustomLines[modifiedIndex - 1];
return previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (lineNumber - previousSpecialLine.lineNumber);
public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void {
this._pendingChanges.push({ kind: PendingChangeKind.LinesDeleted, fromLineNumber, toLineNumber });
lineHeights.ts ×1
this._hasPending = true;
}
public onLinesInserted(fromLineNumber: number, toLineNumber: number): void {
this._pendingChanges.push({ kind: PendingChangeKind.LinesInserted, fromLineNumber, toLineNumber });
lineHeights.ts ×1
this._hasPending = true;
}
private _commit(): void {
}
this._pendingChanges = [];
this._hasPending = false;
const stagedInserts: CustomLine[] = [];
const stagedIdMap = new ArrayMap<string, CustomLine>();
for (const change of changes) {
switch (change.kind) {
case PendingChangeKind.Remove:
break;
this._doInsertOrChangeCustomLineHeight(change.decorationId, change.startLineNumber, change.endLineNumber, change.lineHeight, stagedInserts, stagedIdMap);
lineHeights.ts ×13
break;
this._doLinesDeleted(change.fromLineNumber, change.toLineNumber);
break;
this._doLinesInserted(change.fromLineNumber, change.toLineNumber, stagedInserts, stagedIdMap);
break;
}
this._flushStagedDecorationChanges(stagedInserts, stagedIdMap);
private _doRemoveCustomLineHeight(decorationID: string, stagedIdMap: ArrayMap<string, CustomLine>): void {
if (customLines) {
for (const customLine of customLines) {
customLine.deleted = true;
this._invalidIndex = Math.min(this._invalidIndex, customLine.index);
}
}
if (stagedLines) {
for (const line of stagedLines) {
line.deleted = true;
}
}
private _doInsertOrChangeCustomLineHeight(decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number, stagedInserts: CustomLine[], stagedIdMap: ArrayMap<string, CustomLine>): void {
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
const customLine = new CustomLine(decorationId, -1, lineNumber, lineHeight, 0);
stagedInserts.push(customLine);
stagedIdMap.add(decorationId, customLine);
}
}
private _flushStagedDecorationChanges(stagedInserts: CustomLine[], stagedIdMap: ArrayMap<string, CustomLine>): void {
}
if (pendingChange.deleted) {
}
const candidateInsertionIndex = this._binarySearchOverOrderedCustomLinesArray(pendingChange.lineNumber);
lineHeights.ts ×11
const insertionIndex = candidateInsertionIndex >= 0 ? candidateInsertionIndex : -(candidateInsertionIndex + 1);
lineHeights.ts ×13
this._orderedCustomLines.splice(insertionIndex, 0, pendingChange);
this._invalidIndex = Math.min(this._invalidIndex, insertionIndex);
}
stagedInserts.length = 0;
stagedIdMap.clear();
if (this._invalidIndex === Infinity) {
}
const newOrderedSpecialLines: CustomLine[] = [];
for (let i = 0; i < this._invalidIndex; i++) {
newOrderedSpecialLines.push(customLine);
newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);
}
let numberOfDeletions = 0;
let previousSpecialLine: CustomLine | undefined = (this._invalidIndex > 0) ? newOrderedSpecialLines[this._invalidIndex - 1] : undefined;
lineHeights.ts ×8
for (let i = this._invalidIndex; i < this._orderedCustomLines.length; i++) {
if (customLine.deleted) {
continue;
}
if (previousSpecialLine && previousSpecialLine.lineNumber === customLine.lineNumber) {
customLine.prefixSum = previousSpecialLine.prefixSum;
let maximumSpecialHeight = customLine.specialHeight;
for (let j = i; j < this._orderedCustomLines.length; j++) {
const nextSpecialLine = this._orderedCustomLines[j];
if (nextSpecialLine.deleted) {
}
}
maximumSpecialHeight = Math.max(maximumSpecialHeight, nextSpecialLine.specialHeight);
lineHeights.ts ×11
}
customLine.maximumSpecialHeight = maximumSpecialHeight;
let prefixSum: number;
if (previousSpecialLine) {
prefixSum = previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (customLine.lineNumber - previousSpecialLine.lineNumber - 1);
lineHeights.ts ×1
prefixSum = this._defaultLineHeight * (customLine.lineNumber - 1);
}
customLine.prefixSum = prefixSum;
}
previousSpecialLine = customLine;
newOrderedSpecialLines.push(customLine);
newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);
}
this._orderedCustomLines = newOrderedSpecialLines;
this._decorationIDToCustomLine = newDecorationIDToSpecialLine;
this._invalidIndex = Infinity;
private _doLinesDeleted(fromLineNumber: number, toLineNumber: number): void {
const numberOfCustomLines = this._orderedCustomLines.length;
const candidateStartIndexOfDeletion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber);
let startIndexOfDeletion: number;
if (candidateStartIndexOfDeletion >= 0) {
for (let i = candidateStartIndexOfDeletion - 1; i >= 0; i--) {
startIndexOfDeletion--;
break;
}
}
startIndexOfDeletion = candidateStartIndexOfDeletion === -(numberOfCustomLines + 1) && candidateStartIndexOfDeletion !== -1 ? numberOfCustomLines - 1 : - (candidateStartIndexOfDeletion + 1);
lineHeights.ts ×1
}
const candidateEndIndexOfDeletion = this._binarySearchOverOrderedCustomLinesArray(toLineNumber);
lineHeights.ts ×9
let endIndexOfDeletion: number;
if (candidateEndIndexOfDeletion >= 0) {
for (let i = candidateEndIndexOfDeletion + 1; i < numberOfCustomLines; i++) {
if (this._orderedCustomLines[i].lineNumber === toLineNumber) {
endIndexOfDeletion++;
} else {
break;
}
}
endIndexOfDeletion = candidateEndIndexOfDeletion === -(numberOfCustomLines + 1) && candidateEndIndexOfDeletion !== -1 ? numberOfCustomLines - 1 : - (candidateEndIndexOfDeletion + 1);
lineHeights.ts ×1
}
const isEndIndexBiggerThanStartIndex = endIndexOfDeletion > startIndexOfDeletion;
lineHeights.ts ×9
const isEndIndexEqualToStartIndexAndCoversCustomLine = endIndexOfDeletion === startIndexOfDeletion
&& this._orderedCustomLines[startIndexOfDeletion].lineNumber >= fromLineNumber
lineHeights.ts ×1
&& this._orderedCustomLines[startIndexOfDeletion].lineNumber <= toLineNumber;
lineHeights.ts ×1
if (isEndIndexBiggerThanStartIndex || isEndIndexEqualToStartIndexAndCoversCustomLine) {
for (let i = startIndexOfDeletion; i <= endIndexOfDeletion; i++) {
maximumSpecialHeightOnDeletedInterval = Math.max(maximumSpecialHeightOnDeletedInterval, this._orderedCustomLines[i].maximumSpecialHeight);
}
let prefixSumOnDeletedInterval = 0;
if (startIndexOfDeletion > 0) {
const previousSpecialLine = this._orderedCustomLines[startIndexOfDeletion - 1];
lineHeights.ts ×4
prefixSumOnDeletedInterval = previousSpecialLine.prefixSum + previousSpecialLine.maximumSpecialHeight + this._defaultLineHeight * (fromLineNumber - previousSpecialLine.lineNumber - 1);
prefixSumOnDeletedInterval = fromLineNumber > 0 ? (fromLineNumber - 1) * this._defaultLineHeight : 0;
lineHeights.ts ×2
}
const firstSpecialLineDeleted = this._orderedCustomLines[startIndexOfDeletion];
lineHeights.ts ×7
const lastSpecialLineDeleted = this._orderedCustomLines[endIndexOfDeletion];
const firstSpecialLineAfterDeletion = this._orderedCustomLines[endIndexOfDeletion + 1];
const heightOfFirstLineAfterDeletion = firstSpecialLineAfterDeletion && firstSpecialLineAfterDeletion.lineNumber === toLineNumber + 1 ? firstSpecialLineAfterDeletion.maximumSpecialHeight : this._defaultLineHeight;
const totalHeightDeleted = lastSpecialLineDeleted.prefixSum
+ lastSpecialLineDeleted.maximumSpecialHeight
- firstSpecialLineDeleted.prefixSum
+ this._defaultLineHeight * (toLineNumber - lastSpecialLineDeleted.lineNumber)
+ this._defaultLineHeight * (firstSpecialLineDeleted.lineNumber - fromLineNumber)
+ heightOfFirstLineAfterDeletion - maximumSpecialHeightOnDeletedInterval;
const decorationIdsSeen = new Set<string>();
const newOrderedCustomLines: CustomLine[] = [];
const newDecorationIDToSpecialLine = new ArrayMap<string, CustomLine>();
let numberOfDeletions = 0;
for (let i = 0; i < this._orderedCustomLines.length; i++) {
const customLine = this._orderedCustomLines[i];
if (i < startIndexOfDeletion) {
newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);
const decorationId = customLine.decorationId;
if (!decorationIdsSeen.has(decorationId)) {
customLine.lineNumber = fromLineNumber;
customLine.prefixSum = prefixSumOnDeletedInterval;
customLine.maximumSpecialHeight = maximumSpecialHeightOnDeletedInterval;
newOrderedCustomLines.push(customLine);
newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);
}
customLine.index -= numberOfDeletions;
customLine.lineNumber -= deleteCount;
customLine.prefixSum -= totalHeightDeleted;
newOrderedCustomLines.push(customLine);
newDecorationIDToSpecialLine.add(customLine.decorationId, customLine);
}
}
this._orderedCustomLines = newOrderedCustomLines;
this._decorationIDToCustomLine = newDecorationIDToSpecialLine;
for (let i = endIndexOfDeletion; i < this._orderedCustomLines.length; i++) {
if (customLine.lineNumber > toLineNumber) {
customLine.prefixSum -= totalHeightDeleted;
}
private _doLinesInserted(fromLineNumber: number, toLineNumber: number, stagedInserts: CustomLine[], stagedIdMap: ArrayMap<string, CustomLine>): void {
const candidateStartIndexOfInsertion = this._binarySearchOverOrderedCustomLinesArray(fromLineNumber);
let startIndexOfInsertion: number;
if (candidateStartIndexOfInsertion >= 0) {
for (let i = candidateStartIndexOfInsertion - 1; i >= 0; i--) {
if (this._orderedCustomLines[i].lineNumber === fromLineNumber) {
startIndexOfInsertion--;
break;
}
}
}
const decorationsImmediatelyAfter = new Set<string>();
for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) {
}
for (let i = startIndexOfInsertion - 1; i >= 0; i--) {
decorationsImmediatelyBefore.add(this._orderedCustomLines[i].decorationId);
}
}
const decorationsWithGaps = intersection(decorationsImmediatelyBefore, decorationsImmediatelyAfter);
lineHeights.ts ×8
const prefixSumToAdd = insertCount * this._defaultLineHeight;
for (let i = startIndexOfInsertion; i < this._orderedCustomLines.length; i++) {
this._orderedCustomLines[i].prefixSum += prefixSumToAdd;
}
if (decorationsWithGaps.size > 0) {
const decoration = this._decorationIDToCustomLine.get(decorationId);
if (decoration) {
const startLineNumber = decoration.reduce((min, l) => Math.min(min, l.lineNumber), fromLineNumber); // min
const endLineNumber = decoration.reduce((max, l) => Math.max(max, l.lineNumber), fromLineNumber); // max
const lineHeight = decoration.reduce((max, l) => Math.max(max, l.specialHeight), 0);
toReAdd.push({
decorationId,
startLineNumber,
endLineNumber,
lineHeight
});
}
}
for (const dec of toReAdd) {
this._doInsertOrChangeCustomLineHeight(dec.decorationId, dec.startLineNumber, dec.endLineNumber, dec.lineHeight, stagedInserts, stagedIdMap);
}
}
private _binarySearchOverOrderedCustomLinesArray(lineNumber: number): number {
if (line.lineNumber === lineNumber) {
}
}
export class CustomLineHeightData {
constructor(
readonly startLineNumber: number,
readonly endLineNumber: number,
readonly lineHeight: number
) { }
public static fromDecorations(decorations: IModelDecoration[], coordinatesConverter: ICoordinatesConverter, configuration: IEditorConfiguration): CustomLineHeightData[] {
const defaultLineHeight = configuration.options.get(EditorOption.lineHeight);
return decorations.map((d) => {
const viewRange = coordinatesConverter.convertModelRangeToViewRange(d.range);
return new CustomLineHeightData(
d.id,
viewRange.startLineNumber,
viewRange.endLineNumber,
d.options.lineHeight ? d.options.lineHeight * defaultLineHeight : 0
);
});
}
class ArrayMap<K, T> {
private _map: Map<K, T[]> = new Map<K, T[]>();
constructor() { }
add(key: K, value: T) {
if (!array) {
this._map.set(key, [value]);
} else {
}
get(key: K): T[] | undefined {
}
delete(key: K): void {
}
clear(): void {
}