@adecore/editor-core 0.43.0 → 0.44.1
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 +9 -0
- package/dist/document.js +52 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/dist/postfix-operands.d.ts +6 -0
- package/dist/postfix-operands.js +397 -0
- package/dist/surround-shape.d.ts +9 -0
- package/dist/surround-shape.js +127 -0
- package/dist/text-places.d.ts +7 -0
- package/dist/text-places.js +87 -0
- package/dist/trivia.d.ts +2 -0
- package/dist/trivia.js +19 -0
- package/dist/types.d.ts +1 -0
- package/dist/typing-handlers.d.ts +6 -0
- package/dist/typing-handlers.js +63 -0
- package/dist/unwrap.d.ts +10 -0
- package/dist/unwrap.js +320 -0
- package/package.json +1 -1
- package/src/document.ts +66 -2
- package/src/index.ts +5 -0
- package/src/postfix-operands.ts +542 -0
- package/src/surround-shape.ts +172 -0
- package/src/text-places.ts +126 -0
- package/src/trivia.ts +29 -0
- package/src/types.ts +2 -0
- package/src/typing-handlers.ts +83 -0
- package/src/unwrap.ts +460 -0
package/dist/document.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ import type { DocumentLine } from './rope.ts';
|
|
|
3
3
|
import type { FindMatch, FindNextOptions, FindOptions } from './search.ts';
|
|
4
4
|
import type { FoldingOptions, FoldingRange } from './structure.ts';
|
|
5
5
|
import type { CommandOptions, Disposable, EditOptions, EditorCommand, EditorSnapshot, Position, Selection, TextEdit } from './types.ts';
|
|
6
|
+
import { type TextPlace } from './text-places.ts';
|
|
7
|
+
import { type PostfixOperand } from './postfix-operands.ts';
|
|
8
|
+
import { type SurroundShape } from './surround-shape.ts';
|
|
9
|
+
import { type UnwrapTarget } from './unwrap.ts';
|
|
6
10
|
import { type TodoMatch, type TodoOptions } from './todo.ts';
|
|
7
11
|
import { type WhitespaceCleanupOptions } from './whitespace-cleanup.ts';
|
|
8
12
|
export type { DocumentLine, FindMatch, FindNextOptions, FindOptions, FoldingOptions, FoldingRange };
|
|
@@ -93,6 +97,7 @@ export declare class DocumentModel {
|
|
|
93
97
|
typeText(text: string, options?: TypeTextOptions): boolean;
|
|
94
98
|
private takeBackArrows;
|
|
95
99
|
private completeArrows;
|
|
100
|
+
private completeTemplates;
|
|
96
101
|
paste(text: string, options?: CommandOptions & {
|
|
97
102
|
wholeLines?: boolean;
|
|
98
103
|
}): boolean;
|
|
@@ -135,6 +140,10 @@ export declare class DocumentModel {
|
|
|
135
140
|
private insertNewline;
|
|
136
141
|
private startNewLineBefore;
|
|
137
142
|
private editLines;
|
|
143
|
+
placeAt(offset: number, language: string): TextPlace;
|
|
144
|
+
postfixOperands(dot: number, language: string): PostfixOperand[];
|
|
145
|
+
surroundShape(from: number, to: number, language: string): SurroundShape | null;
|
|
146
|
+
unwrapTargets(offset: number, language: string, indentUnit: string): UnwrapTarget[];
|
|
138
147
|
private editSource;
|
|
139
148
|
private isUnclosedBrace;
|
|
140
149
|
private scriptSource;
|
package/dist/document.js
CHANGED
|
@@ -14,7 +14,7 @@ import { blockFolds } from './block-folds.js';
|
|
|
14
14
|
import { applyFoldHints, markFileHeader } from './fold-roles.js';
|
|
15
15
|
import { lexicalFolds } from './lexical-folds.js';
|
|
16
16
|
import { rangeOf, whitespaceOf } from './line-text.js';
|
|
17
|
-
import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.js';
|
|
17
|
+
import { isQuote, replacesComparison, surround, swapQuotes, templateLiteralEdits, templateString } from './typing-handlers.js';
|
|
18
18
|
import { scanBrackets } from './brackets.js';
|
|
19
19
|
import { BracketTracker } from './bracket-tracker.js';
|
|
20
20
|
import { hasSmartSemicolon, isPhp } from './lexical.js';
|
|
@@ -22,6 +22,10 @@ import { clampInteger, TextRope, ROPE_CHUNK_SIZE } from './rope.js';
|
|
|
22
22
|
import { findMatches, findMatchFrom, replacementText, splitsSurrogate } from './search.js';
|
|
23
23
|
import { dedupeFoldingRanges, deriveFoldingRanges, indentationColumn, scanStructure, tabWidth } from './structure.js';
|
|
24
24
|
import { TypingContexts } from './typing-context.js';
|
|
25
|
+
import { placeAt } from './text-places.js';
|
|
26
|
+
import { postfixOperands } from './postfix-operands.js';
|
|
27
|
+
import { surroundShape } from './surround-shape.js';
|
|
28
|
+
import { unwrapTargets } from './unwrap.js';
|
|
25
29
|
import { todosInLines } from './todo.js';
|
|
26
30
|
import { planConvertIndents, planLineOrder } from './line-order.js';
|
|
27
31
|
import { ModifiedStretches, planWhitespaceCleanup } from './whitespace-cleanup.js';
|
|
@@ -54,6 +58,10 @@ function trailingBlankLength(text) {
|
|
|
54
58
|
function changesText(rope, edit) {
|
|
55
59
|
return edit.text.length !== edit.to - edit.from || rope.slice(edit.from, edit.to) !== edit.text;
|
|
56
60
|
}
|
|
61
|
+
/* Where an offset stands after edits that end at or before it moved it; an insertion at the offset pushes it on. */
|
|
62
|
+
function shifted(edits, offset) {
|
|
63
|
+
return edits.reduce((moved, edit) => (edit.to <= offset ? moved + edit.text.length - (edit.to - edit.from) : moved), offset);
|
|
64
|
+
}
|
|
57
65
|
/* Merges the carets' edits into one simultaneous batch and works out where each caret lands in the result. */
|
|
58
66
|
function selectionTransaction(plans) {
|
|
59
67
|
const edits = [];
|
|
@@ -587,7 +595,8 @@ export class DocumentModel {
|
|
|
587
595
|
/*
|
|
588
596
|
* Types `text` at every caret as one keystroke would: it wraps a selection in a bracket or quote,
|
|
589
597
|
* closes an opener, types over a closer that is already there and, in TypeScript, JavaScript and
|
|
590
|
-
* PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`.
|
|
598
|
+
* PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`. In JavaScript and
|
|
599
|
+
* TypeScript a `{` that completes `${` in a quoted string makes it a template literal. Comments
|
|
591
600
|
* and string contents are left alone. Returns whether anything changed, a swallowed closer included.
|
|
592
601
|
*/
|
|
593
602
|
typeText(text, options = {}) {
|
|
@@ -613,6 +622,7 @@ export class DocumentModel {
|
|
|
613
622
|
surround: options.surroundSelection !== false && single,
|
|
614
623
|
smartSemicolon: options.smartSemicolon !== false && hasSmartSemicolon(language),
|
|
615
624
|
smartArrow: options.smartArrow !== false && single && isPhp(language),
|
|
625
|
+
templateLiterals: options.templateLiterals !== false && text === '{',
|
|
616
626
|
overArrows: text === '>' ? arrows : new Set(),
|
|
617
627
|
dedent: options.smartEnter !== false
|
|
618
628
|
};
|
|
@@ -626,6 +636,7 @@ export class DocumentModel {
|
|
|
626
636
|
this.rememberStatementClosers(plans, transaction, language);
|
|
627
637
|
this.rememberPairs(plans, transaction);
|
|
628
638
|
this.completeArrows(plans, transaction);
|
|
639
|
+
this.completeTemplates(plans, transaction, language);
|
|
629
640
|
}
|
|
630
641
|
return changed || plans.some((plan) => plan.consumed);
|
|
631
642
|
}
|
|
@@ -650,6 +661,23 @@ export class DocumentModel {
|
|
|
650
661
|
this.arrows = new Set(at.map(moved));
|
|
651
662
|
}
|
|
652
663
|
}
|
|
664
|
+
/* Turns the quoted strings a `${` was typed in into template literals, as a step of their own so the first undo leaves the quotes back. */
|
|
665
|
+
completeTemplates(plans, transaction, language) {
|
|
666
|
+
// Carets in one string find the same quotes.
|
|
667
|
+
const strings = new Map();
|
|
668
|
+
for (const plan of plans) {
|
|
669
|
+
if (plan.template) {
|
|
670
|
+
strings.set(shifted(transaction.edits, plan.template.open), shifted(transaction.edits, plan.template.close));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const source = this.editSource(language);
|
|
674
|
+
const edits = [...strings].flatMap(([open, close]) => templateLiteralEdits(source, open, close));
|
|
675
|
+
if (edits.length > 0) {
|
|
676
|
+
// A caret right before a closing quote stays there, which the default mapping of an edit that replaces the quote would not do.
|
|
677
|
+
const selections = this.selections.map((selection) => ({ anchor: shifted(edits, selection.anchor), head: shifted(edits, selection.head) }));
|
|
678
|
+
this.applyEdits(edits, { source: 'input', selections });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
653
681
|
/*
|
|
654
682
|
* Pastes `text` at every selection. With as many lines as carets each caret takes one; `wholeLines`
|
|
655
683
|
* marks text copied from a bare caret, which goes above the lines of bare carets.
|
|
@@ -840,6 +868,12 @@ export class DocumentModel {
|
|
|
840
868
|
return wrapped;
|
|
841
869
|
}
|
|
842
870
|
}
|
|
871
|
+
if (input.templateLiterals && from === to) {
|
|
872
|
+
const template = templateString(this.editSource(language), from, language);
|
|
873
|
+
if (template) {
|
|
874
|
+
return { from, to, text: input.brackets ? '{}' : '{', anchor: 1, head: 1, paired: input.brackets, template };
|
|
875
|
+
}
|
|
876
|
+
}
|
|
843
877
|
if (pairing && pairs[text] !== undefined && from === to && this.shouldClosePair(from, text, after, context, input)) {
|
|
844
878
|
return { from, to, text: text + pairs[text], anchor: 1, head: 1, paired: true };
|
|
845
879
|
}
|
|
@@ -1509,6 +1543,22 @@ export class DocumentModel {
|
|
|
1509
1543
|
});
|
|
1510
1544
|
return this.applyEdits(plan.edits, { source: 'command', selections });
|
|
1511
1545
|
}
|
|
1546
|
+
/* What the text at an offset is written in, with embedded regions resolved, and whether it is code, a comment or a string. */
|
|
1547
|
+
placeAt(offset, language) {
|
|
1548
|
+
return placeAt(this.editSource(language), offset, language);
|
|
1549
|
+
}
|
|
1550
|
+
/* Every expression that ends right before the `.` at `dot`, innermost first; empty where none does. */
|
|
1551
|
+
postfixOperands(dot, language) {
|
|
1552
|
+
return postfixOperands(this.editSource(language), dot, language);
|
|
1553
|
+
}
|
|
1554
|
+
/* What a range can be surrounded with: whole statements, one expression, both or neither. Null where the range is blank. */
|
|
1555
|
+
surroundShape(from, to, language) {
|
|
1556
|
+
return surroundShape(this.editSource(language), from, to, language);
|
|
1557
|
+
}
|
|
1558
|
+
/* What can be unwrapped or removed around an offset, innermost first, each with the edits that do it. */
|
|
1559
|
+
unwrapTargets(offset, language, indentUnit) {
|
|
1560
|
+
return unwrapTargets(this.editSource(language), offset, language, indentUnit);
|
|
1561
|
+
}
|
|
1512
1562
|
editSource(language) {
|
|
1513
1563
|
return {
|
|
1514
1564
|
lineCount: this.getLineCount(),
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { DocumentEditOptions, DocumentLine, OffsetRange, ReplaceAllOptions,
|
|
|
3
3
|
export { scanBrackets, type BracketIndex, type BracketMatch } from './brackets.ts';
|
|
4
4
|
export { findMatches, iterateMatches, findMatchFrom, replacementText, type FindMatch, type FindNextOptions, type FindOptions } from './search.ts';
|
|
5
5
|
export { indentationColumn } from './structure.ts';
|
|
6
|
+
export { commentSyntax, type CommentSyntax } from './languages.ts';
|
|
6
7
|
export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from './doc-comments.ts';
|
|
7
8
|
export { DEFAULT_TODO_WORDS, findTodos, type TodoMatch, type TodoOptions } from './todo.ts';
|
|
8
9
|
export { detectIndentation, type DetectedIndentation } from './line-order.ts';
|
|
@@ -10,3 +11,7 @@ export type { TrailingWhitespace, WhitespaceCleanupOptions } from './whitespace-
|
|
|
10
11
|
export type { FoldHints, FoldRangeHint, FoldRole, FoldSymbolHint, FoldingOptions, FoldingRange } from './structure.ts';
|
|
11
12
|
export type { ChangeSource, CommandOptions, ContentEdit, Disposable, DocumentChange, EditOptions, EditorCommand, EditorSnapshot, Position, Selection, TextEdit } from './types.ts';
|
|
12
13
|
export { changedSpan, changedSpans, type TextSpan } from './text-span.ts';
|
|
14
|
+
export type { TextPlace, TextPlaceKind } from './text-places.ts';
|
|
15
|
+
export type { PostfixOperand } from './postfix-operands.ts';
|
|
16
|
+
export type { SurroundShape } from './surround-shape.ts';
|
|
17
|
+
export type { UnwrapKind, UnwrapTarget } from './unwrap.ts';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { DocumentModel } from './document.js';
|
|
|
2
2
|
export { scanBrackets } from './brackets.js';
|
|
3
3
|
export { findMatches, iterateMatches, findMatchFrom, replacementText } from './search.js';
|
|
4
4
|
export { indentationColumn } from './structure.js';
|
|
5
|
+
export { commentSyntax } from './languages.js';
|
|
5
6
|
export { findDocComments, MAX_DOC_COMMENTS } from './doc-comments.js';
|
|
6
7
|
export { DEFAULT_TODO_WORDS, findTodos } from './todo.js';
|
|
7
8
|
export { detectIndentation } from './line-order.js';
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { isPhp, isScript, isWordCharacter } from './lexical.js';
|
|
2
|
+
import { atStatementStart } from './surround-shape.js';
|
|
3
|
+
import { placeAt } from './text-places.js';
|
|
4
|
+
import { skipTriviaBack } from './trivia.js';
|
|
5
|
+
const TERNARY = 2;
|
|
6
|
+
const LITERAL_LIMIT = 10_000;
|
|
7
|
+
const MAX_OPERATOR_LENGTH = 4;
|
|
8
|
+
const binary = (precedence, right = false) => ({ kind: 'binary', precedence, right });
|
|
9
|
+
const stop = { kind: 'stop', precedence: 0, right: false };
|
|
10
|
+
const SCRIPT_OPERATORS = new Map([
|
|
11
|
+
['**', binary(15, true)],
|
|
12
|
+
['*', binary(14)],
|
|
13
|
+
['/', binary(14)],
|
|
14
|
+
['%', binary(14)],
|
|
15
|
+
['+', binary(13)],
|
|
16
|
+
['-', binary(13)],
|
|
17
|
+
['<<', binary(12)],
|
|
18
|
+
['>>', binary(12)],
|
|
19
|
+
['>>>', binary(12)],
|
|
20
|
+
['<', binary(10)],
|
|
21
|
+
['>', binary(10)],
|
|
22
|
+
['<=', binary(10)],
|
|
23
|
+
['>=', binary(10)],
|
|
24
|
+
['==', binary(9)],
|
|
25
|
+
['!=', binary(9)],
|
|
26
|
+
['===', binary(9)],
|
|
27
|
+
['!==', binary(9)],
|
|
28
|
+
['&', binary(8)],
|
|
29
|
+
['^', binary(7)],
|
|
30
|
+
['|', binary(6)],
|
|
31
|
+
['&&', binary(5)],
|
|
32
|
+
['||', binary(4)],
|
|
33
|
+
['??', binary(3, true)],
|
|
34
|
+
['?:', binary(TERNARY, true)],
|
|
35
|
+
['?', { kind: 'question', precedence: TERNARY, right: true }],
|
|
36
|
+
[':', { kind: 'colon', precedence: TERNARY, right: true }]
|
|
37
|
+
]);
|
|
38
|
+
// An assignment ends the operand, and they are listed so that `>>=` is not read as `>=`.
|
|
39
|
+
for (const text of ['=', '=>', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '**=', '<<=', '>>=', '>>>=', '&&=', '||=', '??=', '?.', '++', '--', '::']) {
|
|
40
|
+
SCRIPT_OPERATORS.set(text, stop);
|
|
41
|
+
}
|
|
42
|
+
const PHP_OPERATORS = new Map(SCRIPT_OPERATORS);
|
|
43
|
+
PHP_OPERATORS.set('.', binary(11));
|
|
44
|
+
PHP_OPERATORS.set('.=', stop);
|
|
45
|
+
PHP_OPERATORS.set('<>', binary(10));
|
|
46
|
+
PHP_OPERATORS.set('<=>', binary(9));
|
|
47
|
+
PHP_OPERATORS.set('->', stop);
|
|
48
|
+
PHP_OPERATORS.set('?->', stop);
|
|
49
|
+
const SCRIPT_WORDS = new Map([
|
|
50
|
+
['in', binary(10)],
|
|
51
|
+
['instanceof', binary(10)]
|
|
52
|
+
]);
|
|
53
|
+
const PHP_WORDS = new Map([
|
|
54
|
+
['instanceof', binary(10)],
|
|
55
|
+
['and', binary(1.5)],
|
|
56
|
+
['xor', binary(1.4)],
|
|
57
|
+
['or', binary(1.3)]
|
|
58
|
+
]);
|
|
59
|
+
const UNARY_WORDS = new Set(['typeof', 'void', 'await', 'delete']);
|
|
60
|
+
// Words that start a statement or a clause, so they are no operand and nothing to the left of them belongs to an expression.
|
|
61
|
+
const KEYWORDS = new Set([
|
|
62
|
+
'return',
|
|
63
|
+
'throw',
|
|
64
|
+
'yield',
|
|
65
|
+
'case',
|
|
66
|
+
'default',
|
|
67
|
+
'const',
|
|
68
|
+
'let',
|
|
69
|
+
'var',
|
|
70
|
+
'echo',
|
|
71
|
+
'print',
|
|
72
|
+
'if',
|
|
73
|
+
'else',
|
|
74
|
+
'elseif',
|
|
75
|
+
'for',
|
|
76
|
+
'foreach',
|
|
77
|
+
'while',
|
|
78
|
+
'do',
|
|
79
|
+
'switch',
|
|
80
|
+
'try',
|
|
81
|
+
'catch',
|
|
82
|
+
'finally',
|
|
83
|
+
'function',
|
|
84
|
+
'class',
|
|
85
|
+
'import',
|
|
86
|
+
'export',
|
|
87
|
+
'of',
|
|
88
|
+
'as',
|
|
89
|
+
'use',
|
|
90
|
+
'new',
|
|
91
|
+
'extends',
|
|
92
|
+
'implements',
|
|
93
|
+
'global',
|
|
94
|
+
'include',
|
|
95
|
+
'require'
|
|
96
|
+
]);
|
|
97
|
+
const CAST = /^\s*(?:int|integer|bool|boolean|float|double|string|array|object|binary|unset)\s*$/i;
|
|
98
|
+
function insideTemplate(context) {
|
|
99
|
+
for (let bracket = context.bracket; bracket !== undefined; bracket = bracket.previous) {
|
|
100
|
+
if (bracket.template === true) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
class Scanner {
|
|
107
|
+
source;
|
|
108
|
+
php;
|
|
109
|
+
typescript;
|
|
110
|
+
operators;
|
|
111
|
+
words;
|
|
112
|
+
constructor(source, place) {
|
|
113
|
+
this.source = source;
|
|
114
|
+
this.php = isPhp(place);
|
|
115
|
+
this.typescript = /^(?:typescript|ts|tsx|typescriptreact|mts|cts)$/i.test(place);
|
|
116
|
+
this.operators = this.php ? PHP_OPERATORS : SCRIPT_OPERATORS;
|
|
117
|
+
this.words = this.php ? PHP_WORDS : SCRIPT_WORDS;
|
|
118
|
+
}
|
|
119
|
+
skip(end) {
|
|
120
|
+
return skipTriviaBack(this.source, end);
|
|
121
|
+
}
|
|
122
|
+
wordStart(end) {
|
|
123
|
+
let from = end;
|
|
124
|
+
while (from > 0 && isWordCharacter(this.source.charAt(from - 1))) {
|
|
125
|
+
from--;
|
|
126
|
+
}
|
|
127
|
+
return from;
|
|
128
|
+
}
|
|
129
|
+
/* The start of the string, template or regex literal that ends at `end`. */
|
|
130
|
+
literalStart(end) {
|
|
131
|
+
const closing = this.source.context(end - 1);
|
|
132
|
+
if (closing.mode === 'code' && !insideTemplate(closing)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
for (let at = end - 2; at >= 0 && end - at <= LITERAL_LIMIT; at--) {
|
|
136
|
+
const context = this.source.context(at);
|
|
137
|
+
if (context.mode === 'code' && !insideTemplate(context)) {
|
|
138
|
+
return at;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/* The word, group or literal that ends at `end`. A block, the parenthesis of a control statement and a stray character are none. */
|
|
144
|
+
atom(end) {
|
|
145
|
+
const { source } = this;
|
|
146
|
+
if (end <= 0) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const char = source.charAt(end - 1);
|
|
150
|
+
if (isWordCharacter(char)) {
|
|
151
|
+
const from = this.wordStart(end);
|
|
152
|
+
const word = source.slice(from, end);
|
|
153
|
+
if (from > 0 && source.charAt(from - 1) === '/' && source.context(from - 1).mode === 'regex' && /^[a-z]+$/i.test(word)) {
|
|
154
|
+
const start = this.literalStart(from);
|
|
155
|
+
return start === null ? null : { from: start };
|
|
156
|
+
}
|
|
157
|
+
return { from, word };
|
|
158
|
+
}
|
|
159
|
+
if (char === '"' || char === "'" || char === '`' || char === '/') {
|
|
160
|
+
const start = this.literalStart(end);
|
|
161
|
+
return start === null ? null : { from: start };
|
|
162
|
+
}
|
|
163
|
+
if (char === ')' || char === ']' || char === '}') {
|
|
164
|
+
const context = source.context(end - 1);
|
|
165
|
+
const bracket = context.bracket;
|
|
166
|
+
if (context.mode !== 'code' || bracket === undefined || bracket.close !== char || bracket.control === true || bracket.template === true) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return char === '}' && bracket.block === true ? null : { from: bracket.at, group: source.charAt(bracket.at) };
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
reserved(atom) {
|
|
174
|
+
return atom.word !== undefined && (KEYWORDS.has(atom.word) || UNARY_WORDS.has(atom.word) || this.words.has(atom.word));
|
|
175
|
+
}
|
|
176
|
+
/* An atom with the calls and indexes right behind it, down to its callee. */
|
|
177
|
+
unit(end, property = false) {
|
|
178
|
+
const { source } = this;
|
|
179
|
+
let at = end;
|
|
180
|
+
// The non-null assertion of TypeScript, as long as it is not the start of `!=`.
|
|
181
|
+
while (this.typescript && source.charAt(at - 1) === '!' && source.charAt(at) !== '=' && /[\p{L}\p{N}_$)\]]/u.test(source.charAt(at - 2))) {
|
|
182
|
+
at--;
|
|
183
|
+
}
|
|
184
|
+
let atom = this.atom(at);
|
|
185
|
+
// A keyword is a fine property name.
|
|
186
|
+
if (atom === null || (!property && this.reserved(atom))) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
let start = atom.from;
|
|
190
|
+
while (atom.group === '(' || atom.group === '[') {
|
|
191
|
+
const callee = this.atom(start);
|
|
192
|
+
if (callee === null || this.reserved(callee)) {
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
start = callee.from;
|
|
196
|
+
atom = callee;
|
|
197
|
+
}
|
|
198
|
+
return start;
|
|
199
|
+
}
|
|
200
|
+
/* The length of the member operator that ends at `end`. */
|
|
201
|
+
member(end) {
|
|
202
|
+
const text = this.source.slice(Math.max(0, end - 3), end);
|
|
203
|
+
if (this.php) {
|
|
204
|
+
if (text.endsWith('?->')) {
|
|
205
|
+
return 3;
|
|
206
|
+
}
|
|
207
|
+
return text.endsWith('->') || text.endsWith('::') ? 2 : Number(text.endsWith('\\'));
|
|
208
|
+
}
|
|
209
|
+
if (text === '...') {
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
return text.endsWith('?.') ? 2 : Number(text.endsWith('.'));
|
|
213
|
+
}
|
|
214
|
+
/* The start of the chain of members, calls and indexes that ends at `end`. */
|
|
215
|
+
chain(end) {
|
|
216
|
+
const first = this.unit(end);
|
|
217
|
+
if (first === null) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
let start = first;
|
|
221
|
+
for (;;) {
|
|
222
|
+
const before = this.skip(start);
|
|
223
|
+
const length = this.member(before);
|
|
224
|
+
if (length === 0) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
// A namespace separator sits between its names without whitespace.
|
|
228
|
+
const separator = this.php && this.source.charAt(before - 1) === '\\';
|
|
229
|
+
const next = separator && before !== start ? null : this.unit(separator ? before - length : this.skip(before - length), true);
|
|
230
|
+
if (next === null) {
|
|
231
|
+
// A namespace may start with a backslash of its own.
|
|
232
|
+
if (this.source.charAt(before - 1) === '\\') {
|
|
233
|
+
start = before - 1;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
start = next;
|
|
238
|
+
}
|
|
239
|
+
const before = this.skip(start);
|
|
240
|
+
const word = this.atom(before)?.word;
|
|
241
|
+
return word === 'new' ? before - 3 : start;
|
|
242
|
+
}
|
|
243
|
+
/* Whether the `.` after the operand is the decimal point of a number. */
|
|
244
|
+
decimalPoint(dot) {
|
|
245
|
+
const last = this.atom(dot);
|
|
246
|
+
return !this.php && last?.word !== undefined && /^\d/.test(last.word) && this.source.charAt(last.from - 1) !== '.';
|
|
247
|
+
}
|
|
248
|
+
operandEnd(end) {
|
|
249
|
+
const atom = this.atom(end);
|
|
250
|
+
return atom !== null && !this.reserved(atom);
|
|
251
|
+
}
|
|
252
|
+
/* The start of the prefix operator right before `end`, or -1. */
|
|
253
|
+
unaryBefore(end) {
|
|
254
|
+
const { source } = this;
|
|
255
|
+
const char = source.charAt(end - 1);
|
|
256
|
+
if (char === '!' || char === '~' || (this.php && char === '@')) {
|
|
257
|
+
return end - 1;
|
|
258
|
+
}
|
|
259
|
+
if (char === '-' || char === '+') {
|
|
260
|
+
const start = source.charAt(end - 2) === char ? end - 2 : end - 1;
|
|
261
|
+
return this.operandEnd(this.skip(start)) ? -1 : start;
|
|
262
|
+
}
|
|
263
|
+
const word = this.atom(end);
|
|
264
|
+
if (word?.word !== undefined) {
|
|
265
|
+
return UNARY_WORDS.has(word.word) ? word.from : -1;
|
|
266
|
+
}
|
|
267
|
+
if (this.php && word?.group === '(' && CAST.test(this.source.slice(word.from + 1, end - 1))) {
|
|
268
|
+
return word.from;
|
|
269
|
+
}
|
|
270
|
+
return -1;
|
|
271
|
+
}
|
|
272
|
+
/* Moves left over the prefix operators of an operand, reporting each start. */
|
|
273
|
+
unaries(start, report) {
|
|
274
|
+
let from = start;
|
|
275
|
+
for (;;) {
|
|
276
|
+
const found = this.unaryBefore(this.skip(from));
|
|
277
|
+
if (found < 0) {
|
|
278
|
+
return from;
|
|
279
|
+
}
|
|
280
|
+
from = found;
|
|
281
|
+
report?.(from);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
operand(end) {
|
|
285
|
+
const start = this.chain(end);
|
|
286
|
+
return start === null ? null : this.unaries(start);
|
|
287
|
+
}
|
|
288
|
+
operatorEndingAt(end) {
|
|
289
|
+
const word = this.atom(end);
|
|
290
|
+
if (word?.word !== undefined) {
|
|
291
|
+
const operator = this.words.get(word.word);
|
|
292
|
+
return operator === undefined ? null : { text: word.word, operator };
|
|
293
|
+
}
|
|
294
|
+
for (let length = MAX_OPERATOR_LENGTH; length > 0; length--) {
|
|
295
|
+
const text = this.source.slice(Math.max(0, end - length), end);
|
|
296
|
+
const operator = this.operators.get(text);
|
|
297
|
+
if (text.length === length && operator !== undefined) {
|
|
298
|
+
return { text, operator };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
/* The binary operator left of an expression that starts at `from`. */
|
|
304
|
+
operatorBefore(from) {
|
|
305
|
+
const end = this.skip(from);
|
|
306
|
+
const found = end === 0 ? null : this.operatorEndingAt(end);
|
|
307
|
+
if (found === null || (found.operator.kind !== 'binary' && found.operator.kind !== 'colon')) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return { ...found.operator, start: end - found.text.length };
|
|
311
|
+
}
|
|
312
|
+
/* Whether a line break stands right before `index`, which ends the line of what is left of it. */
|
|
313
|
+
breaksBefore(index) {
|
|
314
|
+
return /[\n\r]/.test(this.source.slice(this.skip(index), index));
|
|
315
|
+
}
|
|
316
|
+
/* The `?` that belongs to the `:` at `colon`, past the ternaries nested between them. */
|
|
317
|
+
questionBefore(colon) {
|
|
318
|
+
let depth = 0;
|
|
319
|
+
let at = colon;
|
|
320
|
+
for (;;) {
|
|
321
|
+
at = this.skip(at);
|
|
322
|
+
if (at === 0) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const atom = this.atom(at);
|
|
326
|
+
if (atom !== null) {
|
|
327
|
+
if (atom.word !== undefined && KEYWORDS.has(atom.word)) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
at = atom.from;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const found = this.operatorEndingAt(at);
|
|
334
|
+
if (found === null || found.operator.kind === 'stop') {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (found.operator.kind === 'question') {
|
|
338
|
+
if (depth === 0) {
|
|
339
|
+
return at - 1;
|
|
340
|
+
}
|
|
341
|
+
depth--;
|
|
342
|
+
}
|
|
343
|
+
else if (found.operator.kind === 'colon') {
|
|
344
|
+
depth++;
|
|
345
|
+
}
|
|
346
|
+
at -= found.text.length;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/* Every expression that ends right before the `.` at `dot`, innermost first; empty where none does. */
|
|
351
|
+
export function postfixOperands(source, dot, language) {
|
|
352
|
+
if (source.charAt(dot) !== '.') {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
const place = placeAt(source, dot, language);
|
|
356
|
+
if (place.kind !== 'code' || !(isScript(place.language) || isPhp(place.language))) {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
const scanner = new Scanner(source, place.language);
|
|
360
|
+
const end = scanner.skip(dot);
|
|
361
|
+
const start = end === 0 || scanner.decimalPoint(dot) ? null : scanner.chain(end);
|
|
362
|
+
if (start === null) {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
const found = [start];
|
|
366
|
+
const report = (from) => {
|
|
367
|
+
if (from < found.at(-1)) {
|
|
368
|
+
found.push(from);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
let from = scanner.unaries(start, report);
|
|
372
|
+
let minimum = Infinity;
|
|
373
|
+
for (;;) {
|
|
374
|
+
const operator = scanner.operatorBefore(from);
|
|
375
|
+
if (operator === null) {
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
const operatorStart = operator.kind === 'colon' ? scanner.questionBefore(operator.start) : operator.start;
|
|
379
|
+
// A line has to end in the operator for the expression to go on over it.
|
|
380
|
+
if (operatorStart === null || scanner.breaksBefore(operatorStart)) {
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
const left = scanner.operand(scanner.skip(operatorStart));
|
|
384
|
+
if (left === null) {
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
from = left;
|
|
388
|
+
minimum = Math.min(minimum, operator.precedence);
|
|
389
|
+
// The expression is a subtree only when the operator on its left binds looser than everything inside it.
|
|
390
|
+
const next = scanner.operatorBefore(from);
|
|
391
|
+
if (next === null || scanner.breaksBefore(next.start) || next.precedence < minimum || (next.right && next.precedence <= minimum)) {
|
|
392
|
+
report(from);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const widest = found.length - 1;
|
|
396
|
+
return found.map((operandFrom, index) => ({ from: operandFrom, statement: index === widest && atStatementStart(source, operandFrom, language) }));
|
|
397
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
export interface SurroundShape {
|
|
3
|
+
readonly from: number;
|
|
4
|
+
readonly to: number;
|
|
5
|
+
readonly statements: boolean;
|
|
6
|
+
readonly expression: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function atStatementStart(source: EditSource, offset: number, language: string): boolean;
|
|
9
|
+
export declare function surroundShape(source: EditSource, from: number, to: number, language: string): SurroundShape | null;
|