@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
@@ -0,0 +1,331 @@
1
+ const JSX_LANGUAGES = /^(typescriptreact|javascriptreact|tsx|jsx|javascript|js|mjs|cjs)$/i;
2
+ const NAME = /[\w$.:-]/;
3
+ const START_OF_NAME = /[A-Za-z_$>]/;
4
+ const EXPRESSION_KEYWORDS = /^(return|case|yield|await|default|typeof|void|delete|in|of|else|do)$/;
5
+ /* The characters after which an expression, and so a JSX element, may start. */
6
+ const EXPRESSION_STARTS = '(,=:[!&|?{>~^%*+-';
7
+ const CLOSERS = { '{': '}', '(': ')', '[': ']' };
8
+ /*
9
+ * The stretches of a script that are JSX elements, as offsets, outermost first. A scanner and not a
10
+ * parser: an element that does not close as one is left out, and a file that costs more than a few
11
+ * passes over its text gives up with what it found.
12
+ */
13
+ class JsxScanner {
14
+ ranges = [];
15
+ /* The quoted values of attributes, quotes included. */
16
+ values = [];
17
+ text;
18
+ depth = 0;
19
+ budget;
20
+ constructor(text) {
21
+ this.text = text;
22
+ this.budget = text.length * 12 + 1_000;
23
+ }
24
+ scan() {
25
+ this.code(0, null);
26
+ }
27
+ /* Reads code up to the closer that ends it and returns the offset after it; -1 when it never closes as it should. */
28
+ code(from, closer) {
29
+ const { text } = this;
30
+ let at = from;
31
+ let previous = '';
32
+ let word = '';
33
+ while (at < text.length) {
34
+ if (--this.budget < 0) {
35
+ return -1;
36
+ }
37
+ const char = text[at];
38
+ const next = text[at + 1];
39
+ if (/\s/.test(char)) {
40
+ at++;
41
+ continue;
42
+ }
43
+ if (char === '/' && next === '/') {
44
+ const end = text.indexOf('\n', at);
45
+ at = end < 0 ? text.length : end;
46
+ continue;
47
+ }
48
+ if (char === '/' && next === '*') {
49
+ const end = text.indexOf('*/', at + 2);
50
+ at = end < 0 ? text.length : end + 2;
51
+ continue;
52
+ }
53
+ if (char === '"' || char === "'") {
54
+ at = this.quoted(at);
55
+ previous = char;
56
+ word = '';
57
+ continue;
58
+ }
59
+ if (char === '`') {
60
+ at = this.template(at);
61
+ if (at < 0) {
62
+ return -1;
63
+ }
64
+ previous = char;
65
+ word = '';
66
+ continue;
67
+ }
68
+ const expressionStart = previous === '' || EXPRESSION_STARTS.includes(previous) || EXPRESSION_KEYWORDS.test(word);
69
+ if (char === '/' && expressionStart) {
70
+ at = this.regex(at);
71
+ previous = '/';
72
+ word = '';
73
+ continue;
74
+ }
75
+ if (char === '<' && expressionStart && next !== undefined && START_OF_NAME.test(next)) {
76
+ const end = this.element(at, true);
77
+ if (end >= 0) {
78
+ at = end;
79
+ previous = ')';
80
+ word = '';
81
+ continue;
82
+ }
83
+ }
84
+ const closes = CLOSERS[char];
85
+ if (closes !== undefined) {
86
+ const end = this.code(at + 1, closes);
87
+ if (end < 0) {
88
+ return -1;
89
+ }
90
+ at = end;
91
+ previous = closes;
92
+ word = '';
93
+ continue;
94
+ }
95
+ if (char === '}' || char === ')' || char === ']') {
96
+ if (closer === char) {
97
+ return at + 1;
98
+ }
99
+ if (closer !== null) {
100
+ return -1;
101
+ }
102
+ at++;
103
+ previous = char;
104
+ word = '';
105
+ continue;
106
+ }
107
+ if (/[\w$]/.test(char)) {
108
+ const start = at;
109
+ while (at < text.length && /[\w$]/.test(text[at])) {
110
+ at++;
111
+ }
112
+ word = text.slice(start, at);
113
+ previous = word.slice(-1);
114
+ continue;
115
+ }
116
+ previous = char;
117
+ word = '';
118
+ at++;
119
+ }
120
+ return closer === null ? text.length : -1;
121
+ }
122
+ quoted(from) {
123
+ const { text } = this;
124
+ const quote = text[from];
125
+ let at = from + 1;
126
+ while (at < text.length && text[at] !== quote && text[at] !== '\n') {
127
+ at += text[at] === '\\' ? 2 : 1;
128
+ }
129
+ return Math.min(text.length, at + 1);
130
+ }
131
+ template(from) {
132
+ const { text } = this;
133
+ let at = from + 1;
134
+ while (at < text.length) {
135
+ if (text[at] === '\\') {
136
+ at += 2;
137
+ }
138
+ else if (text[at] === '`') {
139
+ return at + 1;
140
+ }
141
+ else if (text[at] === '$' && text[at + 1] === '{') {
142
+ at = this.code(at + 2, '}');
143
+ if (at < 0) {
144
+ return -1;
145
+ }
146
+ }
147
+ else {
148
+ at++;
149
+ }
150
+ }
151
+ return text.length;
152
+ }
153
+ regex(from) {
154
+ const { text } = this;
155
+ let at = from + 1;
156
+ let inClass = false;
157
+ while (at < text.length && text[at] !== '\n') {
158
+ const char = text[at];
159
+ if (char === '\\') {
160
+ at++;
161
+ }
162
+ else if (char === '[') {
163
+ inClass = true;
164
+ }
165
+ else if (char === ']') {
166
+ inClass = false;
167
+ }
168
+ else if (char === '/' && !inClass) {
169
+ return at + 1;
170
+ }
171
+ at++;
172
+ }
173
+ return from + 1;
174
+ }
175
+ /* An element from its `<`, recorded when no element holds it, and the offset after its closing tag; -1 when it is not one. */
176
+ element(from, outermost) {
177
+ this.depth++;
178
+ const end = this.parse(from);
179
+ this.depth--;
180
+ if (end >= 0 && outermost && this.depth === 0) {
181
+ this.ranges.push({ from, to: end });
182
+ }
183
+ return end;
184
+ }
185
+ skipSpace(from) {
186
+ let at = from;
187
+ while (at < this.text.length && /\s/.test(this.text[at])) {
188
+ at++;
189
+ }
190
+ return at;
191
+ }
192
+ parse(from) {
193
+ const { text } = this;
194
+ let at = from + 1;
195
+ while (at < text.length && NAME.test(text[at])) {
196
+ at++;
197
+ }
198
+ const name = text.slice(from + 1, at);
199
+ for (;;) {
200
+ at = this.skipSpace(at);
201
+ const char = text[at];
202
+ if (char === undefined || --this.budget < 0) {
203
+ return -1;
204
+ }
205
+ if (char === '/' && text[at + 1] === '>') {
206
+ return at + 2;
207
+ }
208
+ if (char === '>') {
209
+ at++;
210
+ break;
211
+ }
212
+ if (char === '{') {
213
+ at = this.code(at + 1, '}');
214
+ if (at < 0) {
215
+ return -1;
216
+ }
217
+ continue;
218
+ }
219
+ if (!/[A-Za-z_$]/.test(char)) {
220
+ return -1;
221
+ }
222
+ while (at < text.length && NAME.test(text[at])) {
223
+ at++;
224
+ }
225
+ at = this.skipSpace(at);
226
+ if (text[at] !== '=') {
227
+ continue;
228
+ }
229
+ at = this.skipSpace(at + 1);
230
+ const value = text[at];
231
+ if (value === '"' || value === "'") {
232
+ const close = text.indexOf(value, at + 1);
233
+ if (close < 0) {
234
+ return -1;
235
+ }
236
+ this.values.push({ from: at, to: close + 1 });
237
+ at = close + 1;
238
+ }
239
+ else if (value === '{') {
240
+ at = this.code(at + 1, '}');
241
+ }
242
+ else if (value === '<' && START_OF_NAME.test(text[at + 1] ?? '')) {
243
+ at = this.parse(at);
244
+ }
245
+ else {
246
+ return -1;
247
+ }
248
+ if (at < 0) {
249
+ return -1;
250
+ }
251
+ }
252
+ for (;;) {
253
+ while (at < text.length && text[at] !== '<' && text[at] !== '{') {
254
+ at++;
255
+ }
256
+ if (at >= text.length || --this.budget < 0) {
257
+ return -1;
258
+ }
259
+ if (text[at] === '{') {
260
+ at = this.code(at + 1, '}');
261
+ }
262
+ else if (text[at + 1] === '/') {
263
+ const closing = /^<\/\s*([\w$.:-]*)\s*>/.exec(text.slice(at, at + 200));
264
+ return closing !== null && closing[1] === name ? at + closing[0].length : -1;
265
+ }
266
+ else if (START_OF_NAME.test(text[at + 1] ?? '')) {
267
+ at = this.parse(at);
268
+ }
269
+ else {
270
+ return -1;
271
+ }
272
+ if (at < 0) {
273
+ return -1;
274
+ }
275
+ }
276
+ }
277
+ }
278
+ /* The offsets of the JSX elements of a script, outermost first and in order. */
279
+ export function jsxRanges(text) {
280
+ const scanner = new JsxScanner(text);
281
+ scanner.scan();
282
+ return scanner.ranges;
283
+ }
284
+ /* Whether a language is one whose scripts can hold JSX. */
285
+ export function hasJsx(language) {
286
+ return JSX_LANGUAGES.test(language);
287
+ }
288
+ /* Whether the string that opens at `offset` is the value of a JSX attribute, which is markup and not a string expression. */
289
+ export function isJsxAttributeValue(text, offset) {
290
+ const scanner = new JsxScanner(text);
291
+ scanner.scan();
292
+ return scanner.values.some((value) => value.from === offset);
293
+ }
294
+ /*
295
+ * Whether a line is markup that a command which works from the code around a line must leave as it
296
+ * is: a Vue template and everything outside its script and style blocks, and the elements of JSX
297
+ * with what stands inside them. The HTML of a PHP file and heredocs are modes of the lexer.
298
+ */
299
+ export function markupGuard(source, language) {
300
+ const id = language.toLowerCase();
301
+ if (id === 'vue') {
302
+ return (line) => {
303
+ const region = source.region(line);
304
+ return (region !== 'script' && region !== 'style') || /^\s*<\/?(?:script|style|template)\b/.test(source.line(line).text);
305
+ };
306
+ }
307
+ if (hasJsx(id)) {
308
+ const ranges = jsxRanges(source.slice(0, source.line(source.lineCount - 1).end));
309
+ return (line) => {
310
+ const bounds = source.line(line);
311
+ const start = bounds.start + (/^[\t ]*/.exec(bounds.text)?.[0].length ?? 0);
312
+ let low = 0;
313
+ let high = ranges.length - 1;
314
+ while (low <= high) {
315
+ const middle = (low + high) >> 1;
316
+ const range = ranges[middle];
317
+ if (start < range.from) {
318
+ high = middle - 1;
319
+ }
320
+ else if (start >= range.to) {
321
+ low = middle + 1;
322
+ }
323
+ else {
324
+ return true;
325
+ }
326
+ }
327
+ return false;
328
+ };
329
+ }
330
+ return () => false;
331
+ }
@@ -0,0 +1,2 @@
1
+ import type { TextEdit } from './types.ts';
2
+ export declare function mapOffset(offset: number, edits: readonly TextEdit[], stayBefore?: boolean): number;
@@ -0,0 +1,14 @@
1
+ /* An offset after simultaneous edits. An edit that inserts at it leaves it in front of the new text when `stayBefore`. */
2
+ export function mapOffset(offset, edits, stayBefore = false) {
3
+ let delta = 0;
4
+ for (const edit of edits) {
5
+ if (offset < edit.from || (stayBefore && offset === edit.from && edit.to === edit.from)) {
6
+ break;
7
+ }
8
+ if (offset < edit.to) {
9
+ return edit.from + delta + edit.text.length;
10
+ }
11
+ delta += edit.text.length - (edit.to - edit.from);
12
+ }
13
+ return offset + delta;
14
+ }
@@ -0,0 +1,9 @@
1
+ import type { EditPlan, EditSource } from './edit-source.ts';
2
+ import type { Selection } from './types.ts';
3
+ export interface PasteOptions {
4
+ tabSize: number;
5
+ insertSpaces: boolean;
6
+ wholeLines: boolean;
7
+ reindent: boolean;
8
+ }
9
+ export declare function planPaste(source: EditSource, selections: readonly Selection[], text: string, options: PasteOptions): EditPlan;
package/dist/paste.js ADDED
@@ -0,0 +1,108 @@
1
+ import { mapOffset } from './offsets.js';
2
+ import { indentationColumn } from './structure.js';
3
+ function whitespaceOf(text) {
4
+ return /^[\t ]*/.exec(text)[0];
5
+ }
6
+ function isBlank(text) {
7
+ return text.trim() === '';
8
+ }
9
+ /* A line break at the end is the end of the last line, not a line of its own. */
10
+ function linesOf(text) {
11
+ return text.replace(/\r?\n$/, '').split(/\r?\n/);
12
+ }
13
+ /* One line of the text for each caret, when there are as many lines as carets. */
14
+ function perCaretLines(text, carets) {
15
+ if (carets < 2) {
16
+ return null;
17
+ }
18
+ const lines = linesOf(text);
19
+ return lines.length === carets ? lines : null;
20
+ }
21
+ function indentText(width, options) {
22
+ if (options.insertSpaces) {
23
+ return ' '.repeat(width);
24
+ }
25
+ return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
26
+ }
27
+ /*
28
+ * Where a block of several lines lands, it lands at the indentation of the line it is pasted into:
29
+ * the pasted lines move together by the difference between that indentation and the least indented
30
+ * of them. A first line that starts at the caret stays where it is, and the lines below it are
31
+ * measured against each other, since its own indentation was left behind in the copy.
32
+ */
33
+ function reindent(source, from, text, options) {
34
+ const newline = text.includes('\r\n') ? '\r\n' : '\n';
35
+ const lines = text.split(/\r?\n/);
36
+ if (lines.length < 2) {
37
+ return text;
38
+ }
39
+ const line = source.line(source.lineAt(from));
40
+ const before = source.slice(line.start, from);
41
+ const caretColumn = indentationColumn(before, options.tabSize);
42
+ const lineIndent = indentationColumn(whitespaceOf(line.text), options.tabSize);
43
+ const startsLine = isBlank(before);
44
+ const firstIndent = whitespaceOf(lines[0]);
45
+ const anchored = startsLine ? caretColumn > 0 && firstIndent === '' : true;
46
+ const target = startsLine ? (anchored ? caretColumn : Math.max(caretColumn, lineIndent)) : lineIndent;
47
+ const widths = lines.map((entry) => (isBlank(entry) ? null : indentationColumn(whitespaceOf(entry), options.tabSize)));
48
+ const measured = widths.filter((width, index) => width !== null && !(anchored && index === 0));
49
+ const trailingBreak = isBlank(lines.at(-1));
50
+ if (measured.length > 0) {
51
+ const delta = target - Math.min(...measured);
52
+ for (const [index, entry] of lines.entries()) {
53
+ const width = widths[index];
54
+ if (width === null || width === undefined || (anchored && index === 0)) {
55
+ continue;
56
+ }
57
+ // The first line follows the indentation already in front of the caret.
58
+ const indent = Math.max(0, width + delta - (index === 0 ? caretColumn : 0));
59
+ if (indent !== width) {
60
+ lines[index] = indentText(indent, options) + entry.slice(whitespaceOf(entry).length);
61
+ }
62
+ }
63
+ }
64
+ if (trailingBreak) {
65
+ // What follows the paste stays under the indentation of the line it was on.
66
+ lines[lines.length - 1] = whitespaceOf(before);
67
+ }
68
+ return lines.join(newline);
69
+ }
70
+ /*
71
+ * What pasting `text` does at every selection: one line for each caret when the counts match, text
72
+ * copied from a bare caret as lines above the caret's own, and a block moved to the indentation of
73
+ * the line it lands on, when it lands in code and not in a string, a comment or a heredoc.
74
+ */
75
+ export function planPaste(source, selections, text, options) {
76
+ const ordered = selections.map((selection, index) => ({
77
+ from: Math.min(selection.anchor, selection.head),
78
+ to: Math.max(selection.anchor, selection.head),
79
+ index
80
+ }));
81
+ ordered.sort((left, right) => left.from - right.from);
82
+ const segments = perCaretLines(text, selections.length);
83
+ const newline = text.includes('\r\n') ? '\r\n' : '\n';
84
+ const edits = [];
85
+ if (options.wholeLines && selections.every((selection) => selection.anchor === selection.head)) {
86
+ const lineEdits = new Map();
87
+ for (const [position, { from }] of ordered.entries()) {
88
+ const start = source.line(source.lineAt(from)).start;
89
+ const piece = segments?.[position] ?? text;
90
+ lineEdits.set(start, (lineEdits.get(start) ?? '') + (/\r?\n$/.test(piece) ? piece : piece + newline));
91
+ }
92
+ for (const [start, insert] of lineEdits) {
93
+ edits.push({ from: start, to: start, text: insert });
94
+ }
95
+ return { edits, selections: selections.map((selection) => ({ anchor: mapOffset(selection.anchor, edits), head: mapOffset(selection.head, edits) })) };
96
+ }
97
+ const carets = new Array(selections.length);
98
+ let shift = 0;
99
+ for (const [position, { from, to, index }] of ordered.entries()) {
100
+ const piece = segments?.[position] ?? text;
101
+ const inserted = options.reindent && segments === null && source.context(from).mode === 'code' ? reindent(source, from, piece, options) : piece;
102
+ edits.push({ from, to, text: inserted });
103
+ const head = from + shift + inserted.length;
104
+ carets[index] = { anchor: head, head };
105
+ shift += inserted.length - (to - from);
106
+ }
107
+ return { edits, selections: carets };
108
+ }
@@ -0,0 +1 @@
1
+ export declare function replaceWithCaseRespect(replacement: string, found: string): string;
@@ -0,0 +1,61 @@
1
+ const letter = /\p{L}/u;
2
+ const upper = /\p{Lu}/u;
3
+ const lower = /\p{Ll}/u;
4
+ /*
5
+ * Gives a replacement the case of the text it replaces: the first letter follows the first letter of
6
+ * the match, and the rest follows the tail of the match when that is all upper or all lower case and
7
+ * the replacement's own tail is not already the other way. The platform's "Preserve case" for a replace.
8
+ */
9
+ export function replaceWithCaseRespect(replacement, found) {
10
+ if (found === '' || replacement === '') {
11
+ return replacement;
12
+ }
13
+ const [firstFound] = found;
14
+ const [firstReplacement] = replacement;
15
+ const head = upper.test(firstFound) ? firstReplacement.toUpperCase() : firstReplacement.toLowerCase();
16
+ const replacementTail = replacement.slice(firstReplacement.length);
17
+ if (replacementTail === '') {
18
+ return head;
19
+ }
20
+ const foundTail = found.slice(firstFound.length);
21
+ if (foundTail === '') {
22
+ return head + replacementTail;
23
+ }
24
+ let replacementLower = true;
25
+ let replacementUpper = true;
26
+ for (const character of replacementTail) {
27
+ if (!letter.test(character)) {
28
+ continue;
29
+ }
30
+ replacementLower &&= lower.test(character);
31
+ replacementUpper &&= upper.test(character);
32
+ if (!replacementLower && !replacementUpper) {
33
+ break;
34
+ }
35
+ }
36
+ let tailUpper = true;
37
+ let tailLower = true;
38
+ let tailChecked = false;
39
+ for (const character of foundTail) {
40
+ if (!letter.test(character)) {
41
+ continue;
42
+ }
43
+ tailUpper &&= upper.test(character);
44
+ tailLower &&= lower.test(character);
45
+ tailChecked = true;
46
+ if (!tailUpper && !tailLower) {
47
+ break;
48
+ }
49
+ }
50
+ if (!tailChecked) {
51
+ tailUpper = letter.test(firstFound) && upper.test(firstFound);
52
+ tailLower = letter.test(firstFound) && lower.test(firstFound);
53
+ }
54
+ if (tailUpper && (replacementLower || !replacementUpper)) {
55
+ return head + replacementTail.toUpperCase();
56
+ }
57
+ if (tailLower && (replacementLower || replacementUpper)) {
58
+ return head + replacementTail.toLowerCase();
59
+ }
60
+ return head + replacementTail;
61
+ }
package/dist/rope.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export interface DocumentLine {
2
+ start: number;
3
+ end: number;
4
+ next: number;
5
+ text: string;
6
+ }
7
+ export declare function clampInteger(value: number, maximum: number): number;
8
+ export declare class TextRope {
9
+ private readonly root;
10
+ private constructor();
11
+ static from(text: string): TextRope;
12
+ get length(): number;
13
+ get lineCount(): number;
14
+ charAt(offset: number): string;
15
+ slice(from?: number, to?: number): string;
16
+ replace(from: number, to: number, text: string): TextRope;
17
+ equalsRange(other: TextRope, from: number, to: number): boolean;
18
+ lineAt(offset: number): number;
19
+ lineBounds(index: number): Omit<DocumentLine, 'text'>;
20
+ getLine(index: number): DocumentLine;
21
+ private breakAt;
22
+ }