Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/folding/browser/foldingRanges.ts
3296 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 { SelectedLines } from './folding.js';
7
8
export interface ILineRange {
9
startLineNumber: number;
10
endLineNumber: number;
11
}
12
13
export const enum FoldSource {
14
provider = 0,
15
userDefined = 1,
16
recovered = 2
17
}
18
19
export const foldSourceAbbr = {
20
[FoldSource.provider]: ' ',
21
[FoldSource.userDefined]: 'u',
22
[FoldSource.recovered]: 'r',
23
};
24
25
export interface FoldRange {
26
startLineNumber: number;
27
endLineNumber: number;
28
type: string | undefined;
29
isCollapsed: boolean;
30
source: FoldSource;
31
}
32
33
export const MAX_FOLDING_REGIONS = 0xFFFF;
34
export const MAX_LINE_NUMBER = 0xFFFFFF;
35
36
const MASK_INDENT = 0xFF000000;
37
38
class BitField {
39
private readonly _states: Uint32Array;
40
constructor(size: number) {
41
const numWords = Math.ceil(size / 32);
42
this._states = new Uint32Array(numWords);
43
}
44
45
public get(index: number): boolean {
46
const arrayIndex = (index / 32) | 0;
47
const bit = index % 32;
48
return (this._states[arrayIndex] & (1 << bit)) !== 0;
49
}
50
51
public set(index: number, newState: boolean) {
52
const arrayIndex = (index / 32) | 0;
53
const bit = index % 32;
54
const value = this._states[arrayIndex];
55
if (newState) {
56
this._states[arrayIndex] = value | (1 << bit);
57
} else {
58
this._states[arrayIndex] = value & ~(1 << bit);
59
}
60
}
61
}
62
63
export class FoldingRegions {
64
private readonly _startIndexes: Uint32Array;
65
private readonly _endIndexes: Uint32Array;
66
private readonly _collapseStates: BitField;
67
private readonly _userDefinedStates: BitField;
68
private readonly _recoveredStates: BitField;
69
70
private _parentsComputed: boolean;
71
private readonly _types: Array<string | undefined> | undefined;
72
73
constructor(startIndexes: Uint32Array, endIndexes: Uint32Array, types?: Array<string | undefined>) {
74
if (startIndexes.length !== endIndexes.length || startIndexes.length > MAX_FOLDING_REGIONS) {
75
throw new Error('invalid startIndexes or endIndexes size');
76
}
77
this._startIndexes = startIndexes;
78
this._endIndexes = endIndexes;
79
this._collapseStates = new BitField(startIndexes.length);
80
this._userDefinedStates = new BitField(startIndexes.length);
81
this._recoveredStates = new BitField(startIndexes.length);
82
this._types = types;
83
this._parentsComputed = false;
84
}
85
86
private ensureParentIndices() {
87
if (!this._parentsComputed) {
88
this._parentsComputed = true;
89
const parentIndexes: number[] = [];
90
const isInsideLast = (startLineNumber: number, endLineNumber: number) => {
91
const index = parentIndexes[parentIndexes.length - 1];
92
return this.getStartLineNumber(index) <= startLineNumber && this.getEndLineNumber(index) >= endLineNumber;
93
};
94
for (let i = 0, len = this._startIndexes.length; i < len; i++) {
95
const startLineNumber = this._startIndexes[i];
96
const endLineNumber = this._endIndexes[i];
97
if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) {
98
throw new Error('startLineNumber or endLineNumber must not exceed ' + MAX_LINE_NUMBER);
99
}
100
while (parentIndexes.length > 0 && !isInsideLast(startLineNumber, endLineNumber)) {
101
parentIndexes.pop();
102
}
103
const parentIndex = parentIndexes.length > 0 ? parentIndexes[parentIndexes.length - 1] : -1;
104
parentIndexes.push(i);
105
this._startIndexes[i] = startLineNumber + ((parentIndex & 0xFF) << 24);
106
this._endIndexes[i] = endLineNumber + ((parentIndex & 0xFF00) << 16);
107
}
108
}
109
}
110
111
public get length(): number {
112
return this._startIndexes.length;
113
}
114
115
public getStartLineNumber(index: number): number {
116
return this._startIndexes[index] & MAX_LINE_NUMBER;
117
}
118
119
public getEndLineNumber(index: number): number {
120
return this._endIndexes[index] & MAX_LINE_NUMBER;
121
}
122
123
public getType(index: number): string | undefined {
124
return this._types ? this._types[index] : undefined;
125
}
126
127
public hasTypes() {
128
return !!this._types;
129
}
130
131
public isCollapsed(index: number): boolean {
132
return this._collapseStates.get(index);
133
}
134
135
public setCollapsed(index: number, newState: boolean) {
136
this._collapseStates.set(index, newState);
137
}
138
139
private isUserDefined(index: number): boolean {
140
return this._userDefinedStates.get(index);
141
}
142
143
private setUserDefined(index: number, newState: boolean) {
144
return this._userDefinedStates.set(index, newState);
145
}
146
147
private isRecovered(index: number): boolean {
148
return this._recoveredStates.get(index);
149
}
150
151
private setRecovered(index: number, newState: boolean) {
152
return this._recoveredStates.set(index, newState);
153
}
154
155
public getSource(index: number): FoldSource {
156
if (this.isUserDefined(index)) {
157
return FoldSource.userDefined;
158
} else if (this.isRecovered(index)) {
159
return FoldSource.recovered;
160
}
161
return FoldSource.provider;
162
}
163
164
public setSource(index: number, source: FoldSource): void {
165
if (source === FoldSource.userDefined) {
166
this.setUserDefined(index, true);
167
this.setRecovered(index, false);
168
} else if (source === FoldSource.recovered) {
169
this.setUserDefined(index, false);
170
this.setRecovered(index, true);
171
} else {
172
this.setUserDefined(index, false);
173
this.setRecovered(index, false);
174
}
175
}
176
177
public setCollapsedAllOfType(type: string, newState: boolean) {
178
let hasChanged = false;
179
if (this._types) {
180
for (let i = 0; i < this._types.length; i++) {
181
if (this._types[i] === type) {
182
this.setCollapsed(i, newState);
183
hasChanged = true;
184
}
185
}
186
}
187
return hasChanged;
188
}
189
190
public toRegion(index: number): FoldingRegion {
191
return new FoldingRegion(this, index);
192
}
193
194
public getParentIndex(index: number) {
195
this.ensureParentIndices();
196
const parent = ((this._startIndexes[index] & MASK_INDENT) >>> 24) + ((this._endIndexes[index] & MASK_INDENT) >>> 16);
197
if (parent === MAX_FOLDING_REGIONS) {
198
return -1;
199
}
200
return parent;
201
}
202
203
public contains(index: number, line: number) {
204
return this.getStartLineNumber(index) <= line && this.getEndLineNumber(index) >= line;
205
}
206
207
private findIndex(line: number) {
208
let low = 0, high = this._startIndexes.length;
209
if (high === 0) {
210
return -1; // no children
211
}
212
while (low < high) {
213
const mid = Math.floor((low + high) / 2);
214
if (line < this.getStartLineNumber(mid)) {
215
high = mid;
216
} else {
217
low = mid + 1;
218
}
219
}
220
return low - 1;
221
}
222
223
public findRange(line: number): number {
224
let index = this.findIndex(line);
225
if (index >= 0) {
226
const endLineNumber = this.getEndLineNumber(index);
227
if (endLineNumber >= line) {
228
return index;
229
}
230
index = this.getParentIndex(index);
231
while (index !== -1) {
232
if (this.contains(index, line)) {
233
return index;
234
}
235
index = this.getParentIndex(index);
236
}
237
}
238
return -1;
239
}
240
241
242
public toString() {
243
const res: string[] = [];
244
for (let i = 0; i < this.length; i++) {
245
res[i] = `[${foldSourceAbbr[this.getSource(i)]}${this.isCollapsed(i) ? '+' : '-'}] ${this.getStartLineNumber(i)}/${this.getEndLineNumber(i)}`;
246
}
247
return res.join(', ');
248
}
249
250
public toFoldRange(index: number): FoldRange {
251
return {
252
startLineNumber: this._startIndexes[index] & MAX_LINE_NUMBER,
253
endLineNumber: this._endIndexes[index] & MAX_LINE_NUMBER,
254
type: this._types ? this._types[index] : undefined,
255
isCollapsed: this.isCollapsed(index),
256
source: this.getSource(index)
257
};
258
}
259
260
public static fromFoldRanges(ranges: FoldRange[]): FoldingRegions {
261
const rangesLength = ranges.length;
262
const startIndexes = new Uint32Array(rangesLength);
263
const endIndexes = new Uint32Array(rangesLength);
264
let types: Array<string | undefined> | undefined = [];
265
let gotTypes = false;
266
for (let i = 0; i < rangesLength; i++) {
267
const range = ranges[i];
268
startIndexes[i] = range.startLineNumber;
269
endIndexes[i] = range.endLineNumber;
270
types.push(range.type);
271
if (range.type) {
272
gotTypes = true;
273
}
274
}
275
if (!gotTypes) {
276
types = undefined;
277
}
278
const regions = new FoldingRegions(startIndexes, endIndexes, types);
279
for (let i = 0; i < rangesLength; i++) {
280
if (ranges[i].isCollapsed) {
281
regions.setCollapsed(i, true);
282
}
283
regions.setSource(i, ranges[i].source);
284
}
285
return regions;
286
}
287
288
/**
289
* Two inputs, each a FoldingRegions or a FoldRange[], are merged.
290
* Each input must be pre-sorted on startLineNumber.
291
* The first list is assumed to always include all regions currently defined by range providers.
292
* The second list only contains the previously collapsed and all manual ranges.
293
* If the line position matches, the range of the new range is taken, and the range is no longer manual
294
* When an entry in one list overlaps an entry in the other, the second list's entry "wins" and
295
* overlapping entries in the first list are discarded.
296
* Invalid entries are discarded. An entry is invalid if:
297
* the start and end line numbers aren't a valid range of line numbers,
298
* it is out of sequence or has the same start line as a preceding entry,
299
* it overlaps a preceding entry and is not fully contained by that entry.
300
*/
301
public static sanitizeAndMerge(
302
rangesA: FoldingRegions | FoldRange[],
303
rangesB: FoldingRegions | FoldRange[],
304
maxLineNumber: number | undefined,
305
selection?: SelectedLines
306
): FoldRange[] {
307
308
maxLineNumber = maxLineNumber ?? Number.MAX_VALUE;
309
310
const getIndexedFunction = (r: FoldingRegions | FoldRange[], limit: number) => {
311
return Array.isArray(r)
312
? ((i: number) => { return (i < limit) ? r[i] : undefined; })
313
: ((i: number) => { return (i < limit) ? r.toFoldRange(i) : undefined; });
314
};
315
const getA = getIndexedFunction(rangesA, rangesA.length);
316
const getB = getIndexedFunction(rangesB, rangesB.length);
317
let indexA = 0;
318
let indexB = 0;
319
let nextA = getA(0);
320
let nextB = getB(0);
321
322
const stackedRanges: FoldRange[] = [];
323
let topStackedRange: FoldRange | undefined;
324
let prevLineNumber = 0;
325
const resultRanges: FoldRange[] = [];
326
327
while (nextA || nextB) {
328
329
let useRange: FoldRange | undefined = undefined;
330
if (nextB && (!nextA || nextA.startLineNumber >= nextB.startLineNumber)) {
331
if (nextA && nextA.startLineNumber === nextB.startLineNumber) {
332
if (nextB.source === FoldSource.userDefined) {
333
// a user defined range (possibly unfolded)
334
useRange = nextB;
335
} else {
336
// a previously folded range or a (possibly unfolded) recovered range
337
useRange = nextA;
338
// stays collapsed if the range still has the same number of lines or the selection is not in the range or after it
339
useRange.isCollapsed = nextB.isCollapsed && (nextA.endLineNumber === nextB.endLineNumber || !selection?.startsInside(nextA.startLineNumber + 1, nextA.endLineNumber + 1));
340
useRange.source = FoldSource.provider;
341
}
342
nextA = getA(++indexA); // not necessary, just for speed
343
} else {
344
useRange = nextB;
345
if (nextB.isCollapsed && nextB.source === FoldSource.provider) {
346
// a previously collapsed range
347
useRange.source = FoldSource.recovered;
348
}
349
}
350
nextB = getB(++indexB);
351
} else {
352
// nextA is next. The user folded B set takes precedence and we sometimes need to look
353
// ahead in it to check for an upcoming conflict.
354
let scanIndex = indexB;
355
let prescanB = nextB;
356
while (true) {
357
if (!prescanB || prescanB.startLineNumber > nextA!.endLineNumber) {
358
useRange = nextA;
359
break; // no conflict, use this nextA
360
}
361
if (prescanB.source === FoldSource.userDefined && prescanB.endLineNumber > nextA!.endLineNumber) {
362
// we found a user folded range, it wins
363
break; // without setting nextResult, so this nextA gets skipped
364
}
365
prescanB = getB(++scanIndex);
366
}
367
nextA = getA(++indexA);
368
}
369
370
if (useRange) {
371
while (topStackedRange
372
&& topStackedRange.endLineNumber < useRange.startLineNumber) {
373
topStackedRange = stackedRanges.pop();
374
}
375
if (useRange.endLineNumber > useRange.startLineNumber
376
&& useRange.startLineNumber > prevLineNumber
377
&& useRange.endLineNumber <= maxLineNumber
378
&& (!topStackedRange
379
|| topStackedRange.endLineNumber >= useRange.endLineNumber)) {
380
resultRanges.push(useRange);
381
prevLineNumber = useRange.startLineNumber;
382
if (topStackedRange) {
383
stackedRanges.push(topStackedRange);
384
}
385
topStackedRange = useRange;
386
}
387
}
388
389
}
390
return resultRanges;
391
}
392
393
}
394
395
export class FoldingRegion {
396
397
constructor(private readonly ranges: FoldingRegions, private index: number) {
398
}
399
400
public get startLineNumber() {
401
return this.ranges.getStartLineNumber(this.index);
402
}
403
404
public get endLineNumber() {
405
return this.ranges.getEndLineNumber(this.index);
406
}
407
408
public get regionIndex() {
409
return this.index;
410
}
411
412
public get parentIndex() {
413
return this.ranges.getParentIndex(this.index);
414
}
415
416
public get isCollapsed() {
417
return this.ranges.isCollapsed(this.index);
418
}
419
420
containedBy(range: ILineRange): boolean {
421
return range.startLineNumber <= this.startLineNumber && range.endLineNumber >= this.endLineNumber;
422
}
423
containsLine(lineNumber: number) {
424
return this.startLineNumber <= lineNumber && lineNumber <= this.endLineNumber;
425
}
426
hidesLine(lineNumber: number) {
427
return this.startLineNumber < lineNumber && lineNumber <= this.endLineNumber;
428
}
429
}
430
431