@adecore/editor-core 0.0.1 → 0.17.0-beta.1

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.
Files changed (80) hide show
  1. package/LICENSE +110 -0
  2. package/LICENSE.apache-2.0.txt +201 -0
  3. package/NOTICE +11 -0
  4. package/README.md +54 -0
  5. package/dist/block-folds.d.ts +3 -0
  6. package/dist/block-folds.js +112 -0
  7. package/dist/brackets.d.ts +5 -0
  8. package/dist/brackets.js +136 -0
  9. package/dist/closer-tracker.d.ts +9 -0
  10. package/dist/closer-tracker.js +46 -0
  11. package/dist/comments.d.ts +14 -0
  12. package/dist/comments.js +344 -0
  13. package/dist/document.d.ts +148 -0
  14. package/dist/document.js +1729 -0
  15. package/dist/edit-source.d.ts +19 -0
  16. package/dist/edit-source.js +1 -0
  17. package/dist/enter.d.ts +17 -0
  18. package/dist/enter.js +387 -0
  19. package/dist/fold-roles.d.ts +6 -0
  20. package/dist/fold-roles.js +142 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.js +7 -0
  23. package/dist/languages.d.ts +12 -0
  24. package/dist/languages.js +51 -0
  25. package/dist/lexical-folds.d.ts +10 -0
  26. package/dist/lexical-folds.js +116 -0
  27. package/dist/lexical.d.ts +16 -0
  28. package/dist/lexical.js +74 -0
  29. package/dist/line-commands.d.ts +9 -0
  30. package/dist/line-commands.js +306 -0
  31. package/dist/markup-regions.d.ts +8 -0
  32. package/dist/markup-regions.js +331 -0
  33. package/dist/offsets.d.ts +2 -0
  34. package/dist/offsets.js +14 -0
  35. package/dist/paste.d.ts +9 -0
  36. package/dist/paste.js +108 -0
  37. package/dist/preserve-case.d.ts +1 -0
  38. package/dist/preserve-case.js +61 -0
  39. package/dist/rope.d.ts +22 -0
  40. package/dist/rope.js +246 -0
  41. package/dist/search.d.ts +26 -0
  42. package/dist/search.js +85 -0
  43. package/dist/structure.d.ts +50 -0
  44. package/dist/structure.js +124 -0
  45. package/dist/text-span.d.ts +7 -0
  46. package/dist/text-span.js +211 -0
  47. package/dist/types.d.ts +70 -0
  48. package/dist/types.js +1 -0
  49. package/dist/typing-context.d.ts +30 -0
  50. package/dist/typing-context.js +270 -0
  51. package/dist/typing-handlers.d.ts +15 -0
  52. package/dist/typing-handlers.js +125 -0
  53. package/dist/words.d.ts +7 -0
  54. package/dist/words.js +72 -0
  55. package/package.json +34 -3
  56. package/src/block-folds.ts +117 -0
  57. package/src/brackets.ts +159 -0
  58. package/src/closer-tracker.ts +60 -0
  59. package/src/comments.ts +411 -0
  60. package/src/document.ts +2021 -0
  61. package/src/edit-source.ts +25 -0
  62. package/src/enter.ts +452 -0
  63. package/src/fold-roles.ts +166 -0
  64. package/src/index.ts +22 -0
  65. package/src/languages.ts +77 -0
  66. package/src/lexical-folds.ts +138 -0
  67. package/src/lexical.ts +113 -0
  68. package/src/line-commands.ts +356 -0
  69. package/src/markup-regions.ts +333 -0
  70. package/src/offsets.ts +16 -0
  71. package/src/paste.ts +126 -0
  72. package/src/preserve-case.ts +62 -0
  73. package/src/rope.ts +278 -0
  74. package/src/search.ts +119 -0
  75. package/src/structure.ts +223 -0
  76. package/src/text-span.ts +230 -0
  77. package/src/types.ts +141 -0
  78. package/src/typing-context.ts +329 -0
  79. package/src/typing-handlers.ts +143 -0
  80. package/src/words.ts +83 -0
