@adecore/editor-core 0.17.0-beta.2 → 0.18.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/document.d.ts +6 -0
- package/dist/document.js +124 -10
- package/dist/edit-source.d.ts +1 -1
- package/dist/enter.js +1 -1
- package/dist/fold-roles.js +11 -1
- package/dist/line-commands.d.ts +2 -0
- package/dist/line-commands.js +9 -1
- package/dist/structure.d.ts +4 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/document.ts +143 -10
- package/src/edit-source.ts +2 -2
- package/src/enter.ts +1 -1
- package/src/fold-roles.ts +13 -2
- package/src/line-commands.ts +10 -1
- package/src/structure.ts +6 -0
- package/src/types.ts +2 -0
package/dist/document.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare class DocumentModel {
|
|
|
37
37
|
private selectionHistory;
|
|
38
38
|
private typingContexts;
|
|
39
39
|
private statementClosers;
|
|
40
|
+
private arrows;
|
|
40
41
|
private cloneSession;
|
|
41
42
|
private occurrenceSession;
|
|
42
43
|
occurrencesExhausted: boolean;
|
|
@@ -67,6 +68,8 @@ export declare class DocumentModel {
|
|
|
67
68
|
replaceAll(query: string, replacement: string, options?: ReplaceAllOptions): number;
|
|
68
69
|
getFoldingRanges(options?: FoldingOptions): FoldingRange[];
|
|
69
70
|
typeText(text: string, options?: TypeTextOptions): boolean;
|
|
71
|
+
private takeBackArrows;
|
|
72
|
+
private completeArrows;
|
|
70
73
|
paste(text: string, options?: CommandOptions & {
|
|
71
74
|
wholeLines?: boolean;
|
|
72
75
|
}): boolean;
|
|
@@ -78,6 +81,7 @@ export declare class DocumentModel {
|
|
|
78
81
|
private shouldClosePair;
|
|
79
82
|
private completesTripleQuote;
|
|
80
83
|
private hasMatch;
|
|
84
|
+
private endsOperand;
|
|
81
85
|
private rememberStatementClosers;
|
|
82
86
|
private rememberPairs;
|
|
83
87
|
private insertTab;
|
|
@@ -106,8 +110,10 @@ export declare class DocumentModel {
|
|
|
106
110
|
private startNewLineBefore;
|
|
107
111
|
private editLines;
|
|
108
112
|
private editSource;
|
|
113
|
+
private isUnclosedBrace;
|
|
109
114
|
private bracketIndex;
|
|
110
115
|
private deleteCharacter;
|
|
116
|
+
private unindent;
|
|
111
117
|
private isEmptyPair;
|
|
112
118
|
private backspaceStart;
|
|
113
119
|
private deleteRanges;
|
package/dist/document.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CloserTracker } from './closer-tracker.js';
|
|
2
2
|
import { planBlockComment, planLineComments } from './comments.js';
|
|
3
3
|
import { planEnter } from './enter.js';
|
|
4
|
-
import { planAutoIndent, planJoinLines, planToggleCase } from './line-commands.js';
|
|
4
|
+
import { planAutoIndent, planJoinLines, planToggleCase, properIndent, spacingBetween } from './line-commands.js';
|
|
5
5
|
import { mapOffset } from './offsets.js';
|
|
6
6
|
import { planPaste } from './paste.js';
|
|
7
7
|
import { isPlainText, vueRegionAt } from './languages.js';
|
|
@@ -10,7 +10,7 @@ import { applyFoldHints, markFileHeader } from './fold-roles.js';
|
|
|
10
10
|
import { lexicalFolds } from './lexical-folds.js';
|
|
11
11
|
import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.js';
|
|
12
12
|
import { scanBrackets } from './brackets.js';
|
|
13
|
-
import { hasSmartSemicolon } from './lexical.js';
|
|
13
|
+
import { hasSmartSemicolon, isPhp } from './lexical.js';
|
|
14
14
|
import { clampInteger, TextRope } from './rope.js';
|
|
15
15
|
import { findMatches, replacementText, splitsSurrogate } from './search.js';
|
|
16
16
|
import { dedupeFoldingRanges, deriveFoldingRanges, indentationColumn, scanStructure, tabWidth } from './structure.js';
|
|
@@ -27,6 +27,8 @@ const bracketScanLimit = 2_000_000;
|
|
|
27
27
|
const matchScanLimit = 400_000;
|
|
28
28
|
const pairs = { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' };
|
|
29
29
|
const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
30
|
+
/* What can follow `->`: the first letter of a member name, or the `{` of a dynamic one. */
|
|
31
|
+
const arrowContinues = /^[\p{L}_{]/u;
|
|
30
32
|
const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
|
|
31
33
|
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
32
34
|
function rangeOf(selection) {
|
|
@@ -223,6 +225,8 @@ export class DocumentModel {
|
|
|
223
225
|
selectionHistory = [];
|
|
224
226
|
typingContexts = new TypingContexts((line) => this.getLine(line));
|
|
225
227
|
statementClosers = new Map();
|
|
228
|
+
/* The carets that stand right after a `->` the editor completed, where the next keystroke decides whether it stays. */
|
|
229
|
+
arrows = new Set();
|
|
226
230
|
cloneSession;
|
|
227
231
|
occurrenceSession;
|
|
228
232
|
/* The last select next occurrence found no more, which a host may say. */
|
|
@@ -291,6 +295,7 @@ export class DocumentModel {
|
|
|
291
295
|
this.cloneSession = undefined;
|
|
292
296
|
this.selectionHistory = [];
|
|
293
297
|
this.statementClosers.clear();
|
|
298
|
+
this.arrows.clear();
|
|
294
299
|
if (sameSelections(this.selections, normalized)) {
|
|
295
300
|
return;
|
|
296
301
|
}
|
|
@@ -459,8 +464,8 @@ export class DocumentModel {
|
|
|
459
464
|
/*
|
|
460
465
|
* Types `text` at every caret as one keystroke would: it wraps a selection in a bracket or quote,
|
|
461
466
|
* closes an opener, types over a closer that is already there and, in TypeScript, JavaScript and
|
|
462
|
-
* PHP, moves a `;` to the end of a statement.
|
|
463
|
-
* Returns whether anything changed, a swallowed closer included.
|
|
467
|
+
* PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`. Comments
|
|
468
|
+
* and string contents are left alone. Returns whether anything changed, a swallowed closer included.
|
|
464
469
|
*/
|
|
465
470
|
typeText(text, options = {}) {
|
|
466
471
|
if (options.expectedRevision !== undefined && options.expectedRevision !== this.revision) {
|
|
@@ -469,6 +474,11 @@ export class DocumentModel {
|
|
|
469
474
|
if (!text) {
|
|
470
475
|
return false;
|
|
471
476
|
}
|
|
477
|
+
const arrows = this.arrows;
|
|
478
|
+
this.arrows = new Set();
|
|
479
|
+
if (arrows.size > 0 && text !== '>' && !arrowContinues.test(text)) {
|
|
480
|
+
this.takeBackArrows();
|
|
481
|
+
}
|
|
472
482
|
const language = options.language ?? 'typescript';
|
|
473
483
|
const single = text.length === 1;
|
|
474
484
|
const pairing = options.autoClosingPairs !== false && single && !isPlainText(language);
|
|
@@ -479,6 +489,8 @@ export class DocumentModel {
|
|
|
479
489
|
quotes: pairing && options.autoClosingQuotes !== false,
|
|
480
490
|
surround: options.surroundSelection !== false && single,
|
|
481
491
|
smartSemicolon: options.smartSemicolon !== false && hasSmartSemicolon(language),
|
|
492
|
+
smartArrow: options.smartArrow !== false && single && isPhp(language),
|
|
493
|
+
overArrows: text === '>' ? arrows : new Set(),
|
|
482
494
|
dedent: options.smartEnter !== false
|
|
483
495
|
};
|
|
484
496
|
const plans = this.selections.map((selection) => this.planTyping(selection, input));
|
|
@@ -490,9 +502,31 @@ export class DocumentModel {
|
|
|
490
502
|
if (this.revision === appliedRevision) {
|
|
491
503
|
this.rememberStatementClosers(plans, transaction, language);
|
|
492
504
|
this.rememberPairs(plans, transaction);
|
|
505
|
+
this.completeArrows(plans, transaction);
|
|
493
506
|
}
|
|
494
507
|
return changed || plans.some((plan) => plan.consumed);
|
|
495
508
|
}
|
|
509
|
+
/*
|
|
510
|
+
* A key after a `->` the editor completed that cannot follow it means the `-` was a minus. The `>` is the
|
|
511
|
+
* last undo step, so it goes without a redo, and the key joins the typing of the `-`.
|
|
512
|
+
*/
|
|
513
|
+
takeBackArrows() {
|
|
514
|
+
this.undo();
|
|
515
|
+
this.redoStack.pop();
|
|
516
|
+
this.groupOpen = true;
|
|
517
|
+
}
|
|
518
|
+
/* Adds the `>` after every `-` that became an arrow, as its own undo step, so the first undo leaves the `-` that was typed. */
|
|
519
|
+
completeArrows(plans, transaction) {
|
|
520
|
+
const at = transaction.selections.flatMap((selection, index) => (plans[index]?.arrow ? [selection.head] : [])).sort((left, right) => left - right);
|
|
521
|
+
if (at.length === 0) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const moved = (offset) => offset + at.filter((inserted) => inserted <= offset).length;
|
|
525
|
+
const selections = this.selections.map((selection) => ({ anchor: moved(selection.anchor), head: moved(selection.head) }));
|
|
526
|
+
if (this.applyEdits(at.map((offset) => ({ from: offset, to: offset, text: '>' })), { source: 'input', selections })) {
|
|
527
|
+
this.arrows = new Set(at.map(moved));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
496
530
|
/*
|
|
497
531
|
* Pastes `text` at every selection. With as many lines as carets each caret takes one; `wholeLines`
|
|
498
532
|
* marks text copied from a bare caret, which goes above the lines of bare carets.
|
|
@@ -549,11 +583,11 @@ export class DocumentModel {
|
|
|
549
583
|
}
|
|
550
584
|
const tabSize = tabWidth(options);
|
|
551
585
|
const language = options.language ?? 'typescript';
|
|
586
|
+
const unit = options.insertSpaces === false ? '\t' : ' '.repeat(tabSize);
|
|
552
587
|
if (command === 'smartBackspace' || command === 'deleteForward') {
|
|
553
588
|
const pairing = options.autoClosingPairs !== false && !isPlainText(language);
|
|
554
|
-
return this.deleteCharacter(command === 'smartBackspace', tabSize, { brackets: pairing && options.autoClosingBrackets !== false, quotes: pairing && options.autoClosingQuotes !== false }, language);
|
|
589
|
+
return this.deleteCharacter(command === 'smartBackspace', { tabSize, unit }, { brackets: pairing && options.autoClosingBrackets !== false, quotes: pairing && options.autoClosingQuotes !== false }, language);
|
|
555
590
|
}
|
|
556
|
-
const unit = options.insertSpaces === false ? '\t' : ' '.repeat(tabSize);
|
|
557
591
|
const enter = { language, unit, tabSize, smart: options.smartEnter !== false, reach: true };
|
|
558
592
|
if (command === 'insertNewline' || command === 'startNewLine' || command === 'splitLine') {
|
|
559
593
|
return this.insertNewline(enter, command === 'insertNewline' ? 'enter' : command === 'startNewLine' ? 'below' : 'split');
|
|
@@ -615,12 +649,19 @@ export class DocumentModel {
|
|
|
615
649
|
const after = this.rope.charAt(to);
|
|
616
650
|
const quote = isQuote(text);
|
|
617
651
|
const pairing = quote ? input.quotes : input.brackets;
|
|
652
|
+
if (text === '>' && from === to && input.overArrows.has(from)) {
|
|
653
|
+
return { from, to, text: '', anchor: 0, head: 0, skip: true, consumed: true };
|
|
654
|
+
}
|
|
618
655
|
const pendingClosers = this.statementClosers.get(from);
|
|
619
656
|
if (input.brackets && smartSemicolon && from === to && pendingClosers?.language === language && pendingClosers.text.startsWith(text)) {
|
|
620
657
|
return { from, to, text: '', anchor: 0, head: 0, skip: true, consumed: true, completedClosers: pendingClosers.text.slice(1) };
|
|
621
658
|
}
|
|
622
|
-
const
|
|
659
|
+
const arrow = text === '-' && input.smartArrow && from === to;
|
|
660
|
+
const needsContext = text.length === 1 && (/[()[\]{}'"`<]/.test(text) || (text === ';' && smartSemicolon) || arrow);
|
|
623
661
|
const context = needsContext ? this.typingContext(from, language) : undefined;
|
|
662
|
+
if (arrow && context?.mode === 'code' && context.word !== 'control-close' && this.endsOperand(from)) {
|
|
663
|
+
return { from, to, text, anchor: 1, head: 1, arrow: true };
|
|
664
|
+
}
|
|
624
665
|
const insideQuote = context && (context.mode === 'quote' || context.mode === 'template') && context.quote === text && !context.escaped;
|
|
625
666
|
if (pairing && from === to && after === text && (quote ? insideQuote : context?.mode === 'code' && context.bracket?.close === text)) {
|
|
626
667
|
return { from, to, text: '', anchor: 1, head: 1, skip: true };
|
|
@@ -723,6 +764,23 @@ export class DocumentModel {
|
|
|
723
764
|
const scanned = scanBrackets(text.slice(0, offset) + opener + text.slice(offset), language);
|
|
724
765
|
return scanned.pairs.has(offset);
|
|
725
766
|
}
|
|
767
|
+
/*
|
|
768
|
+
* Whether the text before `offset` ends in something a member is read from: a variable, the name of a
|
|
769
|
+
* property or method after `->` or `?->`, or a `)` or `]`.
|
|
770
|
+
*/
|
|
771
|
+
endsOperand(offset) {
|
|
772
|
+
const line = this.rope.lineBounds(this.rope.lineAt(offset));
|
|
773
|
+
const before = this.slice(line.start, offset);
|
|
774
|
+
if (/[)\]]$/.test(before)) {
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
const name = /[\p{L}_][\p{L}\p{N}_]*$/u.exec(before);
|
|
778
|
+
if (name === null) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
const ahead = before.slice(0, name.index);
|
|
782
|
+
return ahead.endsWith('$') || ahead.endsWith('->');
|
|
783
|
+
}
|
|
726
784
|
/* Remembers the closers a smart semicolon stepped over for the caret that did it, so typing them again is absorbed. */
|
|
727
785
|
rememberStatementClosers(plans, transaction, language) {
|
|
728
786
|
for (const [index, selection] of transaction.selections.entries()) {
|
|
@@ -1259,9 +1317,28 @@ export class DocumentModel {
|
|
|
1259
1317
|
newline: (index) => this.newlineAt(index),
|
|
1260
1318
|
context: (offset) => this.typingContext(offset, language),
|
|
1261
1319
|
region: (line) => (/^vue$/i.test(language) ? vueRegionAt((index) => this.getLine(index).text, line) : null),
|
|
1262
|
-
|
|
1320
|
+
unclosedBrace: (offset) => this.isUnclosedBrace(offset, language)
|
|
1263
1321
|
};
|
|
1264
1322
|
}
|
|
1323
|
+
/*
|
|
1324
|
+
* Whether the `{` at `offset` has no closer of its own: none in the document, or only the closer of a block around
|
|
1325
|
+
* it, which it took from that block. Such a closer sits shallower than the brace's line and leaves an opener before it unmatched.
|
|
1326
|
+
*/
|
|
1327
|
+
isUnclosedBrace(offset, language) {
|
|
1328
|
+
const index = this.bracketIndex(language);
|
|
1329
|
+
if (index === undefined) {
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
if (index.unmatched.has(offset)) {
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
const closer = index.pairs.get(offset);
|
|
1336
|
+
const indentAt = (at) => this.getLine(this.rope.lineAt(at)).text.match(/^[\t ]*/)[0].length;
|
|
1337
|
+
if (closer === undefined || indentAt(closer) >= indentAt(offset)) {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
return [...index.unmatched].some((at) => at < offset && this.rope.charAt(at) === '{');
|
|
1341
|
+
}
|
|
1265
1342
|
/* Undefined for a document too large to scan on every Enter. */
|
|
1266
1343
|
bracketIndex(language) {
|
|
1267
1344
|
if (this.rope.length > bracketScanLimit) {
|
|
@@ -1275,24 +1352,60 @@ export class DocumentModel {
|
|
|
1275
1352
|
this.bracketCache = { revision: this.revision, language, index };
|
|
1276
1353
|
return index;
|
|
1277
1354
|
}
|
|
1278
|
-
deleteCharacter(backwards,
|
|
1355
|
+
deleteCharacter(backwards, indent, pairing, language) {
|
|
1279
1356
|
return this.deleteRanges(this.selections.map((selection) => {
|
|
1280
1357
|
let { from, to } = rangeOf(selection);
|
|
1281
1358
|
if (from === to) {
|
|
1282
1359
|
if (!backwards) {
|
|
1283
1360
|
to = deletionStep(this.rope, to, 1);
|
|
1284
1361
|
}
|
|
1362
|
+
else if (this.arrows.has(from)) {
|
|
1363
|
+
from -= 2;
|
|
1364
|
+
}
|
|
1285
1365
|
else if ((isQuote(this.rope.charAt(from - 1)) ? pairing.quotes : pairing.brackets) && this.isEmptyPair(from, language)) {
|
|
1286
1366
|
from--;
|
|
1287
1367
|
to++;
|
|
1288
1368
|
}
|
|
1289
1369
|
else {
|
|
1290
|
-
from
|
|
1370
|
+
return this.unindent(from, indent, language) ?? { from: this.backspaceStart(from, indent.tabSize), to, text: '' };
|
|
1291
1371
|
}
|
|
1292
1372
|
}
|
|
1293
1373
|
return { from, to, text: '' };
|
|
1294
1374
|
}));
|
|
1295
1375
|
}
|
|
1376
|
+
/*
|
|
1377
|
+
* Backspace with only whitespace before the caret, after the platform's smart backspace: a line indented past
|
|
1378
|
+
* where its brackets put it goes back to there, and one at or before it joins the line above, or takes the place
|
|
1379
|
+
* of that line when it is blank. Null where the brackets say nothing.
|
|
1380
|
+
*/
|
|
1381
|
+
unindent(offset, indent, language) {
|
|
1382
|
+
const index = this.rope.lineAt(offset);
|
|
1383
|
+
const line = this.rope.lineBounds(index);
|
|
1384
|
+
if (!/^[\t ]*$/.test(this.slice(line.start, offset))) {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
const proper = properIndent(this.editSource(language), index, { language, unit: indent.unit });
|
|
1388
|
+
if (proper === null) {
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
let end = offset;
|
|
1392
|
+
while (end < line.end && /[\t ]/.test(this.rope.charAt(end))) {
|
|
1393
|
+
end++;
|
|
1394
|
+
}
|
|
1395
|
+
if (indentationColumn(this.slice(line.start, end), indent.tabSize) > indentationColumn(proper, indent.tabSize)) {
|
|
1396
|
+
return { from: line.start, to: end, text: proper };
|
|
1397
|
+
}
|
|
1398
|
+
if (index === 0) {
|
|
1399
|
+
return { from: 0, to: end, text: '' };
|
|
1400
|
+
}
|
|
1401
|
+
const previous = this.getLine(index - 1);
|
|
1402
|
+
const start = previous.end - trailingBlankLength(previous.text);
|
|
1403
|
+
if (start !== previous.start || start === 0) {
|
|
1404
|
+
const before = this.slice(previous.start, start);
|
|
1405
|
+
return { from: start, to: end, text: before === '' ? '' : spacingBetween(before, this.slice(end, line.end).trimEnd()) };
|
|
1406
|
+
}
|
|
1407
|
+
return { from: start, to: end, text: proper };
|
|
1408
|
+
}
|
|
1296
1409
|
/* Whether the caret sits between an opener and its own closer, which backspace removes together. */
|
|
1297
1410
|
isEmptyPair(offset, language) {
|
|
1298
1411
|
const opener = this.rope.charAt(offset - 1);
|
|
@@ -1699,6 +1812,7 @@ export class DocumentModel {
|
|
|
1699
1812
|
this.structureCache = undefined;
|
|
1700
1813
|
this.selectionHistory = [];
|
|
1701
1814
|
this.statementClosers.clear();
|
|
1815
|
+
this.arrows.clear();
|
|
1702
1816
|
}
|
|
1703
1817
|
restore(state, changes) {
|
|
1704
1818
|
const previous = this.rope;
|
package/dist/edit-source.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface EditSource {
|
|
|
11
11
|
newline(index: number): string;
|
|
12
12
|
context(offset: number): TypingContext;
|
|
13
13
|
region(line: number): VueRegion | null;
|
|
14
|
-
|
|
14
|
+
unclosedBrace(offset: number): boolean;
|
|
15
15
|
}
|
|
16
16
|
export interface EditPlan {
|
|
17
17
|
edits: TextEdit[];
|
package/dist/enter.js
CHANGED
|
@@ -120,7 +120,7 @@ class Enter {
|
|
|
120
120
|
if (next === closers[last]) {
|
|
121
121
|
return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
|
|
122
122
|
}
|
|
123
|
-
const closing = last === '{' && source.
|
|
123
|
+
const closing = last === '{' && source.unclosedBrace(context.bracket.at) ? this.closeBrace(from, afterWhitespace, line, rest, newline, start, inner) : null;
|
|
124
124
|
return closing ?? plain(inner);
|
|
125
125
|
}
|
|
126
126
|
const tag = this.openTag(trimmed, index);
|
package/dist/fold-roles.js
CHANGED
|
@@ -100,6 +100,12 @@ export function applyFoldHints(ranges, hints, lineAt, getLine, slice, minimum) {
|
|
|
100
100
|
for (const hint of hints.ranges ?? []) {
|
|
101
101
|
const startLine = lineAt(hint.from);
|
|
102
102
|
const endLine = lineAt(hint.to);
|
|
103
|
+
const same = result.find((range) => range.startLine === startLine && range.endLine === endLine);
|
|
104
|
+
// The text's own fold of the same lines keeps its kind and role, and takes where the server hides from and shows again from.
|
|
105
|
+
if (same !== undefined) {
|
|
106
|
+
Object.assign(same, edgesOf(hint));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
103
109
|
if (endLine - startLine < minimum || starts.has(startLine)) {
|
|
104
110
|
continue;
|
|
105
111
|
}
|
|
@@ -113,11 +119,15 @@ export function applyFoldHints(ranges, hints, lineAt, getLine, slice, minimum) {
|
|
|
113
119
|
from: first.start + indent,
|
|
114
120
|
to: getLine(endLine).end,
|
|
115
121
|
kind: 'server',
|
|
116
|
-
...(role === undefined ? {} : { role })
|
|
122
|
+
...(role === undefined ? {} : { role }),
|
|
123
|
+
...edgesOf(hint)
|
|
117
124
|
});
|
|
118
125
|
}
|
|
119
126
|
return result.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
120
127
|
}
|
|
128
|
+
function edgesOf(hint) {
|
|
129
|
+
return { ...(hint.head === undefined ? {} : { head: hint.head }), ...(hint.tail === undefined ? {} : { tail: hint.tail }) };
|
|
130
|
+
}
|
|
121
131
|
/* The fold that is the body of a symbol, and what kind of body it is. */
|
|
122
132
|
function bodyOf(ranges, symbol, startLine, endLine, slice) {
|
|
123
133
|
const brace = ranges
|
package/dist/line-commands.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface LineCommandOptions {
|
|
|
4
4
|
language: string;
|
|
5
5
|
unit: string;
|
|
6
6
|
}
|
|
7
|
+
export declare function spacingBetween(previous: string, next: string): string;
|
|
7
8
|
export declare function planJoinLines(source: EditSource, selections: readonly Selection[], options: LineCommandOptions): EditPlan | null;
|
|
8
9
|
export declare function planToggleCase(source: EditSource, selections: readonly Selection[]): EditPlan | null;
|
|
9
10
|
export declare function planAutoIndent(source: EditSource, lines: readonly number[], options: LineCommandOptions): EditPlan | null;
|
|
11
|
+
export declare function properIndent(source: EditSource, line: number, options: LineCommandOptions): string | null;
|
package/dist/line-commands.js
CHANGED
|
@@ -27,7 +27,7 @@ function skipWhitespace(source, from) {
|
|
|
27
27
|
return offset;
|
|
28
28
|
}
|
|
29
29
|
/* What goes where two lines meet: nothing before a closer, a comma or a dot and after an opener, else a space. */
|
|
30
|
-
function spacingBetween(previous, next) {
|
|
30
|
+
export function spacingBetween(previous, next) {
|
|
31
31
|
if (next === '' || /^[)\],;]/.test(next) || /^\.(?![.\d])/.test(next) || /[([]$/.test(previous)) {
|
|
32
32
|
return '';
|
|
33
33
|
}
|
|
@@ -236,6 +236,14 @@ export function planAutoIndent(source, lines, options) {
|
|
|
236
236
|
}
|
|
237
237
|
return edits.length === 0 ? null : { edits, selections: [] };
|
|
238
238
|
}
|
|
239
|
+
/* The indentation the brackets give a line, or null where they say nothing: outside code, in markup and in a language that is not bracketed. */
|
|
240
|
+
export function properIndent(source, line, options) {
|
|
241
|
+
const context = source.context(source.line(line).start);
|
|
242
|
+
if (!indentsByBrackets(options.language) || context.mode !== 'code' || markupGuard(source, options.language)(line)) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return targetIndent(source, options, line, context, (index) => whitespaceOf(source.line(index).text));
|
|
246
|
+
}
|
|
239
247
|
function targetIndent(source, options, line, context, indentOf) {
|
|
240
248
|
const text = source.line(line).text.trimStart();
|
|
241
249
|
const open = frames(context);
|
package/dist/structure.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface FoldingRange {
|
|
|
14
14
|
to: number;
|
|
15
15
|
kind: 'bracket' | 'comment' | 'indentation' | 'imports' | 'line-comments' | 'region' | 'block' | 'section' | 'server';
|
|
16
16
|
role?: FoldRole;
|
|
17
|
+
head?: number;
|
|
18
|
+
tail?: number;
|
|
17
19
|
}
|
|
18
20
|
export interface FoldingOptions {
|
|
19
21
|
brackets?: boolean;
|
|
@@ -36,6 +38,8 @@ export interface FoldRangeHint {
|
|
|
36
38
|
from: number;
|
|
37
39
|
to: number;
|
|
38
40
|
kind?: string;
|
|
41
|
+
head?: number;
|
|
42
|
+
tail?: number;
|
|
39
43
|
}
|
|
40
44
|
export interface FoldHints {
|
|
41
45
|
symbols?: readonly FoldSymbolHint[];
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
package/src/document.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CloserTracker } from './closer-tracker.ts';
|
|
2
2
|
import { planBlockComment, planLineComments } from './comments.ts';
|
|
3
3
|
import { planEnter } from './enter.ts';
|
|
4
|
-
import { planAutoIndent, planJoinLines, planToggleCase } from './line-commands.ts';
|
|
4
|
+
import { planAutoIndent, planJoinLines, planToggleCase, properIndent, spacingBetween } from './line-commands.ts';
|
|
5
5
|
import { mapOffset } from './offsets.ts';
|
|
6
6
|
import { planPaste } from './paste.ts';
|
|
7
7
|
import type { EnterOptions, EnterPlan } from './enter.ts';
|
|
@@ -13,7 +13,7 @@ import { lexicalFolds } from './lexical-folds.ts';
|
|
|
13
13
|
import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.ts';
|
|
14
14
|
import { scanBrackets } from './brackets.ts';
|
|
15
15
|
import type { BracketIndex } from './brackets.ts';
|
|
16
|
-
import { hasSmartSemicolon } from './lexical.ts';
|
|
16
|
+
import { hasSmartSemicolon, isPhp } from './lexical.ts';
|
|
17
17
|
import { clampInteger, TextRope } from './rope.ts';
|
|
18
18
|
import type { DocumentLine } from './rope.ts';
|
|
19
19
|
import { findMatches, replacementText, splitsSurrogate } from './search.ts';
|
|
@@ -88,6 +88,8 @@ interface SelectionEdit extends TextEdit {
|
|
|
88
88
|
consumed?: boolean;
|
|
89
89
|
/* An opener and the closer the editor added with it, with the caret between them. */
|
|
90
90
|
paired?: boolean;
|
|
91
|
+
/* A `-` the editor completes to `->` in a step of its own, once the `-` is in. */
|
|
92
|
+
arrow?: boolean;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
interface SelectionTransaction {
|
|
@@ -108,6 +110,9 @@ interface TypingInput {
|
|
|
108
110
|
quotes: boolean;
|
|
109
111
|
surround: boolean;
|
|
110
112
|
smartSemicolon: boolean;
|
|
113
|
+
smartArrow: boolean;
|
|
114
|
+
/* The carets right after a `->` the editor completed, where a typed `>` goes over the one that is there. */
|
|
115
|
+
overArrows: ReadonlySet<number>;
|
|
111
116
|
/* A closing bracket typed on a line of its own goes back to the indentation of its opener. */
|
|
112
117
|
dedent: boolean;
|
|
113
118
|
}
|
|
@@ -144,6 +149,8 @@ const bracketScanLimit = 2_000_000;
|
|
|
144
149
|
const matchScanLimit = 400_000;
|
|
145
150
|
const pairs: Readonly<Record<string, string>> = { '(': ')', '[': ']', '{': '}', '"': '"', "'": "'", '`': '`' };
|
|
146
151
|
const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
152
|
+
/* What can follow `->`: the first letter of a member name, or the `{` of a dynamic one. */
|
|
153
|
+
const arrowContinues = /^[\p{L}_{]/u;
|
|
147
154
|
const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
|
|
148
155
|
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
149
156
|
|
|
@@ -362,6 +369,8 @@ export class DocumentModel {
|
|
|
362
369
|
private selectionHistory: Selection[][] = [];
|
|
363
370
|
private typingContexts = new TypingContexts((line) => this.getLine(line));
|
|
364
371
|
private statementClosers = new Map<number, { text: string; language: string }>();
|
|
372
|
+
/* The carets that stand right after a `->` the editor completed, where the next keystroke decides whether it stays. */
|
|
373
|
+
private arrows = new Set<number>();
|
|
365
374
|
private cloneSession: { signature: string; frames: { direction: -1 | 1; carets: CaretClone[] }[] } | undefined;
|
|
366
375
|
private occurrenceSession: { wholeWord: boolean; notFound: boolean; signature: string } | undefined;
|
|
367
376
|
/* The last select next occurrence found no more, which a host may say. */
|
|
@@ -442,6 +451,7 @@ export class DocumentModel {
|
|
|
442
451
|
this.cloneSession = undefined;
|
|
443
452
|
this.selectionHistory = [];
|
|
444
453
|
this.statementClosers.clear();
|
|
454
|
+
this.arrows.clear();
|
|
445
455
|
if (sameSelections(this.selections, normalized)) {
|
|
446
456
|
return;
|
|
447
457
|
}
|
|
@@ -630,8 +640,8 @@ export class DocumentModel {
|
|
|
630
640
|
/*
|
|
631
641
|
* Types `text` at every caret as one keystroke would: it wraps a selection in a bracket or quote,
|
|
632
642
|
* closes an opener, types over a closer that is already there and, in TypeScript, JavaScript and
|
|
633
|
-
* PHP, moves a `;` to the end of a statement.
|
|
634
|
-
* Returns whether anything changed, a swallowed closer included.
|
|
643
|
+
* PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`. Comments
|
|
644
|
+
* and string contents are left alone. Returns whether anything changed, a swallowed closer included.
|
|
635
645
|
*/
|
|
636
646
|
typeText(text: string, options: TypeTextOptions = {}): boolean {
|
|
637
647
|
if (options.expectedRevision !== undefined && options.expectedRevision !== this.revision) {
|
|
@@ -640,6 +650,11 @@ export class DocumentModel {
|
|
|
640
650
|
if (!text) {
|
|
641
651
|
return false;
|
|
642
652
|
}
|
|
653
|
+
const arrows = this.arrows;
|
|
654
|
+
this.arrows = new Set();
|
|
655
|
+
if (arrows.size > 0 && text !== '>' && !arrowContinues.test(text)) {
|
|
656
|
+
this.takeBackArrows();
|
|
657
|
+
}
|
|
643
658
|
const language = options.language ?? 'typescript';
|
|
644
659
|
const single = text.length === 1;
|
|
645
660
|
const pairing = options.autoClosingPairs !== false && single && !isPlainText(language);
|
|
@@ -650,6 +665,8 @@ export class DocumentModel {
|
|
|
650
665
|
quotes: pairing && options.autoClosingQuotes !== false,
|
|
651
666
|
surround: options.surroundSelection !== false && single,
|
|
652
667
|
smartSemicolon: options.smartSemicolon !== false && hasSmartSemicolon(language),
|
|
668
|
+
smartArrow: options.smartArrow !== false && single && isPhp(language),
|
|
669
|
+
overArrows: text === '>' ? arrows : new Set(),
|
|
653
670
|
dedent: options.smartEnter !== false
|
|
654
671
|
};
|
|
655
672
|
const plans = this.selections.map((selection) => this.planTyping(selection, input));
|
|
@@ -665,10 +682,39 @@ export class DocumentModel {
|
|
|
665
682
|
if (this.revision === appliedRevision) {
|
|
666
683
|
this.rememberStatementClosers(plans, transaction, language);
|
|
667
684
|
this.rememberPairs(plans, transaction);
|
|
685
|
+
this.completeArrows(plans, transaction);
|
|
668
686
|
}
|
|
669
687
|
return changed || plans.some((plan) => plan.consumed);
|
|
670
688
|
}
|
|
671
689
|
|
|
690
|
+
/*
|
|
691
|
+
* A key after a `->` the editor completed that cannot follow it means the `-` was a minus. The `>` is the
|
|
692
|
+
* last undo step, so it goes without a redo, and the key joins the typing of the `-`.
|
|
693
|
+
*/
|
|
694
|
+
private takeBackArrows(): void {
|
|
695
|
+
this.undo();
|
|
696
|
+
this.redoStack.pop();
|
|
697
|
+
this.groupOpen = true;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/* Adds the `>` after every `-` that became an arrow, as its own undo step, so the first undo leaves the `-` that was typed. */
|
|
701
|
+
private completeArrows(plans: readonly SelectionEdit[], transaction: SelectionTransaction): void {
|
|
702
|
+
const at = transaction.selections.flatMap((selection, index) => (plans[index]?.arrow ? [selection.head] : [])).sort((left, right) => left - right);
|
|
703
|
+
if (at.length === 0) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const moved = (offset: number): number => offset + at.filter((inserted) => inserted <= offset).length;
|
|
707
|
+
const selections = this.selections.map((selection) => ({ anchor: moved(selection.anchor), head: moved(selection.head) }));
|
|
708
|
+
if (
|
|
709
|
+
this.applyEdits(
|
|
710
|
+
at.map((offset) => ({ from: offset, to: offset, text: '>' })),
|
|
711
|
+
{ source: 'input', selections }
|
|
712
|
+
)
|
|
713
|
+
) {
|
|
714
|
+
this.arrows = new Set(at.map(moved));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
672
718
|
/*
|
|
673
719
|
* Pastes `text` at every selection. With as many lines as carets each caret takes one; `wholeLines`
|
|
674
720
|
* marks text copied from a bare caret, which goes above the lines of bare carets.
|
|
@@ -726,16 +772,16 @@ export class DocumentModel {
|
|
|
726
772
|
}
|
|
727
773
|
const tabSize = tabWidth(options);
|
|
728
774
|
const language = options.language ?? 'typescript';
|
|
775
|
+
const unit = options.insertSpaces === false ? '\t' : ' '.repeat(tabSize);
|
|
729
776
|
if (command === 'smartBackspace' || command === 'deleteForward') {
|
|
730
777
|
const pairing = options.autoClosingPairs !== false && !isPlainText(language);
|
|
731
778
|
return this.deleteCharacter(
|
|
732
779
|
command === 'smartBackspace',
|
|
733
|
-
tabSize,
|
|
780
|
+
{ tabSize, unit },
|
|
734
781
|
{ brackets: pairing && options.autoClosingBrackets !== false, quotes: pairing && options.autoClosingQuotes !== false },
|
|
735
782
|
language
|
|
736
783
|
);
|
|
737
784
|
}
|
|
738
|
-
const unit = options.insertSpaces === false ? '\t' : ' '.repeat(tabSize);
|
|
739
785
|
const enter = { language, unit, tabSize, smart: options.smartEnter !== false, reach: true };
|
|
740
786
|
if (command === 'insertNewline' || command === 'startNewLine' || command === 'splitLine') {
|
|
741
787
|
return this.insertNewline(enter, command === 'insertNewline' ? 'enter' : command === 'startNewLine' ? 'below' : 'split');
|
|
@@ -799,13 +845,20 @@ export class DocumentModel {
|
|
|
799
845
|
const after = this.rope.charAt(to);
|
|
800
846
|
const quote = isQuote(text);
|
|
801
847
|
const pairing = quote ? input.quotes : input.brackets;
|
|
848
|
+
if (text === '>' && from === to && input.overArrows.has(from)) {
|
|
849
|
+
return { from, to, text: '', anchor: 0, head: 0, skip: true, consumed: true };
|
|
850
|
+
}
|
|
802
851
|
const pendingClosers = this.statementClosers.get(from);
|
|
803
852
|
if (input.brackets && smartSemicolon && from === to && pendingClosers?.language === language && pendingClosers.text.startsWith(text)) {
|
|
804
853
|
return { from, to, text: '', anchor: 0, head: 0, skip: true, consumed: true, completedClosers: pendingClosers.text.slice(1) };
|
|
805
854
|
}
|
|
806
855
|
|
|
807
|
-
const
|
|
856
|
+
const arrow = text === '-' && input.smartArrow && from === to;
|
|
857
|
+
const needsContext = text.length === 1 && (/[()[\]{}'"`<]/.test(text) || (text === ';' && smartSemicolon) || arrow);
|
|
808
858
|
const context = needsContext ? this.typingContext(from, language) : undefined;
|
|
859
|
+
if (arrow && context?.mode === 'code' && context.word !== 'control-close' && this.endsOperand(from)) {
|
|
860
|
+
return { from, to, text, anchor: 1, head: 1, arrow: true };
|
|
861
|
+
}
|
|
809
862
|
const insideQuote = context && (context.mode === 'quote' || context.mode === 'template') && context.quote === text && !context.escaped;
|
|
810
863
|
if (pairing && from === to && after === text && (quote ? insideQuote : context?.mode === 'code' && context.bracket?.close === text)) {
|
|
811
864
|
return { from, to, text: '', anchor: 1, head: 1, skip: true };
|
|
@@ -920,6 +973,24 @@ export class DocumentModel {
|
|
|
920
973
|
return scanned.pairs.has(offset);
|
|
921
974
|
}
|
|
922
975
|
|
|
976
|
+
/*
|
|
977
|
+
* Whether the text before `offset` ends in something a member is read from: a variable, the name of a
|
|
978
|
+
* property or method after `->` or `?->`, or a `)` or `]`.
|
|
979
|
+
*/
|
|
980
|
+
private endsOperand(offset: number): boolean {
|
|
981
|
+
const line = this.rope.lineBounds(this.rope.lineAt(offset));
|
|
982
|
+
const before = this.slice(line.start, offset);
|
|
983
|
+
if (/[)\]]$/.test(before)) {
|
|
984
|
+
return true;
|
|
985
|
+
}
|
|
986
|
+
const name = /[\p{L}_][\p{L}\p{N}_]*$/u.exec(before);
|
|
987
|
+
if (name === null) {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
const ahead = before.slice(0, name.index);
|
|
991
|
+
return ahead.endsWith('$') || ahead.endsWith('->');
|
|
992
|
+
}
|
|
993
|
+
|
|
923
994
|
/* Remembers the closers a smart semicolon stepped over for the caret that did it, so typing them again is absorbed. */
|
|
924
995
|
private rememberStatementClosers(plans: readonly SelectionEdit[], transaction: SelectionTransaction, language: string): void {
|
|
925
996
|
for (const [index, selection] of transaction.selections.entries()) {
|
|
@@ -1513,10 +1584,30 @@ export class DocumentModel {
|
|
|
1513
1584
|
newline: (index) => this.newlineAt(index),
|
|
1514
1585
|
context: (offset) => this.typingContext(offset, language),
|
|
1515
1586
|
region: (line) => (/^vue$/i.test(language) ? vueRegionAt((index) => this.getLine(index).text, line) : null),
|
|
1516
|
-
|
|
1587
|
+
unclosedBrace: (offset) => this.isUnclosedBrace(offset, language)
|
|
1517
1588
|
};
|
|
1518
1589
|
}
|
|
1519
1590
|
|
|
1591
|
+
/*
|
|
1592
|
+
* Whether the `{` at `offset` has no closer of its own: none in the document, or only the closer of a block around
|
|
1593
|
+
* it, which it took from that block. Such a closer sits shallower than the brace's line and leaves an opener before it unmatched.
|
|
1594
|
+
*/
|
|
1595
|
+
private isUnclosedBrace(offset: number, language: string): boolean {
|
|
1596
|
+
const index = this.bracketIndex(language);
|
|
1597
|
+
if (index === undefined) {
|
|
1598
|
+
return false;
|
|
1599
|
+
}
|
|
1600
|
+
if (index.unmatched.has(offset)) {
|
|
1601
|
+
return true;
|
|
1602
|
+
}
|
|
1603
|
+
const closer = index.pairs.get(offset);
|
|
1604
|
+
const indentAt = (at: number): number => this.getLine(this.rope.lineAt(at)).text.match(/^[\t ]*/)![0].length;
|
|
1605
|
+
if (closer === undefined || indentAt(closer) >= indentAt(offset)) {
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
return [...index.unmatched].some((at) => at < offset && this.rope.charAt(at) === '{');
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1520
1611
|
/* Undefined for a document too large to scan on every Enter. */
|
|
1521
1612
|
private bracketIndex(language: string): BracketIndex | undefined {
|
|
1522
1613
|
if (this.rope.length > bracketScanLimit) {
|
|
@@ -1531,18 +1622,25 @@ export class DocumentModel {
|
|
|
1531
1622
|
return index;
|
|
1532
1623
|
}
|
|
1533
1624
|
|
|
1534
|
-
private deleteCharacter(
|
|
1625
|
+
private deleteCharacter(
|
|
1626
|
+
backwards: boolean,
|
|
1627
|
+
indent: { tabSize: number; unit: string },
|
|
1628
|
+
pairing: { brackets: boolean; quotes: boolean },
|
|
1629
|
+
language: string
|
|
1630
|
+
): boolean {
|
|
1535
1631
|
return this.deleteRanges(
|
|
1536
1632
|
this.selections.map((selection) => {
|
|
1537
1633
|
let { from, to } = rangeOf(selection);
|
|
1538
1634
|
if (from === to) {
|
|
1539
1635
|
if (!backwards) {
|
|
1540
1636
|
to = deletionStep(this.rope, to, 1);
|
|
1637
|
+
} else if (this.arrows.has(from)) {
|
|
1638
|
+
from -= 2;
|
|
1541
1639
|
} else if ((isQuote(this.rope.charAt(from - 1)) ? pairing.quotes : pairing.brackets) && this.isEmptyPair(from, language)) {
|
|
1542
1640
|
from--;
|
|
1543
1641
|
to++;
|
|
1544
1642
|
} else {
|
|
1545
|
-
from
|
|
1643
|
+
return this.unindent(from, indent, language) ?? { from: this.backspaceStart(from, indent.tabSize), to, text: '' };
|
|
1546
1644
|
}
|
|
1547
1645
|
}
|
|
1548
1646
|
return { from, to, text: '' };
|
|
@@ -1550,6 +1648,40 @@ export class DocumentModel {
|
|
|
1550
1648
|
);
|
|
1551
1649
|
}
|
|
1552
1650
|
|
|
1651
|
+
/*
|
|
1652
|
+
* Backspace with only whitespace before the caret, after the platform's smart backspace: a line indented past
|
|
1653
|
+
* where its brackets put it goes back to there, and one at or before it joins the line above, or takes the place
|
|
1654
|
+
* of that line when it is blank. Null where the brackets say nothing.
|
|
1655
|
+
*/
|
|
1656
|
+
private unindent(offset: number, indent: { tabSize: number; unit: string }, language: string): TextEdit | null {
|
|
1657
|
+
const index = this.rope.lineAt(offset);
|
|
1658
|
+
const line = this.rope.lineBounds(index);
|
|
1659
|
+
if (!/^[\t ]*$/.test(this.slice(line.start, offset))) {
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
const proper = properIndent(this.editSource(language), index, { language, unit: indent.unit });
|
|
1663
|
+
if (proper === null) {
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
let end = offset;
|
|
1667
|
+
while (end < line.end && /[\t ]/.test(this.rope.charAt(end))) {
|
|
1668
|
+
end++;
|
|
1669
|
+
}
|
|
1670
|
+
if (indentationColumn(this.slice(line.start, end), indent.tabSize) > indentationColumn(proper, indent.tabSize)) {
|
|
1671
|
+
return { from: line.start, to: end, text: proper };
|
|
1672
|
+
}
|
|
1673
|
+
if (index === 0) {
|
|
1674
|
+
return { from: 0, to: end, text: '' };
|
|
1675
|
+
}
|
|
1676
|
+
const previous = this.getLine(index - 1);
|
|
1677
|
+
const start = previous.end - trailingBlankLength(previous.text);
|
|
1678
|
+
if (start !== previous.start || start === 0) {
|
|
1679
|
+
const before = this.slice(previous.start, start);
|
|
1680
|
+
return { from: start, to: end, text: before === '' ? '' : spacingBetween(before, this.slice(end, line.end).trimEnd()) };
|
|
1681
|
+
}
|
|
1682
|
+
return { from: start, to: end, text: proper };
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1553
1685
|
/* Whether the caret sits between an opener and its own closer, which backspace removes together. */
|
|
1554
1686
|
private isEmptyPair(offset: number, language: string): boolean {
|
|
1555
1687
|
const opener = this.rope.charAt(offset - 1);
|
|
@@ -1989,6 +2121,7 @@ export class DocumentModel {
|
|
|
1989
2121
|
this.structureCache = undefined;
|
|
1990
2122
|
this.selectionHistory = [];
|
|
1991
2123
|
this.statementClosers.clear();
|
|
2124
|
+
this.arrows.clear();
|
|
1992
2125
|
}
|
|
1993
2126
|
|
|
1994
2127
|
private restore(state: State, changes: readonly (readonly DocumentChange[])[]): void {
|
package/src/edit-source.ts
CHANGED
|
@@ -14,8 +14,8 @@ export interface EditSource {
|
|
|
14
14
|
newline(index: number): string;
|
|
15
15
|
context(offset: number): TypingContext;
|
|
16
16
|
region(line: number): VueRegion | null;
|
|
17
|
-
/* Whether a `{` at this offset has no closer
|
|
18
|
-
|
|
17
|
+
/* Whether a `{` at this offset has no closer of its own, so Enter after it adds one. */
|
|
18
|
+
unclosedBrace(offset: number): boolean;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/* Edits to apply together and the selections that stand after them. */
|
package/src/enter.ts
CHANGED
|
@@ -159,7 +159,7 @@ class Enter {
|
|
|
159
159
|
return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
|
|
160
160
|
}
|
|
161
161
|
const closing =
|
|
162
|
-
last === '{' && source.
|
|
162
|
+
last === '{' && source.unclosedBrace(context.bracket!.at) ? this.closeBrace(from, afterWhitespace, line, rest, newline, start, inner) : null;
|
|
163
163
|
return closing ?? plain(inner);
|
|
164
164
|
}
|
|
165
165
|
|
package/src/fold-roles.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isImportLine } from './lexical-folds.ts';
|
|
2
2
|
import type { DocumentLine } from './rope.ts';
|
|
3
|
-
import type { FoldHints, FoldRole, FoldingRange } from './structure.ts';
|
|
3
|
+
import type { FoldHints, FoldRangeHint, FoldRole, FoldingRange } from './structure.ts';
|
|
4
4
|
|
|
5
5
|
const SCRIPT_LANGUAGES = /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|mjs|cjs|mts|cts)$/i;
|
|
6
6
|
/* How far back from a bracket the text before it is read. */
|
|
@@ -116,6 +116,12 @@ export function applyFoldHints(
|
|
|
116
116
|
for (const hint of hints.ranges ?? []) {
|
|
117
117
|
const startLine = lineAt(hint.from);
|
|
118
118
|
const endLine = lineAt(hint.to);
|
|
119
|
+
const same = result.find((range) => range.startLine === startLine && range.endLine === endLine);
|
|
120
|
+
// The text's own fold of the same lines keeps its kind and role, and takes where the server hides from and shows again from.
|
|
121
|
+
if (same !== undefined) {
|
|
122
|
+
Object.assign(same, edgesOf(hint));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
119
125
|
if (endLine - startLine < minimum || starts.has(startLine)) {
|
|
120
126
|
continue;
|
|
121
127
|
}
|
|
@@ -129,12 +135,17 @@ export function applyFoldHints(
|
|
|
129
135
|
from: first.start + indent,
|
|
130
136
|
to: getLine(endLine).end,
|
|
131
137
|
kind: 'server',
|
|
132
|
-
...(role === undefined ? {} : { role })
|
|
138
|
+
...(role === undefined ? {} : { role }),
|
|
139
|
+
...edgesOf(hint)
|
|
133
140
|
});
|
|
134
141
|
}
|
|
135
142
|
return result.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
function edgesOf(hint: FoldRangeHint): Pick<FoldingRange, 'head' | 'tail'> {
|
|
146
|
+
return { ...(hint.head === undefined ? {} : { head: hint.head }), ...(hint.tail === undefined ? {} : { tail: hint.tail }) };
|
|
147
|
+
}
|
|
148
|
+
|
|
138
149
|
/* The fold that is the body of a symbol, and what kind of body it is. */
|
|
139
150
|
function bodyOf(
|
|
140
151
|
ranges: FoldingRange[],
|
package/src/line-commands.ts
CHANGED
|
@@ -43,7 +43,7 @@ function skipWhitespace(source: EditSource, from: number): number {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/* What goes where two lines meet: nothing before a closer, a comma or a dot and after an opener, else a space. */
|
|
46
|
-
function spacingBetween(previous: string, next: string): string {
|
|
46
|
+
export function spacingBetween(previous: string, next: string): string {
|
|
47
47
|
if (next === '' || /^[)\],;]/.test(next) || /^\.(?![.\d])/.test(next) || /[([]$/.test(previous)) {
|
|
48
48
|
return '';
|
|
49
49
|
}
|
|
@@ -279,6 +279,15 @@ export function planAutoIndent(source: EditSource, lines: readonly number[], opt
|
|
|
279
279
|
return edits.length === 0 ? null : { edits, selections: [] };
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
/* The indentation the brackets give a line, or null where they say nothing: outside code, in markup and in a language that is not bracketed. */
|
|
283
|
+
export function properIndent(source: EditSource, line: number, options: LineCommandOptions): string | null {
|
|
284
|
+
const context = source.context(source.line(line).start);
|
|
285
|
+
if (!indentsByBrackets(options.language) || context.mode !== 'code' || markupGuard(source, options.language)(line)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return targetIndent(source, options, line, context, (index) => whitespaceOf(source.line(index).text));
|
|
289
|
+
}
|
|
290
|
+
|
|
282
291
|
function targetIndent(source: EditSource, options: LineCommandOptions, line: number, context: TypingContext, indentOf: (line: number) => string): string {
|
|
283
292
|
const text = source.line(line).text.trimStart();
|
|
284
293
|
const open = frames(context);
|
package/src/structure.ts
CHANGED
|
@@ -44,6 +44,9 @@ export interface FoldingRange {
|
|
|
44
44
|
kind: 'bracket' | 'comment' | 'indentation' | 'imports' | 'line-comments' | 'region' | 'block' | 'section' | 'server';
|
|
45
45
|
/* What the range is, where the text or a language server says; most ranges have none. */
|
|
46
46
|
role?: FoldRole;
|
|
47
|
+
/* Where a collapsed fold hides from on its first line and shows again from on its last, when a server says; else whole lines. */
|
|
48
|
+
head?: number;
|
|
49
|
+
tail?: number;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export interface FoldingOptions {
|
|
@@ -79,6 +82,9 @@ export interface FoldRangeHint {
|
|
|
79
82
|
to: number;
|
|
80
83
|
/* The LSP kind: `comment`, `imports` or `region`. */
|
|
81
84
|
kind?: string;
|
|
85
|
+
/* Where it hides from on its first line and shows again from on its last, from the LSP `startCharacter` and `endCharacter`. */
|
|
86
|
+
head?: number;
|
|
87
|
+
tail?: number;
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
export interface FoldHints {
|
package/src/types.ts
CHANGED
|
@@ -123,6 +123,8 @@ export interface CommandOptions {
|
|
|
123
123
|
/* A language id such as `typescript` or `php`; it decides what counts as a comment or a string. */
|
|
124
124
|
language?: string;
|
|
125
125
|
smartSemicolon?: boolean;
|
|
126
|
+
/* In PHP a `-` typed after a variable, a member or a `)` or `]` becomes `->`. On unless false. */
|
|
127
|
+
smartArrow?: boolean;
|
|
126
128
|
/* Tab steps over a closer the editor inserted. On unless false. */
|
|
127
129
|
tabOutOfClosers?: boolean;
|
|
128
130
|
/* Enter computes the indentation of the new line, continues comments and closes braces. On unless false. */
|