Skip to content

Commit 20885d5

Browse files
committed
[folding] IndentRanges to manage indent ranges efficiently (for #36555)
1 parent ee807e8 commit 20885d5

File tree

10 files changed

+696
-482
lines changed

10 files changed

+696
-482
lines changed

src/vs/editor/common/editorCommon.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
1515
import { Position, IPosition } from 'vs/editor/common/core/position';
1616
import { Range, IRange } from 'vs/editor/common/core/range';
1717
import { Selection, ISelection } from 'vs/editor/common/core/selection';
18-
import { IndentRange } from 'vs/editor/common/model/indentRanges';
18+
import { IndentRanges } from 'vs/editor/common/model/indentRanges';
1919
import { ITextSource } from 'vs/editor/common/model/textSource';
2020
import {
2121
ModelRawContentChangedEvent, IModelContentChangedEvent, IModelDecorationsChangedEvent,
@@ -903,7 +903,7 @@ export interface ITokenizedModel extends ITextModel {
903903
/**
904904
* @internal
905905
*/
906-
getIndentRanges(): IndentRange[];
906+
getIndentRanges(): IndentRanges;
907907

908908
/**
909909
* @internal

src/vs/editor/common/model/indentRanges.ts

+180-26
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,190 @@
88
import { ITextModel } from 'vs/editor/common/editorCommon';
99
import { FoldingMarkers } from 'vs/editor/common/modes/languageConfiguration';
1010

11-
export class IndentRange {
12-
_indentRangeBrand: void;
13-
startLineNumber: number;
14-
endLineNumber: number;
15-
indent: number;
16-
marker: boolean;
17-
18-
constructor(startLineNumber: number, endLineNumber: number, indent: number, marker?: boolean) {
19-
this.startLineNumber = startLineNumber;
20-
this.endLineNumber = endLineNumber;
21-
this.indent = indent;
22-
this.marker = marker;
23-
}
24-
25-
public static deepCloneArr(indentRanges: IndentRange[]): IndentRange[] {
26-
let result: IndentRange[] = [];
27-
for (let i = 0, len = indentRanges.length; i < len; i++) {
28-
let r = indentRanges[i];
29-
result[i] = new IndentRange(r.startLineNumber, r.endLineNumber, r.indent);
11+
export const MAX_FOLDING_REGIONS = 0xFFFF;
12+
13+
const MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT = 5000;
14+
const MASK_LINE_NUMBER = 0xFFFFFF;
15+
const MASK_INDENT = 0xFF000000;
16+
17+
export class IndentRanges {
18+
private _startIndexes: Uint32Array;
19+
private _endIndexes: Uint32Array;
20+
private _model: ITextModel;
21+
private _indentLimit: number;
22+
23+
constructor(startIndexes: Uint32Array, endIndexes: Uint32Array, model: ITextModel, indentLimit: number) {
24+
if (startIndexes.length !== endIndexes.length || startIndexes.length > MAX_FOLDING_REGIONS) {
25+
throw new Error('invalid startIndexes or endIndexes size');
26+
}
27+
this._startIndexes = startIndexes;
28+
this._endIndexes = endIndexes;
29+
this._model = model;
30+
this._indentLimit = indentLimit;
31+
this._computeParentIndices();
32+
}
33+
34+
private _computeParentIndices() {
35+
let parentIndexes = [];
36+
let isInsideLast = (startLineNumber: number, endLineNumber: number) => {
37+
let index = parentIndexes[parentIndexes.length - 1];
38+
return this.getStartLineNumber(index) <= startLineNumber && this.getEndLineNumber(index) >= endLineNumber;
39+
};
40+
for (let i = 0, len = this._startIndexes.length; i < len; i++) {
41+
let startLineNumber = this._startIndexes[i];
42+
let endLineNumber = this._endIndexes[i];
43+
if (startLineNumber > MASK_LINE_NUMBER || endLineNumber > MASK_LINE_NUMBER) {
44+
throw new Error('startLineNumber or endLineNumber must not exceed ' + MASK_LINE_NUMBER);
45+
}
46+
while (parentIndexes.length > 0 && !isInsideLast(startLineNumber, endLineNumber)) {
47+
parentIndexes.pop();
48+
}
49+
let parentIndex = parentIndexes.length > 0 ? parentIndexes[parentIndexes.length - 1] : -1;
50+
parentIndexes.push(i);
51+
this._startIndexes[i] = startLineNumber + ((parentIndex & 0xFF) << 24);
52+
this._endIndexes[i] = endLineNumber + ((parentIndex & 0xFF00) << 16);
53+
}
54+
}
55+
56+
public get length(): number {
57+
return this._startIndexes.length;
58+
}
59+
60+
public getStartLineNumber(index: number): number {
61+
return this._startIndexes[index] & MASK_LINE_NUMBER;
62+
}
63+
64+
public getEndLineNumber(index: number): number {
65+
return this._endIndexes[index] & MASK_LINE_NUMBER;
66+
}
67+
68+
public getParentIndex(index: number) {
69+
let parent = ((this._startIndexes[index] & MASK_INDENT) >>> 24) + ((this._endIndexes[index] & MASK_INDENT) >>> 16);
70+
if (parent === MAX_FOLDING_REGIONS) {
71+
return -1;
72+
}
73+
return parent;
74+
}
75+
76+
public get indentLimit() {
77+
return this._indentLimit;
78+
}
79+
80+
public getIndent(index: number) {
81+
return this._model.getIndentLevel(this.getStartLineNumber(index));
82+
}
83+
84+
public contains(index: number, line: number) {
85+
return this.getStartLineNumber(index) <= line && this.getEndLineNumber(index) >= line;
86+
}
87+
88+
private findIndex(line: number) {
89+
let low = 0, high = this._startIndexes.length;
90+
if (high === 0) {
91+
return -1; // no children
92+
}
93+
while (low < high) {
94+
let mid = Math.floor((low + high) / 2);
95+
if (line < this.getStartLineNumber(mid)) {
96+
high = mid;
97+
} else {
98+
low = mid + 1;
99+
}
30100
}
31-
return result;
101+
return low - 1;
102+
}
103+
104+
105+
public findRange(line: number): number {
106+
let index = this.findIndex(line);
107+
if (index >= 0) {
108+
let endLineNumber = this.getEndLineNumber(index);
109+
if (endLineNumber >= line) {
110+
return index;
111+
}
112+
index = this.getParentIndex(index);
113+
while (index !== -1) {
114+
if (this.contains(index, line)) {
115+
return index;
116+
}
117+
index = this.getParentIndex(index);
118+
}
119+
}
120+
return -1;
32121
}
33122
}
123+
// public only for testing
124+
export class RangesCollector {
125+
private _startIndexes: Uint32Array;
126+
private _endIndexes: Uint32Array;
127+
private _indentOccurrences: number[] = [];
128+
private _length: number;
129+
private _foldingRegionsLimit: number;
130+
131+
constructor(foldingRegionsLimit = MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT) {
132+
this._startIndexes = new Uint32Array(64);
133+
this._endIndexes = new Uint32Array(64);
134+
this._indentOccurrences = [];
135+
this._length = 0;
136+
this._foldingRegionsLimit = foldingRegionsLimit;
137+
}
138+
139+
private expand(arr: Uint32Array): Uint32Array {
140+
let data = new Uint32Array(arr.length * 2);
141+
data.set(arr, 0);
142+
return data;
143+
}
144+
145+
public insertFirst(startLineNumber: number, endLineNumber: number, indent: number) {
146+
if (startLineNumber > MASK_LINE_NUMBER || endLineNumber > MASK_LINE_NUMBER) {
147+
return;
148+
}
149+
let index = this._length;
150+
if (index >= this._startIndexes.length) {
151+
this._startIndexes = this.expand(this._startIndexes);
152+
this._endIndexes = this.expand(this._endIndexes);
153+
}
154+
this._startIndexes[index] = startLineNumber;
155+
this._endIndexes[index] = endLineNumber;
156+
this._length++;
157+
if (indent < 1000) {
158+
this._indentOccurrences[indent] = (this._indentOccurrences[indent] || 0) + 1;
159+
}
160+
}
161+
162+
private _computeMaxIndent() {
163+
let maxEntries = this._foldingRegionsLimit;
164+
let maxIndent = this._indentOccurrences.length;
165+
for (let i = 0; i < this._indentOccurrences.length; i++) {
166+
if (this._indentOccurrences[i]) {
167+
maxEntries -= this._indentOccurrences[i];
168+
if (maxEntries < 0) {
169+
maxIndent = i;
170+
break;
171+
}
172+
}
173+
}
174+
return maxIndent;
175+
}
176+
177+
public toIndentRanges(model: ITextModel) {
178+
// reverse and create arrays of the exact length
179+
let startIndexes = new Uint32Array(this._length);
180+
let endIndexes = new Uint32Array(this._length);
181+
for (let i = this._length - 1, k = 0; i >= 0; i-- , k++) {
182+
startIndexes[k] = this._startIndexes[i];
183+
endIndexes[k] = this._endIndexes[i];
184+
}
185+
return new IndentRanges(startIndexes, endIndexes, model, this._computeMaxIndent());
186+
}
187+
}
188+
34189

35190
interface PreviousRegion { indent: number; line: number; marker: boolean; };
36191

37-
export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldingMarkers, minimumRangeSize: number = 1): IndentRange[] {
192+
export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldingMarkers, minimumRangeSize: number = 1): IndentRanges {
38193

39-
let result: IndentRange[] = [];
194+
let result = new RangesCollector();
40195

41196
let pattern = void 0;
42197
if (markers) {
@@ -70,7 +225,7 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol
70225
previous = previousRegions[i];
71226

72227
// new folding range from pattern, includes the end line
73-
result.push(new IndentRange(line, previous.line, indent, true));
228+
result.insertFirst(line, previous.line, indent);
74229
previous.marker = false;
75230
previous.indent = indent;
76231
previous.line = line;
@@ -93,7 +248,7 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol
93248
// new folding range
94249
let endLineNumber = previous.line - 1;
95250
if (endLineNumber - line >= minimumRangeSize) {
96-
result.push(new IndentRange(line, endLineNumber, indent));
251+
result.insertFirst(line, endLineNumber, indent);
97252
}
98253
}
99254
if (previous.indent === indent) {
@@ -103,6 +258,5 @@ export function computeRanges(model: ITextModel, offSide: boolean, markers?: Fol
103258
previousRegions.push({ indent, line, marker: false });
104259
}
105260
}
106-
107-
return result.reverse();
261+
return result.toIndentRanges(model);
108262
}

src/vs/editor/common/model/textModelWithTokens.ts

+9-29
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { getWordAtText } from 'vs/editor/common/model/wordHelper';
2222
import { TokenizationResult2 } from 'vs/editor/common/core/token';
2323
import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource';
2424
import * as textModelEvents from 'vs/editor/common/model/textModelEvents';
25-
import { IndentRange, computeRanges } from 'vs/editor/common/model/indentRanges';
25+
import { IndentRanges, computeRanges } from 'vs/editor/common/model/indentRanges';
2626

2727
class ModelTokensChangedEventBuilder {
2828

@@ -70,7 +70,7 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke
7070
private _invalidLineStartIndex: number;
7171
private _lastState: IState;
7272

73-
private _indentRanges: IndentRange[];
73+
private _indentRanges: IndentRanges;
7474
private _languageRegistryListener: IDisposable;
7575

7676
private _revalidateTokensTimeout: number;
@@ -841,7 +841,7 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke
841841
this._indentRanges = null;
842842
}
843843

844-
private _getIndentRanges(): IndentRange[] {
844+
private _getIndentRanges(): IndentRanges {
845845
if (!this._indentRanges) {
846846
let foldingRules = LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id);
847847
let offSide = foldingRules && foldingRules.offSide;
@@ -851,10 +851,8 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke
851851
return this._indentRanges;
852852
}
853853

854-
public getIndentRanges(): IndentRange[] {
855-
this._assertNotDisposed();
856-
let indentRanges = this._getIndentRanges();
857-
return IndentRange.deepCloneArr(indentRanges);
854+
public getIndentRanges(): IndentRanges {
855+
return this._getIndentRanges();
858856
}
859857

860858
public getLineIndentGuide(lineNumber: number): number {
@@ -864,29 +862,11 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke
864862
}
865863

866864
let indentRanges = this._getIndentRanges();
867-
868-
for (let i = indentRanges.length - 1; i >= 0; i--) {
869-
let rng = indentRanges[i];
870-
871-
if (rng.startLineNumber === lineNumber) {
872-
return this._toValidLineIndentGuide(lineNumber, Math.ceil(rng.indent / this._options.tabSize));
873-
}
874-
if (rng.startLineNumber < lineNumber && lineNumber <= rng.endLineNumber) {
875-
return this._toValidLineIndentGuide(lineNumber, 1 + Math.floor(rng.indent / this._options.tabSize));
876-
}
877-
if (rng.endLineNumber + 1 === lineNumber) {
878-
let bestIndent = rng.indent;
879-
while (i > 0) {
880-
i--;
881-
rng = indentRanges[i];
882-
if (rng.endLineNumber + 1 === lineNumber) {
883-
bestIndent = rng.indent;
884-
}
885-
}
886-
return this._toValidLineIndentGuide(lineNumber, Math.ceil(bestIndent / this._options.tabSize));
887-
}
865+
let index = indentRanges.findRange(lineNumber);
866+
if (index >= 0) {
867+
let indent = indentRanges.getIndent(index);
868+
return this._toValidLineIndentGuide(lineNumber, Math.ceil(indent / this._options.tabSize));
888869
}
889-
890870
return 0;
891871
}
892872

0 commit comments

Comments
 (0)