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