@adecore/editor-core 0.17.0-beta.2 → 0.17.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 +5 -0
- package/dist/document.js +104 -9
- 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 +122 -9
- 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;
|
|
@@ -108,6 +112,7 @@ export declare class DocumentModel {
|
|
|
108
112
|
private editSource;
|
|
109
113
|
private bracketIndex;
|
|
110
114
|
private deleteCharacter;
|
|
115
|
+
private unindent;
|
|
111
116
|
private isEmptyPair;
|
|
112
117
|
private backspaceStart;
|
|
113
118
|
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()) {
|
|
@@ -1275,24 +1333,60 @@ export class DocumentModel {
|
|
|
1275
1333
|
this.bracketCache = { revision: this.revision, language, index };
|
|
1276
1334
|
return index;
|
|
1277
1335
|
}
|
|
1278
|
-
deleteCharacter(backwards,
|
|
1336
|
+
deleteCharacter(backwards, indent, pairing, language) {
|
|
1279
1337
|
return this.deleteRanges(this.selections.map((selection) => {
|
|
1280
1338
|
let { from, to } = rangeOf(selection);
|
|
1281
1339
|
if (from === to) {
|
|
1282
1340
|
if (!backwards) {
|
|
1283
1341
|
to = deletionStep(this.rope, to, 1);
|
|
1284
1342
|
}
|
|
1343
|
+
else if (this.arrows.has(from)) {
|
|
1344
|
+
from -= 2;
|
|
1345
|
+
}
|
|
1285
1346
|
else if ((isQuote(this.rope.charAt(from - 1)) ? pairing.quotes : pairing.brackets) && this.isEmptyPair(from, language)) {
|
|
1286
1347
|
from--;
|
|
1287
1348
|
to++;
|
|
1288
1349
|
}
|
|
1289
1350
|
else {
|
|
1290
|
-
from
|
|
1351
|
+
return this.unindent(from, indent, language) ?? { from: this.backspaceStart(from, indent.tabSize), to, text: '' };
|
|
1291
1352
|
}
|
|
1292
1353
|
}
|
|
1293
1354
|
return { from, to, text: '' };
|
|
1294
1355
|
}));
|
|
1295
1356
|
}
|
|
1357
|
+
/*
|
|
1358
|
+
* Backspace with only whitespace before the caret, after the platform's smart backspace: a line indented past
|
|
1359
|
+
* where its brackets put it goes back to there, and one at or before it joins the line above, or takes the place
|
|
1360
|
+
* of that line when it is blank. Null where the brackets say nothing.
|
|
1361
|
+
*/
|
|
1362
|
+
unindent(offset, indent, language) {
|
|
1363
|
+
const index = this.rope.lineAt(offset);
|
|
1364
|
+
const line = this.rope.lineBounds(index);
|
|
1365
|
+
if (!/^[\t ]*$/.test(this.slice(line.start, offset))) {
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
const proper = properIndent(this.editSource(language), index, { language, unit: indent.unit });
|
|
1369
|
+
if (proper === null) {
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
let end = offset;
|
|
1373
|
+
while (end < line.end && /[\t ]/.test(this.rope.charAt(end))) {
|
|
1374
|
+
end++;
|
|
1375
|
+
}
|
|
1376
|
+
if (indentationColumn(this.slice(line.start, end), indent.tabSize) > indentationColumn(proper, indent.tabSize)) {
|
|
1377
|
+
return { from: line.start, to: end, text: proper };
|
|
1378
|
+
}
|
|
1379
|
+
if (index === 0) {
|
|
1380
|
+
return { from: 0, to: end, text: '' };
|
|
1381
|
+
}
|
|
1382
|
+
const previous = this.getLine(index - 1);
|
|
1383
|
+
const start = previous.end - trailingBlankLength(previous.text);
|
|
1384
|
+
if (start !== previous.start || start === 0) {
|
|
1385
|
+
const before = this.slice(previous.start, start);
|
|
1386
|
+
return { from: start, to: end, text: before === '' ? '' : spacingBetween(before, this.slice(end, line.end).trimEnd()) };
|
|
1387
|
+
}
|
|
1388
|
+
return { from: start, to: end, text: proper };
|
|
1389
|
+
}
|
|
1296
1390
|
/* Whether the caret sits between an opener and its own closer, which backspace removes together. */
|
|
1297
1391
|
isEmptyPair(offset, language) {
|
|
1298
1392
|
const opener = this.rope.charAt(offset - 1);
|
|
@@ -1699,6 +1793,7 @@ export class DocumentModel {
|
|
|
1699
1793
|
this.structureCache = undefined;
|
|
1700
1794
|
this.selectionHistory = [];
|
|
1701
1795
|
this.statementClosers.clear();
|
|
1796
|
+
this.arrows.clear();
|
|
1702
1797
|
}
|
|
1703
1798
|
restore(state, changes) {
|
|
1704
1799
|
const previous = this.rope;
|
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()) {
|
|
@@ -1531,18 +1602,25 @@ export class DocumentModel {
|
|
|
1531
1602
|
return index;
|
|
1532
1603
|
}
|
|
1533
1604
|
|
|
1534
|
-
private deleteCharacter(
|
|
1605
|
+
private deleteCharacter(
|
|
1606
|
+
backwards: boolean,
|
|
1607
|
+
indent: { tabSize: number; unit: string },
|
|
1608
|
+
pairing: { brackets: boolean; quotes: boolean },
|
|
1609
|
+
language: string
|
|
1610
|
+
): boolean {
|
|
1535
1611
|
return this.deleteRanges(
|
|
1536
1612
|
this.selections.map((selection) => {
|
|
1537
1613
|
let { from, to } = rangeOf(selection);
|
|
1538
1614
|
if (from === to) {
|
|
1539
1615
|
if (!backwards) {
|
|
1540
1616
|
to = deletionStep(this.rope, to, 1);
|
|
1617
|
+
} else if (this.arrows.has(from)) {
|
|
1618
|
+
from -= 2;
|
|
1541
1619
|
} else if ((isQuote(this.rope.charAt(from - 1)) ? pairing.quotes : pairing.brackets) && this.isEmptyPair(from, language)) {
|
|
1542
1620
|
from--;
|
|
1543
1621
|
to++;
|
|
1544
1622
|
} else {
|
|
1545
|
-
from
|
|
1623
|
+
return this.unindent(from, indent, language) ?? { from: this.backspaceStart(from, indent.tabSize), to, text: '' };
|
|
1546
1624
|
}
|
|
1547
1625
|
}
|
|
1548
1626
|
return { from, to, text: '' };
|
|
@@ -1550,6 +1628,40 @@ export class DocumentModel {
|
|
|
1550
1628
|
);
|
|
1551
1629
|
}
|
|
1552
1630
|
|
|
1631
|
+
/*
|
|
1632
|
+
* Backspace with only whitespace before the caret, after the platform's smart backspace: a line indented past
|
|
1633
|
+
* where its brackets put it goes back to there, and one at or before it joins the line above, or takes the place
|
|
1634
|
+
* of that line when it is blank. Null where the brackets say nothing.
|
|
1635
|
+
*/
|
|
1636
|
+
private unindent(offset: number, indent: { tabSize: number; unit: string }, language: string): TextEdit | null {
|
|
1637
|
+
const index = this.rope.lineAt(offset);
|
|
1638
|
+
const line = this.rope.lineBounds(index);
|
|
1639
|
+
if (!/^[\t ]*$/.test(this.slice(line.start, offset))) {
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
const proper = properIndent(this.editSource(language), index, { language, unit: indent.unit });
|
|
1643
|
+
if (proper === null) {
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
let end = offset;
|
|
1647
|
+
while (end < line.end && /[\t ]/.test(this.rope.charAt(end))) {
|
|
1648
|
+
end++;
|
|
1649
|
+
}
|
|
1650
|
+
if (indentationColumn(this.slice(line.start, end), indent.tabSize) > indentationColumn(proper, indent.tabSize)) {
|
|
1651
|
+
return { from: line.start, to: end, text: proper };
|
|
1652
|
+
}
|
|
1653
|
+
if (index === 0) {
|
|
1654
|
+
return { from: 0, to: end, text: '' };
|
|
1655
|
+
}
|
|
1656
|
+
const previous = this.getLine(index - 1);
|
|
1657
|
+
const start = previous.end - trailingBlankLength(previous.text);
|
|
1658
|
+
if (start !== previous.start || start === 0) {
|
|
1659
|
+
const before = this.slice(previous.start, start);
|
|
1660
|
+
return { from: start, to: end, text: before === '' ? '' : spacingBetween(before, this.slice(end, line.end).trimEnd()) };
|
|
1661
|
+
}
|
|
1662
|
+
return { from: start, to: end, text: proper };
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1553
1665
|
/* Whether the caret sits between an opener and its own closer, which backspace removes together. */
|
|
1554
1666
|
private isEmptyPair(offset: number, language: string): boolean {
|
|
1555
1667
|
const opener = this.rope.charAt(offset - 1);
|
|
@@ -1989,6 +2101,7 @@ export class DocumentModel {
|
|
|
1989
2101
|
this.structureCache = undefined;
|
|
1990
2102
|
this.selectionHistory = [];
|
|
1991
2103
|
this.statementClosers.clear();
|
|
2104
|
+
this.arrows.clear();
|
|
1992
2105
|
}
|
|
1993
2106
|
|
|
1994
2107
|
private restore(state: State, changes: readonly (readonly DocumentChange[])[]): void {
|
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. */
|