package/src/rope.ts ADDED
@@ -0,0 +1,278 @@
1
+ /* `end` excludes the line break and `next` is where the following line starts, so the break is `[end, next)`. */
2
+ export interface DocumentLine {
3
+ start: number;
4
+ end: number;
5
+ next: number;
6
+ text: string;
7
+ }
8
+
9
+ /*
10
+ * A persistent AVL tree of text chunks of at most `chunkSize` UTF-16 code units. Every node knows
11
+ * its length and its LF count, a leaf also where its LFs are, so offsets and line numbers resolve
12
+ * without scanning.
13
+ */
14
+ type Leaf = { readonly text: string; readonly breaks: readonly number[]; readonly length: number; readonly lines: number; readonly height: number };
15
+ type Branch = { readonly left: Node; readonly right: Node; readonly length: number; readonly lines: number; readonly height: number };
16
+ type Node = Leaf | Branch;
17
+ const chunkSize = 2048;
18
+
19
+ function leaf(text: string): Leaf | null {
20
+ if (!text.length) {
21
+ return null;
22
+ }
23
+ const breaks: number[] = [];
24
+ for (let index = text.indexOf('\n'); index !== -1; index = text.indexOf('\n', index + 1)) {
25
+ breaks.push(index);
26
+ }
27
+ return { text, breaks, length: text.length, lines: breaks.length, height: 1 };
28
+ }
29
+
30
+ function branch(left: Node, right: Node): Branch {
31
+ return { left, right, length: left.length + right.length, lines: left.lines + right.lines, height: Math.max(left.height, right.height) + 1 };
32
+ }
33
+
34
+ function balance(left: Node, right: Node): Node {
35
+ if (left.height > right.height + 1 && 'left' in left) {
36
+ if (left.left.height >= left.right.height) {
37
+ return branch(left.left, branch(left.right, right));
38
+ }
39
+ const middle = left.right as Branch;
40
+ return branch(branch(left.left, middle.left), branch(middle.right, right));
41
+ }
42
+ if (right.height > left.height + 1 && 'left' in right) {
43
+ if (right.right.height >= right.left.height) {
44
+ return branch(branch(left, right.left), right.right);
45
+ }
46
+ const middle = right.left as Branch;
47
+ return branch(branch(left, middle.left), branch(middle.right, right.right));
48
+ }
49
+ return branch(left, right);
50
+ }
51
+
52
+ function join(left: Node | null, right: Node | null): Node | null {
53
+ if (!left) {
54
+ return right;
55
+ }
56
+ if (!right) {
57
+ return left;
58
+ }
59
+ if ('text' in left && 'text' in right && left.length + right.length <= chunkSize) {
60
+ return leaf(left.text + right.text);
61
+ }
62
+ if (left.height > right.height + 1 && 'left' in left) {
63
+ return balance(left.left, join(left.right, right)!);
64
+ }
65
+ if (right.height > left.height + 1 && 'left' in right) {
66
+ return balance(join(left, right.left)!, right.right);
67
+ }
68
+ return branch(left, right);
69
+ }
70
+
71
+ function split(node: Node | null, offset: number): [Node | null, Node | null] {
72
+ if (!node || offset <= 0) {
73
+ return [null, node];
74
+ }
75
+ if (offset >= node.length) {
76
+ return [node, null];
77
+ }
78
+ if ('text' in node) {
79
+ return [leaf(node.text.slice(0, offset)), leaf(node.text.slice(offset))];
80
+ }
81
+ if (offset < node.left.length) {
82
+ const [before, after] = split(node.left, offset);
83
+ return [before, join(after, node.right)];
84
+ }
85
+ const [before, after] = split(node.right, offset - node.left.length);
86
+ return [join(node.left, before), after];
87
+ }
88
+
89
+ function build(text: string): Node | null {
90
+ const leaves: Node[] = [];
91
+ for (let offset = 0; offset < text.length; offset += chunkSize) {
92
+ leaves.push(leaf(text.slice(offset, offset + chunkSize))!);
93
+ }
94
+ const tree = (from: number, to: number): Node | null => {
95
+ if (from === to) {
96
+ return null;
97
+ }
98
+ if (to - from === 1) {
99
+ return leaves[from]!;
100
+ }
101
+ const middle = (from + to) >>> 1;
102
+ return branch(tree(from, middle)!, tree(middle, to)!);
103
+ };
104
+ return tree(0, leaves.length);
105
+ }
106
+
107
+ function collect(node: Node | null, from: number, to: number, chunks: string[]): void {
108
+ if (!node || from >= to) {
109
+ return;
110
+ }
111
+ if ('text' in node) {
112
+ chunks.push(node.text.slice(from, to));
113
+ return;
114
+ }
115
+ if (from < node.left.length) {
116
+ collect(node.left, from, Math.min(to, node.left.length), chunks);
117
+ }
118
+ if (to > node.left.length) {
119
+ collect(node.right, Math.max(0, from - node.left.length), to - node.left.length, chunks);
120
+ }
121
+ }
122
+
123
+ function* chunksOf(node: Node | null, from: number, to: number): Generator<string> {
124
+ if (!node || from >= to) {
125
+ return;
126
+ }
127
+ if ('text' in node) {
128
+ yield node.text.slice(from, to);
129
+ return;
130
+ }
131
+ if (from < node.left.length) {
132
+ yield* chunksOf(node.left, from, Math.min(to, node.left.length));
133
+ }
134
+ if (to > node.left.length) {
135
+ yield* chunksOf(node.right, Math.max(0, from - node.left.length), to - node.left.length);
136
+ }
137
+ }
138
+
139
+ /* Truncates to an integer in `[0, maximum]`; anything that is not a finite number becomes 0. */
140
+ export function clampInteger(value: number, maximum: number): number {
141
+ return Math.max(0, Math.min(maximum, Number.isFinite(value) ? Math.trunc(value) : 0));
142
+ }
143
+
144
+ /* Immutable: an edit returns a new rope and shares every branch it did not touch, which is what makes history cheap. */
145
+ export class TextRope {
146
+ private readonly root: Node | null;
147
+
148
+ private constructor(root: Node | null) {
149
+ this.root = root;
150
+ }
151
+
152
+ static from(text: string): TextRope {
153
+ return new TextRope(build(text));
154
+ }
155
+ get length(): number {
156
+ return this.root?.length ?? 0;
157
+ }
158
+ get lineCount(): number {
159
+ return (this.root?.lines ?? 0) + 1;
160
+ }
161
+
162
+ charAt(offset: number): string {
163
+ if (offset < 0 || offset >= this.length) {
164
+ return '';
165
+ }
166
+ let node = this.root!;
167
+ while ('left' in node) {
168
+ if (offset < node.left.length) {
169
+ node = node.left;
170
+ } else {
171
+ offset -= node.left.length;
172
+ node = node.right;
173
+ }
174
+ }
175
+ return node.text.charAt(offset);
176
+ }
177
+
178
+ slice(from = 0, to = this.length): string {
179
+ const chunks: string[] = [];
180
+ collect(this.root, clampInteger(from, this.length), clampInteger(to, this.length), chunks);
181
+ return chunks.join('');
182
+ }
183
+
184
+ replace(from: number, to: number, text: string): TextRope {
185
+ const [before, rest] = split(this.root, from);
186
+ const [, after] = split(rest, to - from);
187
+ return new TextRope(join(join(before, build(text)), after));
188
+ }
189
+
190
+ /* Whether both ropes hold the same text in `[from, to)`, which has to be the same range of both. */
191
+ equalsRange(other: TextRope, from: number, to: number): boolean {
192
+ if (this.root === other.root) {
193
+ return true;
194
+ }
195
+ const left = chunksOf(this.root, from, to);
196
+ const right = chunksOf(other.root, from, to);
197
+ let leftChunk = left.next();
198
+ let rightChunk = right.next();
199
+ let leftAt = 0;
200
+ let rightAt = 0;
201
+ while (!leftChunk.done && !rightChunk.done) {
202
+ const length = Math.min(leftChunk.value.length - leftAt, rightChunk.value.length - rightAt);
203
+ if (leftChunk.value.slice(leftAt, leftAt + length) !== rightChunk.value.slice(rightAt, rightAt + length)) {
204
+ return false;
205
+ }
206
+ leftAt += length;
207
+ rightAt += length;
208
+ if (leftAt === leftChunk.value.length) {
209
+ leftChunk = left.next();
210
+ leftAt = 0;
211
+ }
212
+ if (rightAt === rightChunk.value.length) {
213
+ rightChunk = right.next();
214
+ rightAt = 0;
215
+ }
216
+ }
217
+ return Boolean(leftChunk.done && rightChunk.done);
218
+ }
219
+
220
+ lineAt(offset: number): number {
221
+ offset = clampInteger(offset, this.length);
222
+ let node = this.root;
223
+ let count = 0;
224
+ if (!node) {
225
+ return count;
226
+ }
227
+ while ('left' in node) {
228
+ if (offset < node.left.length) {
229
+ node = node.left;
230
+ } else {
231
+ offset -= node.left.length;
232
+ count += node.left.lines;
233
+ node = node.right;
234
+ }
235
+ }
236
+ let low = 0;
237
+ let high = node.breaks.length;
238
+ while (low < high) {
239
+ const middle = (low + high) >>> 1;
240
+ if (node.breaks[middle]! < offset) {
241
+ low = middle + 1;
242
+ } else {
243
+ high = middle;
244
+ }
245
+ }
246
+ return count + low;
247
+ }
248
+
249
+ lineBounds(index: number): Omit<DocumentLine, 'text'> {
250
+ index = clampInteger(index, this.lineCount - 1);
251
+ const start = index === 0 ? 0 : this.breakAt(index - 1) + 1;
252
+ if (index === this.lineCount - 1) {
253
+ return { start, end: this.length, next: this.length };
254
+ }
255
+ const newline = this.breakAt(index);
256
+ return { start, end: newline - (this.charAt(newline - 1) === '\r' ? 1 : 0), next: newline + 1 };
257
+ }
258
+
259
+ getLine(index: number): DocumentLine {
260
+ const bounds = this.lineBounds(index);
261
+ return { ...bounds, text: this.slice(bounds.start, bounds.end) };
262
+ }
263
+
264
+ private breakAt(index: number): number {
265
+ let node = this.root!;
266
+ let offset = 0;
267
+ while ('left' in node) {
268
+ if (index < node.left.lines) {
269
+ node = node.left;
270
+ } else {
271
+ index -= node.left.lines;
272
+ offset += node.left.length;
273
+ node = node.right;
274
+ }
275
+ }
276
+ return offset + node.breaks[index]!;
277
+ }
278
+ }
package/src/search.ts ADDED
@@ -0,0 +1,119 @@
1
+ import { replaceWithCaseRespect } from './preserve-case.ts';
2
+
3
+ export interface FindOptions {
4
+ caseSensitive?: boolean;
5
+ wholeWord?: boolean;
6
+ regex?: boolean;
7
+ /* Bounds filter matches of the whole document, so anchors and lookarounds still see their context. */
8
+ from?: number;
9
+ to?: number;
10
+ maxResults?: number;
11
+ }
12
+
13
+ export interface FindNextOptions extends FindOptions {
14
+ backwards?: boolean;
15
+ /* On unless `false`. */
16
+ wrap?: boolean;
17
+ }
18
+
19
+ /* Only valid for the revision it was found in; `replace` refuses a stale one. */
20
+ export interface FindMatch {
21
+ from: number;
22
+ to: number;
23
+ text: string;
24
+ captures: readonly (string | undefined)[];
25
+ groups?: Readonly<Record<string, string | undefined>>;
26
+ regex: boolean;
27
+ revision: number;
28
+ }
29
+
30
+ const wordCharacter = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
31
+
32
+ /* Whether `offset` sits between the two halves of a surrogate pair. */
33
+ export function splitsSurrogate(text: { charAt(offset: number): string }, offset: number): boolean {
34
+ return /[\ud800-\udbff]/.test(text.charAt(offset - 1)) && /[\udc00-\udfff]/.test(text.charAt(offset));
35
+ }
36
+
37
+ function characterBefore(text: string, offset: number): string {
38
+ return text.slice(offset - (splitsSurrogate(text, offset - 1) ? 2 : 1), offset);
39
+ }
40
+
41
+ /* Invalid bounds throw `RangeError`, an invalid expression `SyntaxError`. A zero-width match advances by one code point. */
42
+ export function findMatches(text: string, query: string, revision: number, options: FindOptions = {}): FindMatch[] {
43
+ if (!query && !options.regex) {
44
+ return [];
45
+ }
46
+ const from = options.from ?? 0;
47
+ const to = options.to ?? text.length;
48
+ const maximum = options.maxResults ?? Infinity;
49
+ if (!Number.isInteger(from) || !Number.isInteger(to) || from < 0 || to < from || to > text.length) {
50
+ throw new RangeError('Search bounds must be ordered offsets inside the document.');
51
+ }
52
+ if (maximum !== Infinity && (!Number.isInteger(maximum) || maximum < 0)) {
53
+ throw new RangeError('maxResults must be a nonnegative integer.');
54
+ }
55
+ const source = options.regex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
56
+ const expression = new RegExp(source, `gmu${options.caseSensitive ? '' : 'i'}`);
57
+ expression.lastIndex = from;
58
+ const matches: FindMatch[] = [];
59
+ while (matches.length < maximum) {
60
+ const match = expression.exec(text);
61
+ if (!match || match.index > to) {
62
+ break;
63
+ }
64
+ const end = match.index + match[0].length;
65
+ const wholeWord =
66
+ !options.wholeWord ||
67
+ (!wordCharacter.test(characterBefore(text, match.index)) && !wordCharacter.test(String.fromCodePoint(text.codePointAt(end) ?? 0)));
68
+ if (match.index >= from && end <= to && !splitsSurrogate(text, match.index) && !splitsSurrogate(text, end) && wholeWord) {
69
+ matches.push({
70
+ from: match.index,
71
+ to: end,
72
+ text: match[0],
73
+ captures: match.slice(1),
74
+ groups: match.groups ? { ...match.groups } : undefined,
75
+ regex: options.regex === true,
76
+ revision
77
+ });
78
+ }
79
+ if (!match[0].length) {
80
+ expression.lastIndex = match.index + ((text.codePointAt(match.index) ?? 0) > 0xffff ? 2 : 1);
81
+ }
82
+ }
83
+ return matches;
84
+ }
85
+
86
+ /* Expands the JavaScript `$` patterns of a regex replacement, and gives it the case of the match when asked. Literal replacements are returned as they are. */
87
+ export function replacementText(text: string, match: FindMatch, replacement: string, literal = false, preserveCase = false): string {
88
+ const expanded = literal || !match.regex ? replacement : expandPatterns(text, match, replacement);
89
+ return preserveCase ? replaceWithCaseRespect(expanded, match.text) : expanded;
90
+ }
91
+
92
+ function expandPatterns(text: string, match: FindMatch, replacement: string): string {
93
+ return replacement.replace(/\$(\$|&|`|'|\d{1,2}|<[^>]*>)/g, (token: string, group: string) => {
94
+ if (group === '$') {
95
+ return '$';
96
+ }
97
+ if (group === '&') {
98
+ return match.text;
99
+ }
100
+ if (group === '`') {
101
+ return text.slice(0, match.from);
102
+ }
103
+ if (group === "'") {
104
+ return text.slice(match.to);
105
+ }
106
+ if (group.startsWith('<')) {
107
+ return match.groups ? (match.groups[group.slice(1, -1)] ?? '') : token;
108
+ }
109
+ const index = Number(group);
110
+ if (index > 0 && index <= match.captures.length) {
111
+ return match.captures[index - 1] ?? '';
112
+ }
113
+ const first = Number(group[0]);
114
+ if (group.length === 2 && first > 0 && first <= match.captures.length) {
115
+ return (match.captures[first - 1] ?? '') + group[1];
116
+ }
117
+ return token;
118
+ });
119
+ }
@@ -0,0 +1,223 @@
1
+ import { bracketRole, commentRole } from './fold-roles.ts';
2
+ import { openers } from './lexical.ts';
3
+ import type { DocumentLine } from './rope.ts';
4
+
5
+ /* A bracket pair, string or block comment. `inner*` is the range without its delimiters. */
6
+ export interface StructureRange {
7
+ from: number;
8
+ to: number;
9
+ innerFrom: number;
10
+ innerTo: number;
11
+ kind: 'bracket' | 'string' | 'comment';
12
+ }
13
+
14
+ /* What a fold is, which the settings choose by to fold it when a file opens. */
15
+ export type FoldRole =
16
+ | 'file-header'
17
+ | 'imports'
18
+ | 'doc-comment'
19
+ | 'region'
20
+ | 'function-body'
21
+ | 'method-body'
22
+ | 'class-body'
23
+ | 'object-literal'
24
+ | 'array-literal'
25
+ | 'tag'
26
+ | 'attribute'
27
+ | 'php-tag'
28
+ | 'heredoc'
29
+ | 'front-matter'
30
+ | 'code-fence'
31
+ | 'table';
32
+
33
+ /*
34
+ * Lines are zero-based and the offsets half-open. Fold from the end of `startLine` through `endLine`.
35
+ * `kind` is the shape: `imports` is a run of import lines, `line-comments` a run of lines of `//` comments,
36
+ * `region` what sits between `// region` and `// endregion`, `block` a run of whole lines such as a code
37
+ * fence, `section` what a Markdown heading holds and `server` a range a language server named.
38
+ */
39
+ export interface FoldingRange {
40
+ startLine: number;
41
+ endLine: number;
42
+ from: number;
43
+ to: number;
44
+ kind: 'bracket' | 'comment' | 'indentation' | 'imports' | 'line-comments' | 'region' | 'block' | 'section' | 'server';
45
+ /* What the range is, where the text or a language server says; most ranges have none. */
46
+ role?: FoldRole;
47
+ }
48
+
49
+ export interface FoldingOptions {
50
+ /* On unless `false`. */
51
+ brackets?: boolean;
52
+ /* On unless `false`. */
53
+ comments?: boolean;
54
+ indentation?: boolean;
55
+ /* The language id, which decides what an import line and a line comment look like. Without one only brackets, block comments and indentation fold. */
56
+ language?: string;
57
+ /* Each on unless `false`, and only with a language. */
58
+ imports?: boolean;
59
+ regions?: boolean;
60
+ lineComments?: boolean;
61
+ tabSize?: number;
62
+ /* How many lines a range spans past its first one before it can fold. */
63
+ minLines?: number;
64
+ /* What a language server said about the document, in offsets of its text. */
65
+ hints?: FoldHints;
66
+ }
67
+
68
+ /* A symbol's range and the kind of body it has. */
69
+ export interface FoldSymbolHint {
70
+ from: number;
71
+ to: number;
72
+ /* `value` is a variable or property, which has a function body when its initializer is a function. */
73
+ body: 'function' | 'method' | 'class' | 'value';
74
+ }
75
+
76
+ /* A range a server folds, from any offset on its first line to any offset on its last. */
77
+ export interface FoldRangeHint {
78
+ from: number;
79
+ to: number;
80
+ /* The LSP kind: `comment`, `imports` or `region`. */
81
+ kind?: string;
82
+ }
83
+
84
+ export interface FoldHints {
85
+ symbols?: readonly FoldSymbolHint[];
86
+ ranges?: readonly FoldRangeHint[];
87
+ }
88
+
89
+ interface Header {
90
+ line: number;
91
+ indent: number;
92
+ }
93
+
94
+ /* Clamped to 1 through 16, with 4 for anything that is not a finite number. */
95
+ export function tabWidth(options: { tabSize?: number }): number {
96
+ const requested = options.tabSize ?? 4;
97
+ return Number.isFinite(requested) ? Math.max(1, Math.min(16, Math.trunc(requested))) : 4;
98
+ }
99
+
100
+ export function indentationColumn(text: string, tabSize: number): number {
101
+ let column = 0;
102
+ for (const character of text) {
103
+ column += character === '\t' ? tabSize - (column % tabSize) : 1;
104
+ }
105
+ return column;
106
+ }
107
+
108
+ /* Outermost first. It knows only line and block comments and the three quote characters, so a regex literal can confuse it. */
109
+ export function scanStructure(text: string): StructureRange[] {
110
+ const result: StructureRange[] = [];
111
+ const stack: { from: number; closer: string }[] = [];
112
+ for (let offset = 0; offset < text.length; offset++) {
113
+ const character = text[offset]!;
114
+ if (character === '/' && text[offset + 1] === '/') {
115
+ const end = text.indexOf('\n', offset + 2);
116
+ offset = end === -1 ? text.length : end;
117
+ } else if (character === '/' && text[offset + 1] === '*') {
118
+ const end = text.indexOf('*/', offset + 2);
119
+ if (end === -1) {
120
+ break;
121
+ }
122
+ result.push({ from: offset, to: end + 2, innerFrom: offset + 2, innerTo: end, kind: 'comment' });
123
+ offset = end + 1;
124
+ } else if (character === '"' || character === "'" || character === '`') {
125
+ const start = offset;
126
+ for (offset++; offset < text.length; offset++) {
127
+ if (text[offset] === '\\') {
128
+ offset++;
129
+ } else if (text[offset] === character) {
130
+ result.push({ from: start, to: offset + 1, innerFrom: start + 1, innerTo: offset, kind: 'string' });
131
+ break;
132
+ } else if (character !== '`' && (text[offset] === '\n' || text[offset] === '\r')) {
133
+ break;
134
+ }
135
+ }
136
+ } else if (openers[character]) {
137
+ stack.push({ from: offset, closer: openers[character] });
138
+ } else if (stack.at(-1)?.closer === character) {
139
+ const opener = stack.pop()!;
140
+ result.push({ from: opener.from, to: offset + 1, innerFrom: opener.from + 1, innerTo: offset, kind: 'bracket' });
141
+ }
142
+ }
143
+ return result.sort((left, right) => left.from - right.from || right.to - left.to);
144
+ }
145
+
146
+ function indentationFolds(lineCount: number, getLine: (line: number) => DocumentLine, tabSize: number, minimum: number): FoldingRange[] {
147
+ const folds: FoldingRange[] = [];
148
+ const headers: Header[] = [];
149
+ let previous: Header | undefined;
150
+
151
+ const close = (header: Header, endLine: number): void => {
152
+ if (endLine - header.line >= minimum) {
153
+ folds.push({ from: getLine(header.line).start, to: getLine(endLine).end, startLine: header.line, endLine, kind: 'indentation' });
154
+ }
155
+ };
156
+
157
+ for (let line = 0; line < lineCount; line++) {
158
+ const text = getLine(line).text;
159
+ if (!text.trim()) {
160
+ continue;
161
+ }
162
+ const indent = indentationColumn(text.match(/^[\t ]*/)?.[0] ?? '', tabSize);
163
+ while (headers.length > 0 && indent <= headers.at(-1)!.indent) {
164
+ close(headers.pop()!, previous!.line);
165
+ }
166
+ if (previous && indent > previous.indent) {
167
+ headers.push(previous);
168
+ }
169
+ previous = { line, indent };
170
+ }
171
+ while (headers.length > 0) {
172
+ close(headers.pop()!, previous!.line);
173
+ }
174
+ return folds;
175
+ }
176
+
177
+ /* Ranges that span the same lines collapse into the first, which is the outermost. */
178
+ export function deriveFoldingRanges(
179
+ ranges: readonly StructureRange[],
180
+ lineAt: (offset: number) => number,
181
+ lineCount: number,
182
+ getLine: (line: number) => DocumentLine,
183
+ options: FoldingOptions = {},
184
+ slice?: (from: number, to: number) => string
185
+ ): FoldingRange[] {
186
+ const minimum = options.minLines === undefined ? 1 : Math.max(1, Number.isFinite(options.minLines) ? Math.trunc(options.minLines) : 1);
187
+ const result: FoldingRange[] = [];
188
+ for (const range of ranges) {
189
+ if (range.kind === 'string' || (range.kind === 'bracket' ? options.brackets === false : options.comments === false)) {
190
+ continue;
191
+ }
192
+ const startLine = lineAt(range.from);
193
+ const endLine = lineAt(range.to - 1);
194
+ if (endLine - startLine >= minimum) {
195
+ const role =
196
+ slice === undefined
197
+ ? undefined
198
+ : range.kind === 'comment'
199
+ ? commentRole(slice, range.from, range.to)
200
+ : bracketRole(slice, range.from, options.language ?? '');
201
+ result.push({ from: range.from, to: range.to, startLine, endLine, kind: range.kind, ...(role === undefined ? {} : { role }) });
202
+ }
203
+ }
204
+ if (options.indentation) {
205
+ result.push(...indentationFolds(lineCount, getLine, tabWidth(options), minimum));
206
+ }
207
+ result.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
208
+ return dedupeFoldingRanges(result);
209
+ }
210
+
211
+ /* Of ranges that span the same lines, sorted outermost first, the first stays and takes the role of a later one when it has none. */
212
+ export function dedupeFoldingRanges(sorted: readonly FoldingRange[]): FoldingRange[] {
213
+ const unique: FoldingRange[] = [];
214
+ for (const range of sorted) {
215
+ const last = unique.at(-1);
216
+ if (last === undefined || range.startLine !== last.startLine || range.endLine !== last.endLine) {
217
+ unique.push(range);
218
+ } else if (last.role === undefined && range.role !== undefined) {
219
+ last.role = range.role;
220
+ }
221
+ }
222
+ return unique;
223
+ }