@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/src/enter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EditSource } from './edit-source.ts';
|
|
2
2
|
import { commentSyntax } from './languages.ts';
|
|
3
|
-
import { hasHashComments, isBraced, isPhp } from './lexical.ts';
|
|
3
|
+
import { hasHashComments, isBraced, isPhp, openers } from './lexical.ts';
|
|
4
|
+
import { whitespaceOf } from './line-text.ts';
|
|
4
5
|
import { hasJsx, isJsxAttributeValue } from './markup-regions.ts';
|
|
5
6
|
import type { DocumentLine } from './rope.ts';
|
|
6
7
|
import { indentationColumn } from './structure.ts';
|
|
@@ -25,7 +26,6 @@ export interface EnterPlan {
|
|
|
25
26
|
caret: number;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const closers: Readonly<Record<string, string>> = { '(': ')', '[': ']', '{': '}' };
|
|
29
29
|
const concatenation = new Map<string, string>();
|
|
30
30
|
for (const id of 'typescript javascript typescriptreact javascriptreact tsx jsx ts js java csharp cs kotlin scala dart swift go groovy'.split(' ')) {
|
|
31
31
|
concatenation.set(id, '+');
|
|
@@ -41,11 +41,7 @@ const jsxScanLimit = 2_000_000;
|
|
|
41
41
|
/* How far past the caret a comment is searched for its end. */
|
|
42
42
|
const commentReach = 20_000;
|
|
43
43
|
|
|
44
|
-
function
|
|
45
|
-
return /^[\t ]*/.exec(text)![0];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isBlank(text: string): boolean {
|
|
44
|
+
function isIndentOnly(text: string): boolean {
|
|
49
45
|
return /^[\t ]*$/.test(text);
|
|
50
46
|
}
|
|
51
47
|
|
|
@@ -90,16 +86,21 @@ class Enter {
|
|
|
90
86
|
return { from, to, text: newline + leading, caret: newline.length + leading.length };
|
|
91
87
|
}
|
|
92
88
|
const context = source.context(from);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
return this.inLiteral(from, to, context, index) ?? this.inCode(from, to, context, index);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* The plan inside a comment or a string, or null where Enter acts as in code. */
|
|
93
|
+
private inLiteral(from: number, to: number, context: TypingContext, index: number): EnterPlan | null {
|
|
94
|
+
if (context.mode === 'block-comment') {
|
|
95
|
+
return this.inBlockComment(from, to, context, index);
|
|
96
|
+
}
|
|
97
|
+
if (from !== to) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (context.mode === 'line-comment') {
|
|
101
|
+
return this.inLineComment(from, context, index);
|
|
102
|
+
}
|
|
103
|
+
return context.mode === 'quote' ? this.inString(from, context, index) : null;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/* The indentation a new statement after this line gets, which is not the line's own when the line is a continuation. */
|
|
@@ -115,7 +116,7 @@ class Enter {
|
|
|
115
116
|
|
|
116
117
|
private previousCodeLine(index: number): number | null {
|
|
117
118
|
for (let candidate = index - 1; candidate >= 0 && candidate >= index - lookBack; candidate--) {
|
|
118
|
-
if (!
|
|
119
|
+
if (!isIndentOnly(this.source.line(candidate).text)) {
|
|
119
120
|
return candidate;
|
|
120
121
|
}
|
|
121
122
|
}
|
|
@@ -137,7 +138,7 @@ class Enter {
|
|
|
137
138
|
const { unit, language } = options;
|
|
138
139
|
// A comment that trails the code is not part of what decides the indentation.
|
|
139
140
|
const commentAt = lineContext.mode === 'line-comment' ? lineContext.commentStart : undefined;
|
|
140
|
-
const trailing = commentAt !== undefined && commentAt >= line.start && !
|
|
141
|
+
const trailing = commentAt !== undefined && commentAt >= line.start && !isIndentOnly(source.slice(line.start, commentAt));
|
|
141
142
|
const trimmed = trailing ? source.slice(line.start, commentAt).trimEnd() : before.trimEnd();
|
|
142
143
|
const context = trailing ? source.context(line.start + trimmed.length) : lineContext;
|
|
143
144
|
const last = trimmed.at(-1) ?? '';
|
|
@@ -152,10 +153,10 @@ class Enter {
|
|
|
152
153
|
return plain(leading);
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
const opener =
|
|
156
|
+
const opener = openers[last] !== undefined && context.bracket?.close === openers[last] && context.bracket.at === line.start + trimmed.length - 1;
|
|
156
157
|
if (opener) {
|
|
157
158
|
const inner = start + unit;
|
|
158
|
-
if (next ===
|
|
159
|
+
if (next === openers[last]) {
|
|
159
160
|
return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
|
|
160
161
|
}
|
|
161
162
|
const closing =
|
|
@@ -177,7 +178,8 @@ class Enter {
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
if (continues(trimmed, context, language)) {
|
|
180
|
-
const
|
|
181
|
+
const previous = this.previousCodeLine(index);
|
|
182
|
+
const continued = previous !== null && this.lineContinues(previous);
|
|
181
183
|
return plain(continued ? leading : start + unit);
|
|
182
184
|
}
|
|
183
185
|
return plain(start);
|
|
@@ -272,7 +274,7 @@ class Enter {
|
|
|
272
274
|
return { from, to: textStart, text: newline + leading, caret: newline.length + leading.length };
|
|
273
275
|
}
|
|
274
276
|
const code = source.slice(line.start, commentStart);
|
|
275
|
-
const onlyComment =
|
|
277
|
+
const onlyComment = isIndentOnly(code);
|
|
276
278
|
let spacing = ' ';
|
|
277
279
|
if (onlyComment) {
|
|
278
280
|
const gap = whitespaceOf(source.slice(commentStart + marker.length, line.end));
|
|
@@ -405,7 +407,13 @@ class Enter {
|
|
|
405
407
|
}
|
|
406
408
|
return { from, to: textStart, text: newline + star, caret: newline.length + star.length };
|
|
407
409
|
}
|
|
408
|
-
if (
|
|
410
|
+
if (
|
|
411
|
+
isDoc &&
|
|
412
|
+
onOpener &&
|
|
413
|
+
source.lineAt(closeAt) === index &&
|
|
414
|
+
isIndentOnly(source.slice(to, closeAt)) &&
|
|
415
|
+
isIndentOnly(source.slice(closeAt + 2, line.end))
|
|
416
|
+
) {
|
|
409
417
|
// Nothing left to say after the caret: the closer gets a line of its own, under the opener's star.
|
|
410
418
|
const star = this.starPrefix(line, lead, false, isDoc);
|
|
411
419
|
return { from, to: line.end, text: `${newline}${star}${newline}${lead} */`, caret: newline.length + star.length };
|
package/src/fold-roles.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isImportLine } from './lexical-folds.ts';
|
|
1
|
+
import { isImportLine, rangeOf, textStart } from './lexical-folds.ts';
|
|
2
|
+
import { isBlank } from './line-text.ts';
|
|
2
3
|
import type { DocumentLine } from './rope.ts';
|
|
3
4
|
import type { FoldHints, FoldRangeHint, FoldRole, FoldingRange } from './structure.ts';
|
|
4
5
|
|
|
@@ -46,10 +47,6 @@ export function bracketRole(slice: (from: number, to: number) => string, from: n
|
|
|
46
47
|
return /^\s*(?:case\b|default\s*:|(?:export\s+)?(?:declare\s+)?(?:type|interface)\b)/.test(head) ? undefined : 'object-literal';
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function isBlank(text: string): boolean {
|
|
50
|
-
return text.trim() === '';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
50
|
/*
|
|
54
51
|
* The comment at the very top of a file is its header, unless it is the documentation of what follows
|
|
55
52
|
* right under it. Only the first comment can be one, and only behind a shebang, an open tag or blank lines.
|
|
@@ -65,8 +62,7 @@ export function markFileHeader(ranges: FoldingRange[], getLine: (line: number) =
|
|
|
65
62
|
return;
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
|
-
|
|
69
|
-
if (candidate.from > start.start + (start.text.length - start.text.trimStart().length)) {
|
|
65
|
+
if (candidate.from > textStart(getLine(candidate.startLine))) {
|
|
70
66
|
return;
|
|
71
67
|
}
|
|
72
68
|
const next = candidate.endLine + 1 < lineCount ? getLine(candidate.endLine + 1).text : '';
|
|
@@ -125,19 +121,9 @@ export function applyFoldHints(
|
|
|
125
121
|
if (endLine - startLine < minimum || starts.has(startLine)) {
|
|
126
122
|
continue;
|
|
127
123
|
}
|
|
128
|
-
const first = getLine(startLine);
|
|
129
|
-
const indent = first.text.length - first.text.trimStart().length;
|
|
130
|
-
const role = serverRole(hint.kind, first.text.trimStart());
|
|
131
124
|
starts.add(startLine);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
endLine,
|
|
135
|
-
from: first.start + indent,
|
|
136
|
-
to: getLine(endLine).end,
|
|
137
|
-
kind: 'server',
|
|
138
|
-
...(role === undefined ? {} : { role }),
|
|
139
|
-
...edgesOf(hint)
|
|
140
|
-
});
|
|
125
|
+
const role = serverRole(hint.kind, getLine(startLine).text.trimStart());
|
|
126
|
+
result.push({ ...rangeOf(startLine, endLine, getLine, 'server', role), ...edgesOf(hint) });
|
|
141
127
|
}
|
|
142
128
|
return result.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
143
129
|
}
|
package/src/lexical-folds.ts
CHANGED
|
@@ -48,6 +48,11 @@ function bracketDelta(text: string): number {
|
|
|
48
48
|
return depth;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/* The offset of the first character of a line that is not indentation. */
|
|
52
|
+
export function textStart(line: DocumentLine): number {
|
|
53
|
+
return line.start + (line.text.length - line.text.trimStart().length);
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
/* A fold of whole lines, from the end of the first to the end of the last. */
|
|
52
57
|
export function rangeOf(
|
|
53
58
|
startLine: number,
|
|
@@ -56,11 +61,10 @@ export function rangeOf(
|
|
|
56
61
|
kind: FoldingRange['kind'],
|
|
57
62
|
role?: FoldRole
|
|
58
63
|
): FoldingRange {
|
|
59
|
-
const start = getLine(startLine);
|
|
60
64
|
return {
|
|
61
65
|
startLine,
|
|
62
66
|
endLine,
|
|
63
|
-
from:
|
|
67
|
+
from: textStart(getLine(startLine)),
|
|
64
68
|
to: getLine(endLine).end,
|
|
65
69
|
kind,
|
|
66
70
|
...(role === undefined ? {} : { role })
|
package/src/line-commands.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { EditPlan, EditSource } from './edit-source.ts';
|
|
|
2
2
|
import { commentSyntax } from './languages.ts';
|
|
3
3
|
import { continues } from './enter.ts';
|
|
4
4
|
import { indentsByBrackets } from './lexical.ts';
|
|
5
|
+
import { isBlank, rangeOf, whitespaceOf } from './line-text.ts';
|
|
5
6
|
import { markupGuard } from './markup-regions.ts';
|
|
6
7
|
import { mapOffset } from './offsets.ts';
|
|
7
8
|
import type { Selection, TextEdit } from './types.ts';
|
|
@@ -15,18 +16,6 @@ export interface LineCommandOptions {
|
|
|
15
16
|
|
|
16
17
|
const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
17
18
|
|
|
18
|
-
function rangeOf(selection: Selection): { from: number; to: number } {
|
|
19
|
-
return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function whitespaceOf(text: string): string {
|
|
23
|
-
return /^[\t ]*/.exec(text)![0];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function isBlank(text: string): boolean {
|
|
27
|
-
return text.trim() === '';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
19
|
/* How far the edits in front of an offset moved it. An edit that starts at the offset is not in front of it. */
|
|
31
20
|
function shiftBefore(offset: number, edits: readonly TextEdit[]): number {
|
|
32
21
|
return edits.filter((edit) => edit.from < offset).reduce((sum, edit) => sum + edit.text.length - (Math.min(edit.to, offset) - edit.from), 0);
|
|
@@ -137,7 +126,7 @@ export function planJoinLines(source: EditSource, selections: readonly Selection
|
|
|
137
126
|
!isBlank(source.slice(source.line(line).start, comment.start)) &&
|
|
138
127
|
!source.slice(nextStart, nextLine.end).startsWith(comment.marker.trimEnd()) &&
|
|
139
128
|
commentSyntax(options.language, source.region(line)).block?.open === '/*';
|
|
140
|
-
if (converts
|
|
129
|
+
if (converts) {
|
|
141
130
|
const body = source.slice(comment.start + comment.marker.length, join.from);
|
|
142
131
|
if (!body.includes('*/')) {
|
|
143
132
|
edits.push({ from: comment.start, to: comment.start + comment.marker.length, text: '/*' });
|
package/src/line-text.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Selection } from './types.ts';
|
|
2
|
+
|
|
3
|
+
/* The spaces and tabs a line starts with. */
|
|
4
|
+
export function whitespaceOf(text: string): string {
|
|
5
|
+
return /^[\t ]*/.exec(text)![0];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isBlank(text: string): boolean {
|
|
9
|
+
return text.trim() === '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Indentation `width` columns wide, in tabs as far as they reach when spaces are off. */
|
|
13
|
+
export function indentText(width: number, options: { tabSize: number; insertSpaces: boolean }): string {
|
|
14
|
+
if (options.insertSpaces) {
|
|
15
|
+
return ' '.repeat(width);
|
|
16
|
+
}
|
|
17
|
+
return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rangeOf(selection: Selection): { from: number; to: number } {
|
|
21
|
+
return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
22
|
+
}
|
package/src/markup-regions.ts
CHANGED
|
@@ -179,7 +179,7 @@ class JsxScanner {
|
|
|
179
179
|
/* An element from its `<`, recorded when no element holds it, and the offset after its closing tag; -1 when it is not one. */
|
|
180
180
|
private element(from: number, outermost: boolean): number {
|
|
181
181
|
this.depth++;
|
|
182
|
-
const end = this.
|
|
182
|
+
const end = this.parseElement(from);
|
|
183
183
|
this.depth--;
|
|
184
184
|
if (end >= 0 && outermost && this.depth === 0) {
|
|
185
185
|
this.ranges.push({ from, to: end });
|
|
@@ -195,7 +195,7 @@ class JsxScanner {
|
|
|
195
195
|
return at;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
private
|
|
198
|
+
private parseElement(from: number): number {
|
|
199
199
|
const { text } = this;
|
|
200
200
|
let at = from + 1;
|
|
201
201
|
while (at < text.length && NAME.test(text[at]!)) {
|
|
@@ -244,7 +244,7 @@ class JsxScanner {
|
|
|
244
244
|
} else if (value === '{') {
|
|
245
245
|
at = this.code(at + 1, '}');
|
|
246
246
|
} else if (value === '<' && START_OF_NAME.test(text[at + 1] ?? '')) {
|
|
247
|
-
at = this.
|
|
247
|
+
at = this.parseElement(at);
|
|
248
248
|
} else {
|
|
249
249
|
return -1;
|
|
250
250
|
}
|
|
@@ -265,7 +265,7 @@ class JsxScanner {
|
|
|
265
265
|
const closing = /^<\/\s*([\w$.:-]*)\s*>/.exec(text.slice(at, at + 200));
|
|
266
266
|
return closing !== null && closing[1] === name ? at + closing[0].length : -1;
|
|
267
267
|
} else if (START_OF_NAME.test(text[at + 1] ?? '')) {
|
|
268
|
-
at = this.
|
|
268
|
+
at = this.parseElement(at);
|
|
269
269
|
} else {
|
|
270
270
|
return -1;
|
|
271
271
|
}
|
|
@@ -283,6 +283,24 @@ export function jsxRanges(text: string): { from: number; to: number }[] {
|
|
|
283
283
|
return scanner.ranges;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
/* Whether `offset` falls inside one of `ranges`, which are sorted and do not overlap. */
|
|
287
|
+
function insideAny(ranges: readonly { from: number; to: number }[], offset: number): boolean {
|
|
288
|
+
let low = 0;
|
|
289
|
+
let high = ranges.length - 1;
|
|
290
|
+
while (low <= high) {
|
|
291
|
+
const middle = (low + high) >> 1;
|
|
292
|
+
const range = ranges[middle]!;
|
|
293
|
+
if (offset < range.from) {
|
|
294
|
+
high = middle - 1;
|
|
295
|
+
} else if (offset >= range.to) {
|
|
296
|
+
low = middle + 1;
|
|
297
|
+
} else {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
286
304
|
/* Whether a language is one whose scripts can hold JSX. */
|
|
287
305
|
export function hasJsx(language: string): boolean {
|
|
288
306
|
return JSX_LANGUAGES.test(language);
|
|
@@ -313,20 +331,7 @@ export function markupGuard(source: EditSource, language: string): (line: number
|
|
|
313
331
|
return (line) => {
|
|
314
332
|
const bounds = source.line(line);
|
|
315
333
|
const start = bounds.start + (/^[\t ]*/.exec(bounds.text)?.[0].length ?? 0);
|
|
316
|
-
|
|
317
|
-
let high = ranges.length - 1;
|
|
318
|
-
while (low <= high) {
|
|
319
|
-
const middle = (low + high) >> 1;
|
|
320
|
-
const range = ranges[middle]!;
|
|
321
|
-
if (start < range.from) {
|
|
322
|
-
high = middle - 1;
|
|
323
|
-
} else if (start >= range.to) {
|
|
324
|
-
low = middle + 1;
|
|
325
|
-
} else {
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return false;
|
|
334
|
+
return insideAny(ranges, start);
|
|
330
335
|
};
|
|
331
336
|
}
|
|
332
337
|
return () => false;
|
package/src/paste.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
|
+
import { indentText, isBlank, whitespaceOf } from './line-text.ts';
|
|
2
3
|
import { mapOffset } from './offsets.ts';
|
|
3
4
|
import { indentationColumn } from './structure.ts';
|
|
4
5
|
import type { Selection, TextEdit } from './types.ts';
|
|
@@ -12,14 +13,6 @@ export interface PasteOptions {
|
|
|
12
13
|
reindent: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function whitespaceOf(text: string): string {
|
|
16
|
-
return /^[\t ]*/.exec(text)![0];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isBlank(text: string): boolean {
|
|
20
|
-
return text.trim() === '';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
/* A line break at the end is the end of the last line, not a line of its own. */
|
|
24
17
|
function linesOf(text: string): string[] {
|
|
25
18
|
return text.replace(/\r?\n$/, '').split(/\r?\n/);
|
|
@@ -34,13 +27,6 @@ function perCaretLines(text: string, carets: number): string[] | null {
|
|
|
34
27
|
return lines.length === carets ? lines : null;
|
|
35
28
|
}
|
|
36
29
|
|
|
37
|
-
function indentText(width: number, options: PasteOptions): string {
|
|
38
|
-
if (options.insertSpaces) {
|
|
39
|
-
return ' '.repeat(width);
|
|
40
|
-
}
|
|
41
|
-
return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
30
|
/*
|
|
45
31
|
* Where a block of several lines lands, it lands at the indentation of the line it is pasted into:
|
|
46
32
|
* the pasted lines move together by the difference between that indentation and the least indented
|
package/src/preserve-case.ts
CHANGED
|
@@ -22,41 +22,35 @@ export function replaceWithCaseRespect(replacement: string, found: string): stri
|
|
|
22
22
|
if (foundTail === '') {
|
|
23
23
|
return head + replacementTail;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
25
|
+
const replacementCase = letterCase(replacementTail);
|
|
26
|
+
const foundCase = letterCase(foundTail);
|
|
27
|
+
// A tail without letters, like `x2`, follows the first letter of the match.
|
|
28
|
+
const tailUpper = foundCase.hasLetters ? foundCase.upper : upper.test(firstFound!);
|
|
29
|
+
const tailLower = foundCase.hasLetters ? foundCase.lower : lower.test(firstFound!);
|
|
30
|
+
if (tailUpper && (replacementCase.lower || !replacementCase.upper)) {
|
|
31
|
+
return head + replacementTail.toUpperCase();
|
|
32
|
+
}
|
|
33
|
+
if (tailLower && (replacementCase.lower || replacementCase.upper)) {
|
|
34
|
+
return head + replacementTail.toLowerCase();
|
|
36
35
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
return head + replacementTail;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Whether every letter of `text` is upper case and whether every one is lower case; both hold without letters. */
|
|
40
|
+
function letterCase(text: string): { upper: boolean; lower: boolean; hasLetters: boolean } {
|
|
41
|
+
let allUpper = true;
|
|
42
|
+
let allLower = true;
|
|
43
|
+
let hasLetters = false;
|
|
44
|
+
for (const character of text) {
|
|
41
45
|
if (!letter.test(character)) {
|
|
42
46
|
continue;
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!
|
|
48
|
+
hasLetters = true;
|
|
49
|
+
allUpper &&= upper.test(character);
|
|
50
|
+
allLower &&= lower.test(character);
|
|
51
|
+
if (!allUpper && !allLower) {
|
|
48
52
|
break;
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
|
-
|
|
52
|
-
tailUpper = letter.test(firstFound!) && upper.test(firstFound!);
|
|
53
|
-
tailLower = letter.test(firstFound!) && lower.test(firstFound!);
|
|
54
|
-
}
|
|
55
|
-
if (tailUpper && (replacementLower || !replacementUpper)) {
|
|
56
|
-
return head + replacementTail.toUpperCase();
|
|
57
|
-
}
|
|
58
|
-
if (tailLower && (replacementLower || replacementUpper)) {
|
|
59
|
-
return head + replacementTail.toLowerCase();
|
|
60
|
-
}
|
|
61
|
-
return head + replacementTail;
|
|
55
|
+
return { upper: allUpper, lower: allLower, hasLetters };
|
|
62
56
|
}
|
package/src/structure.ts
CHANGED
|
@@ -50,9 +50,8 @@ export interface FoldingRange {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export interface FoldingOptions {
|
|
53
|
-
/*
|
|
53
|
+
/* Both on unless `false`. */
|
|
54
54
|
brackets?: boolean;
|
|
55
|
-
/* On unless `false`. */
|
|
56
55
|
comments?: boolean;
|
|
57
56
|
indentation?: boolean;
|
|
58
57
|
/* The language id, which decides what an import line and a line comment look like. Without one only brackets, block comments and indentation fold. */
|
|
@@ -189,7 +188,7 @@ export function deriveFoldingRanges(
|
|
|
189
188
|
options: FoldingOptions = {},
|
|
190
189
|
slice?: (from: number, to: number) => string
|
|
191
190
|
): FoldingRange[] {
|
|
192
|
-
const minimum = options.minLines
|
|
191
|
+
const minimum = Number.isFinite(options.minLines) ? Math.max(1, Math.trunc(options.minLines!)) : 1;
|
|
193
192
|
const result: FoldingRange[] = [];
|
|
194
193
|
for (const range of ranges) {
|
|
195
194
|
if (range.kind === 'string' || (range.kind === 'bracket' ? options.brackets === false : options.comments === false)) {
|
package/src/typing-context.ts
CHANGED
|
@@ -117,6 +117,12 @@ function scanLiteral(state: TypingContext, text: string, at: number, end: number
|
|
|
117
117
|
return at;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function openComment(state: TypingContext, mode: 'line-comment' | 'block-comment' | 'html-comment', start: number, resume: number): number {
|
|
121
|
+
state.mode = mode;
|
|
122
|
+
state.commentStart = start;
|
|
123
|
+
return resume;
|
|
124
|
+
}
|
|
125
|
+
|
|
120
126
|
function scanCode(state: TypingContext, rules: Rules, text: string, at: number, end: number, offset: number): number {
|
|
121
127
|
const char = text[at];
|
|
122
128
|
const next = text[at + 1];
|
|
@@ -125,29 +131,16 @@ function scanCode(state: TypingContext, rules: Rules, text: string, at: number,
|
|
|
125
131
|
return at;
|
|
126
132
|
}
|
|
127
133
|
if (rules.markup && text.startsWith('<!--', at)) {
|
|
128
|
-
state
|
|
129
|
-
state.commentStart = offset + at;
|
|
130
|
-
return at + 3;
|
|
134
|
+
return openComment(state, 'html-comment', offset + at, at + 3);
|
|
131
135
|
}
|
|
132
136
|
if (rules.hash && char === '#' && !(rules.php && next === '[') && (!rules.hashAtWordStart || startsWord(text, at))) {
|
|
133
|
-
state
|
|
134
|
-
state.commentStart = offset + at;
|
|
135
|
-
return at;
|
|
137
|
+
return openComment(state, 'line-comment', offset + at, at);
|
|
136
138
|
}
|
|
137
|
-
if (rules.dash && char === '-' && next === '-') {
|
|
138
|
-
state
|
|
139
|
-
state.commentStart = offset + at;
|
|
140
|
-
return at + 1;
|
|
141
|
-
}
|
|
142
|
-
if (rules.slash && char === '/' && next === '/') {
|
|
143
|
-
state.mode = 'line-comment';
|
|
144
|
-
state.commentStart = offset + at;
|
|
145
|
-
return at + 1;
|
|
139
|
+
if ((rules.dash && char === '-' && next === '-') || (rules.slash && char === '/' && next === '/')) {
|
|
140
|
+
return openComment(state, 'line-comment', offset + at, at + 1);
|
|
146
141
|
}
|
|
147
142
|
if (rules.block && char === '/' && next === '*') {
|
|
148
|
-
state
|
|
149
|
-
state.commentStart = offset + at;
|
|
150
|
-
return at + 1;
|
|
143
|
+
return openComment(state, 'block-comment', offset + at, at + 1);
|
|
151
144
|
}
|
|
152
145
|
if (rules.php && char === '<' && text.startsWith('<<<', at)) {
|
|
153
146
|
const opener = PHP_HEREDOC.exec(text.slice(at));
|
package/src/words.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// 7184b03af6c5370e79a01ded0df68cf56d7669da, platform/platform-impl/src/com/intellij/openapi/editor/actions/EditorActionUtil.java,
|
|
7
7
|
// lines 934-982.
|
|
8
8
|
|
|
9
|
+
import { splitsSurrogate } from './search.ts';
|
|
10
|
+
|
|
9
11
|
// Java's Character.isJavaIdentifierPart, which counts the ignorable control characters as part of a word.
|
|
10
12
|
// oxlint-disable-next-line no-control-regex
|
|
11
13
|
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;
|
|
@@ -70,11 +72,8 @@ export function isWordBoundary(text: WordText, offset: number, camel: boolean, i
|
|
|
70
72
|
export function wordBoundary(text: WordText, offset: number, direction: -1 | 1, camel = true): number {
|
|
71
73
|
let position = offset + direction;
|
|
72
74
|
while (position > 0 && position < text.length) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const splitsPair = /[\ud800-\udbff]/.test(before) && /[\udc00-\udfff]/.test(after);
|
|
76
|
-
const splitsLineBreak = before === '\r' && after === '\n';
|
|
77
|
-
if (!splitsPair && !splitsLineBreak && isWordBoundary(text, position, camel, direction === -1)) {
|
|
75
|
+
const splitsLineBreak = text.charAt(position - 1) === '\r' && text.charAt(position) === '\n';
|
|
76
|
+
if (!splitsSurrogate(text, position) && !splitsLineBreak && isWordBoundary(text, position, camel, direction === -1)) {
|
|
78
77
|
return position;
|
|
79
78
|
}
|
|
80
79
|
position += direction;
|