Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/parser/node/indentationStructure.ts
13401 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 type * as vscode from 'vscode';
7
import { CharCode } from '../../../util/vs/base/common/charCode';
8
import { BugIndicatingError } from '../../../util/vs/base/common/errors';
9
import { Position } from '../../../vscodeTypes';
10
import { AbstractDocument } from '../../editing/common/abstractText';
11
import { OverlayNode } from './nodes';
12
13
export function getStructureUsingIndentation(
14
document: AbstractDocument,
15
languageId: string,
16
formattingOptions: vscode.FormattingOptions | undefined
17
): OverlayNode {
18
const lines = document.getText().split(/\r\n|\r|\n/g);
19
const opts = formattingOptions || { tabSize: 4 };
20
const simpleModel = {
21
getLineCount: () => lines.length,
22
getLineContent: (lineNumber: number) => lines[lineNumber - 1],
23
getOptions: () => opts
24
} satisfies ISimpleTextModel;
25
26
try {
27
const regions = generateFoldingRegions(simpleModel, languageId);
28
const [foldingRanges,] = createFoldingRangeTree(document, regions, undefined);
29
foldingRanges.adjust(document, isOffSide(languageId));
30
return foldingRanges.toOverlayNode(document, true);
31
} catch (err) {
32
const foldingRanges = new FoldingRangeNode(1, document.getLineCount(), []);
33
return foldingRanges.toOverlayNode(document, true);
34
}
35
}
36
37
function createFoldingRangeTree(doc: AbstractDocument, regions: FoldingRegions, regionIndex: number | undefined): [FoldingRangeNode, number] {
38
if (typeof regionIndex !== 'undefined' && regionIndex >= regions.length) {
39
throw new Error(`Invalid region index ${regionIndex}`);
40
}
41
42
const regionStartLineNumber = (typeof regionIndex === 'undefined' ? 1 : regions.getStartLineNumber(regionIndex));
43
const regionEndLineNumber = (typeof regionIndex === 'undefined' ? doc.getLineCount() : regions.getEndLineNumber(regionIndex));
44
const children: FoldingRangeNode[] = [];
45
let childNode: FoldingRangeNode | null = null;
46
47
regionIndex = (typeof regionIndex === 'undefined' ? 0 : regionIndex + 1);
48
while (regionIndex < regions.length) {
49
const startLineNumber = regions.getStartLineNumber(regionIndex);
50
const endLineNumber = regions.getEndLineNumber(regionIndex);
51
52
if (startLineNumber > regionEndLineNumber || endLineNumber > regionEndLineNumber) {
53
// We are done with children of this region
54
break;
55
}
56
57
const prevChildNode = childNode;
58
[childNode, regionIndex] = createFoldingRangeTree(doc, regions, regionIndex);
59
if (prevChildNode && childNode.startLineNumber <= prevChildNode.endLineNumber) {
60
throw new BugIndicatingError('Invalid Folding Ranges: overlapping children');
61
}
62
if (childNode.startLineNumber < regionStartLineNumber) {
63
throw new BugIndicatingError('Invalid Folding Ranges: child starts before parent');
64
}
65
children.push(childNode);
66
}
67
68
return [
69
new FoldingRangeNode(regionStartLineNumber, regionEndLineNumber, children),
70
regionIndex,
71
];
72
}
73
74
class FoldingRangeNode {
75
76
constructor(
77
public startLineNumber: number,
78
public endLineNumber: number,
79
readonly children: FoldingRangeNode[]
80
) {
81
if (startLineNumber > endLineNumber) {
82
throw new BugIndicatingError('Invalid Folding Ranges: startLineNumber > endLineNumber');
83
}
84
}
85
86
public adjust(document: AbstractDocument, isOffside: boolean): void {
87
if (isOffside) {
88
this._adjustOffside();
89
} else {
90
this._adjustRegular(document, document.getLineCount());
91
}
92
}
93
94
private _adjustOffside(): void {
95
this.startLineNumber++;
96
for (const child of this.children) {
97
child._adjustOffside();
98
}
99
}
100
101
private _adjustRegular(document: AbstractDocument, maxEndLineNumber: number): void {
102
if (this.endLineNumber < maxEndLineNumber) {
103
const nextLine = document.getLineText(this.endLineNumber).trim();
104
const isClosingBracket = /^[\}\]\)];?$/.test(nextLine);
105
const isClosingTag = /^<\/\w/.test(nextLine);
106
if (isClosingBracket || isClosingTag) {
107
this.endLineNumber++;
108
}
109
}
110
111
for (let i = this.children.length - 1; i >= 0; i--) {
112
const child = this.children[i];
113
const childMaxEndLineNumber = (i + 1 < this.children.length ? this.children[i + 1].startLineNumber - 1 : maxEndLineNumber);
114
child._adjustRegular(document, childMaxEndLineNumber);
115
}
116
}
117
118
toOverlayNode(document: AbstractDocument, isRoot: boolean): OverlayNode {
119
const children: OverlayNode[] = [];
120
let nextLineNumber = (isRoot && this.startLineNumber === 1 ? 1 : this.startLineNumber + 1);
121
122
// for (let lineNumber = this.startLineNumber + 1;)
123
for (const child of this.children) {
124
125
// Generate a node for each skipped line
126
for (let lineNumber = nextLineNumber; lineNumber < child.startLineNumber; lineNumber++) {
127
const node = createOverlayNode(document, lineNumber, lineNumber, 'LINE', []);
128
if (node) {
129
children.push(node);
130
}
131
}
132
133
children.push(child.toOverlayNode(document, false));
134
135
nextLineNumber = child.endLineNumber + 1;
136
}
137
138
// Generate a node for each skipped line
139
for (let lineNumber = nextLineNumber; lineNumber < this.endLineNumber; lineNumber++) {
140
const node = createOverlayNode(document, lineNumber, lineNumber, 'LINE', []);
141
if (node) {
142
children.push(node);
143
}
144
}
145
146
return createOverlayNode(document, this.startLineNumber, this.endLineNumber, 'FOLD', children);
147
}
148
}
149
150
function createOverlayNode(doc: AbstractDocument, startLineNumber: number, endLineNumber: number, kind: string, children: OverlayNode[]): OverlayNode {
151
const startOffset = doc.getOffsetAtPosition(new Position(startLineNumber - 1, 0));
152
const endPosition = (
153
endLineNumber < doc.getLineCount()
154
? new Position(endLineNumber, 0)
155
: new Position(endLineNumber - 1, doc.getLineLength(endLineNumber - 1))
156
);
157
const endOffset = doc.getOffsetAtPosition(endPosition);
158
return new OverlayNode(
159
startOffset,
160
endOffset,
161
kind,
162
children
163
);
164
}
165
166
167
export interface ISimpleTextModel {
168
getLineCount(): number;
169
getLineContent(lineNumber: number): string;
170
getOptions(): { tabSize: number };
171
}
172
173
function generateFoldingRegions(model: ISimpleTextModel, languageId: string): FoldingRegions {
174
return _computeRanges(model, isOffSide(languageId));
175
}
176
177
function isOffSide(languageId: string): boolean {
178
return ['clojure', 'coffeescript', 'fsharp', 'latex', 'markdown', 'pug', 'python', 'sql', 'yaml'].includes(languageId);
179
}
180
181
function _computeRanges(model: ISimpleTextModel, offSide: boolean): FoldingRegions {
182
const tabSize = model.getOptions().tabSize;
183
const result = new RangesCollector();
184
185
const previousRegions: PreviousRegion[] = [];
186
const line = model.getLineCount() + 1;
187
previousRegions.push({ indent: -1, endAbove: line, line }); // sentinel, to make sure there's at least one entry
188
189
for (let line = model.getLineCount(); line > 0; line--) {
190
const lineContent = model.getLineContent(line);
191
const indent = computeIndentLevel(lineContent, tabSize);
192
let previous = previousRegions[previousRegions.length - 1];
193
if (indent === -1) {
194
if (offSide) {
195
// for offSide languages, empty lines are associated to the previous block
196
// note: the next block is already written to the results, so this only
197
// impacts the end position of the block before
198
previous.endAbove = line;
199
}
200
continue; // only whitespace
201
}
202
if (previous.indent > indent) {
203
// discard all regions with larger indent
204
do {
205
previousRegions.pop();
206
previous = previousRegions[previousRegions.length - 1];
207
} while (previous.indent > indent);
208
209
// new folding range
210
const endLineNumber = previous.endAbove - 1;
211
if (endLineNumber - line >= 1) { // needs at east size 1
212
result.insertFirst(line, endLineNumber, indent);
213
}
214
}
215
if (previous.indent === indent) {
216
previous.endAbove = line;
217
} else { // previous.indent < indent
218
// new region with a bigger indent
219
previousRegions.push({ indent, endAbove: line, line });
220
}
221
}
222
return result.toIndentRanges();
223
}
224
225
interface PreviousRegion {
226
indent: number; // indent or -2 if a marker
227
endAbove: number; // end line number for the region above
228
line: number; // start line of the region. Only used for marker regions.
229
}
230
231
const MAX_FOLDING_REGIONS = 0xFFFF;
232
const MAX_LINE_NUMBER = 0xFFFFFF;
233
const MASK_INDENT = 0xFF000000;
234
235
class RangesCollector {
236
private readonly _startIndexes: number[];
237
private readonly _endIndexes: number[];
238
private readonly _indentOccurrences: number[];
239
private _length: number;
240
241
constructor() {
242
this._startIndexes = [];
243
this._endIndexes = [];
244
this._indentOccurrences = [];
245
this._length = 0;
246
}
247
248
public insertFirst(startLineNumber: number, endLineNumber: number, indent: number) {
249
if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) {
250
return;
251
}
252
const index = this._length;
253
this._startIndexes[index] = startLineNumber;
254
this._endIndexes[index] = endLineNumber;
255
this._length++;
256
if (indent < 1000) {
257
this._indentOccurrences[indent] = (this._indentOccurrences[indent] || 0) + 1;
258
}
259
}
260
261
public toIndentRanges() {
262
// reverse and create arrays of the exact length
263
const startIndexes = new Uint32Array(this._length);
264
const endIndexes = new Uint32Array(this._length);
265
for (let i = this._length - 1, k = 0; i >= 0; i--, k++) {
266
startIndexes[k] = this._startIndexes[i];
267
endIndexes[k] = this._endIndexes[i];
268
}
269
return new FoldingRegions(startIndexes, endIndexes);
270
}
271
}
272
273
/**
274
* Returns:
275
* - -1 => the line consists of whitespace
276
* - otherwise => the indent level is returned value
277
*/
278
function computeIndentLevel(line: string, tabSize: number): number {
279
let indent = 0;
280
let i = 0;
281
const len = line.length;
282
283
while (i < len) {
284
const chCode = line.charCodeAt(i);
285
if (chCode === CharCode.Space) {
286
indent++;
287
} else if (chCode === CharCode.Tab) {
288
indent = indent - indent % tabSize + tabSize;
289
} else {
290
break;
291
}
292
i++;
293
}
294
295
if (i === len) {
296
return -1; // line only consists of whitespace
297
}
298
299
return indent;
300
}
301
302
class FoldingRegions {
303
private readonly _startIndexes: Uint32Array;
304
private readonly _endIndexes: Uint32Array;
305
306
private _parentsComputed: boolean;
307
308
constructor(startIndexes: Uint32Array, endIndexes: Uint32Array) {
309
this._startIndexes = startIndexes;
310
this._endIndexes = endIndexes;
311
this._parentsComputed = false;
312
// this.ensureParentIndices();
313
}
314
315
private ensureParentIndices() {
316
if (!this._parentsComputed) {
317
this._parentsComputed = true;
318
const parentIndexes: number[] = [];
319
const isInsideLast = (startLineNumber: number, endLineNumber: number) => {
320
const index = parentIndexes[parentIndexes.length - 1];
321
return this.getStartLineNumber(index) <= startLineNumber && this.getEndLineNumber(index) >= endLineNumber;
322
};
323
for (let i = 0, len = this._startIndexes.length; i < len; i++) {
324
const startLineNumber = this._startIndexes[i];
325
const endLineNumber = this._endIndexes[i];
326
if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) {
327
throw new Error('startLineNumber or endLineNumber must not exceed ' + MAX_LINE_NUMBER);
328
}
329
while (parentIndexes.length > 0 && !isInsideLast(startLineNumber, endLineNumber)) {
330
parentIndexes.pop();
331
}
332
const parentIndex = parentIndexes.length > 0 ? parentIndexes[parentIndexes.length - 1] : -1;
333
parentIndexes.push(i);
334
this._startIndexes[i] = startLineNumber + ((parentIndex & 0xFF) << 24);
335
this._endIndexes[i] = endLineNumber + ((parentIndex & 0xFF00) << 16);
336
}
337
}
338
}
339
340
public get length(): number {
341
return this._startIndexes.length;
342
}
343
344
public getStartLineNumber(index: number): number {
345
return this._startIndexes[index] & MAX_LINE_NUMBER;
346
}
347
348
public getEndLineNumber(index: number): number {
349
return this._endIndexes[index] & MAX_LINE_NUMBER;
350
}
351
352
public getParentIndex(index: number) {
353
this.ensureParentIndices();
354
const parent = ((this._startIndexes[index] & MASK_INDENT) >>> 24) + ((this._endIndexes[index] & MASK_INDENT) >>> 16);
355
if (parent === MAX_FOLDING_REGIONS) {
356
return -1;
357
}
358
return parent;
359
}
360
361
public contains(index: number, line: number) {
362
return this.getStartLineNumber(index) <= line && this.getEndLineNumber(index) >= line;
363
}
364
365
private findIndex(line: number) {
366
let low = 0, high = this._startIndexes.length;
367
if (high === 0) {
368
return -1; // no children
369
}
370
while (low < high) {
371
const mid = Math.floor((low + high) / 2);
372
if (line < this.getStartLineNumber(mid)) {
373
high = mid;
374
} else {
375
low = mid + 1;
376
}
377
}
378
return low - 1;
379
}
380
381
public findRange(line: number): number {
382
let index = this.findIndex(line);
383
if (index >= 0) {
384
const endLineNumber = this.getEndLineNumber(index);
385
if (endLineNumber >= line) {
386
return index;
387
}
388
index = this.getParentIndex(index);
389
while (index !== -1) {
390
if (this.contains(index, line)) {
391
return index;
392
}
393
index = this.getParentIndex(index);
394
}
395
}
396
return -1;
397
}
398
}
399
400