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