@adecore/editor-core 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/block-folds.js +28 -19
- package/dist/closer-tracker.js +2 -2
- package/dist/comments.js +10 -25
- package/dist/document.d.ts +1 -0
- package/dist/document.js +29 -36
- package/dist/enter.js +29 -22
- package/dist/fold-roles.js +5 -18
- package/dist/lexical-folds.d.ts +1 -0
- package/dist/lexical-folds.js +5 -2
- package/dist/line-commands.js +2 -10
- package/dist/line-text.d.ts +11 -0
- package/dist/line-text.js +17 -0
- package/dist/markup-regions.js +24 -20
- package/dist/paste.js +1 -12
- package/dist/preserve-case.js +23 -30
- package/dist/structure.js +1 -1
- package/dist/typing-context.js +10 -18
- package/dist/words.js +3 -5
- package/package.json +1 -1
- package/src/block-folds.ts +30 -19
- package/src/closer-tracker.ts +2 -2
- package/src/comments.ts +11 -29
- package/src/document.ts +31 -42
- package/src/enter.ts +32 -24
- package/src/fold-roles.ts +5 -19
- package/src/lexical-folds.ts +6 -2
- package/src/line-commands.ts +2 -13
- package/src/line-text.ts +22 -0
- package/src/markup-regions.ts +23 -18
- package/src/paste.ts +1 -15
- package/src/preserve-case.ts +24 -30
- package/src/structure.ts +2 -3
- package/src/typing-context.ts +11 -18
- package/src/words.ts +4 -5
package/dist/block-folds.js
CHANGED
|
@@ -11,16 +11,9 @@ const HEREDOC = /<<<[ \t]*(["']?)([A-Za-z_]\w*)\1\s*$/;
|
|
|
11
11
|
function markdownFolds(lineCount, getLine) {
|
|
12
12
|
const result = [];
|
|
13
13
|
const headings = [];
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
const text = getLine(line).text.trim();
|
|
18
|
-
if (text === '---' || text === '...') {
|
|
19
|
-
result.push(rangeOf(0, line, getLine, 'block', 'front-matter'));
|
|
20
|
-
first = line + 1;
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
14
|
+
const frontMatter = frontMatterEnd(lineCount, getLine);
|
|
15
|
+
if (frontMatter !== -1) {
|
|
16
|
+
result.push(rangeOf(0, frontMatter, getLine, 'block', 'front-matter'));
|
|
24
17
|
}
|
|
25
18
|
let tableStart = -1;
|
|
26
19
|
const flushTable = (end) => {
|
|
@@ -29,19 +22,12 @@ function markdownFolds(lineCount, getLine) {
|
|
|
29
22
|
}
|
|
30
23
|
tableStart = -1;
|
|
31
24
|
};
|
|
32
|
-
for (let line =
|
|
25
|
+
for (let line = frontMatter + 1; line < lineCount; line++) {
|
|
33
26
|
const text = getLine(line).text;
|
|
34
27
|
const fence = FENCE.exec(text);
|
|
35
28
|
if (fence !== null) {
|
|
36
29
|
flushTable(line - 1);
|
|
37
|
-
const
|
|
38
|
-
let end = -1;
|
|
39
|
-
for (let next = line + 1; next < lineCount && end === -1; next++) {
|
|
40
|
-
const closer = FENCE.exec(getLine(next).text);
|
|
41
|
-
if (closer !== null && closer[1][0] === marker[0] && closer[1].length >= marker.length && getLine(next).text.trim() === closer[1]) {
|
|
42
|
-
end = next;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
30
|
+
const end = fenceEnd(line, fence[1], lineCount, getLine);
|
|
45
31
|
if (end !== -1) {
|
|
46
32
|
result.push(rangeOf(line, end, getLine, 'block', 'code-fence'));
|
|
47
33
|
line = end;
|
|
@@ -71,6 +57,29 @@ function markdownFolds(lineCount, getLine) {
|
|
|
71
57
|
}
|
|
72
58
|
return result;
|
|
73
59
|
}
|
|
60
|
+
/* The line that closes front matter opened on the first line, or -1. */
|
|
61
|
+
function frontMatterEnd(lineCount, getLine) {
|
|
62
|
+
if (lineCount === 0 || getLine(0).text.trim() !== '---') {
|
|
63
|
+
return -1;
|
|
64
|
+
}
|
|
65
|
+
for (let line = 1; line < lineCount; line++) {
|
|
66
|
+
const text = getLine(line).text.trim();
|
|
67
|
+
if (text === '---' || text === '...') {
|
|
68
|
+
return line;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
/* The line that closes a fence opened on `line` with `marker`, or -1. */
|
|
74
|
+
function fenceEnd(line, marker, lineCount, getLine) {
|
|
75
|
+
for (let next = line + 1; next < lineCount; next++) {
|
|
76
|
+
const closer = FENCE.exec(getLine(next).text)?.[1];
|
|
77
|
+
if (closer !== undefined && closer[0] === marker[0] && closer.length >= marker.length && getLine(next).text.trim() === closer) {
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return -1;
|
|
82
|
+
}
|
|
74
83
|
/* `<?php ... ?>` blocks that sit between markup, and heredocs and nowdocs. A block that opens the file is the file, not a fold. */
|
|
75
84
|
function phpFolds(lineCount, getLine) {
|
|
76
85
|
const result = [];
|
package/dist/closer-tracker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const maxMarks = 64;
|
|
2
2
|
/*
|
|
3
3
|
* The closers the editor inserted by itself and the caret has not left yet, which Tab steps over. A
|
|
4
4
|
* mark follows its opener and closer through edits and goes as soon as an edit replaces either one
|
|
@@ -8,7 +8,7 @@ export class CloserTracker {
|
|
|
8
8
|
marks = [];
|
|
9
9
|
add(open, close) {
|
|
10
10
|
this.marks.push({ open, close });
|
|
11
|
-
if (this.marks.length >
|
|
11
|
+
if (this.marks.length > maxMarks) {
|
|
12
12
|
this.marks.shift();
|
|
13
13
|
}
|
|
14
14
|
}
|
package/dist/comments.js
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { commentSyntax } from './languages.js';
|
|
2
|
+
import { indentText, isBlank, rangeOf, whitespaceOf } from './line-text.js';
|
|
2
3
|
import { mapOffset } from './offsets.js';
|
|
3
4
|
import { indentationColumn } from './structure.js';
|
|
4
|
-
function whitespaceOf(text) {
|
|
5
|
-
return /^[\t ]*/.exec(text)[0];
|
|
6
|
-
}
|
|
7
|
-
function isBlank(text) {
|
|
8
|
-
return text.trim() === '';
|
|
9
|
-
}
|
|
10
5
|
function syntaxOf(source, options, line) {
|
|
11
6
|
const syntax = commentSyntax(options.language, source.region(line));
|
|
12
7
|
return options.lineToken === undefined ? syntax : { ...syntax, line: options.lineToken };
|
|
13
8
|
}
|
|
14
|
-
function rangeOf(selection) {
|
|
15
|
-
return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
16
|
-
}
|
|
17
9
|
/* The runs of adjacent lines the selections touch. A selection that ends at the start of a line does not touch it. */
|
|
18
10
|
function blocksOf(source, selections, span) {
|
|
19
11
|
const blocks = [];
|
|
@@ -50,12 +42,6 @@ function isCommented(text, syntax, multiline) {
|
|
|
50
42
|
}
|
|
51
43
|
return trimmed.length >= block.open.length + block.close.length && trimmed.startsWith(block.open) && trimmed.endsWith(block.close);
|
|
52
44
|
}
|
|
53
|
-
function indentText(width, options) {
|
|
54
|
-
if (options.insertSpaces) {
|
|
55
|
-
return ' '.repeat(width);
|
|
56
|
-
}
|
|
57
|
-
return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
|
|
58
|
-
}
|
|
59
45
|
/* The new place of a line after the edits, which never cross a line break. */
|
|
60
46
|
function geometryOf(source, edits, index) {
|
|
61
47
|
const bounds = source.line(index);
|
|
@@ -128,14 +114,19 @@ export function planLineComments(source, selections, options) {
|
|
|
128
114
|
}
|
|
129
115
|
return { edits, selections: finalSelections(source, selections, blocks, edits, allCommented, placed, lineBased, options.lineSpan) };
|
|
130
116
|
}
|
|
131
|
-
|
|
117
|
+
/* The indentation of the least indented non-blank line, infinite when every line is blank. */
|
|
118
|
+
function smallestIndent(source, first, last, tabSize) {
|
|
132
119
|
let minimum = Number.POSITIVE_INFINITY;
|
|
133
|
-
for (let line =
|
|
120
|
+
for (let line = first; line <= last; line++) {
|
|
134
121
|
const text = source.line(line).text;
|
|
135
122
|
if (!isBlank(text)) {
|
|
136
|
-
minimum = Math.min(minimum, indentationColumn(whitespaceOf(text),
|
|
123
|
+
minimum = Math.min(minimum, indentationColumn(whitespaceOf(text), tabSize));
|
|
137
124
|
}
|
|
138
125
|
}
|
|
126
|
+
return minimum;
|
|
127
|
+
}
|
|
128
|
+
function minimumIndent(source, block, syntax, options) {
|
|
129
|
+
let minimum = smallestIndent(source, block.first, block.last, options.tabSize);
|
|
139
130
|
if (block.first > 0) {
|
|
140
131
|
const previous = source.line(block.first - 1).text;
|
|
141
132
|
const marker = syntax.line ?? syntax.block?.open;
|
|
@@ -327,13 +318,7 @@ function wrapRange(source, from, to, block, options) {
|
|
|
327
318
|
{ from: to, to, text: ` ${block.close}` }
|
|
328
319
|
];
|
|
329
320
|
}
|
|
330
|
-
|
|
331
|
-
for (let line = first; line <= last; line++) {
|
|
332
|
-
const text = source.line(line).text;
|
|
333
|
-
if (!isBlank(text)) {
|
|
334
|
-
minimum = Math.min(minimum, indentationColumn(whitespaceOf(text), options.tabSize));
|
|
335
|
-
}
|
|
336
|
-
}
|
|
321
|
+
const minimum = smallestIndent(source, first, last, options.tabSize);
|
|
337
322
|
const space = indentText(Number.isFinite(minimum) ? minimum : 0, options);
|
|
338
323
|
const newline = source.newline(first);
|
|
339
324
|
const closing = source.charAt(to - 1) === '\n' ? `${space}${block.close}${newline}` : `${newline}${space}${block.close}`;
|
package/dist/document.d.ts
CHANGED
package/dist/document.js
CHANGED
|
@@ -8,6 +8,7 @@ import { isPlainText, vueRegionAt } from './languages.js';
|
|
|
8
8
|
import { blockFolds } from './block-folds.js';
|
|
9
9
|
import { applyFoldHints, markFileHeader } from './fold-roles.js';
|
|
10
10
|
import { lexicalFolds } from './lexical-folds.js';
|
|
11
|
+
import { rangeOf, whitespaceOf } from './line-text.js';
|
|
11
12
|
import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.js';
|
|
12
13
|
import { scanBrackets } from './brackets.js';
|
|
13
14
|
import { hasSmartSemicolon, isPhp } from './lexical.js';
|
|
@@ -31,15 +32,9 @@ const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
|
31
32
|
const arrowContinues = /^[\p{L}_{]/u;
|
|
32
33
|
const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
|
|
33
34
|
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
34
|
-
function rangeOf(selection) {
|
|
35
|
-
return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
36
|
-
}
|
|
37
35
|
function sameSelections(left, right) {
|
|
38
36
|
return left.length === right.length && left.every((selection, index) => selection.anchor === right[index].anchor && selection.head === right[index].head);
|
|
39
37
|
}
|
|
40
|
-
function indentLength(text) {
|
|
41
|
-
return text.match(/^[\t ]*/)?.[0].length ?? 0;
|
|
42
|
-
}
|
|
43
38
|
function trailingBlankLength(text) {
|
|
44
39
|
return text.match(/[\t ]*$/)?.[0].length ?? 0;
|
|
45
40
|
}
|
|
@@ -141,6 +136,7 @@ function inverseChanges(changes) {
|
|
|
141
136
|
function range(first, last) {
|
|
142
137
|
return Array.from({ length: last - first + 1 }, (_, index) => first + index);
|
|
143
138
|
}
|
|
139
|
+
/* Runs of consecutive line numbers, from a sorted list. */
|
|
144
140
|
function blocks(indices) {
|
|
145
141
|
const result = [];
|
|
146
142
|
for (const index of indices) {
|
|
@@ -172,7 +168,8 @@ function indentStopLength(prefix, tabSize) {
|
|
|
172
168
|
}
|
|
173
169
|
function positionIn(rope, offset) {
|
|
174
170
|
const line = rope.lineAt(offset);
|
|
175
|
-
|
|
171
|
+
const bounds = rope.lineBounds(line);
|
|
172
|
+
return { line, column: Math.min(offset, bounds.end) - bounds.start };
|
|
176
173
|
}
|
|
177
174
|
/* Simultaneous edits of `rope` as a sequence, last to first, so each edit's position in `rope` is also its position once the ones after it are applied. */
|
|
178
175
|
function editsOf(rope, edits) {
|
|
@@ -207,6 +204,14 @@ function stretchEdits(before, after, transactions) {
|
|
|
207
204
|
}
|
|
208
205
|
return [{ start: positionIn(before, prefix), end: positionIn(before, oldEnd), text: after.slice(prefix, newEnd) }];
|
|
209
206
|
}
|
|
207
|
+
/* An occurrence selected with its caret `shift` characters into it, as the caret stood in the selection it repeats. */
|
|
208
|
+
function occurrenceSelection(match, shift) {
|
|
209
|
+
if (shift === 0) {
|
|
210
|
+
return { anchor: match.to, head: match.from };
|
|
211
|
+
}
|
|
212
|
+
const caret = Math.min(match.from + shift, match.to);
|
|
213
|
+
return { anchor: caret === match.to ? match.from : match.to, head: caret };
|
|
214
|
+
}
|
|
210
215
|
/*
|
|
211
216
|
* Text with a selection, undo history and the editing commands of a code editor, and no idea how it
|
|
212
217
|
* is drawn. Offsets are UTF-16 code units, lines and columns zero-based. The text is a persistent
|
|
@@ -391,10 +396,7 @@ export class DocumentModel {
|
|
|
391
396
|
return true;
|
|
392
397
|
}
|
|
393
398
|
positionAt(offset) {
|
|
394
|
-
|
|
395
|
-
const line = this.rope.lineAt(clamped);
|
|
396
|
-
const bounds = this.rope.lineBounds(line);
|
|
397
|
-
return { line, column: Math.min(clamped, bounds.end) - bounds.start };
|
|
399
|
+
return positionIn(this.rope, offsetIn(this.rope, offset));
|
|
398
400
|
}
|
|
399
401
|
/* A column past the end of its line lands on the end of the line, before the line break. */
|
|
400
402
|
offsetAt(position) {
|
|
@@ -727,7 +729,7 @@ export class DocumentModel {
|
|
|
727
729
|
if (!/^[\t ]*$/.test(before)) {
|
|
728
730
|
return null;
|
|
729
731
|
}
|
|
730
|
-
const indent = this.getLine(this.rope.lineAt(opener)).text
|
|
732
|
+
const indent = whitespaceOf(this.getLine(this.rope.lineAt(opener)).text);
|
|
731
733
|
if (indent === before) {
|
|
732
734
|
return null;
|
|
733
735
|
}
|
|
@@ -1094,16 +1096,11 @@ export class DocumentModel {
|
|
|
1094
1096
|
: planBlockComment(source, this.selections, commentOptions);
|
|
1095
1097
|
return plan !== null && this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
1096
1098
|
}
|
|
1097
|
-
/* The lines the carets and selections touch
|
|
1099
|
+
/* The lines the carets and selections touch, sorted. */
|
|
1098
1100
|
selectedLines(span) {
|
|
1099
1101
|
const indices = new Set();
|
|
1100
1102
|
for (const selection of this.selections) {
|
|
1101
|
-
|
|
1102
|
-
let first = this.rope.lineAt(from);
|
|
1103
|
-
let last = this.rope.lineAt(to);
|
|
1104
|
-
if (to > from && this.rope.lineBounds(last).start === to) {
|
|
1105
|
-
last--;
|
|
1106
|
-
}
|
|
1103
|
+
let { first, last } = this.touchedLines(selection);
|
|
1107
1104
|
if (span) {
|
|
1108
1105
|
first = span(first).first;
|
|
1109
1106
|
last = Math.max(last, span(last).last);
|
|
@@ -1114,6 +1111,13 @@ export class DocumentModel {
|
|
|
1114
1111
|
}
|
|
1115
1112
|
return [...indices].sort((left, right) => left - right);
|
|
1116
1113
|
}
|
|
1114
|
+
/* The lines a selection touches; one that ends at the start of a line does not touch it. */
|
|
1115
|
+
touchedLines(selection) {
|
|
1116
|
+
const { from, to } = rangeOf(selection);
|
|
1117
|
+
const first = this.rope.lineAt(from);
|
|
1118
|
+
const last = this.rope.lineAt(to);
|
|
1119
|
+
return { first, last: last > first && this.rope.lineBounds(last).start === to ? last - 1 : last };
|
|
1120
|
+
}
|
|
1117
1121
|
/* A selection is copied in place, its copy selected; a caret duplicates its line and moves down with it. */
|
|
1118
1122
|
duplicate(span) {
|
|
1119
1123
|
const carets = this.selections.filter((selection) => selection.anchor === selection.head);
|
|
@@ -1281,7 +1285,7 @@ export class DocumentModel {
|
|
|
1281
1285
|
const index = this.rope.lineAt(selection.head);
|
|
1282
1286
|
const line = this.rope.lineBounds(index);
|
|
1283
1287
|
const text = this.getLine(index).text;
|
|
1284
|
-
const indent = (text
|
|
1288
|
+
const indent = whitespaceOf(text) + (/^[\t ]*[)\]}]/.test(text) ? unit : '');
|
|
1285
1289
|
return { from: line.start, to: line.start, text: indent + this.newlineAt(index), anchor: indent.length, head: indent.length };
|
|
1286
1290
|
}), { source: 'command' });
|
|
1287
1291
|
}
|
|
@@ -1333,7 +1337,7 @@ export class DocumentModel {
|
|
|
1333
1337
|
return true;
|
|
1334
1338
|
}
|
|
1335
1339
|
const closer = index.pairs.get(offset);
|
|
1336
|
-
const indentAt = (at) => this.getLine(this.rope.lineAt(at)).text
|
|
1340
|
+
const indentAt = (at) => whitespaceOf(this.getLine(this.rope.lineAt(at)).text).length;
|
|
1337
1341
|
if (closer === undefined || indentAt(closer) >= indentAt(offset)) {
|
|
1338
1342
|
return false;
|
|
1339
1343
|
}
|
|
@@ -1665,7 +1669,7 @@ export class DocumentModel {
|
|
|
1665
1669
|
{ from: 0, to: this.rope.length }
|
|
1666
1670
|
];
|
|
1667
1671
|
if (first.start === last.start) {
|
|
1668
|
-
candidates.push({ from: first.start +
|
|
1672
|
+
candidates.push({ from: first.start + whitespaceOf(first.text).length, to: first.end - trailingBlankLength(first.text) });
|
|
1669
1673
|
}
|
|
1670
1674
|
for (const range of this.structure()) {
|
|
1671
1675
|
candidates.push({ from: range.innerFrom, to: range.innerTo }, range);
|
|
@@ -1734,10 +1738,7 @@ export class DocumentModel {
|
|
|
1734
1738
|
this.rememberOccurrence(wholeWord, notFoundBefore);
|
|
1735
1739
|
return false;
|
|
1736
1740
|
}
|
|
1737
|
-
const
|
|
1738
|
-
const caret = Math.min(match.from + shift, match.to);
|
|
1739
|
-
const added = shift === 0 ? { anchor: match.to, head: match.from } : { anchor: caret === match.to ? match.from : match.to, head: caret };
|
|
1740
|
-
const changed = this.changeSelections([...this.selections, added]);
|
|
1741
|
+
const changed = this.changeSelections([...this.selections, occurrenceSelection(match, primary.head - from)]);
|
|
1741
1742
|
this.rememberOccurrence(wholeWord, false);
|
|
1742
1743
|
return changed;
|
|
1743
1744
|
}
|
|
@@ -1769,10 +1770,7 @@ export class DocumentModel {
|
|
|
1769
1770
|
if (matches.length === 0) {
|
|
1770
1771
|
return false;
|
|
1771
1772
|
}
|
|
1772
|
-
const changed = this.changeSelections(matches.map((match) =>
|
|
1773
|
-
const caret = Math.min(match.from + shift, match.to);
|
|
1774
|
-
return shift === 0 ? { anchor: match.to, head: match.from } : { anchor: caret === match.to ? match.from : match.to, head: caret };
|
|
1775
|
-
}));
|
|
1773
|
+
const changed = this.changeSelections(matches.map((match) => occurrenceSelection(match, shift)));
|
|
1776
1774
|
this.rememberOccurrence(wholeWord, false);
|
|
1777
1775
|
return changed;
|
|
1778
1776
|
}
|
|
@@ -1781,12 +1779,7 @@ export class DocumentModel {
|
|
|
1781
1779
|
const forward = this.selections.at(-1).head >= this.selections.at(-1).anchor;
|
|
1782
1780
|
const carets = [];
|
|
1783
1781
|
for (const selection of this.selections) {
|
|
1784
|
-
const {
|
|
1785
|
-
const first = this.rope.lineAt(from);
|
|
1786
|
-
let last = this.rope.lineAt(to);
|
|
1787
|
-
if (last > first && to === this.rope.lineBounds(last).start) {
|
|
1788
|
-
last--;
|
|
1789
|
-
}
|
|
1782
|
+
const { first, last } = this.touchedLines(selection);
|
|
1790
1783
|
for (let line = first; line <= last; line++) {
|
|
1791
1784
|
const end = this.rope.lineBounds(line).end;
|
|
1792
1785
|
carets.push({ anchor: end, head: end });
|
package/dist/enter.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { commentSyntax } from './languages.js';
|
|
2
|
-
import { hasHashComments, isBraced, isPhp } from './lexical.js';
|
|
2
|
+
import { hasHashComments, isBraced, isPhp, openers } from './lexical.js';
|
|
3
|
+
import { whitespaceOf } from './line-text.js';
|
|
3
4
|
import { hasJsx, isJsxAttributeValue } from './markup-regions.js';
|
|
4
5
|
import { indentationColumn } from './structure.js';
|
|
5
|
-
const closers = { '(': ')', '[': ']', '{': '}' };
|
|
6
6
|
const concatenation = new Map();
|
|
7
7
|
for (const id of 'typescript javascript typescriptreact javascriptreact tsx jsx ts js java csharp cs kotlin scala dart swift go groovy'.split(' ')) {
|
|
8
8
|
concatenation.set(id, '+');
|
|
@@ -16,10 +16,7 @@ const lookBack = 50;
|
|
|
16
16
|
const jsxScanLimit = 2_000_000;
|
|
17
17
|
/* How far past the caret a comment is searched for its end. */
|
|
18
18
|
const commentReach = 20_000;
|
|
19
|
-
function
|
|
20
|
-
return /^[\t ]*/.exec(text)[0];
|
|
21
|
-
}
|
|
22
|
-
function isBlank(text) {
|
|
19
|
+
function isIndentOnly(text) {
|
|
23
20
|
return /^[\t ]*$/.test(text);
|
|
24
21
|
}
|
|
25
22
|
function escapeForPattern(text) {
|
|
@@ -59,15 +56,20 @@ class Enter {
|
|
|
59
56
|
return { from, to, text: newline + leading, caret: newline.length + leading.length };
|
|
60
57
|
}
|
|
61
58
|
const context = source.context(from);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
return this.inLiteral(from, to, context, index) ?? this.inCode(from, to, context, index);
|
|
60
|
+
}
|
|
61
|
+
/* The plan inside a comment or a string, or null where Enter acts as in code. */
|
|
62
|
+
inLiteral(from, to, context, index) {
|
|
63
|
+
if (context.mode === 'block-comment') {
|
|
64
|
+
return this.inBlockComment(from, to, context, index);
|
|
65
|
+
}
|
|
66
|
+
if (from !== to) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (context.mode === 'line-comment') {
|
|
70
|
+
return this.inLineComment(from, context, index);
|
|
71
|
+
}
|
|
72
|
+
return context.mode === 'quote' ? this.inString(from, context, index) : null;
|
|
71
73
|
}
|
|
72
74
|
/* The indentation a new statement after this line gets, which is not the line's own when the line is a continuation. */
|
|
73
75
|
baseIndent(index, depth = 0) {
|
|
@@ -81,7 +83,7 @@ class Enter {
|
|
|
81
83
|
}
|
|
82
84
|
previousCodeLine(index) {
|
|
83
85
|
for (let candidate = index - 1; candidate >= 0 && candidate >= index - lookBack; candidate--) {
|
|
84
|
-
if (!
|
|
86
|
+
if (!isIndentOnly(this.source.line(candidate).text)) {
|
|
85
87
|
return candidate;
|
|
86
88
|
}
|
|
87
89
|
}
|
|
@@ -101,7 +103,7 @@ class Enter {
|
|
|
101
103
|
const { unit, language } = options;
|
|
102
104
|
// A comment that trails the code is not part of what decides the indentation.
|
|
103
105
|
const commentAt = lineContext.mode === 'line-comment' ? lineContext.commentStart : undefined;
|
|
104
|
-
const trailing = commentAt !== undefined && commentAt >= line.start && !
|
|
106
|
+
const trailing = commentAt !== undefined && commentAt >= line.start && !isIndentOnly(source.slice(line.start, commentAt));
|
|
105
107
|
const trimmed = trailing ? source.slice(line.start, commentAt).trimEnd() : before.trimEnd();
|
|
106
108
|
const context = trailing ? source.context(line.start + trimmed.length) : lineContext;
|
|
107
109
|
const last = trimmed.at(-1) ?? '';
|
|
@@ -114,10 +116,10 @@ class Enter {
|
|
|
114
116
|
if (context.mode !== 'code') {
|
|
115
117
|
return plain(leading);
|
|
116
118
|
}
|
|
117
|
-
const opener =
|
|
119
|
+
const opener = openers[last] !== undefined && context.bracket?.close === openers[last] && context.bracket.at === line.start + trimmed.length - 1;
|
|
118
120
|
if (opener) {
|
|
119
121
|
const inner = start + unit;
|
|
120
|
-
if (next ===
|
|
122
|
+
if (next === openers[last]) {
|
|
121
123
|
return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
|
|
122
124
|
}
|
|
123
125
|
const closing = last === '{' && source.unclosedBrace(context.bracket.at) ? this.closeBrace(from, afterWhitespace, line, rest, newline, start, inner) : null;
|
|
@@ -135,7 +137,8 @@ class Enter {
|
|
|
135
137
|
return plain(this.blockIndent(line.text, start) + unit);
|
|
136
138
|
}
|
|
137
139
|
if (continues(trimmed, context, language)) {
|
|
138
|
-
const
|
|
140
|
+
const previous = this.previousCodeLine(index);
|
|
141
|
+
const continued = previous !== null && this.lineContinues(previous);
|
|
139
142
|
return plain(continued ? leading : start + unit);
|
|
140
143
|
}
|
|
141
144
|
return plain(start);
|
|
@@ -215,7 +218,7 @@ class Enter {
|
|
|
215
218
|
return { from, to: textStart, text: newline + leading, caret: newline.length + leading.length };
|
|
216
219
|
}
|
|
217
220
|
const code = source.slice(line.start, commentStart);
|
|
218
|
-
const onlyComment =
|
|
221
|
+
const onlyComment = isIndentOnly(code);
|
|
219
222
|
let spacing = ' ';
|
|
220
223
|
if (onlyComment) {
|
|
221
224
|
const gap = whitespaceOf(source.slice(commentStart + marker.length, line.end));
|
|
@@ -344,7 +347,11 @@ class Enter {
|
|
|
344
347
|
}
|
|
345
348
|
return { from, to: textStart, text: newline + star, caret: newline.length + star.length };
|
|
346
349
|
}
|
|
347
|
-
if (isDoc &&
|
|
350
|
+
if (isDoc &&
|
|
351
|
+
onOpener &&
|
|
352
|
+
source.lineAt(closeAt) === index &&
|
|
353
|
+
isIndentOnly(source.slice(to, closeAt)) &&
|
|
354
|
+
isIndentOnly(source.slice(closeAt + 2, line.end))) {
|
|
348
355
|
// Nothing left to say after the caret: the closer gets a line of its own, under the opener's star.
|
|
349
356
|
const star = this.starPrefix(line, lead, false, isDoc);
|
|
350
357
|
return { from, to: line.end, text: `${newline}${star}${newline}${lead} */`, caret: newline.length + star.length };
|
package/dist/fold-roles.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isImportLine } from './lexical-folds.js';
|
|
1
|
+
import { isImportLine, rangeOf, textStart } from './lexical-folds.js';
|
|
2
|
+
import { isBlank } from './line-text.js';
|
|
2
3
|
const SCRIPT_LANGUAGES = /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|mjs|cjs|mts|cts)$/i;
|
|
3
4
|
/* How far back from a bracket the text before it is read. */
|
|
4
5
|
const LOOK_BEHIND = 200;
|
|
@@ -40,9 +41,6 @@ export function bracketRole(slice, from, language) {
|
|
|
40
41
|
// A `case x: {` and a `type A = {` are a block and a type, not a value.
|
|
41
42
|
return /^\s*(?:case\b|default\s*:|(?:export\s+)?(?:declare\s+)?(?:type|interface)\b)/.test(head) ? undefined : 'object-literal';
|
|
42
43
|
}
|
|
43
|
-
function isBlank(text) {
|
|
44
|
-
return text.trim() === '';
|
|
45
|
-
}
|
|
46
44
|
/*
|
|
47
45
|
* The comment at the very top of a file is its header, unless it is the documentation of what follows
|
|
48
46
|
* right under it. Only the first comment can be one, and only behind a shebang, an open tag or blank lines.
|
|
@@ -58,8 +56,7 @@ export function markFileHeader(ranges, getLine, lineCount, language) {
|
|
|
58
56
|
return;
|
|
59
57
|
}
|
|
60
58
|
}
|
|
61
|
-
|
|
62
|
-
if (candidate.from > start.start + (start.text.length - start.text.trimStart().length)) {
|
|
59
|
+
if (candidate.from > textStart(getLine(candidate.startLine))) {
|
|
63
60
|
return;
|
|
64
61
|
}
|
|
65
62
|
const next = candidate.endLine + 1 < lineCount ? getLine(candidate.endLine + 1).text : '';
|
|
@@ -109,19 +106,9 @@ export function applyFoldHints(ranges, hints, lineAt, getLine, slice, minimum) {
|
|
|
109
106
|
if (endLine - startLine < minimum || starts.has(startLine)) {
|
|
110
107
|
continue;
|
|
111
108
|
}
|
|
112
|
-
const first = getLine(startLine);
|
|
113
|
-
const indent = first.text.length - first.text.trimStart().length;
|
|
114
|
-
const role = serverRole(hint.kind, first.text.trimStart());
|
|
115
109
|
starts.add(startLine);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
endLine,
|
|
119
|
-
from: first.start + indent,
|
|
120
|
-
to: getLine(endLine).end,
|
|
121
|
-
kind: 'server',
|
|
122
|
-
...(role === undefined ? {} : { role }),
|
|
123
|
-
...edgesOf(hint)
|
|
124
|
-
});
|
|
110
|
+
const role = serverRole(hint.kind, getLine(startLine).text.trimStart());
|
|
111
|
+
result.push({ ...rangeOf(startLine, endLine, getLine, 'server', role), ...edgesOf(hint) });
|
|
125
112
|
}
|
|
126
113
|
return result.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
127
114
|
}
|
package/dist/lexical-folds.d.ts
CHANGED
|
@@ -6,5 +6,6 @@ export interface LexicalFoldOptions {
|
|
|
6
6
|
regions?: boolean;
|
|
7
7
|
lineComments?: boolean;
|
|
8
8
|
}
|
|
9
|
+
export declare function textStart(line: DocumentLine): number;
|
|
9
10
|
export declare function rangeOf(startLine: number, endLine: number, getLine: (line: number) => DocumentLine, kind: FoldingRange['kind'], role?: FoldRole): FoldingRange;
|
|
10
11
|
export declare function lexicalFolds(lineCount: number, getLine: (line: number) => DocumentLine, language: string, options?: LexicalFoldOptions): FoldingRange[];
|
package/dist/lexical-folds.js
CHANGED
|
@@ -30,13 +30,16 @@ function bracketDelta(text) {
|
|
|
30
30
|
}
|
|
31
31
|
return depth;
|
|
32
32
|
}
|
|
33
|
+
/* The offset of the first character of a line that is not indentation. */
|
|
34
|
+
export function textStart(line) {
|
|
35
|
+
return line.start + (line.text.length - line.text.trimStart().length);
|
|
36
|
+
}
|
|
33
37
|
/* A fold of whole lines, from the end of the first to the end of the last. */
|
|
34
38
|
export function rangeOf(startLine, endLine, getLine, kind, role) {
|
|
35
|
-
const start = getLine(startLine);
|
|
36
39
|
return {
|
|
37
40
|
startLine,
|
|
38
41
|
endLine,
|
|
39
|
-
from:
|
|
42
|
+
from: textStart(getLine(startLine)),
|
|
40
43
|
to: getLine(endLine).end,
|
|
41
44
|
kind,
|
|
42
45
|
...(role === undefined ? {} : { role })
|
package/dist/line-commands.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { commentSyntax } from './languages.js';
|
|
2
2
|
import { continues } from './enter.js';
|
|
3
3
|
import { indentsByBrackets } from './lexical.js';
|
|
4
|
+
import { isBlank, rangeOf, whitespaceOf } from './line-text.js';
|
|
4
5
|
import { markupGuard } from './markup-regions.js';
|
|
5
6
|
import { mapOffset } from './offsets.js';
|
|
6
7
|
const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
7
|
-
function rangeOf(selection) {
|
|
8
|
-
return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
9
|
-
}
|
|
10
|
-
function whitespaceOf(text) {
|
|
11
|
-
return /^[\t ]*/.exec(text)[0];
|
|
12
|
-
}
|
|
13
|
-
function isBlank(text) {
|
|
14
|
-
return text.trim() === '';
|
|
15
|
-
}
|
|
16
8
|
/* How far the edits in front of an offset moved it. An edit that starts at the offset is not in front of it. */
|
|
17
9
|
function shiftBefore(offset, edits) {
|
|
18
10
|
return edits.filter((edit) => edit.from < offset).reduce((sum, edit) => sum + edit.text.length - (Math.min(edit.to, offset) - edit.from), 0);
|
|
@@ -106,7 +98,7 @@ export function planJoinLines(source, selections, options) {
|
|
|
106
98
|
!isBlank(source.slice(source.line(line).start, comment.start)) &&
|
|
107
99
|
!source.slice(nextStart, nextLine.end).startsWith(comment.marker.trimEnd()) &&
|
|
108
100
|
commentSyntax(options.language, source.region(line)).block?.open === '/*';
|
|
109
|
-
if (converts
|
|
101
|
+
if (converts) {
|
|
110
102
|
const body = source.slice(comment.start + comment.marker.length, join.from);
|
|
111
103
|
if (!body.includes('*/')) {
|
|
112
104
|
edits.push({ from: comment.start, to: comment.start + comment.marker.length, text: '/*' });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Selection } from './types.ts';
|
|
2
|
+
export declare function whitespaceOf(text: string): string;
|
|
3
|
+
export declare function isBlank(text: string): boolean;
|
|
4
|
+
export declare function indentText(width: number, options: {
|
|
5
|
+
tabSize: number;
|
|
6
|
+
insertSpaces: boolean;
|
|
7
|
+
}): string;
|
|
8
|
+
export declare function rangeOf(selection: Selection): {
|
|
9
|
+
from: number;
|
|
10
|
+
to: number;
|
|
11
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* The spaces and tabs a line starts with. */
|
|
2
|
+
export function whitespaceOf(text) {
|
|
3
|
+
return /^[\t ]*/.exec(text)[0];
|
|
4
|
+
}
|
|
5
|
+
export function isBlank(text) {
|
|
6
|
+
return text.trim() === '';
|
|
7
|
+
}
|
|
8
|
+
/* Indentation `width` columns wide, in tabs as far as they reach when spaces are off. */
|
|
9
|
+
export function indentText(width, options) {
|
|
10
|
+
if (options.insertSpaces) {
|
|
11
|
+
return ' '.repeat(width);
|
|
12
|
+
}
|
|
13
|
+
return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
|
|
14
|
+
}
|
|
15
|
+
export function rangeOf(selection) {
|
|
16
|
+
return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
17
|
+
}
|