@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.
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 +40 -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,19 @@
1
+ import type { VueRegion } from './languages.ts';
2
+ import type { DocumentLine } from './rope.ts';
3
+ import type { TypingContext } from './typing-context.ts';
4
+ import type { Selection, TextEdit } from './types.ts';
5
+ export interface EditSource {
6
+ slice(from: number, to: number): string;
7
+ charAt(offset: number): string;
8
+ lineAt(offset: number): number;
9
+ lineCount: number;
10
+ line(index: number): DocumentLine;
11
+ newline(index: number): string;
12
+ context(offset: number): TypingContext;
13
+ region(line: number): VueRegion | null;
14
+ unmatchedBrace(offset: number): boolean;
15
+ }
16
+ export interface EditPlan {
17
+ edits: TextEdit[];
18
+ selections: Selection[];
19
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { EditSource } from './edit-source.ts';
2
+ import type { TypingContext } from './typing-context.ts';
3
+ export interface EnterOptions {
4
+ language: string;
5
+ unit: string;
6
+ tabSize: number;
7
+ smart: boolean;
8
+ reach: boolean;
9
+ }
10
+ export interface EnterPlan {
11
+ from: number;
12
+ to: number;
13
+ text: string;
14
+ caret: number;
15
+ }
16
+ export declare function continues(text: string, context: TypingContext, language: string): boolean;
17
+ export declare function planEnter(source: EditSource, from: number, to: number, options: EnterOptions): EnterPlan;
package/dist/enter.js ADDED
@@ -0,0 +1,387 @@
1
+ import { commentSyntax } from './languages.js';
2
+ import { hasHashComments, isBraced, isPhp } from './lexical.js';
3
+ import { hasJsx, isJsxAttributeValue } from './markup-regions.js';
4
+ import { indentationColumn } from './structure.js';
5
+ const closers = { '(': ')', '[': ']', '{': '}' };
6
+ const concatenation = new Map();
7
+ for (const id of 'typescript javascript typescriptreact javascriptreact tsx jsx ts js java csharp cs kotlin scala dart swift go groovy'.split(' ')) {
8
+ concatenation.set(id, '+');
9
+ }
10
+ concatenation.set('php', '.');
11
+ const markupLanguages = /^(html|xml|svg|vue|svelte|astro|mdx|jsx|tsx|javascriptreact|typescriptreact)$/i;
12
+ const voidElements = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i;
13
+ const trailingOperator = /(?:=>|&&|\|\||\?\?|[=*%?|&]|(?<!\+)\+|(?<!-)-)$/;
14
+ const lookBack = 50;
15
+ /* The longest text a JSX attribute is looked for in on an Enter. */
16
+ const jsxScanLimit = 2_000_000;
17
+ /* How far past the caret a comment is searched for its end. */
18
+ const commentReach = 20_000;
19
+ function whitespaceOf(text) {
20
+ return /^[\t ]*/.exec(text)[0];
21
+ }
22
+ function isBlank(text) {
23
+ return /^[\t ]*$/.test(text);
24
+ }
25
+ function escapeForPattern(text) {
26
+ return text.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&');
27
+ }
28
+ /*
29
+ * Whether the code before `end` leaves its statement open: after the parenthesis of a braceless
30
+ * `if` or `for`, after `else`, `=>` or a binary operator. Languages with braces only.
31
+ */
32
+ export function continues(text, context, language) {
33
+ if (!isBraced(language) || context.mode !== 'code') {
34
+ return false;
35
+ }
36
+ if (context.word === 'control-close' || context.word === 'else' || context.word === 'do') {
37
+ return true;
38
+ }
39
+ if (isPhp(language) && /(?<!\.)\.$/.test(text)) {
40
+ return true;
41
+ }
42
+ return trailingOperator.test(text);
43
+ }
44
+ class Enter {
45
+ source;
46
+ options;
47
+ constructor(source, options) {
48
+ this.source = source;
49
+ this.options = options;
50
+ }
51
+ plan(from, to) {
52
+ const { source, options } = this;
53
+ const index = source.lineAt(from);
54
+ const line = source.line(index);
55
+ const before = source.slice(line.start, from);
56
+ const leading = whitespaceOf(before);
57
+ const newline = source.newline(index);
58
+ if (!options.smart) {
59
+ return { from, to, text: newline + leading, caret: newline.length + leading.length };
60
+ }
61
+ const context = source.context(from);
62
+ const selection = from !== to;
63
+ const planned = context.mode === 'block-comment'
64
+ ? this.inBlockComment(from, to, context, index)
65
+ : context.mode === 'line-comment' && !selection
66
+ ? this.inLineComment(from, context, index)
67
+ : context.mode === 'quote' && !selection
68
+ ? this.inString(from, context, index)
69
+ : null;
70
+ return planned ?? this.inCode(from, to, context, index);
71
+ }
72
+ /* The indentation a new statement after this line gets, which is not the line's own when the line is a continuation. */
73
+ baseIndent(index, depth = 0) {
74
+ const line = this.source.line(index);
75
+ const own = whitespaceOf(line.text);
76
+ const previous = this.previousCodeLine(index);
77
+ if (previous === null || depth > lookBack || !this.lineContinues(previous)) {
78
+ return own;
79
+ }
80
+ return this.baseIndent(previous, depth + 1);
81
+ }
82
+ previousCodeLine(index) {
83
+ for (let candidate = index - 1; candidate >= 0 && candidate >= index - lookBack; candidate--) {
84
+ if (!isBlank(this.source.line(candidate).text)) {
85
+ return candidate;
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+ lineContinues(index) {
91
+ const line = this.source.line(index);
92
+ const text = line.text.trimEnd();
93
+ return text.length > 0 && continues(text, this.source.context(line.start + text.length), this.options.language);
94
+ }
95
+ inCode(from, to, lineContext, index) {
96
+ const { source, options } = this;
97
+ const line = source.line(index);
98
+ const before = source.slice(line.start, from);
99
+ const leading = whitespaceOf(before);
100
+ const newline = source.newline(index);
101
+ const { unit, language } = options;
102
+ // A comment that trails the code is not part of what decides the indentation.
103
+ const commentAt = lineContext.mode === 'line-comment' ? lineContext.commentStart : undefined;
104
+ const trailing = commentAt !== undefined && commentAt >= line.start && !isBlank(source.slice(line.start, commentAt));
105
+ const trimmed = trailing ? source.slice(line.start, commentAt).trimEnd() : before.trimEnd();
106
+ const context = trailing ? source.context(line.start + trimmed.length) : lineContext;
107
+ const last = trimmed.at(-1) ?? '';
108
+ const rest = source.slice(to, line.end);
109
+ const afterWhitespace = to + whitespaceOf(rest).length;
110
+ const next = source.charAt(afterWhitespace);
111
+ const base = context.mode === 'code' ? this.baseIndent(index) : leading;
112
+ const start = leading.length < before.length ? base : leading;
113
+ const plain = (indent) => ({ from, to: afterWhitespace, text: newline + indent, caret: newline.length + indent.length });
114
+ if (context.mode !== 'code') {
115
+ return plain(leading);
116
+ }
117
+ const opener = closers[last] !== undefined && context.bracket?.close === closers[last] && context.bracket.at === line.start + trimmed.length - 1;
118
+ if (opener) {
119
+ const inner = start + unit;
120
+ if (next === closers[last]) {
121
+ return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
122
+ }
123
+ const closing = last === '{' && source.unmatchedBrace(context.bracket.at) ? this.closeBrace(from, afterWhitespace, line, rest, newline, start, inner) : null;
124
+ return closing ?? plain(inner);
125
+ }
126
+ const tag = this.openTag(trimmed, index);
127
+ if (tag) {
128
+ const inner = start + unit;
129
+ if (new RegExp(`^</${escapeForPattern(tag)}\\s*>`, 'i').test(source.slice(afterWhitespace, line.end))) {
130
+ return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
131
+ }
132
+ return plain(inner);
133
+ }
134
+ if (this.opensIndentedBlock(trimmed, context)) {
135
+ return plain(this.blockIndent(line.text, start) + unit);
136
+ }
137
+ if (continues(trimmed, context, language)) {
138
+ const continued = this.previousCodeLine(index) !== null && this.lineContinues(this.previousCodeLine(index));
139
+ return plain(continued ? leading : start + unit);
140
+ }
141
+ return plain(start);
142
+ }
143
+ /* The `}` of a `{` that nothing closes goes under the line it opens, or after what follows the caret when that is a statement. */
144
+ closeBrace(from, afterWhitespace, line, rest, newline, base, inner) {
145
+ const tail = rest.trimStart();
146
+ if (tail === '' || /^[)\];,%<?]/.test(tail)) {
147
+ const text = newline + inner + newline + base + '}';
148
+ return { from, to: afterWhitespace, text, caret: newline.length + inner.length };
149
+ }
150
+ if (!this.options.reach) {
151
+ return null;
152
+ }
153
+ const text = newline + inner + tail.trimEnd() + newline + base + '}';
154
+ return { from, to: line.end, text, caret: newline.length + inner.length };
155
+ }
156
+ /* The name of the tag the text ends in, when it is an open tag with children to come. */
157
+ openTag(trimmed, index) {
158
+ const { language } = this.options;
159
+ if (!markupLanguages.test(language)) {
160
+ return null;
161
+ }
162
+ if (/^vue$/i.test(language) && this.source.region(index) === 'script') {
163
+ return null;
164
+ }
165
+ const match = /(?:^|[^\w$)\]])<([A-Za-z][\w:.-]*)(?:\s(?:[^<>]|=>)*)?>$/.exec(trimmed);
166
+ if (!match || trimmed.endsWith('/>') || (voidElements.test(match[1]) && !/^xml$/i.test(language))) {
167
+ return null;
168
+ }
169
+ return match[1];
170
+ }
171
+ /* Python's `def f():`, a YAML `key:` and a `case x:`. */
172
+ opensIndentedBlock(trimmed, context) {
173
+ const { language } = this.options;
174
+ if (/^(python|py)$/i.test(language)) {
175
+ return trimmed.endsWith(':') && context.bracket === undefined;
176
+ }
177
+ if (/^(yaml|yml)$/i.test(language)) {
178
+ return /(?::|[|>][+-]?)$/.test(trimmed) && !trimmed.trimStart().startsWith('#');
179
+ }
180
+ if (isBraced(language)) {
181
+ return (context.bracket === undefined || context.bracket.close === '}') && /^(?:case\b[^]*|default)\s*:$/.test(trimmed.trimStart());
182
+ }
183
+ return false;
184
+ }
185
+ /* A YAML list item's children line up with its key, not with its dash. */
186
+ blockIndent(text, start) {
187
+ if (!/^(yaml|yml)$/i.test(this.options.language)) {
188
+ return start;
189
+ }
190
+ const dashes = /^[\t ]*(?:- +)+/.exec(text)?.[0];
191
+ return dashes ? start + ' '.repeat(dashes.length - whitespaceOf(text).length) : start;
192
+ }
193
+ inLineComment(from, context, index) {
194
+ const { source, options } = this;
195
+ const line = source.line(index);
196
+ const commentStart = context.commentStart;
197
+ const syntax = commentSyntax(options.language, source.region(index));
198
+ // A hash comment of a language that has both, as in PHP, continues with its own marker.
199
+ const token = commentStart !== undefined && hasHashComments(options.language) && source.charAt(commentStart) === '#' ? '#' : syntax.line;
200
+ if (commentStart === undefined || commentStart < line.start || token === null) {
201
+ return null;
202
+ }
203
+ const marker = new RegExp(`^${escapeForPattern(token)}${token === '//' ? '[/!]?' : token === '#' ? '#*' : ''}`).exec(source.slice(commentStart, line.end))?.[0];
204
+ if (marker === undefined || from < commentStart + marker.length) {
205
+ return null;
206
+ }
207
+ const rest = source.slice(from, line.end);
208
+ const textStart = from + whitespaceOf(rest).length;
209
+ if (textStart >= line.end) {
210
+ return null;
211
+ }
212
+ const newline = source.newline(index);
213
+ const leading = whitespaceOf(source.slice(line.start, from));
214
+ if (source.slice(textStart, textStart + token.length) === token) {
215
+ return { from, to: textStart, text: newline + leading, caret: newline.length + leading.length };
216
+ }
217
+ const code = source.slice(line.start, commentStart);
218
+ const onlyComment = isBlank(code);
219
+ let spacing = ' ';
220
+ if (onlyComment) {
221
+ const gap = whitespaceOf(source.slice(commentStart + marker.length, line.end));
222
+ spacing = gap === '' ? ' ' : gap;
223
+ }
224
+ else if (source.charAt(from) === ' ') {
225
+ spacing = '';
226
+ }
227
+ const text = newline + leading + marker + spacing;
228
+ return { from, to: textStart, text, caret: text.length };
229
+ }
230
+ inString(from, context, index) {
231
+ const { source, options } = this;
232
+ const operator = concatenation.get(options.language.toLowerCase());
233
+ const quote = context.quote;
234
+ if (operator === undefined || (quote !== '"' && quote !== "'")) {
235
+ return null;
236
+ }
237
+ const line = source.line(index);
238
+ const opener = this.stringStart(line, from, quote);
239
+ if (opener !== null && (this.isModulePath(line, opener, index) || this.isMarkupAttribute(opener))) {
240
+ return null;
241
+ }
242
+ let at = from;
243
+ if (context.escaped) {
244
+ at += /^(?:u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|.)/.exec(source.slice(from, line.end))?.[0].length ?? 0;
245
+ }
246
+ if (!this.closesOnLine(at, quote, line)) {
247
+ return null;
248
+ }
249
+ const newline = source.newline(index);
250
+ const previous = this.previousCodeLine(index);
251
+ const leading = whitespaceOf(line.text);
252
+ const continued = previous !== null && this.lineContinues(previous);
253
+ const indent = continued ? leading : leading + options.unit;
254
+ const text = `${quote} ${operator}${newline}${indent}${quote}`;
255
+ const rest = source.slice(at, line.end);
256
+ return { from: at, to: at + whitespaceOf(rest).length, text, caret: text.length };
257
+ }
258
+ /* Where the string the caret is in opened, when it opened on this line. */
259
+ stringStart(line, from, quote) {
260
+ let open = null;
261
+ for (let at = line.start; at < from; at++) {
262
+ const char = this.source.charAt(at);
263
+ if (open === null) {
264
+ if (char === '"' || char === "'" || char === '`') {
265
+ open = { at, quote: char };
266
+ }
267
+ }
268
+ else if (char === '\\') {
269
+ at++;
270
+ }
271
+ else if (char === open.quote) {
272
+ open = null;
273
+ }
274
+ }
275
+ return open?.quote === quote ? open.at : null;
276
+ }
277
+ /* A path an import, a `require` or an `export ... from` names, where a break and a concatenation would name another file. */
278
+ isModulePath(line, opener, index) {
279
+ const before = this.source.slice(line.start, opener).trimEnd();
280
+ if (/(?:^|[^\w$.])(?:import|from|require|require_once|include|include_once)\s*\(?$/.test(before)) {
281
+ return true;
282
+ }
283
+ return /^go$/i.test(this.options.language) && before === '' && this.inGoImportBlock(index);
284
+ }
285
+ inGoImportBlock(index) {
286
+ for (let candidate = index - 1; candidate >= 0 && candidate >= index - lookBack; candidate--) {
287
+ const text = this.source.line(candidate).text.trim();
288
+ if (/^import\s*\($/.test(text)) {
289
+ return true;
290
+ }
291
+ if (text.startsWith(')')) {
292
+ return false;
293
+ }
294
+ }
295
+ return false;
296
+ }
297
+ /* The value of a JSX attribute, which has to stay one string. */
298
+ isMarkupAttribute(opener) {
299
+ if (!hasJsx(this.options.language)) {
300
+ return false;
301
+ }
302
+ const last = this.source.line(this.source.lineCount - 1).end;
303
+ return last <= jsxScanLimit && isJsxAttributeValue(this.source.slice(0, last), opener);
304
+ }
305
+ closesOnLine(from, quote, line) {
306
+ const text = this.source.slice(from, line.end);
307
+ for (let at = 0; at < text.length; at++) {
308
+ if (text[at] === '\\') {
309
+ at++;
310
+ }
311
+ else if (text[at] === quote) {
312
+ return true;
313
+ }
314
+ }
315
+ return false;
316
+ }
317
+ inBlockComment(from, to, context, index) {
318
+ const { source, options } = this;
319
+ const syntax = commentSyntax(options.language, source.region(index));
320
+ const open = context.commentStart;
321
+ if (syntax.block?.open !== '/*' || open === undefined || from < open + 2) {
322
+ return null;
323
+ }
324
+ const startIndex = source.lineAt(open);
325
+ const startLine = source.line(startIndex);
326
+ const line = source.line(index);
327
+ const code = source.slice(startLine.start, open);
328
+ const lead = this.commentLead(code);
329
+ const newline = source.newline(index);
330
+ const closeAt = this.closingOf(open, from);
331
+ const isDoc = source.slice(open, open + 3) === '/**' && source.slice(open, open + 4) !== '/**/';
332
+ const caretText = line.text.trimStart();
333
+ const onStar = index > startIndex && caretText.startsWith('*') && !caretText.startsWith('*/');
334
+ const onOpener = index === startIndex;
335
+ const rest = source.slice(to, line.end);
336
+ const textStart = to + whitespaceOf(rest).length;
337
+ if (closeAt === null) {
338
+ const star = onOpener || onStar ? this.starPrefix(line, lead, onStar, isDoc) : whitespaceOf(line.text);
339
+ const tail = source.slice(textStart, line.end).trimEnd();
340
+ const closer = `${newline}${lead} */`;
341
+ if (options.reach) {
342
+ const text = newline + star + tail + closer;
343
+ return { from, to: line.end, text, caret: newline.length + star.length };
344
+ }
345
+ return { from, to: textStart, text: newline + star, caret: newline.length + star.length };
346
+ }
347
+ if (isDoc && onOpener && source.lineAt(closeAt) === index && isBlank(source.slice(to, closeAt)) && isBlank(source.slice(closeAt + 2, line.end))) {
348
+ // Nothing left to say after the caret: the closer gets a line of its own, under the opener's star.
349
+ const star = this.starPrefix(line, lead, false, isDoc);
350
+ return { from, to: line.end, text: `${newline}${star}${newline}${lead} */`, caret: newline.length + star.length };
351
+ }
352
+ const second = source.line(startIndex + 1).text.trimStart();
353
+ const continuesStars = onOpener ? isDoc : second.startsWith('*') && !second.startsWith('*/');
354
+ if (!continuesStars) {
355
+ return null;
356
+ }
357
+ const star = this.starPrefix(line, lead, onStar, isDoc);
358
+ return { from, to: textStart, text: newline + star, caret: newline.length + star.length };
359
+ }
360
+ /* The whitespace that puts a star under the first star of an opener that stands after `code`. */
361
+ commentLead(code) {
362
+ const indent = whitespaceOf(code);
363
+ const gap = indentationColumn(code, this.options.tabSize) - indentationColumn(indent, this.options.tabSize);
364
+ return indent + ' '.repeat(gap);
365
+ }
366
+ /* A line of a comment that starts with `*` keeps its own alignment and the indentation inside a doc comment; any other line gets the opener's. */
367
+ starPrefix(line, lead, onStar, isDoc) {
368
+ if (!onStar) {
369
+ return `${lead} * `;
370
+ }
371
+ const inside = /^\*([\t ]*)/.exec(line.text.trimStart())?.[1] ?? '';
372
+ return `${whitespaceOf(line.text)}*${isDoc && inside !== '' ? inside : ' '}`;
373
+ }
374
+ /* Where the comment opened at `open` ends after `from`, or null when it runs on or swallows another comment. */
375
+ closingOf(open, from) {
376
+ const text = this.source.slice(from, from + commentReach);
377
+ const close = text.indexOf('*/');
378
+ if (close < 0) {
379
+ return null;
380
+ }
381
+ return this.source.slice(open + 2, from + close).includes('/*') ? null : from + close;
382
+ }
383
+ }
384
+ /* Where one Enter puts its break and caret, as a replacement of text around the caret. */
385
+ export function planEnter(source, from, to, options) {
386
+ return new Enter(source, options).plan(from, to);
387
+ }
@@ -0,0 +1,6 @@
1
+ import type { DocumentLine } from './rope.ts';
2
+ import type { FoldHints, FoldRole, FoldingRange } from './structure.ts';
3
+ export declare function commentRole(slice: (from: number, to: number) => string, from: number, to: number): FoldRole | undefined;
4
+ export declare function bracketRole(slice: (from: number, to: number) => string, from: number, language: string): FoldRole | undefined;
5
+ export declare function markFileHeader(ranges: FoldingRange[], getLine: (line: number) => DocumentLine, lineCount: number, language: string): void;
6
+ export declare function applyFoldHints(ranges: readonly FoldingRange[], hints: FoldHints, lineAt: (offset: number) => number, getLine: (line: number) => DocumentLine, slice: (from: number, to: number) => string, minimum: number): FoldingRange[];
@@ -0,0 +1,142 @@
1
+ import { isImportLine } from './lexical-folds.js';
2
+ const SCRIPT_LANGUAGES = /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|mjs|cjs|mts|cts)$/i;
3
+ /* How far back from a bracket the text before it is read. */
4
+ const LOOK_BEHIND = 200;
5
+ /* A file header sits at the top, behind at most a shebang or an open tag and a blank line or two. */
6
+ const HEADER_LINES = 4;
7
+ /* Words an expression can follow, so a bracket after one is a value and not a block. */
8
+ const VALUE_WORDS = /\b(?:return|default|yield|await|of|in)$/;
9
+ /* A block comment that opens with two stars is documentation; `/**` closed at once is an empty one. */
10
+ export function commentRole(slice, from, to) {
11
+ return to - from > 4 && slice(from, from + 3) === '/**' ? 'doc-comment' : undefined;
12
+ }
13
+ /*
14
+ * Whether a bracket opens an array or an object literal in a script, or a PHP attribute list, read from what
15
+ * comes before it. A block (`) {`, `else {`) and an index (`items[`) have a word or a closer in front of them.
16
+ */
17
+ export function bracketRole(slice, from, language) {
18
+ const opener = slice(from, from + 1);
19
+ const before = slice(Math.max(0, from - LOOK_BEHIND), from);
20
+ if (language.toLowerCase() === 'php') {
21
+ return opener === '[' && before.endsWith('#') ? 'attribute' : undefined;
22
+ }
23
+ if (!SCRIPT_LANGUAGES.test(language) || (opener !== '{' && opener !== '[')) {
24
+ return undefined;
25
+ }
26
+ const head = before.slice(before.lastIndexOf('\n') + 1);
27
+ const trimmed = before.trimEnd();
28
+ const previous = trimmed.at(-1);
29
+ if (previous === undefined) {
30
+ return undefined;
31
+ }
32
+ const arrow = trimmed.endsWith('=>');
33
+ const value = (/[=:(,[?]/.test(previous) && !arrow) || VALUE_WORDS.test(trimmed) || (arrow && opener === '[');
34
+ if (!value) {
35
+ return undefined;
36
+ }
37
+ if (opener === '[') {
38
+ return 'array-literal';
39
+ }
40
+ // A `case x: {` and a `type A = {` are a block and a type, not a value.
41
+ return /^\s*(?:case\b|default\s*:|(?:export\s+)?(?:declare\s+)?(?:type|interface)\b)/.test(head) ? undefined : 'object-literal';
42
+ }
43
+ function isBlank(text) {
44
+ return text.trim() === '';
45
+ }
46
+ /*
47
+ * The comment at the very top of a file is its header, unless it is the documentation of what follows
48
+ * right under it. Only the first comment can be one, and only behind a shebang, an open tag or blank lines.
49
+ */
50
+ export function markFileHeader(ranges, getLine, lineCount, language) {
51
+ const candidate = ranges.find((range) => (range.kind === 'comment' || range.kind === 'line-comments') && range.startLine <= HEADER_LINES);
52
+ if (candidate === undefined) {
53
+ return;
54
+ }
55
+ for (let line = 0; line < candidate.startLine; line++) {
56
+ const text = getLine(line).text;
57
+ if (!isBlank(text) && !(line === 0 && /^(#!|<\?php\s*$)/.test(text))) {
58
+ return;
59
+ }
60
+ }
61
+ const start = getLine(candidate.startLine);
62
+ if (candidate.from > start.start + (start.text.length - start.text.trimStart().length)) {
63
+ return;
64
+ }
65
+ const next = candidate.endLine + 1 < lineCount ? getLine(candidate.endLine + 1).text : '';
66
+ const attached = candidate.role === 'doc-comment' && !isBlank(next) && !isImportLine(language, next.trimStart());
67
+ if (!attached) {
68
+ candidate.role = 'file-header';
69
+ }
70
+ }
71
+ /* The role a language server's range kind says, when the text under it says nothing else. */
72
+ function serverRole(kind, text) {
73
+ if (kind === 'imports') {
74
+ return 'imports';
75
+ }
76
+ if (kind === 'region') {
77
+ return 'region';
78
+ }
79
+ if (kind === 'comment') {
80
+ return text.startsWith('/**') ? 'doc-comment' : undefined;
81
+ }
82
+ return text.startsWith('<') ? 'tag' : undefined;
83
+ }
84
+ /*
85
+ * What a language server adds to the folds the text gives. A symbol names the body among the brackets: the
86
+ * pair that closes where it ends, or the indented block that does. A range the text has no fold for becomes a
87
+ * fold of its own, one per line it starts on, so the server's `{` block is not a second fold under the pair.
88
+ */
89
+ export function applyFoldHints(ranges, hints, lineAt, getLine, slice, minimum) {
90
+ const result = ranges.map((range) => ({ ...range }));
91
+ for (const symbol of hints.symbols ?? []) {
92
+ const startLine = lineAt(symbol.from);
93
+ const endLine = lineAt(Math.max(symbol.from, symbol.to - 1));
94
+ const body = bodyOf(result, symbol, startLine, endLine, slice);
95
+ if (body !== undefined) {
96
+ body.range.role = body.role;
97
+ }
98
+ }
99
+ const starts = new Set(result.map((range) => range.startLine));
100
+ for (const hint of hints.ranges ?? []) {
101
+ const startLine = lineAt(hint.from);
102
+ const endLine = lineAt(hint.to);
103
+ if (endLine - startLine < minimum || starts.has(startLine)) {
104
+ continue;
105
+ }
106
+ const first = getLine(startLine);
107
+ const indent = first.text.length - first.text.trimStart().length;
108
+ const role = serverRole(hint.kind, first.text.trimStart());
109
+ starts.add(startLine);
110
+ result.push({
111
+ startLine,
112
+ endLine,
113
+ from: first.start + indent,
114
+ to: getLine(endLine).end,
115
+ kind: 'server',
116
+ ...(role === undefined ? {} : { role })
117
+ });
118
+ }
119
+ return result.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
120
+ }
121
+ /* The fold that is the body of a symbol, and what kind of body it is. */
122
+ function bodyOf(ranges, symbol, startLine, endLine, slice) {
123
+ const brace = ranges
124
+ .filter((range) => range.kind === 'bracket' && range.from >= symbol.from && range.to <= symbol.to && slice(range.from, range.from + 1) === '{')
125
+ .filter((range) => /^[\s;,]*$/.test(slice(range.to, symbol.to)))
126
+ .sort((left, right) => right.to - left.to || left.from - right.from)[0];
127
+ const range = brace ??
128
+ ranges
129
+ .filter((candidate) => candidate.kind === 'indentation' && candidate.endLine === endLine && candidate.startLine >= startLine)
130
+ .sort((left, right) => left.startLine - right.startLine)[0];
131
+ if (range === undefined) {
132
+ return undefined;
133
+ }
134
+ if (symbol.body === 'class') {
135
+ return { range, role: 'class-body' };
136
+ }
137
+ if (symbol.body === 'value') {
138
+ const signature = slice(symbol.from, range.from);
139
+ return /=>|\bfunction\b/.test(signature) ? { range, role: 'function-body' } : undefined;
140
+ }
141
+ return { range, role: symbol.body === 'method' ? 'method-body' : 'function-body' };
142
+ }
@@ -0,0 +1,10 @@
1
+ export { DocumentModel } from './document.ts';
2
+ export type { DocumentEditOptions, DocumentLine, OffsetRange, ReplaceAllOptions, ReplaceOptions, TypeTextOptions } from './document.ts';
3
+ export { scanBrackets, type BracketIndex } from './brackets.ts';
4
+ export { replaceWithCaseRespect } from './preserve-case.ts';
5
+ export { findMatches, replacementText, type FindMatch, type FindNextOptions, type FindOptions } from './search.ts';
6
+ export { indentationColumn } from './structure.ts';
7
+ export type { FoldHints, FoldRangeHint, FoldRole, FoldSymbolHint, FoldingOptions, FoldingRange } from './structure.ts';
8
+ export { isHumpBoundary, isWordBoundary, wordBoundary, type WordText } from './words.ts';
9
+ export type { ChangeSource, CommandOptions, ContentEdit, Disposable, DocumentChange, EditOptions, EditorCommand, EditorSnapshot, Position, Selection, TextEdit } from './types.ts';
10
+ export { changedSpan, changedSpans, type TextSpan } from './text-span.ts';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { DocumentModel } from './document.js';
2
+ export { scanBrackets } from './brackets.js';
3
+ export { replaceWithCaseRespect } from './preserve-case.js';
4
+ export { findMatches, replacementText } from './search.js';
5
+ export { indentationColumn } from './structure.js';
6
+ export { isHumpBoundary, isWordBoundary, wordBoundary } from './words.js';
7
+ export { changedSpan, changedSpans } from './text-span.js';
@@ -0,0 +1,12 @@
1
+ export interface BlockComment {
2
+ open: string;
3
+ close: string;
4
+ }
5
+ export interface CommentSyntax {
6
+ line: string | null;
7
+ block: BlockComment | null;
8
+ }
9
+ export type VueRegion = 'template' | 'script' | 'style';
10
+ export declare function commentSyntax(language: string, region?: VueRegion | null): CommentSyntax;
11
+ export declare function isPlainText(language: string): boolean;
12
+ export declare function vueRegionAt(lineText: (line: number) => string, line: number): VueRegion | null;
@@ -0,0 +1,51 @@
1
+ const slashStar = { open: '/*', close: '*/' };
2
+ const html = { open: '<!--', close: '-->' };
3
+ const cLike = { line: '//', block: slashStar };
4
+ const hash = { line: '#', block: null };
5
+ const markup = { line: null, block: html };
6
+ const none = { line: null, block: null };
7
+ const syntaxes = new Map();
8
+ function register(ids, syntax) {
9
+ for (const id of ids.split(' ')) {
10
+ syntaxes.set(id, syntax);
11
+ }
12
+ }
13
+ register('typescript javascript typescriptreact javascriptreact tsx jsx ts js mjs cjs mts cts java c cpp c++ csharp cs go rust rs swift kotlin kt scala dart php scss less zig groq json jsonc json5 prisma astro svelte objective-c objc glsl hlsl proto protobuf hcl terraform', cLike);
14
+ register('css', { line: null, block: slashStar });
15
+ register('html xml svg xsl markdown md mdx vue', markup);
16
+ register('python py yaml yml shellscript shell sh bash zsh fish toml ruby rb perl r make makefile docker dockerfile dotenv ignore nix graphql powershell ps1 properties julia jl elixir ex exs coffeescript coffee cmake', hash);
17
+ register('ini', { line: ';', block: null });
18
+ register('sql', { line: '--', block: slashStar });
19
+ register('lua', { line: '--', block: { open: '--[[', close: ']]' } });
20
+ register('haskell hs', { line: '--', block: { open: '{-', close: '-}' } });
21
+ register('plaintext text txt log', none);
22
+ /* An id nothing knows has no comment syntax, which toggling a comment answers by doing nothing. */
23
+ export function commentSyntax(language, region) {
24
+ const id = language.toLowerCase();
25
+ if (id === 'vue') {
26
+ if (region === 'script') {
27
+ return cLike;
28
+ }
29
+ return region === 'style' ? { line: null, block: slashStar } : markup;
30
+ }
31
+ return syntaxes.get(id) ?? none;
32
+ }
33
+ /* Plain text has no structure to pair, indent or comment. */
34
+ export function isPlainText(language) {
35
+ return /^(plaintext|text|txt|log)$/i.test(language);
36
+ }
37
+ /* The region of a `.vue` file a line is in, from the top-level tags above it. */
38
+ export function vueRegionAt(lineText, line) {
39
+ let region = null;
40
+ for (let index = 0; index <= line; index++) {
41
+ const text = lineText(index);
42
+ const open = /^<(template|script|style)\b/.exec(text);
43
+ if (open) {
44
+ region = open[1];
45
+ }
46
+ else if (/^<\/(template|script|style)\s*>/.test(text) && index < line) {
47
+ region = null;
48
+ }
49
+ }
50
+ return region;
51
+ }