@adecore/editor-core 0.23.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bracket-tracker.d.ts +25 -0
- package/dist/bracket-tracker.js +272 -0
- package/dist/brackets.d.ts +35 -0
- package/dist/brackets.js +167 -99
- package/dist/document.d.ts +3 -0
- package/dist/document.js +84 -32
- package/dist/index.d.ts +1 -1
- package/dist/lexical.d.ts +1 -0
- package/dist/lexical.js +13 -3
- package/dist/paste.js +2 -1
- package/dist/rope.js +35 -8
- package/dist/structure.js +4 -1
- package/package.json +1 -1
- package/src/bracket-tracker.ts +321 -0
- package/src/brackets.ts +201 -98
- package/src/document.ts +82 -31
- package/src/index.ts +1 -1
- package/src/lexical.ts +14 -3
- package/src/paste.ts +2 -1
- package/src/rope.ts +43 -12
- package/src/structure.ts +4 -1
package/src/document.ts
CHANGED
|
@@ -14,7 +14,8 @@ import { lexicalFolds } from './lexical-folds.ts';
|
|
|
14
14
|
import { rangeOf, whitespaceOf } from './line-text.ts';
|
|
15
15
|
import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.ts';
|
|
16
16
|
import { scanBrackets } from './brackets.ts';
|
|
17
|
-
import type { BracketIndex } from './brackets.ts';
|
|
17
|
+
import type { BracketIndex, BracketMatch } from './brackets.ts';
|
|
18
|
+
import { BracketTracker } from './bracket-tracker.ts';
|
|
18
19
|
import { hasSmartSemicolon, isPhp } from './lexical.ts';
|
|
19
20
|
import { clampInteger, TextRope, ROPE_CHUNK_SIZE } from './rope.ts';
|
|
20
21
|
import type { DocumentLine } from './rope.ts';
|
|
@@ -155,7 +156,7 @@ const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
|
155
156
|
/* What can follow `->`: the first letter of a member name, or the `{` of a dynamic one. */
|
|
156
157
|
const arrowContinues = /^[\p{L}_{]/u;
|
|
157
158
|
const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
|
|
158
|
-
|
|
159
|
+
let graphemes: Intl.Segmenter | undefined;
|
|
159
160
|
|
|
160
161
|
function sameSelections(left: readonly Selection[], right: readonly Selection[]): boolean {
|
|
161
162
|
return left.length === right.length && left.every((selection, index) => selection.anchor === right[index]!.anchor && selection.head === right[index]!.head);
|
|
@@ -178,15 +179,35 @@ function selectionTransaction(plans: readonly SelectionEdit[]): SelectionTransac
|
|
|
178
179
|
edits.push(plan);
|
|
179
180
|
}
|
|
180
181
|
}
|
|
182
|
+
// What the edits before each one add to the text, so a caret finds its shift by a binary search.
|
|
183
|
+
const shifts = [0];
|
|
184
|
+
for (const edit of edits) {
|
|
185
|
+
shifts.push(shifts.at(-1)! + edit.text.length - (edit.to - edit.from));
|
|
186
|
+
}
|
|
181
187
|
const selections = plans.map((plan) => {
|
|
182
|
-
|
|
188
|
+
let low = 0;
|
|
189
|
+
let high = edits.length;
|
|
190
|
+
while (low < high) {
|
|
191
|
+
const middle = (low + high) >>> 1;
|
|
192
|
+
if (edits[middle]!.from < plan.from) {
|
|
193
|
+
low = middle + 1;
|
|
194
|
+
} else {
|
|
195
|
+
high = middle;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const delta = shifts[low]!;
|
|
183
199
|
return { anchor: plan.from + delta + plan.anchor, head: plan.from + delta + plan.head };
|
|
184
200
|
});
|
|
185
201
|
return { edits, selections };
|
|
186
202
|
}
|
|
187
203
|
|
|
188
204
|
function splitsCluster(rope: TextRope, offset: number): boolean {
|
|
189
|
-
|
|
205
|
+
const before = rope.charAt(offset - 1);
|
|
206
|
+
// One read of the rope settles most offsets, which follow neither a `\r` nor the first half of a surrogate pair.
|
|
207
|
+
if (before === '\r') {
|
|
208
|
+
return rope.charAt(offset) === '\n';
|
|
209
|
+
}
|
|
210
|
+
return /[\ud800-\udbff]/.test(before) && splitsSurrogate(rope, offset);
|
|
190
211
|
}
|
|
191
212
|
|
|
192
213
|
/* Clamps to the document and moves back off the middle of a surrogate pair or a CRLF. */
|
|
@@ -217,6 +238,7 @@ function deletionStep(rope: TextRope, offset: number, direction: -1 | 1): number
|
|
|
217
238
|
}
|
|
218
239
|
const line = rope.getLine(rope.lineAt(offset));
|
|
219
240
|
let previous = line.start;
|
|
241
|
+
graphemes ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
220
242
|
for (const segment of graphemes.segment(line.text)) {
|
|
221
243
|
const at = line.start + segment.index;
|
|
222
244
|
if (direction < 0 && at >= offset) {
|
|
@@ -231,33 +253,32 @@ function deletionStep(rope: TextRope, offset: number, direction: -1 | 1): number
|
|
|
231
253
|
}
|
|
232
254
|
|
|
233
255
|
/*
|
|
234
|
-
* Clamps every selection and merges the ones that overlap or touch, so the result is disjoint.
|
|
235
|
-
*
|
|
256
|
+
* Clamps every selection and merges the ones that overlap or touch, so the result is disjoint. A merged
|
|
257
|
+
* selection takes the place and the direction of the last added of the ones it holds, so the primary caret stays last.
|
|
236
258
|
*/
|
|
237
259
|
function normalizeSelections(rope: TextRope, selections: readonly Selection[]): Selection[] {
|
|
238
260
|
const input = selections.length > 0 ? selections : [{ anchor: 0, head: 0 }];
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
261
|
+
const clamped = input.map((raw) => {
|
|
262
|
+
const anchor = offsetIn(rope, raw.anchor);
|
|
263
|
+
return { anchor, head: raw.head === raw.anchor ? anchor : offsetIn(rope, raw.head) };
|
|
264
|
+
});
|
|
265
|
+
const order = clamped.map((selection, index) => ({ ...rangeOf(selection), index })).sort((left, right) => left.from - right.from);
|
|
266
|
+
const groups: { from: number; to: number; last: number }[] = [];
|
|
267
|
+
for (const { from, to, index } of order) {
|
|
268
|
+
const group = groups.at(-1);
|
|
269
|
+
if (group !== undefined && from <= group.to) {
|
|
270
|
+
group.to = Math.max(group.to, to);
|
|
271
|
+
group.last = Math.max(group.last, index);
|
|
272
|
+
} else {
|
|
273
|
+
groups.push({ from, to, last: index });
|
|
253
274
|
}
|
|
254
|
-
result.push({ selection, primary });
|
|
255
|
-
}
|
|
256
|
-
const primary = result.findIndex((entry) => entry.primary);
|
|
257
|
-
if (primary !== result.length - 1) {
|
|
258
|
-
result.push(...result.splice(primary, 1));
|
|
259
275
|
}
|
|
260
|
-
return
|
|
276
|
+
return groups
|
|
277
|
+
.sort((left, right) => left.last - right.last)
|
|
278
|
+
.map((group) => {
|
|
279
|
+
const selection = clamped[group.last]!;
|
|
280
|
+
return selection.anchor > selection.head ? { anchor: group.to, head: group.from } : { anchor: group.from, head: group.to };
|
|
281
|
+
});
|
|
261
282
|
}
|
|
262
283
|
|
|
263
284
|
function inverseChanges(changes: readonly DocumentChange[]): DocumentChange[] {
|
|
@@ -323,6 +344,17 @@ function editsOf(rope: TextRope, edits: readonly TextEdit[]): ContentEdit[] {
|
|
|
323
344
|
* either end, and is a little more than the change when two of them are far apart.
|
|
324
345
|
*/
|
|
325
346
|
function stretchEdits(before: TextRope, after: TextRope, transactions: readonly (readonly DocumentChange[])[]): ContentEdit[] {
|
|
347
|
+
const stretch = stretchOf(before, after, transactions);
|
|
348
|
+
if (stretch === undefined) {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
return [
|
|
352
|
+
{ start: positionIn(before, stretch.from), end: positionIn(before, stretch.to), text: after.slice(stretch.from, stretch.from + stretch.insertedLength) }
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* The one change in the coordinates of `before` that holds every change of the transactions, or undefined when they change nothing. */
|
|
357
|
+
function stretchOf(before: TextRope, after: TextRope, transactions: readonly (readonly DocumentChange[])[]): DocumentChange | undefined {
|
|
326
358
|
let length = before.length;
|
|
327
359
|
let prefix = Number.POSITIVE_INFINITY;
|
|
328
360
|
let suffix = Number.POSITIVE_INFINITY;
|
|
@@ -336,15 +368,15 @@ function stretchEdits(before: TextRope, after: TextRope, transactions: readonly
|
|
|
336
368
|
length += delta;
|
|
337
369
|
}
|
|
338
370
|
if (!Number.isFinite(prefix)) {
|
|
339
|
-
return
|
|
371
|
+
return undefined;
|
|
340
372
|
}
|
|
341
373
|
const kept = Math.min(Math.max(0, suffix), before.length - prefix, after.length - prefix);
|
|
342
374
|
const oldEnd = before.length - kept;
|
|
343
375
|
const newEnd = after.length - kept;
|
|
344
376
|
if (oldEnd === prefix && newEnd === prefix) {
|
|
345
|
-
return
|
|
377
|
+
return undefined;
|
|
346
378
|
}
|
|
347
|
-
return
|
|
379
|
+
return { from: prefix, to: oldEnd, insertedLength: newEnd - prefix };
|
|
348
380
|
}
|
|
349
381
|
|
|
350
382
|
/* An occurrence selected with its caret `shift` characters into it, as the caret stood in the selection it repeats. */
|
|
@@ -382,6 +414,7 @@ export class DocumentModel {
|
|
|
382
414
|
occurrencesExhausted = false;
|
|
383
415
|
private closers = new CloserTracker();
|
|
384
416
|
private bracketCache?: { revision: number; language: string; index: BracketIndex };
|
|
417
|
+
private bracketTracker = new BracketTracker(() => this.rope);
|
|
385
418
|
private scriptCache?: { revision: number; sources: Map<number, { limit: number; text?: string; index?: BracketIndex }> };
|
|
386
419
|
|
|
387
420
|
constructor(text = '') {
|
|
@@ -417,6 +450,16 @@ export class DocumentModel {
|
|
|
417
450
|
return this.rope.slice(from, to);
|
|
418
451
|
}
|
|
419
452
|
|
|
453
|
+
/*
|
|
454
|
+
* Where the bracket at `offset` pairs, as `scanBrackets` pairs it, with no mate for one it leaves unmatched.
|
|
455
|
+
* Undefined where no bracket of the code stands, such as in a string or a comment. An edit lexes only the few
|
|
456
|
+
* kilobytes around it again, so this stays cheap on every keystroke of a large document. With `limit` it lexes
|
|
457
|
+
* at most about that many characters and returns null when that was not enough; asking again goes on from there.
|
|
458
|
+
*/
|
|
459
|
+
matchBracket(offset: number, language = 'typescript', limit?: number): BracketMatch | null | undefined {
|
|
460
|
+
return this.bracketTracker.match(offset, language, limit);
|
|
461
|
+
}
|
|
462
|
+
|
|
420
463
|
getSelections(): readonly Selection[] {
|
|
421
464
|
return this.selections.map((selection) => ({ ...selection }));
|
|
422
465
|
}
|
|
@@ -516,15 +559,16 @@ export class DocumentModel {
|
|
|
516
559
|
const before = this.state();
|
|
517
560
|
const firstLine = this.rope.lineAt(changed[0]!.from);
|
|
518
561
|
const previous = this.rope;
|
|
562
|
+
const changes = changed.map((edit) => ({ from: edit.from, to: edit.to, insertedLength: edit.text.length }));
|
|
519
563
|
this.rope = next;
|
|
520
564
|
this.invalidate();
|
|
521
565
|
this.typingContexts.invalidate(firstLine);
|
|
566
|
+
this.bracketTracker.edited(previous, next, changes);
|
|
522
567
|
this.selections = normalizeSelections(this.rope, nextSelections);
|
|
523
568
|
this.closers.map(changed);
|
|
524
569
|
this.closers.prune(this.selections);
|
|
525
570
|
this.revision++;
|
|
526
571
|
const after = this.state();
|
|
527
|
-
const changes = changed.map((edit) => ({ from: edit.from, to: edit.to, insertedLength: edit.text.length }));
|
|
528
572
|
const last = this.undoStack.at(-1);
|
|
529
573
|
const joinsGroup =
|
|
530
574
|
options.historyGroup &&
|
|
@@ -2211,6 +2255,8 @@ export class DocumentModel {
|
|
|
2211
2255
|
this.invalidate();
|
|
2212
2256
|
this.closers.clear();
|
|
2213
2257
|
this.typingContexts.invalidate();
|
|
2258
|
+
const stretch = stretchOf(previous, state.rope, changes);
|
|
2259
|
+
this.bracketTracker.edited(previous, state.rope, stretch === undefined ? [] : [stretch]);
|
|
2214
2260
|
this.selections = state.selections.map((selection) => ({ ...selection }));
|
|
2215
2261
|
this.revision++;
|
|
2216
2262
|
this.groupOpen = false;
|
|
@@ -2237,7 +2283,12 @@ export class DocumentModel {
|
|
|
2237
2283
|
}
|
|
2238
2284
|
}
|
|
2239
2285
|
for (const listener of this.listeners) {
|
|
2240
|
-
|
|
2286
|
+
try {
|
|
2287
|
+
listener(snapshot);
|
|
2288
|
+
} catch (error) {
|
|
2289
|
+
// The text has changed by now, so a failing listener must not throw at the caller or keep the others, such as a language server's sync, from hearing it.
|
|
2290
|
+
console.error('A document listener failed.', error);
|
|
2291
|
+
}
|
|
2241
2292
|
}
|
|
2242
2293
|
}
|
|
2243
2294
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { DocumentModel } from './document.ts';
|
|
2
2
|
export type { DocumentEditOptions, DocumentLine, OffsetRange, ReplaceAllOptions, ReplaceOptions, TypeTextOptions } from './document.ts';
|
|
3
|
-
export { scanBrackets, type BracketIndex } from './brackets.ts';
|
|
3
|
+
export { scanBrackets, type BracketIndex, type BracketMatch } from './brackets.ts';
|
|
4
4
|
export { replaceWithCaseRespect } from './preserve-case.ts';
|
|
5
5
|
export { findMatches, iterateMatches, findMatchFrom, replacementText, type FindMatch, type FindNextOptions, type FindOptions } from './search.ts';
|
|
6
6
|
export { indentationColumn } from './structure.ts';
|
package/src/lexical.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export const openers: Readonly<Record<string, string>> = { '(': ')', '[': ']', '{': '}' };
|
|
2
2
|
|
|
3
3
|
/* Words after which a `/` starts a regex and a `{` an object or a block, not a division or a statement end. */
|
|
4
|
-
const expressionWords =
|
|
4
|
+
const expressionWords = new Set(['return', 'throw', 'case', 'yield', 'await', 'new', 'delete', 'typeof', 'void', 'in', 'of', 'instanceof']);
|
|
5
|
+
const expressionInitials = new Set([...expressionWords].map((word) => word[0]));
|
|
6
|
+
const expressionPunctuation = new Set('=,:;!?+-*%&|^~<>');
|
|
5
7
|
|
|
6
8
|
/* Whether a `#` at this index stands where a word would start, after whitespace, a separator or an opener. */
|
|
7
9
|
export function startsWord(text: string, at: number): boolean {
|
|
@@ -100,14 +102,23 @@ export function hasSmartSemicolon(language: string): boolean {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
export function isWordCharacter(character: string): boolean {
|
|
105
|
+
const code = character.charCodeAt(0);
|
|
106
|
+
if (code < 128) {
|
|
107
|
+
return (code >= 97 && code <= 122) || (code >= 65 && code <= 90) || (code >= 48 && code <= 57) || code === 95 || code === 36;
|
|
108
|
+
}
|
|
103
109
|
return /[\p{L}\p{N}_$]/u.test(character);
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
export function startsExpression(word: string): boolean {
|
|
107
|
-
return expressionWords.
|
|
113
|
+
return expressionWords.has(word);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* `startsExpression` for the word from `from` to `to` in `text`, which cuts the word out only when it could be one. */
|
|
117
|
+
export function startsExpressionAt(text: string, from: number, to: number): boolean {
|
|
118
|
+
return to - from >= 2 && to - from <= 10 && expressionInitials.has(text[from]!) && expressionWords.has(text.slice(from, to));
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
/* Whether what follows a punctuation character is an expression, so a `/` there is a regex. */
|
|
111
122
|
export function startsExpressionAfter(character: string): boolean {
|
|
112
|
-
return
|
|
123
|
+
return expressionPunctuation.has(character);
|
|
113
124
|
}
|
package/src/paste.ts
CHANGED
|
@@ -51,7 +51,8 @@ function reindent(source: EditSource, from: number, text: string, options: Paste
|
|
|
51
51
|
const measured = widths.filter((width, index): width is number => width !== null && !(anchored && index === 0));
|
|
52
52
|
const trailingBreak = isBlank(lines.at(-1)!);
|
|
53
53
|
if (measured.length > 0) {
|
|
54
|
-
|
|
54
|
+
// A reduce, not a spread: a paste of a few hundred thousand lines is more arguments than V8 takes in one call.
|
|
55
|
+
const delta = target - measured.reduce((least, width) => Math.min(least, width));
|
|
55
56
|
for (const [index, entry] of lines.entries()) {
|
|
56
57
|
const width = widths[index];
|
|
57
58
|
if (width === null || width === undefined || (anchored && index === 0)) {
|
package/src/rope.ts
CHANGED
|
@@ -8,27 +8,56 @@ export interface DocumentLine {
|
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
10
|
* A persistent AVL tree of text chunks of at most `ROPE_CHUNK_SIZE` UTF-16 code units. Every node knows
|
|
11
|
-
* its length and its
|
|
12
|
-
* without scanning.
|
|
11
|
+
* its length and its line break count, a leaf also where its breaks are, so offsets and line numbers
|
|
12
|
+
* resolve without scanning. A line ends at `\n`, `\r\n` or a lone `\r`, as the language server protocol
|
|
13
|
+
* counts lines, and a break sits at the last code unit of its line ending.
|
|
13
14
|
*/
|
|
14
|
-
type
|
|
15
|
-
type
|
|
15
|
+
type Edges = { readonly startsWithLf: boolean; readonly endsWithCr: boolean };
|
|
16
|
+
type Leaf = Edges & { readonly text: string; readonly breaks: readonly number[]; readonly length: number; readonly lines: number; readonly height: number };
|
|
17
|
+
type Branch = Edges & { readonly left: Node; readonly right: Node; readonly length: number; readonly lines: number; readonly height: number };
|
|
16
18
|
type Node = Leaf | Branch;
|
|
17
19
|
export const ROPE_CHUNK_SIZE = 2048;
|
|
18
20
|
|
|
21
|
+
const CR = 13;
|
|
22
|
+
const LF = 10;
|
|
23
|
+
|
|
24
|
+
/* A node counts a `\r` it ends with as a break; when the next node starts with `\n`, the two are one CRLF and only the `\n` is. */
|
|
25
|
+
function sharedBreak(left: Node, right: Node): number {
|
|
26
|
+
return left.endsWithCr && right.startsWithLf ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
function leaf(text: string): Leaf | null {
|
|
20
30
|
if (!text.length) {
|
|
21
31
|
return null;
|
|
22
32
|
}
|
|
23
33
|
const breaks: number[] = [];
|
|
24
|
-
for (let index =
|
|
25
|
-
|
|
34
|
+
for (let index = 0; index < text.length; index++) {
|
|
35
|
+
const code = text.charCodeAt(index);
|
|
36
|
+
if (code === LF || (code === CR && text.charCodeAt(index + 1) !== LF)) {
|
|
37
|
+
breaks.push(index);
|
|
38
|
+
}
|
|
26
39
|
}
|
|
27
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
text,
|
|
42
|
+
breaks,
|
|
43
|
+
length: text.length,
|
|
44
|
+
lines: breaks.length,
|
|
45
|
+
height: 1,
|
|
46
|
+
startsWithLf: text.charCodeAt(0) === LF,
|
|
47
|
+
endsWithCr: text.charCodeAt(text.length - 1) === CR
|
|
48
|
+
};
|
|
28
49
|
}
|
|
29
50
|
|
|
30
51
|
function branch(left: Node, right: Node): Branch {
|
|
31
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
left,
|
|
54
|
+
right,
|
|
55
|
+
length: left.length + right.length,
|
|
56
|
+
lines: left.lines + right.lines - sharedBreak(left, right),
|
|
57
|
+
height: Math.max(left.height, right.height) + 1,
|
|
58
|
+
startsWithLf: left.startsWithLf,
|
|
59
|
+
endsWithCr: right.endsWithCr
|
|
60
|
+
};
|
|
32
61
|
}
|
|
33
62
|
|
|
34
63
|
function balance(left: Node, right: Node): Node {
|
|
@@ -259,7 +288,7 @@ export class TextRope {
|
|
|
259
288
|
node = node.left;
|
|
260
289
|
} else {
|
|
261
290
|
offset -= node.left.length;
|
|
262
|
-
count += node.left.lines;
|
|
291
|
+
count += node.left.lines - sharedBreak(node.left, node.right);
|
|
263
292
|
node = node.right;
|
|
264
293
|
}
|
|
265
294
|
}
|
|
@@ -283,7 +312,8 @@ export class TextRope {
|
|
|
283
312
|
return { start, end: this.length, next: this.length };
|
|
284
313
|
}
|
|
285
314
|
const newline = this.breakAt(index);
|
|
286
|
-
|
|
315
|
+
const crlf = this.charAt(newline) === '\n' && this.charAt(newline - 1) === '\r';
|
|
316
|
+
return { start, end: newline - (crlf ? 1 : 0), next: newline + 1 };
|
|
287
317
|
}
|
|
288
318
|
|
|
289
319
|
getLine(index: number): DocumentLine {
|
|
@@ -295,10 +325,11 @@ export class TextRope {
|
|
|
295
325
|
let node = this.root!;
|
|
296
326
|
let offset = 0;
|
|
297
327
|
while ('left' in node) {
|
|
298
|
-
|
|
328
|
+
const leftLines = node.left.lines - sharedBreak(node.left, node.right);
|
|
329
|
+
if (index < leftLines) {
|
|
299
330
|
node = node.left;
|
|
300
331
|
} else {
|
|
301
|
-
index -=
|
|
332
|
+
index -= leftLines;
|
|
302
333
|
offset += node.left.length;
|
|
303
334
|
node = node.right;
|
|
304
335
|
}
|
package/src/structure.ts
CHANGED
|
@@ -207,7 +207,10 @@ export function deriveFoldingRanges(
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
if (options.indentation) {
|
|
210
|
-
|
|
210
|
+
// One push per fold: a long file has more of them than V8 takes as arguments in one call.
|
|
211
|
+
for (const fold of indentationFolds(lineCount, getLine, tabWidth(options), minimum)) {
|
|
212
|
+
result.push(fold);
|
|
213
|
+
}
|
|
211
214
|
}
|
|
212
215
|
result.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
213
216
|
return dedupeFoldingRanges(result);
|