@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/markup-regions.js
CHANGED
|
@@ -175,7 +175,7 @@ class JsxScanner {
|
|
|
175
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
176
|
element(from, outermost) {
|
|
177
177
|
this.depth++;
|
|
178
|
-
const end = this.
|
|
178
|
+
const end = this.parseElement(from);
|
|
179
179
|
this.depth--;
|
|
180
180
|
if (end >= 0 && outermost && this.depth === 0) {
|
|
181
181
|
this.ranges.push({ from, to: end });
|
|
@@ -189,7 +189,7 @@ class JsxScanner {
|
|
|
189
189
|
}
|
|
190
190
|
return at;
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
parseElement(from) {
|
|
193
193
|
const { text } = this;
|
|
194
194
|
let at = from + 1;
|
|
195
195
|
while (at < text.length && NAME.test(text[at])) {
|
|
@@ -240,7 +240,7 @@ class JsxScanner {
|
|
|
240
240
|
at = this.code(at + 1, '}');
|
|
241
241
|
}
|
|
242
242
|
else if (value === '<' && START_OF_NAME.test(text[at + 1] ?? '')) {
|
|
243
|
-
at = this.
|
|
243
|
+
at = this.parseElement(at);
|
|
244
244
|
}
|
|
245
245
|
else {
|
|
246
246
|
return -1;
|
|
@@ -264,7 +264,7 @@ class JsxScanner {
|
|
|
264
264
|
return closing !== null && closing[1] === name ? at + closing[0].length : -1;
|
|
265
265
|
}
|
|
266
266
|
else if (START_OF_NAME.test(text[at + 1] ?? '')) {
|
|
267
|
-
at = this.
|
|
267
|
+
at = this.parseElement(at);
|
|
268
268
|
}
|
|
269
269
|
else {
|
|
270
270
|
return -1;
|
|
@@ -281,6 +281,25 @@ export function jsxRanges(text) {
|
|
|
281
281
|
scanner.scan();
|
|
282
282
|
return scanner.ranges;
|
|
283
283
|
}
|
|
284
|
+
/* Whether `offset` falls inside one of `ranges`, which are sorted and do not overlap. */
|
|
285
|
+
function insideAny(ranges, offset) {
|
|
286
|
+
let low = 0;
|
|
287
|
+
let high = ranges.length - 1;
|
|
288
|
+
while (low <= high) {
|
|
289
|
+
const middle = (low + high) >> 1;
|
|
290
|
+
const range = ranges[middle];
|
|
291
|
+
if (offset < range.from) {
|
|
292
|
+
high = middle - 1;
|
|
293
|
+
}
|
|
294
|
+
else if (offset >= range.to) {
|
|
295
|
+
low = middle + 1;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
284
303
|
/* Whether a language is one whose scripts can hold JSX. */
|
|
285
304
|
export function hasJsx(language) {
|
|
286
305
|
return JSX_LANGUAGES.test(language);
|
|
@@ -309,22 +328,7 @@ export function markupGuard(source, language) {
|
|
|
309
328
|
return (line) => {
|
|
310
329
|
const bounds = source.line(line);
|
|
311
330
|
const start = bounds.start + (/^[\t ]*/.exec(bounds.text)?.[0].length ?? 0);
|
|
312
|
-
|
|
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;
|
|
331
|
+
return insideAny(ranges, start);
|
|
328
332
|
};
|
|
329
333
|
}
|
|
330
334
|
return () => false;
|
package/dist/paste.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
+
import { indentText, isBlank, whitespaceOf } from './line-text.js';
|
|
1
2
|
import { mapOffset } from './offsets.js';
|
|
2
3
|
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
4
|
/* A line break at the end is the end of the last line, not a line of its own. */
|
|
10
5
|
function linesOf(text) {
|
|
11
6
|
return text.replace(/\r?\n$/, '').split(/\r?\n/);
|
|
@@ -18,12 +13,6 @@ function perCaretLines(text, carets) {
|
|
|
18
13
|
const lines = linesOf(text);
|
|
19
14
|
return lines.length === carets ? lines : null;
|
|
20
15
|
}
|
|
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
16
|
/*
|
|
28
17
|
* Where a block of several lines lands, it lands at the indentation of the line it is pasted into:
|
|
29
18
|
* the pasted lines move together by the difference between that indentation and the least indented
|
package/dist/preserve-case.js
CHANGED
|
@@ -21,41 +21,34 @@ export function replaceWithCaseRespect(replacement, found) {
|
|
|
21
21
|
if (foundTail === '') {
|
|
22
22
|
return head + replacementTail;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
24
|
+
const replacementCase = letterCase(replacementTail);
|
|
25
|
+
const foundCase = letterCase(foundTail);
|
|
26
|
+
// A tail without letters, like `x2`, follows the first letter of the match.
|
|
27
|
+
const tailUpper = foundCase.hasLetters ? foundCase.upper : upper.test(firstFound);
|
|
28
|
+
const tailLower = foundCase.hasLetters ? foundCase.lower : lower.test(firstFound);
|
|
29
|
+
if (tailUpper && (replacementCase.lower || !replacementCase.upper)) {
|
|
30
|
+
return head + replacementTail.toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
if (tailLower && (replacementCase.lower || replacementCase.upper)) {
|
|
33
|
+
return head + replacementTail.toLowerCase();
|
|
35
34
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
return head + replacementTail;
|
|
36
|
+
}
|
|
37
|
+
/* Whether every letter of `text` is upper case and whether every one is lower case; both hold without letters. */
|
|
38
|
+
function letterCase(text) {
|
|
39
|
+
let allUpper = true;
|
|
40
|
+
let allLower = true;
|
|
41
|
+
let hasLetters = false;
|
|
42
|
+
for (const character of text) {
|
|
40
43
|
if (!letter.test(character)) {
|
|
41
44
|
continue;
|
|
42
45
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!
|
|
46
|
+
hasLetters = true;
|
|
47
|
+
allUpper &&= upper.test(character);
|
|
48
|
+
allLower &&= lower.test(character);
|
|
49
|
+
if (!allUpper && !allLower) {
|
|
47
50
|
break;
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
|
-
|
|
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;
|
|
53
|
+
return { upper: allUpper, lower: allLower, hasLetters };
|
|
61
54
|
}
|
package/dist/structure.js
CHANGED
|
@@ -85,7 +85,7 @@ function indentationFolds(lineCount, getLine, tabSize, minimum) {
|
|
|
85
85
|
}
|
|
86
86
|
/* Ranges that span the same lines collapse into the first, which is the outermost. */
|
|
87
87
|
export function deriveFoldingRanges(ranges, lineAt, lineCount, getLine, options = {}, slice) {
|
|
88
|
-
const minimum = options.minLines
|
|
88
|
+
const minimum = Number.isFinite(options.minLines) ? Math.max(1, Math.trunc(options.minLines)) : 1;
|
|
89
89
|
const result = [];
|
|
90
90
|
for (const range of ranges) {
|
|
91
91
|
if (range.kind === 'string' || (range.kind === 'bracket' ? options.brackets === false : options.comments === false)) {
|
package/dist/typing-context.js
CHANGED
|
@@ -61,6 +61,11 @@ function scanLiteral(state, text, at, end, offset) {
|
|
|
61
61
|
}
|
|
62
62
|
return at;
|
|
63
63
|
}
|
|
64
|
+
function openComment(state, mode, start, resume) {
|
|
65
|
+
state.mode = mode;
|
|
66
|
+
state.commentStart = start;
|
|
67
|
+
return resume;
|
|
68
|
+
}
|
|
64
69
|
function scanCode(state, rules, text, at, end, offset) {
|
|
65
70
|
const char = text[at];
|
|
66
71
|
const next = text[at + 1];
|
|
@@ -68,29 +73,16 @@ function scanCode(state, rules, text, at, end, offset) {
|
|
|
68
73
|
return at;
|
|
69
74
|
}
|
|
70
75
|
if (rules.markup && text.startsWith('<!--', at)) {
|
|
71
|
-
state
|
|
72
|
-
state.commentStart = offset + at;
|
|
73
|
-
return at + 3;
|
|
76
|
+
return openComment(state, 'html-comment', offset + at, at + 3);
|
|
74
77
|
}
|
|
75
78
|
if (rules.hash && char === '#' && !(rules.php && next === '[') && (!rules.hashAtWordStart || startsWord(text, at))) {
|
|
76
|
-
state
|
|
77
|
-
state.commentStart = offset + at;
|
|
78
|
-
return at;
|
|
79
|
-
}
|
|
80
|
-
if (rules.dash && char === '-' && next === '-') {
|
|
81
|
-
state.mode = 'line-comment';
|
|
82
|
-
state.commentStart = offset + at;
|
|
83
|
-
return at + 1;
|
|
79
|
+
return openComment(state, 'line-comment', offset + at, at);
|
|
84
80
|
}
|
|
85
|
-
if (rules.slash && char === '/' && next === '/') {
|
|
86
|
-
state
|
|
87
|
-
state.commentStart = offset + at;
|
|
88
|
-
return at + 1;
|
|
81
|
+
if ((rules.dash && char === '-' && next === '-') || (rules.slash && char === '/' && next === '/')) {
|
|
82
|
+
return openComment(state, 'line-comment', offset + at, at + 1);
|
|
89
83
|
}
|
|
90
84
|
if (rules.block && char === '/' && next === '*') {
|
|
91
|
-
state
|
|
92
|
-
state.commentStart = offset + at;
|
|
93
|
-
return at + 1;
|
|
85
|
+
return openComment(state, 'block-comment', offset + at, at + 1);
|
|
94
86
|
}
|
|
95
87
|
if (rules.php && char === '<' && text.startsWith('<<<', at)) {
|
|
96
88
|
const opener = PHP_HEREDOC.exec(text.slice(at));
|
package/dist/words.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// `isLowerCaseOrDigit` and `isPunctuation` from JetBrains/intellij-community at
|
|
6
6
|
// 7184b03af6c5370e79a01ded0df68cf56d7669da, platform/platform-impl/src/com/intellij/openapi/editor/actions/EditorActionUtil.java,
|
|
7
7
|
// lines 934-982.
|
|
8
|
+
import { splitsSurrogate } from './search.js';
|
|
8
9
|
// Java's Character.isJavaIdentifierPart, which counts the ignorable control characters as part of a word.
|
|
9
10
|
// oxlint-disable-next-line no-control-regex
|
|
10
11
|
const identifierPart = /[\p{L}\p{Nl}\p{Sc}\p{Pc}\p{Nd}\p{Mc}\p{Mn}\p{Cf}\u0000-\u0008\u000e-\u001b\u007f-\u009f]/u;
|
|
@@ -59,11 +60,8 @@ export function isWordBoundary(text, offset, camel, isStart) {
|
|
|
59
60
|
export function wordBoundary(text, offset, direction, camel = true) {
|
|
60
61
|
let position = offset + direction;
|
|
61
62
|
while (position > 0 && position < text.length) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const splitsPair = /[\ud800-\udbff]/.test(before) && /[\udc00-\udfff]/.test(after);
|
|
65
|
-
const splitsLineBreak = before === '\r' && after === '\n';
|
|
66
|
-
if (!splitsPair && !splitsLineBreak && isWordBoundary(text, position, camel, direction === -1)) {
|
|
63
|
+
const splitsLineBreak = text.charAt(position - 1) === '\r' && text.charAt(position) === '\n';
|
|
64
|
+
if (!splitsSurrogate(text, position) && !splitsLineBreak && isWordBoundary(text, position, camel, direction === -1)) {
|
|
67
65
|
return position;
|
|
68
66
|
}
|
|
69
67
|
position += direction;
|
package/package.json
CHANGED
package/src/block-folds.ts
CHANGED
|
@@ -15,16 +15,9 @@ const HEREDOC = /<<<[ \t]*(["']?)([A-Za-z_]\w*)\1\s*$/;
|
|
|
15
15
|
function markdownFolds(lineCount: number, getLine: (line: number) => DocumentLine): FoldingRange[] {
|
|
16
16
|
const result: FoldingRange[] = [];
|
|
17
17
|
const headings: { line: number; level: number }[] = [];
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
const text = getLine(line).text.trim();
|
|
22
|
-
if (text === '---' || text === '...') {
|
|
23
|
-
result.push(rangeOf(0, line, getLine, 'block', 'front-matter'));
|
|
24
|
-
first = line + 1;
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
18
|
+
const frontMatter = frontMatterEnd(lineCount, getLine);
|
|
19
|
+
if (frontMatter !== -1) {
|
|
20
|
+
result.push(rangeOf(0, frontMatter, getLine, 'block', 'front-matter'));
|
|
28
21
|
}
|
|
29
22
|
let tableStart = -1;
|
|
30
23
|
const flushTable = (end: number): void => {
|
|
@@ -33,19 +26,12 @@ function markdownFolds(lineCount: number, getLine: (line: number) => DocumentLin
|
|
|
33
26
|
}
|
|
34
27
|
tableStart = -1;
|
|
35
28
|
};
|
|
36
|
-
for (let line =
|
|
29
|
+
for (let line = frontMatter + 1; line < lineCount; line++) {
|
|
37
30
|
const text = getLine(line).text;
|
|
38
31
|
const fence = FENCE.exec(text);
|
|
39
32
|
if (fence !== null) {
|
|
40
33
|
flushTable(line - 1);
|
|
41
|
-
const
|
|
42
|
-
let end = -1;
|
|
43
|
-
for (let next = line + 1; next < lineCount && end === -1; next++) {
|
|
44
|
-
const closer = FENCE.exec(getLine(next).text);
|
|
45
|
-
if (closer !== null && closer[1]![0] === marker[0] && closer[1]!.length >= marker.length && getLine(next).text.trim() === closer[1]) {
|
|
46
|
-
end = next;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
34
|
+
const end = fenceEnd(line, fence[1]!, lineCount, getLine);
|
|
49
35
|
if (end !== -1) {
|
|
50
36
|
result.push(rangeOf(line, end, getLine, 'block', 'code-fence'));
|
|
51
37
|
line = end;
|
|
@@ -76,6 +62,31 @@ function markdownFolds(lineCount: number, getLine: (line: number) => DocumentLin
|
|
|
76
62
|
return result;
|
|
77
63
|
}
|
|
78
64
|
|
|
65
|
+
/* The line that closes front matter opened on the first line, or -1. */
|
|
66
|
+
function frontMatterEnd(lineCount: number, getLine: (line: number) => DocumentLine): number {
|
|
67
|
+
if (lineCount === 0 || getLine(0).text.trim() !== '---') {
|
|
68
|
+
return -1;
|
|
69
|
+
}
|
|
70
|
+
for (let line = 1; line < lineCount; line++) {
|
|
71
|
+
const text = getLine(line).text.trim();
|
|
72
|
+
if (text === '---' || text === '...') {
|
|
73
|
+
return line;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* The line that closes a fence opened on `line` with `marker`, or -1. */
|
|
80
|
+
function fenceEnd(line: number, marker: string, lineCount: number, getLine: (line: number) => DocumentLine): number {
|
|
81
|
+
for (let next = line + 1; next < lineCount; next++) {
|
|
82
|
+
const closer = FENCE.exec(getLine(next).text)?.[1];
|
|
83
|
+
if (closer !== undefined && closer[0] === marker[0] && closer.length >= marker.length && getLine(next).text.trim() === closer) {
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return -1;
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
/* `<?php ... ?>` blocks that sit between markup, and heredocs and nowdocs. A block that opens the file is the file, not a fold. */
|
|
80
91
|
function phpFolds(lineCount: number, getLine: (line: number) => DocumentLine): FoldingRange[] {
|
|
81
92
|
const result: FoldingRange[] = [];
|
package/src/closer-tracker.ts
CHANGED
|
@@ -5,7 +5,7 @@ interface Mark {
|
|
|
5
5
|
close: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const maxMarks = 64;
|
|
9
9
|
|
|
10
10
|
/*
|
|
11
11
|
* The closers the editor inserted by itself and the caret has not left yet, which Tab steps over. A
|
|
@@ -17,7 +17,7 @@ export class CloserTracker {
|
|
|
17
17
|
|
|
18
18
|
add(open: number, close: number): void {
|
|
19
19
|
this.marks.push({ open, close });
|
|
20
|
-
if (this.marks.length >
|
|
20
|
+
if (this.marks.length > maxMarks) {
|
|
21
21
|
this.marks.shift();
|
|
22
22
|
}
|
|
23
23
|
}
|
package/src/comments.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
2
|
import { type BlockComment, type CommentSyntax, commentSyntax } from './languages.ts';
|
|
3
|
+
import { indentText, isBlank, rangeOf, whitespaceOf } from './line-text.ts';
|
|
3
4
|
import { mapOffset } from './offsets.ts';
|
|
4
5
|
import { indentationColumn } from './structure.ts';
|
|
5
6
|
import type { Selection, TextEdit } from './types.ts';
|
|
@@ -26,23 +27,11 @@ interface Geometry {
|
|
|
26
27
|
length: number;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function whitespaceOf(text: string): string {
|
|
30
|
-
return /^[\t ]*/.exec(text)![0];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isBlank(text: string): boolean {
|
|
34
|
-
return text.trim() === '';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
30
|
function syntaxOf(source: EditSource, options: CommentOptions, line: number): CommentSyntax {
|
|
38
31
|
const syntax = commentSyntax(options.language, source.region(line));
|
|
39
32
|
return options.lineToken === undefined ? syntax : { ...syntax, line: options.lineToken };
|
|
40
33
|
}
|
|
41
34
|
|
|
42
|
-
function rangeOf(selection: Selection): { from: number; to: number } {
|
|
43
|
-
return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
35
|
/* The runs of adjacent lines the selections touch. A selection that ends at the start of a line does not touch it. */
|
|
47
36
|
function blocksOf(source: EditSource, selections: readonly Selection[], span?: CommentOptions['lineSpan']): Block[] {
|
|
48
37
|
const blocks: Block[] = [];
|
|
@@ -80,13 +69,6 @@ function isCommented(text: string, syntax: CommentSyntax, multiline: boolean): b
|
|
|
80
69
|
return trimmed.length >= block.open.length + block.close.length && trimmed.startsWith(block.open) && trimmed.endsWith(block.close);
|
|
81
70
|
}
|
|
82
71
|
|
|
83
|
-
function indentText(width: number, options: CommentOptions): string {
|
|
84
|
-
if (options.insertSpaces) {
|
|
85
|
-
return ' '.repeat(width);
|
|
86
|
-
}
|
|
87
|
-
return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
72
|
/* The new place of a line after the edits, which never cross a line break. */
|
|
91
73
|
function geometryOf(source: EditSource, edits: readonly TextEdit[], index: number): Geometry {
|
|
92
74
|
const bounds = source.line(index);
|
|
@@ -162,14 +144,20 @@ export function planLineComments(source: EditSource, selections: readonly Select
|
|
|
162
144
|
return { edits, selections: finalSelections(source, selections, blocks, edits, allCommented, placed, lineBased, options.lineSpan) };
|
|
163
145
|
}
|
|
164
146
|
|
|
165
|
-
|
|
147
|
+
/* The indentation of the least indented non-blank line, infinite when every line is blank. */
|
|
148
|
+
function smallestIndent(source: EditSource, first: number, last: number, tabSize: number): number {
|
|
166
149
|
let minimum = Number.POSITIVE_INFINITY;
|
|
167
|
-
for (let line =
|
|
150
|
+
for (let line = first; line <= last; line++) {
|
|
168
151
|
const text = source.line(line).text;
|
|
169
152
|
if (!isBlank(text)) {
|
|
170
|
-
minimum = Math.min(minimum, indentationColumn(whitespaceOf(text),
|
|
153
|
+
minimum = Math.min(minimum, indentationColumn(whitespaceOf(text), tabSize));
|
|
171
154
|
}
|
|
172
155
|
}
|
|
156
|
+
return minimum;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function minimumIndent(source: EditSource, block: Block, syntax: CommentSyntax, options: CommentOptions): number {
|
|
160
|
+
let minimum = smallestIndent(source, block.first, block.last, options.tabSize);
|
|
173
161
|
if (block.first > 0) {
|
|
174
162
|
const previous = source.line(block.first - 1).text;
|
|
175
163
|
const marker = syntax.line ?? syntax.block?.open;
|
|
@@ -394,13 +382,7 @@ function wrapRange(source: EditSource, from: number, to: number, block: BlockCom
|
|
|
394
382
|
{ from: to, to, text: ` ${block.close}` }
|
|
395
383
|
];
|
|
396
384
|
}
|
|
397
|
-
|
|
398
|
-
for (let line = first; line <= last; line++) {
|
|
399
|
-
const text = source.line(line).text;
|
|
400
|
-
if (!isBlank(text)) {
|
|
401
|
-
minimum = Math.min(minimum, indentationColumn(whitespaceOf(text), options.tabSize));
|
|
402
|
-
}
|
|
403
|
-
}
|
|
385
|
+
const minimum = smallestIndent(source, first, last, options.tabSize);
|
|
404
386
|
const space = indentText(Number.isFinite(minimum) ? minimum : 0, options);
|
|
405
387
|
const newline = source.newline(first);
|
|
406
388
|
const closing = source.charAt(to - 1) === '\n' ? `${space}${block.close}${newline}` : `${newline}${space}${block.close}`;
|
package/src/document.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { isPlainText, vueRegionAt } from './languages.ts';
|
|
|
10
10
|
import { blockFolds } from './block-folds.ts';
|
|
11
11
|
import { applyFoldHints, markFileHeader } from './fold-roles.ts';
|
|
12
12
|
import { lexicalFolds } from './lexical-folds.ts';
|
|
13
|
+
import { rangeOf, whitespaceOf } from './line-text.ts';
|
|
13
14
|
import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.ts';
|
|
14
15
|
import { scanBrackets } from './brackets.ts';
|
|
15
16
|
import type { BracketIndex } from './brackets.ts';
|
|
@@ -38,7 +39,6 @@ import { isHumpBoundary, isWordBoundary } from './words.ts';
|
|
|
38
39
|
|
|
39
40
|
export type { DocumentLine, FindMatch, FindNextOptions, FindOptions, FoldingOptions, FoldingRange };
|
|
40
41
|
|
|
41
|
-
/* A stretch of the text by offsets. */
|
|
42
42
|
export interface OffsetRange {
|
|
43
43
|
readonly from: number;
|
|
44
44
|
readonly to: number;
|
|
@@ -154,18 +154,10 @@ const arrowContinues = /^[\p{L}_{]/u;
|
|
|
154
154
|
const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
|
|
155
155
|
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
156
156
|
|
|
157
|
-
function rangeOf(selection: Selection): { from: number; to: number } {
|
|
158
|
-
return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
157
|
function sameSelections(left: readonly Selection[], right: readonly Selection[]): boolean {
|
|
162
158
|
return left.length === right.length && left.every((selection, index) => selection.anchor === right[index]!.anchor && selection.head === right[index]!.head);
|
|
163
159
|
}
|
|
164
160
|
|
|
165
|
-
function indentLength(text: string): number {
|
|
166
|
-
return text.match(/^[\t ]*/)?.[0].length ?? 0;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
161
|
function trailingBlankLength(text: string): number {
|
|
170
162
|
return text.match(/[\t ]*$/)?.[0].length ?? 0;
|
|
171
163
|
}
|
|
@@ -274,13 +266,13 @@ function inverseChanges(changes: readonly DocumentChange[]): DocumentChange[] {
|
|
|
274
266
|
});
|
|
275
267
|
}
|
|
276
268
|
|
|
277
|
-
/* Runs of consecutive line numbers, from a sorted list. */
|
|
278
269
|
type LineSpanOf = NonNullable<CommandOptions['lineSpan']>;
|
|
279
270
|
|
|
280
271
|
function range(first: number, last: number): number[] {
|
|
281
272
|
return Array.from({ length: last - first + 1 }, (_, index) => first + index);
|
|
282
273
|
}
|
|
283
274
|
|
|
275
|
+
/* Runs of consecutive line numbers, from a sorted list. */
|
|
284
276
|
function blocks(indices: readonly number[]): LineBlock[] {
|
|
285
277
|
const result: LineBlock[] = [];
|
|
286
278
|
for (const index of indices) {
|
|
@@ -313,7 +305,8 @@ function indentStopLength(prefix: string, tabSize: number): number {
|
|
|
313
305
|
|
|
314
306
|
function positionIn(rope: TextRope, offset: number): Position {
|
|
315
307
|
const line = rope.lineAt(offset);
|
|
316
|
-
|
|
308
|
+
const bounds = rope.lineBounds(line);
|
|
309
|
+
return { line, column: Math.min(offset, bounds.end) - bounds.start };
|
|
317
310
|
}
|
|
318
311
|
|
|
319
312
|
/* 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. */
|
|
@@ -351,6 +344,15 @@ function stretchEdits(before: TextRope, after: TextRope, transactions: readonly
|
|
|
351
344
|
return [{ start: positionIn(before, prefix), end: positionIn(before, oldEnd), text: after.slice(prefix, newEnd) }];
|
|
352
345
|
}
|
|
353
346
|
|
|
347
|
+
/* An occurrence selected with its caret `shift` characters into it, as the caret stood in the selection it repeats. */
|
|
348
|
+
function occurrenceSelection(match: OffsetRange, shift: number): Selection {
|
|
349
|
+
if (shift === 0) {
|
|
350
|
+
return { anchor: match.to, head: match.from };
|
|
351
|
+
}
|
|
352
|
+
const caret = Math.min(match.from + shift, match.to);
|
|
353
|
+
return { anchor: caret === match.to ? match.from : match.to, head: caret };
|
|
354
|
+
}
|
|
355
|
+
|
|
354
356
|
/*
|
|
355
357
|
* Text with a selection, undo history and the editing commands of a code editor, and no idea how it
|
|
356
358
|
* is drawn. Offsets are UTF-16 code units, lines and columns zero-based. The text is a persistent
|
|
@@ -557,10 +559,7 @@ export class DocumentModel {
|
|
|
557
559
|
}
|
|
558
560
|
|
|
559
561
|
positionAt(offset: number): Position {
|
|
560
|
-
|
|
561
|
-
const line = this.rope.lineAt(clamped);
|
|
562
|
-
const bounds = this.rope.lineBounds(line);
|
|
563
|
-
return { line, column: Math.min(clamped, bounds.end) - bounds.start };
|
|
562
|
+
return positionIn(this.rope, offsetIn(this.rope, offset));
|
|
564
563
|
}
|
|
565
564
|
|
|
566
565
|
/* A column past the end of its line lands on the end of the line, before the line break. */
|
|
@@ -930,7 +929,7 @@ export class DocumentModel {
|
|
|
930
929
|
if (!/^[\t ]*$/.test(before)) {
|
|
931
930
|
return null;
|
|
932
931
|
}
|
|
933
|
-
const indent = this.getLine(this.rope.lineAt(opener)).text
|
|
932
|
+
const indent = whitespaceOf(this.getLine(this.rope.lineAt(opener)).text);
|
|
934
933
|
if (indent === before) {
|
|
935
934
|
return null;
|
|
936
935
|
}
|
|
@@ -1341,16 +1340,11 @@ export class DocumentModel {
|
|
|
1341
1340
|
return plan !== null && this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
1342
1341
|
}
|
|
1343
1342
|
|
|
1344
|
-
/* The lines the carets and selections touch
|
|
1343
|
+
/* The lines the carets and selections touch, sorted. */
|
|
1345
1344
|
private selectedLines(span?: LineSpanOf): number[] {
|
|
1346
1345
|
const indices = new Set<number>();
|
|
1347
1346
|
for (const selection of this.selections) {
|
|
1348
|
-
|
|
1349
|
-
let first = this.rope.lineAt(from);
|
|
1350
|
-
let last = this.rope.lineAt(to);
|
|
1351
|
-
if (to > from && this.rope.lineBounds(last).start === to) {
|
|
1352
|
-
last--;
|
|
1353
|
-
}
|
|
1347
|
+
let { first, last } = this.touchedLines(selection);
|
|
1354
1348
|
if (span) {
|
|
1355
1349
|
first = span(first).first;
|
|
1356
1350
|
last = Math.max(last, span(last).last);
|
|
@@ -1362,6 +1356,14 @@ export class DocumentModel {
|
|
|
1362
1356
|
return [...indices].sort((left, right) => left - right);
|
|
1363
1357
|
}
|
|
1364
1358
|
|
|
1359
|
+
/* The lines a selection touches; one that ends at the start of a line does not touch it. */
|
|
1360
|
+
private touchedLines(selection: Selection): LineBlock {
|
|
1361
|
+
const { from, to } = rangeOf(selection);
|
|
1362
|
+
const first = this.rope.lineAt(from);
|
|
1363
|
+
const last = this.rope.lineAt(to);
|
|
1364
|
+
return { first, last: last > first && this.rope.lineBounds(last).start === to ? last - 1 : last };
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1365
1367
|
/* A selection is copied in place, its copy selected; a caret duplicates its line and moves down with it. */
|
|
1366
1368
|
private duplicate(span?: LineSpanOf): boolean {
|
|
1367
1369
|
const carets = this.selections.filter((selection) => selection.anchor === selection.head);
|
|
@@ -1542,7 +1544,7 @@ export class DocumentModel {
|
|
|
1542
1544
|
const index = this.rope.lineAt(selection.head);
|
|
1543
1545
|
const line = this.rope.lineBounds(index);
|
|
1544
1546
|
const text = this.getLine(index).text;
|
|
1545
|
-
const indent = (text
|
|
1547
|
+
const indent = whitespaceOf(text) + (/^[\t ]*[)\]}]/.test(text) ? unit : '');
|
|
1546
1548
|
return { from: line.start, to: line.start, text: indent + this.newlineAt(index), anchor: indent.length, head: indent.length };
|
|
1547
1549
|
}),
|
|
1548
1550
|
{ source: 'command' }
|
|
@@ -1601,7 +1603,7 @@ export class DocumentModel {
|
|
|
1601
1603
|
return true;
|
|
1602
1604
|
}
|
|
1603
1605
|
const closer = index.pairs.get(offset);
|
|
1604
|
-
const indentAt = (at: number): number => this.getLine(this.rope.lineAt(at)).text
|
|
1606
|
+
const indentAt = (at: number): number => whitespaceOf(this.getLine(this.rope.lineAt(at)).text).length;
|
|
1605
1607
|
if (closer === undefined || indentAt(closer) >= indentAt(offset)) {
|
|
1606
1608
|
return false;
|
|
1607
1609
|
}
|
|
@@ -1958,7 +1960,7 @@ export class DocumentModel {
|
|
|
1958
1960
|
{ from: 0, to: this.rope.length }
|
|
1959
1961
|
];
|
|
1960
1962
|
if (first.start === last.start) {
|
|
1961
|
-
candidates.push({ from: first.start +
|
|
1963
|
+
candidates.push({ from: first.start + whitespaceOf(first.text).length, to: first.end - trailingBlankLength(first.text) });
|
|
1962
1964
|
}
|
|
1963
1965
|
for (const range of this.structure()) {
|
|
1964
1966
|
candidates.push({ from: range.innerFrom, to: range.innerTo }, range);
|
|
@@ -2033,10 +2035,7 @@ export class DocumentModel {
|
|
|
2033
2035
|
this.rememberOccurrence(wholeWord, notFoundBefore);
|
|
2034
2036
|
return false;
|
|
2035
2037
|
}
|
|
2036
|
-
const
|
|
2037
|
-
const caret = Math.min(match.from + shift, match.to);
|
|
2038
|
-
const added = shift === 0 ? { anchor: match.to, head: match.from } : { anchor: caret === match.to ? match.from : match.to, head: caret };
|
|
2039
|
-
const changed = this.changeSelections([...this.selections, added]);
|
|
2038
|
+
const changed = this.changeSelections([...this.selections, occurrenceSelection(match, primary.head - from)]);
|
|
2040
2039
|
this.rememberOccurrence(wholeWord, false);
|
|
2041
2040
|
return changed;
|
|
2042
2041
|
}
|
|
@@ -2071,12 +2070,7 @@ export class DocumentModel {
|
|
|
2071
2070
|
if (matches.length === 0) {
|
|
2072
2071
|
return false;
|
|
2073
2072
|
}
|
|
2074
|
-
const changed = this.changeSelections(
|
|
2075
|
-
matches.map((match) => {
|
|
2076
|
-
const caret = Math.min(match.from + shift, match.to);
|
|
2077
|
-
return shift === 0 ? { anchor: match.to, head: match.from } : { anchor: caret === match.to ? match.from : match.to, head: caret };
|
|
2078
|
-
})
|
|
2079
|
-
);
|
|
2073
|
+
const changed = this.changeSelections(matches.map((match) => occurrenceSelection(match, shift)));
|
|
2080
2074
|
this.rememberOccurrence(wholeWord, false);
|
|
2081
2075
|
return changed;
|
|
2082
2076
|
}
|
|
@@ -2086,12 +2080,7 @@ export class DocumentModel {
|
|
|
2086
2080
|
const forward = this.selections.at(-1)!.head >= this.selections.at(-1)!.anchor;
|
|
2087
2081
|
const carets: Selection[] = [];
|
|
2088
2082
|
for (const selection of this.selections) {
|
|
2089
|
-
const {
|
|
2090
|
-
const first = this.rope.lineAt(from);
|
|
2091
|
-
let last = this.rope.lineAt(to);
|
|
2092
|
-
if (last > first && to === this.rope.lineBounds(last).start) {
|
|
2093
|
-
last--;
|
|
2094
|
-
}
|
|
2083
|
+
const { first, last } = this.touchedLines(selection);
|
|
2095
2084
|
for (let line = first; line <= last; line++) {
|
|
2096
2085
|
const end = this.rope.lineBounds(line).end;
|
|
2097
2086
|
carets.push({ anchor: end, head: end });
|