@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/src/document.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { blockFolds } from './block-folds.ts';
|
|
|
16
16
|
import { applyFoldHints, markFileHeader } from './fold-roles.ts';
|
|
17
17
|
import { lexicalFolds } from './lexical-folds.ts';
|
|
18
18
|
import { rangeOf, whitespaceOf } from './line-text.ts';
|
|
19
|
-
import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.ts';
|
|
19
|
+
import { isQuote, replacesComparison, surround, swapQuotes, templateLiteralEdits, templateString } from './typing-handlers.ts';
|
|
20
20
|
import { scanBrackets } from './brackets.ts';
|
|
21
21
|
import type { BracketIndex, BracketMatch } from './brackets.ts';
|
|
22
22
|
import { BracketTracker } from './bracket-tracker.ts';
|
|
@@ -40,6 +40,10 @@ import type {
|
|
|
40
40
|
TextEdit
|
|
41
41
|
} from './types.ts';
|
|
42
42
|
import { TypingContexts } from './typing-context.ts';
|
|
43
|
+
import { placeAt, type TextPlace } from './text-places.ts';
|
|
44
|
+
import { postfixOperands, type PostfixOperand } from './postfix-operands.ts';
|
|
45
|
+
import { surroundShape, type SurroundShape } from './surround-shape.ts';
|
|
46
|
+
import { unwrapTargets, type UnwrapTarget } from './unwrap.ts';
|
|
43
47
|
import type { TypingContext } from './typing-context.ts';
|
|
44
48
|
import { todosInLines, type TodoMatch, type TodoOptions } from './todo.ts';
|
|
45
49
|
import { type LineOrder, planConvertIndents, planLineOrder } from './line-order.ts';
|
|
@@ -105,6 +109,8 @@ interface SelectionEdit extends TextEdit {
|
|
|
105
109
|
paired?: boolean;
|
|
106
110
|
/* A `-` the editor completes to `->` in a step of its own, once the `-` is in. */
|
|
107
111
|
arrow?: boolean;
|
|
112
|
+
/* A `${` typed in a quoted string, which becomes a template literal in a step of its own; where its quotes stand before the edit. */
|
|
113
|
+
template?: { open: number; close: number };
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
interface SelectionTransaction {
|
|
@@ -126,6 +132,7 @@ interface TypingInput {
|
|
|
126
132
|
surround: boolean;
|
|
127
133
|
smartSemicolon: boolean;
|
|
128
134
|
smartArrow: boolean;
|
|
135
|
+
templateLiterals: boolean;
|
|
129
136
|
/* The carets right after a `->` the editor completed, where a typed `>` goes over the one that is there. */
|
|
130
137
|
overArrows: ReadonlySet<number>;
|
|
131
138
|
/* A closing bracket typed on a line of its own goes back to the indentation of its opener. */
|
|
@@ -183,6 +190,11 @@ function changesText(rope: TextRope, edit: TextEdit): boolean {
|
|
|
183
190
|
return edit.text.length !== edit.to - edit.from || rope.slice(edit.from, edit.to) !== edit.text;
|
|
184
191
|
}
|
|
185
192
|
|
|
193
|
+
/* Where an offset stands after edits that end at or before it moved it; an insertion at the offset pushes it on. */
|
|
194
|
+
function shifted(edits: readonly TextEdit[], offset: number): number {
|
|
195
|
+
return edits.reduce((moved, edit) => (edit.to <= offset ? moved + edit.text.length - (edit.to - edit.from) : moved), offset);
|
|
196
|
+
}
|
|
197
|
+
|
|
186
198
|
/* Merges the carets' edits into one simultaneous batch and works out where each caret lands in the result. */
|
|
187
199
|
function selectionTransaction(plans: readonly SelectionEdit[]): SelectionTransaction {
|
|
188
200
|
const edits: TextEdit[] = [];
|
|
@@ -879,7 +891,8 @@ export class DocumentModel {
|
|
|
879
891
|
/*
|
|
880
892
|
* Types `text` at every caret as one keystroke would: it wraps a selection in a bracket or quote,
|
|
881
893
|
* closes an opener, types over a closer that is already there and, in TypeScript, JavaScript and
|
|
882
|
-
* PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`.
|
|
894
|
+
* PHP, moves a `;` to the end of a statement. In PHP a `-` after a variable becomes `->`. In JavaScript and
|
|
895
|
+
* TypeScript a `{` that completes `${` in a quoted string makes it a template literal. Comments
|
|
883
896
|
* and string contents are left alone. Returns whether anything changed, a swallowed closer included.
|
|
884
897
|
*/
|
|
885
898
|
typeText(text: string, options: TypeTextOptions = {}): boolean {
|
|
@@ -910,6 +923,7 @@ export class DocumentModel {
|
|
|
910
923
|
surround: options.surroundSelection !== false && single,
|
|
911
924
|
smartSemicolon: options.smartSemicolon !== false && hasSmartSemicolon(language),
|
|
912
925
|
smartArrow: options.smartArrow !== false && single && isPhp(language),
|
|
926
|
+
templateLiterals: options.templateLiterals !== false && text === '{',
|
|
913
927
|
overArrows: text === '>' ? arrows : new Set(),
|
|
914
928
|
dedent: options.smartEnter !== false
|
|
915
929
|
};
|
|
@@ -931,6 +945,7 @@ export class DocumentModel {
|
|
|
931
945
|
this.rememberStatementClosers(plans, transaction, language);
|
|
932
946
|
this.rememberPairs(plans, transaction);
|
|
933
947
|
this.completeArrows(plans, transaction);
|
|
948
|
+
this.completeTemplates(plans, transaction, language);
|
|
934
949
|
}
|
|
935
950
|
|
|
936
951
|
return changed || plans.some((plan) => plan.consumed);
|
|
@@ -968,6 +983,27 @@ export class DocumentModel {
|
|
|
968
983
|
}
|
|
969
984
|
}
|
|
970
985
|
|
|
986
|
+
/* 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. */
|
|
987
|
+
private completeTemplates(plans: readonly SelectionEdit[], transaction: SelectionTransaction, language: string): void {
|
|
988
|
+
// Carets in one string find the same quotes.
|
|
989
|
+
const strings = new Map<number, number>();
|
|
990
|
+
|
|
991
|
+
for (const plan of plans) {
|
|
992
|
+
if (plan.template) {
|
|
993
|
+
strings.set(shifted(transaction.edits, plan.template.open), shifted(transaction.edits, plan.template.close));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const source = this.editSource(language);
|
|
998
|
+
const edits = [...strings].flatMap(([open, close]) => templateLiteralEdits(source, open, close));
|
|
999
|
+
|
|
1000
|
+
if (edits.length > 0) {
|
|
1001
|
+
// A caret right before a closing quote stays there, which the default mapping of an edit that replaces the quote would not do.
|
|
1002
|
+
const selections = this.selections.map((selection) => ({ anchor: shifted(edits, selection.anchor), head: shifted(edits, selection.head) }));
|
|
1003
|
+
this.applyEdits(edits, { source: 'input', selections });
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
971
1007
|
/*
|
|
972
1008
|
* Pastes `text` at every selection. With as many lines as carets each caret takes one; `wholeLines`
|
|
973
1009
|
* marks text copied from a bare caret, which goes above the lines of bare carets.
|
|
@@ -1229,6 +1265,14 @@ export class DocumentModel {
|
|
|
1229
1265
|
}
|
|
1230
1266
|
}
|
|
1231
1267
|
|
|
1268
|
+
if (input.templateLiterals && from === to) {
|
|
1269
|
+
const template = templateString(this.editSource(language), from, language);
|
|
1270
|
+
|
|
1271
|
+
if (template) {
|
|
1272
|
+
return { from, to, text: input.brackets ? '{}' : '{', anchor: 1, head: 1, paired: input.brackets, template };
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1232
1276
|
if (pairing && pairs[text] !== undefined && from === to && this.shouldClosePair(from, text, after, context, input)) {
|
|
1233
1277
|
return { from, to, text: text + pairs[text], anchor: 1, head: 1, paired: true };
|
|
1234
1278
|
}
|
|
@@ -2150,6 +2194,26 @@ export class DocumentModel {
|
|
|
2150
2194
|
return this.applyEdits(plan.edits, { source: 'command', selections });
|
|
2151
2195
|
}
|
|
2152
2196
|
|
|
2197
|
+
/* What the text at an offset is written in, with embedded regions resolved, and whether it is code, a comment or a string. */
|
|
2198
|
+
placeAt(offset: number, language: string): TextPlace {
|
|
2199
|
+
return placeAt(this.editSource(language), offset, language);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/* Every expression that ends right before the `.` at `dot`, innermost first; empty where none does. */
|
|
2203
|
+
postfixOperands(dot: number, language: string): PostfixOperand[] {
|
|
2204
|
+
return postfixOperands(this.editSource(language), dot, language);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/* What a range can be surrounded with: whole statements, one expression, both or neither. Null where the range is blank. */
|
|
2208
|
+
surroundShape(from: number, to: number, language: string): SurroundShape | null {
|
|
2209
|
+
return surroundShape(this.editSource(language), from, to, language);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
/* What can be unwrapped or removed around an offset, innermost first, each with the edits that do it. */
|
|
2213
|
+
unwrapTargets(offset: number, language: string, indentUnit: string): UnwrapTarget[] {
|
|
2214
|
+
return unwrapTargets(this.editSource(language), offset, language, indentUnit);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2153
2217
|
private editSource(language: string): EditSource {
|
|
2154
2218
|
return {
|
|
2155
2219
|
lineCount: this.getLineCount(),
|
package/src/index.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';
|
|
@@ -24,3 +25,7 @@ export type {
|
|
|
24
25
|
} from './types.ts';
|
|
25
26
|
|
|
26
27
|
export { changedSpan, changedSpans, type TextSpan } from './text-span.ts';
|
|
28
|
+
export type { TextPlace, TextPlaceKind } from './text-places.ts';
|
|
29
|
+
export type { PostfixOperand } from './postfix-operands.ts';
|
|
30
|
+
export type { SurroundShape } from './surround-shape.ts';
|
|
31
|
+
export type { UnwrapKind, UnwrapTarget } from './unwrap.ts';
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
import { isPhp, isScript, isWordCharacter } from './lexical.ts';
|
|
3
|
+
import { atStatementStart } from './surround-shape.ts';
|
|
4
|
+
import { placeAt } from './text-places.ts';
|
|
5
|
+
import { skipTriviaBack } from './trivia.ts';
|
|
6
|
+
import type { TypingContext } from './typing-context.ts';
|
|
7
|
+
|
|
8
|
+
export interface PostfixOperand {
|
|
9
|
+
/* The expression is `[from, dot)`, trailing whitespace left out. */
|
|
10
|
+
readonly from: number;
|
|
11
|
+
/* Whether `from` starts a statement; only the widest operand can be true. */
|
|
12
|
+
readonly statement: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type OperatorKind = 'binary' | 'stop' | 'question' | 'colon';
|
|
16
|
+
|
|
17
|
+
interface Operator {
|
|
18
|
+
readonly kind: OperatorKind;
|
|
19
|
+
readonly precedence: number;
|
|
20
|
+
readonly right: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Atom {
|
|
24
|
+
readonly from: number;
|
|
25
|
+
/* The text of a word, a number or a variable. */
|
|
26
|
+
readonly word?: string;
|
|
27
|
+
/* The opening character of a bracket group. */
|
|
28
|
+
readonly group?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface FoundOperator extends Operator {
|
|
32
|
+
readonly start: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const TERNARY = 2;
|
|
36
|
+
const LITERAL_LIMIT = 10_000;
|
|
37
|
+
const MAX_OPERATOR_LENGTH = 4;
|
|
38
|
+
|
|
39
|
+
const binary = (precedence: number, right = false): Operator => ({ kind: 'binary', precedence, right });
|
|
40
|
+
const stop: Operator = { kind: 'stop', precedence: 0, right: false };
|
|
41
|
+
|
|
42
|
+
const SCRIPT_OPERATORS = new Map<string, Operator>([
|
|
43
|
+
['**', binary(15, true)],
|
|
44
|
+
['*', binary(14)],
|
|
45
|
+
['/', binary(14)],
|
|
46
|
+
['%', binary(14)],
|
|
47
|
+
['+', binary(13)],
|
|
48
|
+
['-', binary(13)],
|
|
49
|
+
['<<', binary(12)],
|
|
50
|
+
['>>', binary(12)],
|
|
51
|
+
['>>>', binary(12)],
|
|
52
|
+
['<', binary(10)],
|
|
53
|
+
['>', binary(10)],
|
|
54
|
+
['<=', binary(10)],
|
|
55
|
+
['>=', binary(10)],
|
|
56
|
+
['==', binary(9)],
|
|
57
|
+
['!=', binary(9)],
|
|
58
|
+
['===', binary(9)],
|
|
59
|
+
['!==', binary(9)],
|
|
60
|
+
['&', binary(8)],
|
|
61
|
+
['^', binary(7)],
|
|
62
|
+
['|', binary(6)],
|
|
63
|
+
['&&', binary(5)],
|
|
64
|
+
['||', binary(4)],
|
|
65
|
+
['??', binary(3, true)],
|
|
66
|
+
['?:', binary(TERNARY, true)],
|
|
67
|
+
['?', { kind: 'question', precedence: TERNARY, right: true }],
|
|
68
|
+
[':', { kind: 'colon', precedence: TERNARY, right: true }]
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
// An assignment ends the operand, and they are listed so that `>>=` is not read as `>=`.
|
|
72
|
+
for (const text of ['=', '=>', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '**=', '<<=', '>>=', '>>>=', '&&=', '||=', '??=', '?.', '++', '--', '::']) {
|
|
73
|
+
SCRIPT_OPERATORS.set(text, stop);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const PHP_OPERATORS = new Map(SCRIPT_OPERATORS);
|
|
77
|
+
|
|
78
|
+
PHP_OPERATORS.set('.', binary(11));
|
|
79
|
+
PHP_OPERATORS.set('.=', stop);
|
|
80
|
+
PHP_OPERATORS.set('<>', binary(10));
|
|
81
|
+
PHP_OPERATORS.set('<=>', binary(9));
|
|
82
|
+
PHP_OPERATORS.set('->', stop);
|
|
83
|
+
PHP_OPERATORS.set('?->', stop);
|
|
84
|
+
|
|
85
|
+
const SCRIPT_WORDS = new Map<string, Operator>([
|
|
86
|
+
['in', binary(10)],
|
|
87
|
+
['instanceof', binary(10)]
|
|
88
|
+
]);
|
|
89
|
+
const PHP_WORDS = new Map<string, Operator>([
|
|
90
|
+
['instanceof', binary(10)],
|
|
91
|
+
['and', binary(1.5)],
|
|
92
|
+
['xor', binary(1.4)],
|
|
93
|
+
['or', binary(1.3)]
|
|
94
|
+
]);
|
|
95
|
+
const UNARY_WORDS = new Set(['typeof', 'void', 'await', 'delete']);
|
|
96
|
+
// Words that start a statement or a clause, so they are no operand and nothing to the left of them belongs to an expression.
|
|
97
|
+
const KEYWORDS = new Set([
|
|
98
|
+
'return',
|
|
99
|
+
'throw',
|
|
100
|
+
'yield',
|
|
101
|
+
'case',
|
|
102
|
+
'default',
|
|
103
|
+
'const',
|
|
104
|
+
'let',
|
|
105
|
+
'var',
|
|
106
|
+
'echo',
|
|
107
|
+
'print',
|
|
108
|
+
'if',
|
|
109
|
+
'else',
|
|
110
|
+
'elseif',
|
|
111
|
+
'for',
|
|
112
|
+
'foreach',
|
|
113
|
+
'while',
|
|
114
|
+
'do',
|
|
115
|
+
'switch',
|
|
116
|
+
'try',
|
|
117
|
+
'catch',
|
|
118
|
+
'finally',
|
|
119
|
+
'function',
|
|
120
|
+
'class',
|
|
121
|
+
'import',
|
|
122
|
+
'export',
|
|
123
|
+
'of',
|
|
124
|
+
'as',
|
|
125
|
+
'use',
|
|
126
|
+
'new',
|
|
127
|
+
'extends',
|
|
128
|
+
'implements',
|
|
129
|
+
'global',
|
|
130
|
+
'include',
|
|
131
|
+
'require'
|
|
132
|
+
]);
|
|
133
|
+
const CAST = /^\s*(?:int|integer|bool|boolean|float|double|string|array|object|binary|unset)\s*$/i;
|
|
134
|
+
|
|
135
|
+
function insideTemplate(context: TypingContext): boolean {
|
|
136
|
+
for (let bracket = context.bracket; bracket !== undefined; bracket = bracket.previous) {
|
|
137
|
+
if (bracket.template === true) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
class Scanner {
|
|
146
|
+
private readonly source: EditSource;
|
|
147
|
+
private readonly php: boolean;
|
|
148
|
+
private readonly typescript: boolean;
|
|
149
|
+
private readonly operators: ReadonlyMap<string, Operator>;
|
|
150
|
+
private readonly words: ReadonlyMap<string, Operator>;
|
|
151
|
+
|
|
152
|
+
constructor(source: EditSource, place: string) {
|
|
153
|
+
this.source = source;
|
|
154
|
+
this.php = isPhp(place);
|
|
155
|
+
this.typescript = /^(?:typescript|ts|tsx|typescriptreact|mts|cts)$/i.test(place);
|
|
156
|
+
this.operators = this.php ? PHP_OPERATORS : SCRIPT_OPERATORS;
|
|
157
|
+
this.words = this.php ? PHP_WORDS : SCRIPT_WORDS;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
skip(end: number): number {
|
|
161
|
+
return skipTriviaBack(this.source, end);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private wordStart(end: number): number {
|
|
165
|
+
let from = end;
|
|
166
|
+
|
|
167
|
+
while (from > 0 && isWordCharacter(this.source.charAt(from - 1))) {
|
|
168
|
+
from--;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return from;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* The start of the string, template or regex literal that ends at `end`. */
|
|
175
|
+
private literalStart(end: number): number | null {
|
|
176
|
+
const closing = this.source.context(end - 1);
|
|
177
|
+
|
|
178
|
+
if (closing.mode === 'code' && !insideTemplate(closing)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (let at = end - 2; at >= 0 && end - at <= LITERAL_LIMIT; at--) {
|
|
183
|
+
const context = this.source.context(at);
|
|
184
|
+
|
|
185
|
+
if (context.mode === 'code' && !insideTemplate(context)) {
|
|
186
|
+
return at;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* The word, group or literal that ends at `end`. A block, the parenthesis of a control statement and a stray character are none. */
|
|
194
|
+
atom(end: number): Atom | null {
|
|
195
|
+
const { source } = this;
|
|
196
|
+
|
|
197
|
+
if (end <= 0) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const char = source.charAt(end - 1);
|
|
202
|
+
|
|
203
|
+
if (isWordCharacter(char)) {
|
|
204
|
+
const from = this.wordStart(end);
|
|
205
|
+
const word = source.slice(from, end);
|
|
206
|
+
|
|
207
|
+
if (from > 0 && source.charAt(from - 1) === '/' && source.context(from - 1).mode === 'regex' && /^[a-z]+$/i.test(word)) {
|
|
208
|
+
const start = this.literalStart(from);
|
|
209
|
+
|
|
210
|
+
return start === null ? null : { from: start };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { from, word };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (char === '"' || char === "'" || char === '`' || char === '/') {
|
|
217
|
+
const start = this.literalStart(end);
|
|
218
|
+
|
|
219
|
+
return start === null ? null : { from: start };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (char === ')' || char === ']' || char === '}') {
|
|
223
|
+
const context = source.context(end - 1);
|
|
224
|
+
const bracket = context.bracket;
|
|
225
|
+
|
|
226
|
+
if (context.mode !== 'code' || bracket === undefined || bracket.close !== char || bracket.control === true || bracket.template === true) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return char === '}' && bracket.block === true ? null : { from: bracket.at, group: source.charAt(bracket.at) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private reserved(atom: Atom): boolean {
|
|
237
|
+
return atom.word !== undefined && (KEYWORDS.has(atom.word) || UNARY_WORDS.has(atom.word) || this.words.has(atom.word));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* An atom with the calls and indexes right behind it, down to its callee. */
|
|
241
|
+
private unit(end: number, property = false): number | null {
|
|
242
|
+
const { source } = this;
|
|
243
|
+
let at = end;
|
|
244
|
+
|
|
245
|
+
// The non-null assertion of TypeScript, as long as it is not the start of `!=`.
|
|
246
|
+
while (this.typescript && source.charAt(at - 1) === '!' && source.charAt(at) !== '=' && /[\p{L}\p{N}_$)\]]/u.test(source.charAt(at - 2))) {
|
|
247
|
+
at--;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let atom = this.atom(at);
|
|
251
|
+
|
|
252
|
+
// A keyword is a fine property name.
|
|
253
|
+
if (atom === null || (!property && this.reserved(atom))) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let start = atom.from;
|
|
258
|
+
|
|
259
|
+
while (atom.group === '(' || atom.group === '[') {
|
|
260
|
+
const callee = this.atom(start);
|
|
261
|
+
|
|
262
|
+
if (callee === null || this.reserved(callee)) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
start = callee.from;
|
|
267
|
+
atom = callee;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return start;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* The length of the member operator that ends at `end`. */
|
|
274
|
+
private member(end: number): number {
|
|
275
|
+
const text = this.source.slice(Math.max(0, end - 3), end);
|
|
276
|
+
|
|
277
|
+
if (this.php) {
|
|
278
|
+
if (text.endsWith('?->')) {
|
|
279
|
+
return 3;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return text.endsWith('->') || text.endsWith('::') ? 2 : Number(text.endsWith('\\'));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (text === '...') {
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return text.endsWith('?.') ? 2 : Number(text.endsWith('.'));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* The start of the chain of members, calls and indexes that ends at `end`. */
|
|
293
|
+
chain(end: number): number | null {
|
|
294
|
+
const first = this.unit(end);
|
|
295
|
+
|
|
296
|
+
if (first === null) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let start = first;
|
|
301
|
+
|
|
302
|
+
for (;;) {
|
|
303
|
+
const before = this.skip(start);
|
|
304
|
+
const length = this.member(before);
|
|
305
|
+
|
|
306
|
+
if (length === 0) {
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// A namespace separator sits between its names without whitespace.
|
|
311
|
+
const separator = this.php && this.source.charAt(before - 1) === '\\';
|
|
312
|
+
const next: number | null = separator && before !== start ? null : this.unit(separator ? before - length : this.skip(before - length), true);
|
|
313
|
+
|
|
314
|
+
if (next === null) {
|
|
315
|
+
// A namespace may start with a backslash of its own.
|
|
316
|
+
if (this.source.charAt(before - 1) === '\\') {
|
|
317
|
+
start = before - 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
start = next;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const before = this.skip(start);
|
|
327
|
+
const word = this.atom(before)?.word;
|
|
328
|
+
|
|
329
|
+
return word === 'new' ? before - 3 : start;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Whether the `.` after the operand is the decimal point of a number. */
|
|
333
|
+
decimalPoint(dot: number): boolean {
|
|
334
|
+
const last = this.atom(dot);
|
|
335
|
+
|
|
336
|
+
return !this.php && last?.word !== undefined && /^\d/.test(last.word) && this.source.charAt(last.from - 1) !== '.';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private operandEnd(end: number): boolean {
|
|
340
|
+
const atom = this.atom(end);
|
|
341
|
+
|
|
342
|
+
return atom !== null && !this.reserved(atom);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* The start of the prefix operator right before `end`, or -1. */
|
|
346
|
+
private unaryBefore(end: number): number {
|
|
347
|
+
const { source } = this;
|
|
348
|
+
const char = source.charAt(end - 1);
|
|
349
|
+
|
|
350
|
+
if (char === '!' || char === '~' || (this.php && char === '@')) {
|
|
351
|
+
return end - 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (char === '-' || char === '+') {
|
|
355
|
+
const start = source.charAt(end - 2) === char ? end - 2 : end - 1;
|
|
356
|
+
|
|
357
|
+
return this.operandEnd(this.skip(start)) ? -1 : start;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const word = this.atom(end);
|
|
361
|
+
|
|
362
|
+
if (word?.word !== undefined) {
|
|
363
|
+
return UNARY_WORDS.has(word.word) ? word.from : -1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (this.php && word?.group === '(' && CAST.test(this.source.slice(word.from + 1, end - 1))) {
|
|
367
|
+
return word.from;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return -1;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* Moves left over the prefix operators of an operand, reporting each start. */
|
|
374
|
+
unaries(start: number, report?: (from: number) => void): number {
|
|
375
|
+
let from = start;
|
|
376
|
+
|
|
377
|
+
for (;;) {
|
|
378
|
+
const found = this.unaryBefore(this.skip(from));
|
|
379
|
+
|
|
380
|
+
if (found < 0) {
|
|
381
|
+
return from;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
from = found;
|
|
385
|
+
report?.(from);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
operand(end: number): number | null {
|
|
390
|
+
const start = this.chain(end);
|
|
391
|
+
|
|
392
|
+
return start === null ? null : this.unaries(start);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private operatorEndingAt(end: number): { text: string; operator: Operator } | null {
|
|
396
|
+
const word = this.atom(end);
|
|
397
|
+
|
|
398
|
+
if (word?.word !== undefined) {
|
|
399
|
+
const operator = this.words.get(word.word);
|
|
400
|
+
|
|
401
|
+
return operator === undefined ? null : { text: word.word, operator };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
for (let length = MAX_OPERATOR_LENGTH; length > 0; length--) {
|
|
405
|
+
const text = this.source.slice(Math.max(0, end - length), end);
|
|
406
|
+
const operator = this.operators.get(text);
|
|
407
|
+
|
|
408
|
+
if (text.length === length && operator !== undefined) {
|
|
409
|
+
return { text, operator };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* The binary operator left of an expression that starts at `from`. */
|
|
417
|
+
operatorBefore(from: number): FoundOperator | null {
|
|
418
|
+
const end = this.skip(from);
|
|
419
|
+
const found = end === 0 ? null : this.operatorEndingAt(end);
|
|
420
|
+
|
|
421
|
+
if (found === null || (found.operator.kind !== 'binary' && found.operator.kind !== 'colon')) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return { ...found.operator, start: end - found.text.length };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* Whether a line break stands right before `index`, which ends the line of what is left of it. */
|
|
429
|
+
breaksBefore(index: number): boolean {
|
|
430
|
+
return /[\n\r]/.test(this.source.slice(this.skip(index), index));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* The `?` that belongs to the `:` at `colon`, past the ternaries nested between them. */
|
|
434
|
+
questionBefore(colon: number): number | null {
|
|
435
|
+
let depth = 0;
|
|
436
|
+
let at = colon;
|
|
437
|
+
|
|
438
|
+
for (;;) {
|
|
439
|
+
at = this.skip(at);
|
|
440
|
+
|
|
441
|
+
if (at === 0) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const atom = this.atom(at);
|
|
446
|
+
|
|
447
|
+
if (atom !== null) {
|
|
448
|
+
if (atom.word !== undefined && KEYWORDS.has(atom.word)) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
at = atom.from;
|
|
453
|
+
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const found = this.operatorEndingAt(at);
|
|
458
|
+
|
|
459
|
+
if (found === null || found.operator.kind === 'stop') {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (found.operator.kind === 'question') {
|
|
464
|
+
if (depth === 0) {
|
|
465
|
+
return at - 1;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
depth--;
|
|
469
|
+
} else if (found.operator.kind === 'colon') {
|
|
470
|
+
depth++;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
at -= found.text.length;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* Every expression that ends right before the `.` at `dot`, innermost first; empty where none does. */
|
|
479
|
+
export function postfixOperands(source: EditSource, dot: number, language: string): PostfixOperand[] {
|
|
480
|
+
if (source.charAt(dot) !== '.') {
|
|
481
|
+
return [];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const place = placeAt(source, dot, language);
|
|
485
|
+
|
|
486
|
+
if (place.kind !== 'code' || !(isScript(place.language) || isPhp(place.language))) {
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const scanner = new Scanner(source, place.language);
|
|
491
|
+
const end = scanner.skip(dot);
|
|
492
|
+
const start = end === 0 || scanner.decimalPoint(dot) ? null : scanner.chain(end);
|
|
493
|
+
|
|
494
|
+
if (start === null) {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const found = [start];
|
|
499
|
+
const report = (from: number): void => {
|
|
500
|
+
if (from < found.at(-1)!) {
|
|
501
|
+
found.push(from);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
let from = scanner.unaries(start, report);
|
|
506
|
+
let minimum = Infinity;
|
|
507
|
+
|
|
508
|
+
for (;;) {
|
|
509
|
+
const operator = scanner.operatorBefore(from);
|
|
510
|
+
|
|
511
|
+
if (operator === null) {
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const operatorStart = operator.kind === 'colon' ? scanner.questionBefore(operator.start) : operator.start;
|
|
516
|
+
|
|
517
|
+
// A line has to end in the operator for the expression to go on over it.
|
|
518
|
+
if (operatorStart === null || scanner.breaksBefore(operatorStart)) {
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const left = scanner.operand(scanner.skip(operatorStart));
|
|
523
|
+
|
|
524
|
+
if (left === null) {
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
from = left;
|
|
529
|
+
minimum = Math.min(minimum, operator.precedence);
|
|
530
|
+
|
|
531
|
+
// The expression is a subtree only when the operator on its left binds looser than everything inside it.
|
|
532
|
+
const next = scanner.operatorBefore(from);
|
|
533
|
+
|
|
534
|
+
if (next === null || scanner.breaksBefore(next.start) || next.precedence < minimum || (next.right && next.precedence <= minimum)) {
|
|
535
|
+
report(from);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const widest = found.length - 1;
|
|
540
|
+
|
|
541
|
+
return found.map((operandFrom, index) => ({ from: operandFrom, statement: index === widest && atStatementStart(source, operandFrom, language) }));
|
|
542
|
+
}
|