Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/viewLayout/linesLayout.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 { IEditorWhitespace, IPartialViewLinesViewportData, ILineHeightChangeAccessor, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from '../viewModel.js';
7
import * as strings from '../../../base/common/strings.js';
8
import { ICustomLineHeightData, LineHeightsManager } from './lineHeights.js';
9
10
interface IPendingChange { id: string; newAfterLineNumber: number; newHeight: number }
11
interface IPendingRemove { id: string }
12
13
class PendingChanges {
14
private _hasPending: boolean;
15
private _inserts: EditorWhitespace[];
16
private _changes: IPendingChange[];
17
private _removes: IPendingRemove[];
18
19
constructor() {
20
this._hasPending = false;
21
this._inserts = [];
22
this._changes = [];
23
this._removes = [];
24
}
25
26
public insert(x: EditorWhitespace): void {
27
this._hasPending = true;
28
this._inserts.push(x);
29
}
30
31
public change(x: IPendingChange): void {
32
this._hasPending = true;
33
this._changes.push(x);
34
}
35
36
public remove(x: IPendingRemove): void {
37
this._hasPending = true;
38
this._removes.push(x);
39
}
40
41
public commit(linesLayout: LinesLayout): void {
42
if (!this._hasPending) {
43
return;
44
}
45
46
const inserts = this._inserts;
47
const changes = this._changes;
48
const removes = this._removes;
49
50
this._hasPending = false;
51
this._inserts = [];
52
this._changes = [];
53
this._removes = [];
54
55
linesLayout._commitPendingChanges(inserts, changes, removes);
56
}
57
}
58
59
export class EditorWhitespace implements IEditorWhitespace {
60
public id: string;
61
public afterLineNumber: number;
62
public ordinal: number;
63
public height: number;
64
public minWidth: number;
65
public prefixSum: number;
66
67
constructor(id: string, afterLineNumber: number, ordinal: number, height: number, minWidth: number) {
68
this.id = id;
69
this.afterLineNumber = afterLineNumber;
70
this.ordinal = ordinal;
71
this.height = height;
72
this.minWidth = minWidth;
73
this.prefixSum = 0;
74
}
75
}
76
77
/**
78
* Layouting of objects that take vertical space (by having a height) and push down other objects.
79
*
80
* These objects are basically either text (lines) or spaces between those lines (whitespaces).
81
* This provides commodity operations for working with lines that contain whitespace that pushes lines lower (vertically).
82
*/
83
export class LinesLayout {
84
85
private static INSTANCE_COUNT = 0;
86
87
private readonly _instanceId: string;
88
private readonly _pendingChanges: PendingChanges;
89
private _lastWhitespaceId: number;
90
private _arr: EditorWhitespace[];
91
private _prefixSumValidIndex: number;
92
private _minWidth: number;
93
private _lineCount: number;
94
private _paddingTop: number;
95
private _paddingBottom: number;
96
private _lineHeightsManager: LineHeightsManager;
97
98
constructor(lineCount: number, defaultLineHeight: number, paddingTop: number, paddingBottom: number, customLineHeightData: ICustomLineHeightData[]) {
99
this._instanceId = strings.singleLetterHash(++LinesLayout.INSTANCE_COUNT);
100
this._pendingChanges = new PendingChanges();
101
this._lastWhitespaceId = 0;
102
this._arr = [];
103
this._prefixSumValidIndex = -1;
104
this._minWidth = -1; /* marker for not being computed */
105
this._lineCount = lineCount;
106
this._paddingTop = paddingTop;
107
this._paddingBottom = paddingBottom;
108
this._lineHeightsManager = new LineHeightsManager(defaultLineHeight, customLineHeightData);
109
}
110
111
/**
112
* Find the insertion index for a new value inside a sorted array of values.
113
* If the value is already present in the sorted array, the insertion index will be after the already existing value.
114
*/
115
public static findInsertionIndex(arr: EditorWhitespace[], afterLineNumber: number, ordinal: number): number {
116
let low = 0;
117
let high = arr.length;
118
119
while (low < high) {
120
const mid = ((low + high) >>> 1);
121
122
if (afterLineNumber === arr[mid].afterLineNumber) {
123
if (ordinal < arr[mid].ordinal) {
124
high = mid;
125
} else {
126
low = mid + 1;
127
}
128
} else if (afterLineNumber < arr[mid].afterLineNumber) {
129
high = mid;
130
} else {
131
low = mid + 1;
132
}
133
}
134
135
return low;
136
}
137
138
/**
139
* Change the height of a line in pixels.
140
*/
141
public setDefaultLineHeight(lineHeight: number): void {
142
this._lineHeightsManager.defaultLineHeight = lineHeight;
143
}
144
145
/**
146
* Changes the padding used to calculate vertical offsets.
147
*/
148
public setPadding(paddingTop: number, paddingBottom: number): void {
149
this._paddingTop = paddingTop;
150
this._paddingBottom = paddingBottom;
151
}
152
153
/**
154
* Set the number of lines.
155
*
156
* @param lineCount New number of lines.
157
*/
158
public onFlushed(lineCount: number, customLineHeightData: ICustomLineHeightData[]): void {
159
this._lineCount = lineCount;
160
this._lineHeightsManager = new LineHeightsManager(this._lineHeightsManager.defaultLineHeight, customLineHeightData);
161
}
162
163
public changeLineHeights(callback: (accessor: ILineHeightChangeAccessor) => void): boolean {
164
let hadAChange = false;
165
try {
166
const accessor: ILineHeightChangeAccessor = {
167
insertOrChangeCustomLineHeight: (decorationId: string, startLineNumber: number, endLineNumber: number, lineHeight: number): void => {
168
hadAChange = true;
169
this._lineHeightsManager.insertOrChangeCustomLineHeight(decorationId, startLineNumber, endLineNumber, lineHeight);
170
},
171
removeCustomLineHeight: (decorationId: string): void => {
172
hadAChange = true;
173
this._lineHeightsManager.removeCustomLineHeight(decorationId);
174
}
175
};
176
callback(accessor);
177
} finally {
178
this._lineHeightsManager.commit();
179
}
180
return hadAChange;
181
}
182
183
public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): boolean {
184
let hadAChange = false;
185
try {
186
const accessor: IWhitespaceChangeAccessor = {
187
insertWhitespace: (afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string => {
188
hadAChange = true;
189
afterLineNumber = afterLineNumber | 0;
190
ordinal = ordinal | 0;
191
heightInPx = heightInPx | 0;
192
minWidth = minWidth | 0;
193
const id = this._instanceId + (++this._lastWhitespaceId);
194
this._pendingChanges.insert(new EditorWhitespace(id, afterLineNumber, ordinal, heightInPx, minWidth));
195
return id;
196
},
197
changeOneWhitespace: (id: string, newAfterLineNumber: number, newHeight: number): void => {
198
hadAChange = true;
199
newAfterLineNumber = newAfterLineNumber | 0;
200
newHeight = newHeight | 0;
201
this._pendingChanges.change({ id, newAfterLineNumber, newHeight });
202
},
203
removeWhitespace: (id: string): void => {
204
hadAChange = true;
205
this._pendingChanges.remove({ id });
206
}
207
};
208
callback(accessor);
209
} finally {
210
this._pendingChanges.commit(this);
211
}
212
return hadAChange;
213
}
214
215
public _commitPendingChanges(inserts: EditorWhitespace[], changes: IPendingChange[], removes: IPendingRemove[]): void {
216
if (inserts.length > 0 || removes.length > 0) {
217
this._minWidth = -1; /* marker for not being computed */
218
}
219
220
if (inserts.length + changes.length + removes.length <= 1) {
221
// when only one thing happened, handle it "delicately"
222
for (const insert of inserts) {
223
this._insertWhitespace(insert);
224
}
225
for (const change of changes) {
226
this._changeOneWhitespace(change.id, change.newAfterLineNumber, change.newHeight);
227
}
228
for (const remove of removes) {
229
const index = this._findWhitespaceIndex(remove.id);
230
if (index === -1) {
231
continue;
232
}
233
this._removeWhitespace(index);
234
}
235
return;
236
}
237
238
// simply rebuild the entire datastructure
239
240
const toRemove = new Set<string>();
241
for (const remove of removes) {
242
toRemove.add(remove.id);
243
}
244
245
const toChange = new Map<string, IPendingChange>();
246
for (const change of changes) {
247
toChange.set(change.id, change);
248
}
249
250
const applyRemoveAndChange = (whitespaces: EditorWhitespace[]): EditorWhitespace[] => {
251
const result: EditorWhitespace[] = [];
252
for (const whitespace of whitespaces) {
253
if (toRemove.has(whitespace.id)) {
254
continue;
255
}
256
if (toChange.has(whitespace.id)) {
257
const change = toChange.get(whitespace.id)!;
258
whitespace.afterLineNumber = change.newAfterLineNumber;
259
whitespace.height = change.newHeight;
260
}
261
result.push(whitespace);
262
}
263
return result;
264
};
265
266
const result = applyRemoveAndChange(this._arr).concat(applyRemoveAndChange(inserts));
267
result.sort((a, b) => {
268
if (a.afterLineNumber === b.afterLineNumber) {
269
return a.ordinal - b.ordinal;
270
}
271
return a.afterLineNumber - b.afterLineNumber;
272
});
273
274
this._arr = result;
275
this._prefixSumValidIndex = -1;
276
}
277
278
private _insertWhitespace(whitespace: EditorWhitespace): void {
279
const insertIndex = LinesLayout.findInsertionIndex(this._arr, whitespace.afterLineNumber, whitespace.ordinal);
280
this._arr.splice(insertIndex, 0, whitespace);
281
this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, insertIndex - 1);
282
}
283
284
private _findWhitespaceIndex(id: string): number {
285
const arr = this._arr;
286
for (let i = 0, len = arr.length; i < len; i++) {
287
if (arr[i].id === id) {
288
return i;
289
}
290
}
291
return -1;
292
}
293
294
private _changeOneWhitespace(id: string, newAfterLineNumber: number, newHeight: number): void {
295
const index = this._findWhitespaceIndex(id);
296
if (index === -1) {
297
return;
298
}
299
if (this._arr[index].height !== newHeight) {
300
this._arr[index].height = newHeight;
301
this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, index - 1);
302
}
303
if (this._arr[index].afterLineNumber !== newAfterLineNumber) {
304
// `afterLineNumber` changed for this whitespace
305
306
// Record old whitespace
307
const whitespace = this._arr[index];
308
309
// Since changing `afterLineNumber` can trigger a reordering, we're gonna remove this whitespace
310
this._removeWhitespace(index);
311
312
whitespace.afterLineNumber = newAfterLineNumber;
313
314
// And add it again
315
this._insertWhitespace(whitespace);
316
}
317
}
318
319
private _removeWhitespace(removeIndex: number): void {
320
this._arr.splice(removeIndex, 1);
321
this._prefixSumValidIndex = Math.min(this._prefixSumValidIndex, removeIndex - 1);
322
}
323
324
/**
325
* Notify the layouter that lines have been deleted (a continuous zone of lines).
326
*
327
* @param fromLineNumber The line number at which the deletion started, inclusive
328
* @param toLineNumber The line number at which the deletion ended, inclusive
329
*/
330
public onLinesDeleted(fromLineNumber: number, toLineNumber: number): void {
331
fromLineNumber = fromLineNumber | 0;
332
toLineNumber = toLineNumber | 0;
333
334
this._lineCount -= (toLineNumber - fromLineNumber + 1);
335
for (let i = 0, len = this._arr.length; i < len; i++) {
336
const afterLineNumber = this._arr[i].afterLineNumber;
337
338
if (fromLineNumber <= afterLineNumber && afterLineNumber <= toLineNumber) {
339
// The line this whitespace was after has been deleted
340
// => move whitespace to before first deleted line
341
this._arr[i].afterLineNumber = fromLineNumber - 1;
342
} else if (afterLineNumber > toLineNumber) {
343
// The line this whitespace was after has been moved up
344
// => move whitespace up
345
this._arr[i].afterLineNumber -= (toLineNumber - fromLineNumber + 1);
346
}
347
}
348
this._lineHeightsManager.onLinesDeleted(fromLineNumber, toLineNumber);
349
}
350
351
/**
352
* Notify the layouter that lines have been inserted (a continuous zone of lines).
353
*
354
* @param fromLineNumber The line number at which the insertion started, inclusive
355
* @param toLineNumber The line number at which the insertion ended, inclusive.
356
*/
357
public onLinesInserted(fromLineNumber: number, toLineNumber: number): void {
358
fromLineNumber = fromLineNumber | 0;
359
toLineNumber = toLineNumber | 0;
360
361
this._lineCount += (toLineNumber - fromLineNumber + 1);
362
for (let i = 0, len = this._arr.length; i < len; i++) {
363
const afterLineNumber = this._arr[i].afterLineNumber;
364
365
if (fromLineNumber <= afterLineNumber) {
366
this._arr[i].afterLineNumber += (toLineNumber - fromLineNumber + 1);
367
}
368
}
369
this._lineHeightsManager.onLinesInserted(fromLineNumber, toLineNumber);
370
}
371
372
/**
373
* Get the sum of all the whitespaces.
374
*/
375
public getWhitespacesTotalHeight(): number {
376
if (this._arr.length === 0) {
377
return 0;
378
}
379
return this.getWhitespacesAccumulatedHeight(this._arr.length - 1);
380
}
381
382
/**
383
* Return the sum of the heights of the whitespaces at [0..index].
384
* This includes the whitespace at `index`.
385
*
386
* @param index The index of the whitespace.
387
* @return The sum of the heights of all whitespaces before the one at `index`, including the one at `index`.
388
*/
389
public getWhitespacesAccumulatedHeight(index: number): number {
390
index = index | 0;
391
392
let startIndex = Math.max(0, this._prefixSumValidIndex + 1);
393
if (startIndex === 0) {
394
this._arr[0].prefixSum = this._arr[0].height;
395
startIndex++;
396
}
397
398
for (let i = startIndex; i <= index; i++) {
399
this._arr[i].prefixSum = this._arr[i - 1].prefixSum + this._arr[i].height;
400
}
401
this._prefixSumValidIndex = Math.max(this._prefixSumValidIndex, index);
402
return this._arr[index].prefixSum;
403
}
404
405
/**
406
* Get the sum of heights for all objects.
407
*
408
* @return The sum of heights for all objects.
409
*/
410
public getLinesTotalHeight(): number {
411
const linesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(this._lineCount);
412
const whitespacesHeight = this.getWhitespacesTotalHeight();
413
414
return linesHeight + whitespacesHeight + this._paddingTop + this._paddingBottom;
415
}
416
417
/**
418
* Returns the accumulated height of whitespaces before the given line number.
419
*
420
* @param lineNumber The line number
421
*/
422
public getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber: number): number {
423
lineNumber = lineNumber | 0;
424
425
const lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeLineNumber(lineNumber);
426
427
if (lastWhitespaceBeforeLineNumber === -1) {
428
return 0;
429
}
430
431
return this.getWhitespacesAccumulatedHeight(lastWhitespaceBeforeLineNumber);
432
}
433
434
private _findLastWhitespaceBeforeLineNumber(lineNumber: number): number {
435
lineNumber = lineNumber | 0;
436
437
// Find the whitespace before line number
438
const arr = this._arr;
439
let low = 0;
440
let high = arr.length - 1;
441
442
while (low <= high) {
443
const delta = (high - low) | 0;
444
const halfDelta = (delta / 2) | 0;
445
const mid = (low + halfDelta) | 0;
446
447
if (arr[mid].afterLineNumber < lineNumber) {
448
if (mid + 1 >= arr.length || arr[mid + 1].afterLineNumber >= lineNumber) {
449
return mid;
450
} else {
451
low = (mid + 1) | 0;
452
}
453
} else {
454
high = (mid - 1) | 0;
455
}
456
}
457
458
return -1;
459
}
460
461
private _findFirstWhitespaceAfterLineNumber(lineNumber: number): number {
462
lineNumber = lineNumber | 0;
463
464
const lastWhitespaceBeforeLineNumber = this._findLastWhitespaceBeforeLineNumber(lineNumber);
465
const firstWhitespaceAfterLineNumber = lastWhitespaceBeforeLineNumber + 1;
466
467
if (firstWhitespaceAfterLineNumber < this._arr.length) {
468
return firstWhitespaceAfterLineNumber;
469
}
470
471
return -1;
472
}
473
474
/**
475
* Find the index of the first whitespace which has `afterLineNumber` >= `lineNumber`.
476
* @return The index of the first whitespace with `afterLineNumber` >= `lineNumber` or -1 if no whitespace is found.
477
*/
478
public getFirstWhitespaceIndexAfterLineNumber(lineNumber: number): number {
479
lineNumber = lineNumber | 0;
480
481
return this._findFirstWhitespaceAfterLineNumber(lineNumber);
482
}
483
484
/**
485
* Get the vertical offset (the sum of heights for all objects above) a certain line number.
486
*
487
* @param lineNumber The line number
488
* @return The sum of heights for all objects above `lineNumber`.
489
*/
490
public getVerticalOffsetForLineNumber(lineNumber: number, includeViewZones = false): number {
491
lineNumber = lineNumber | 0;
492
493
let previousLinesHeight: number;
494
if (lineNumber > 1) {
495
previousLinesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(lineNumber - 1);
496
} else {
497
previousLinesHeight = 0;
498
}
499
500
const previousWhitespacesHeight = this.getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber - (includeViewZones ? 1 : 0));
501
502
return previousLinesHeight + previousWhitespacesHeight + this._paddingTop;
503
}
504
505
public getLineHeightForLineNumber(lineNumber: number): number {
506
return this._lineHeightsManager.heightForLineNumber(lineNumber);
507
}
508
509
/**
510
* Get the vertical offset (the sum of heights for all objects above) a certain line number and also the line height of the line.
511
*
512
* @param lineNumber The line number
513
* @return The sum of heights for all objects above `lineNumber`.
514
*/
515
public getVerticalOffsetAfterLineNumber(lineNumber: number, includeViewZones = false): number {
516
lineNumber = lineNumber | 0;
517
const previousLinesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(lineNumber);
518
const previousWhitespacesHeight = this.getWhitespaceAccumulatedHeightBeforeLineNumber(lineNumber + (includeViewZones ? 1 : 0));
519
return previousLinesHeight + previousWhitespacesHeight + this._paddingTop;
520
}
521
522
/**
523
* Returns if there is any whitespace in the document.
524
*/
525
public hasWhitespace(): boolean {
526
return this.getWhitespacesCount() > 0;
527
}
528
529
/**
530
* The maximum min width for all whitespaces.
531
*/
532
public getWhitespaceMinWidth(): number {
533
if (this._minWidth === -1) {
534
let minWidth = 0;
535
for (let i = 0, len = this._arr.length; i < len; i++) {
536
minWidth = Math.max(minWidth, this._arr[i].minWidth);
537
}
538
this._minWidth = minWidth;
539
}
540
return this._minWidth;
541
}
542
543
/**
544
* Check if `verticalOffset` is below all lines.
545
*/
546
public isAfterLines(verticalOffset: number): boolean {
547
const totalHeight = this.getLinesTotalHeight();
548
return verticalOffset > totalHeight;
549
}
550
551
public isInTopPadding(verticalOffset: number): boolean {
552
if (this._paddingTop === 0) {
553
return false;
554
}
555
return (verticalOffset < this._paddingTop);
556
}
557
558
public isInBottomPadding(verticalOffset: number): boolean {
559
if (this._paddingBottom === 0) {
560
return false;
561
}
562
const totalHeight = this.getLinesTotalHeight();
563
return (verticalOffset >= totalHeight - this._paddingBottom);
564
}
565
566
/**
567
* Find the first line number that is at or after vertical offset `verticalOffset`.
568
* i.e. if getVerticalOffsetForLine(line) is x and getVerticalOffsetForLine(line + 1) is y, then
569
* getLineNumberAtOrAfterVerticalOffset(i) = line, x <= i < y.
570
*
571
* @param verticalOffset The vertical offset to search at.
572
* @return The line number at or after vertical offset `verticalOffset`.
573
*/
574
public getLineNumberAtOrAfterVerticalOffset(verticalOffset: number): number {
575
verticalOffset = verticalOffset | 0;
576
577
if (verticalOffset < 0) {
578
return 1;
579
}
580
581
const linesCount = this._lineCount | 0;
582
let minLineNumber = 1;
583
let maxLineNumber = linesCount;
584
585
while (minLineNumber < maxLineNumber) {
586
const midLineNumber = ((minLineNumber + maxLineNumber) / 2) | 0;
587
588
const lineHeight = this.getLineHeightForLineNumber(midLineNumber);
589
const midLineNumberVerticalOffset = this.getVerticalOffsetForLineNumber(midLineNumber) | 0;
590
591
if (verticalOffset >= midLineNumberVerticalOffset + lineHeight) {
592
// vertical offset is after mid line number
593
minLineNumber = midLineNumber + 1;
594
} else if (verticalOffset >= midLineNumberVerticalOffset) {
595
// Hit
596
return midLineNumber;
597
} else {
598
// vertical offset is before mid line number, but mid line number could still be what we're searching for
599
maxLineNumber = midLineNumber;
600
}
601
}
602
603
if (minLineNumber > linesCount) {
604
return linesCount;
605
}
606
607
return minLineNumber;
608
}
609
610
/**
611
* Get all the lines and their relative vertical offsets that are positioned between `verticalOffset1` and `verticalOffset2`.
612
*
613
* @param verticalOffset1 The beginning of the viewport.
614
* @param verticalOffset2 The end of the viewport.
615
* @return A structure describing the lines positioned between `verticalOffset1` and `verticalOffset2`.
616
*/
617
public getLinesViewportData(verticalOffset1: number, verticalOffset2: number): IPartialViewLinesViewportData {
618
verticalOffset1 = verticalOffset1 | 0;
619
verticalOffset2 = verticalOffset2 | 0;
620
621
// Find first line number
622
// We don't live in a perfect world, so the line number might start before or after verticalOffset1
623
const startLineNumber = this.getLineNumberAtOrAfterVerticalOffset(verticalOffset1) | 0;
624
const startLineNumberVerticalOffset = this.getVerticalOffsetForLineNumber(startLineNumber) | 0;
625
626
let endLineNumber = this._lineCount | 0;
627
628
// Also keep track of what whitespace we've got
629
let whitespaceIndex = this.getFirstWhitespaceIndexAfterLineNumber(startLineNumber) | 0;
630
const whitespaceCount = this.getWhitespacesCount() | 0;
631
let currentWhitespaceHeight: number;
632
let currentWhitespaceAfterLineNumber: number;
633
634
if (whitespaceIndex === -1) {
635
whitespaceIndex = whitespaceCount;
636
currentWhitespaceAfterLineNumber = endLineNumber + 1;
637
currentWhitespaceHeight = 0;
638
} else {
639
currentWhitespaceAfterLineNumber = this.getAfterLineNumberForWhitespaceIndex(whitespaceIndex) | 0;
640
currentWhitespaceHeight = this.getHeightForWhitespaceIndex(whitespaceIndex) | 0;
641
}
642
643
let currentVerticalOffset = startLineNumberVerticalOffset;
644
let currentLineRelativeOffset = currentVerticalOffset;
645
646
// IE (all versions) cannot handle units above about 1,533,908 px, so every 500k pixels bring numbers down
647
const STEP_SIZE = 500000;
648
let bigNumbersDelta = 0;
649
if (startLineNumberVerticalOffset >= STEP_SIZE) {
650
// Compute a delta that guarantees that lines are positioned at `lineHeight` increments
651
bigNumbersDelta = Math.floor(startLineNumberVerticalOffset / STEP_SIZE) * STEP_SIZE;
652
bigNumbersDelta = Math.floor(bigNumbersDelta / this._lineHeightsManager.defaultLineHeight) * this._lineHeightsManager.defaultLineHeight;
653
654
currentLineRelativeOffset -= bigNumbersDelta;
655
}
656
657
const linesOffsets: number[] = [];
658
659
const verticalCenter = verticalOffset1 + (verticalOffset2 - verticalOffset1) / 2;
660
let centeredLineNumber = -1;
661
662
// Figure out how far the lines go
663
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
664
const lineHeight = this.getLineHeightForLineNumber(lineNumber);
665
if (centeredLineNumber === -1) {
666
const currentLineTop = currentVerticalOffset;
667
const currentLineBottom = currentVerticalOffset + lineHeight;
668
if ((currentLineTop <= verticalCenter && verticalCenter < currentLineBottom) || currentLineTop > verticalCenter) {
669
centeredLineNumber = lineNumber;
670
}
671
}
672
673
// Count current line height in the vertical offsets
674
currentVerticalOffset += lineHeight;
675
linesOffsets[lineNumber - startLineNumber] = currentLineRelativeOffset;
676
677
// Next line starts immediately after this one
678
currentLineRelativeOffset += lineHeight;
679
while (currentWhitespaceAfterLineNumber === lineNumber) {
680
// Push down next line with the height of the current whitespace
681
currentLineRelativeOffset += currentWhitespaceHeight;
682
683
// Count current whitespace in the vertical offsets
684
currentVerticalOffset += currentWhitespaceHeight;
685
whitespaceIndex++;
686
687
if (whitespaceIndex >= whitespaceCount) {
688
currentWhitespaceAfterLineNumber = endLineNumber + 1;
689
} else {
690
currentWhitespaceAfterLineNumber = this.getAfterLineNumberForWhitespaceIndex(whitespaceIndex) | 0;
691
currentWhitespaceHeight = this.getHeightForWhitespaceIndex(whitespaceIndex) | 0;
692
}
693
}
694
695
if (currentVerticalOffset >= verticalOffset2) {
696
// We have covered the entire viewport area, time to stop
697
endLineNumber = lineNumber;
698
break;
699
}
700
}
701
702
if (centeredLineNumber === -1) {
703
centeredLineNumber = endLineNumber;
704
}
705
706
const endLineNumberVerticalOffset = this.getVerticalOffsetForLineNumber(endLineNumber) | 0;
707
708
let completelyVisibleStartLineNumber = startLineNumber;
709
let completelyVisibleEndLineNumber = endLineNumber;
710
711
if (completelyVisibleStartLineNumber < completelyVisibleEndLineNumber) {
712
if (startLineNumberVerticalOffset < verticalOffset1) {
713
completelyVisibleStartLineNumber++;
714
}
715
}
716
if (completelyVisibleStartLineNumber < completelyVisibleEndLineNumber) {
717
const endLineHeight = this.getLineHeightForLineNumber(endLineNumber);
718
if (endLineNumberVerticalOffset + endLineHeight > verticalOffset2) {
719
completelyVisibleEndLineNumber--;
720
}
721
}
722
723
return {
724
bigNumbersDelta: bigNumbersDelta,
725
startLineNumber: startLineNumber,
726
endLineNumber: endLineNumber,
727
relativeVerticalOffset: linesOffsets,
728
centeredLineNumber: centeredLineNumber,
729
completelyVisibleStartLineNumber: completelyVisibleStartLineNumber,
730
completelyVisibleEndLineNumber: completelyVisibleEndLineNumber,
731
lineHeight: this._lineHeightsManager.defaultLineHeight,
732
};
733
}
734
735
public getVerticalOffsetForWhitespaceIndex(whitespaceIndex: number): number {
736
whitespaceIndex = whitespaceIndex | 0;
737
738
const afterLineNumber = this.getAfterLineNumberForWhitespaceIndex(whitespaceIndex);
739
740
let previousLinesHeight: number;
741
if (afterLineNumber >= 1) {
742
previousLinesHeight = this._lineHeightsManager.getAccumulatedLineHeightsIncludingLineNumber(afterLineNumber);
743
} else {
744
previousLinesHeight = 0;
745
}
746
747
let previousWhitespacesHeight: number;
748
if (whitespaceIndex > 0) {
749
previousWhitespacesHeight = this.getWhitespacesAccumulatedHeight(whitespaceIndex - 1);
750
} else {
751
previousWhitespacesHeight = 0;
752
}
753
return previousLinesHeight + previousWhitespacesHeight + this._paddingTop;
754
}
755
756
public getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset: number): number {
757
verticalOffset = verticalOffset | 0;
758
759
let minWhitespaceIndex = 0;
760
let maxWhitespaceIndex = this.getWhitespacesCount() - 1;
761
762
if (maxWhitespaceIndex < 0) {
763
return -1;
764
}
765
766
// Special case: nothing to be found
767
const maxWhitespaceVerticalOffset = this.getVerticalOffsetForWhitespaceIndex(maxWhitespaceIndex);
768
const maxWhitespaceHeight = this.getHeightForWhitespaceIndex(maxWhitespaceIndex);
769
if (verticalOffset >= maxWhitespaceVerticalOffset + maxWhitespaceHeight) {
770
return -1;
771
}
772
773
while (minWhitespaceIndex < maxWhitespaceIndex) {
774
const midWhitespaceIndex = Math.floor((minWhitespaceIndex + maxWhitespaceIndex) / 2);
775
776
const midWhitespaceVerticalOffset = this.getVerticalOffsetForWhitespaceIndex(midWhitespaceIndex);
777
const midWhitespaceHeight = this.getHeightForWhitespaceIndex(midWhitespaceIndex);
778
779
if (verticalOffset >= midWhitespaceVerticalOffset + midWhitespaceHeight) {
780
// vertical offset is after whitespace
781
minWhitespaceIndex = midWhitespaceIndex + 1;
782
} else if (verticalOffset >= midWhitespaceVerticalOffset) {
783
// Hit
784
return midWhitespaceIndex;
785
} else {
786
// vertical offset is before whitespace, but midWhitespaceIndex might still be what we're searching for
787
maxWhitespaceIndex = midWhitespaceIndex;
788
}
789
}
790
return minWhitespaceIndex;
791
}
792
793
/**
794
* Get exactly the whitespace that is layouted at `verticalOffset`.
795
*
796
* @param verticalOffset The vertical offset.
797
* @return Precisely the whitespace that is layouted at `verticaloffset` or null.
798
*/
799
public getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null {
800
verticalOffset = verticalOffset | 0;
801
802
const candidateIndex = this.getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset);
803
804
if (candidateIndex < 0) {
805
return null;
806
}
807
808
if (candidateIndex >= this.getWhitespacesCount()) {
809
return null;
810
}
811
812
const candidateTop = this.getVerticalOffsetForWhitespaceIndex(candidateIndex);
813
814
if (candidateTop > verticalOffset) {
815
return null;
816
}
817
818
const candidateHeight = this.getHeightForWhitespaceIndex(candidateIndex);
819
const candidateId = this.getIdForWhitespaceIndex(candidateIndex);
820
const candidateAfterLineNumber = this.getAfterLineNumberForWhitespaceIndex(candidateIndex);
821
822
return {
823
id: candidateId,
824
afterLineNumber: candidateAfterLineNumber,
825
verticalOffset: candidateTop,
826
height: candidateHeight
827
};
828
}
829
830
/**
831
* Get a list of whitespaces that are positioned between `verticalOffset1` and `verticalOffset2`.
832
*
833
* @param verticalOffset1 The beginning of the viewport.
834
* @param verticalOffset2 The end of the viewport.
835
* @return An array with all the whitespaces in the viewport. If no whitespace is in viewport, the array is empty.
836
*/
837
public getWhitespaceViewportData(verticalOffset1: number, verticalOffset2: number): IViewWhitespaceViewportData[] {
838
verticalOffset1 = verticalOffset1 | 0;
839
verticalOffset2 = verticalOffset2 | 0;
840
841
const startIndex = this.getWhitespaceIndexAtOrAfterVerticallOffset(verticalOffset1);
842
const endIndex = this.getWhitespacesCount() - 1;
843
844
if (startIndex < 0) {
845
return [];
846
}
847
848
const result: IViewWhitespaceViewportData[] = [];
849
for (let i = startIndex; i <= endIndex; i++) {
850
const top = this.getVerticalOffsetForWhitespaceIndex(i);
851
const height = this.getHeightForWhitespaceIndex(i);
852
if (top >= verticalOffset2) {
853
break;
854
}
855
856
result.push({
857
id: this.getIdForWhitespaceIndex(i),
858
afterLineNumber: this.getAfterLineNumberForWhitespaceIndex(i),
859
verticalOffset: top,
860
height: height
861
});
862
}
863
864
return result;
865
}
866
867
/**
868
* Get all whitespaces.
869
*/
870
public getWhitespaces(): IEditorWhitespace[] {
871
return this._arr.slice(0);
872
}
873
874
/**
875
* The number of whitespaces.
876
*/
877
public getWhitespacesCount(): number {
878
return this._arr.length;
879
}
880
881
/**
882
* Get the `id` for whitespace at index `index`.
883
*
884
* @param index The index of the whitespace.
885
* @return `id` of whitespace at `index`.
886
*/
887
public getIdForWhitespaceIndex(index: number): string {
888
index = index | 0;
889
890
return this._arr[index].id;
891
}
892
893
/**
894
* Get the `afterLineNumber` for whitespace at index `index`.
895
*
896
* @param index The index of the whitespace.
897
* @return `afterLineNumber` of whitespace at `index`.
898
*/
899
public getAfterLineNumberForWhitespaceIndex(index: number): number {
900
index = index | 0;
901
902
return this._arr[index].afterLineNumber;
903
}
904
905
/**
906
* Get the `height` for whitespace at index `index`.
907
*
908
* @param index The index of the whitespace.
909
* @return `height` of whitespace at `index`.
910
*/
911
public getHeightForWhitespaceIndex(index: number): number {
912
index = index | 0;
913
914
return this._arr[index].height;
915
}
916
}
917
918