@adecore/editor-core 0.42.0 → 0.44.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/lexical.js CHANGED
@@ -78,3 +78,9 @@ export function startsExpressionAt(text, from, to) {
78
78
  export function startsExpressionAfter(character) {
79
79
  return expressionPunctuation.has(character);
80
80
  }
81
+ /* The character that ends a statement in a language, or null where a line break does. */
82
+ export function statementTerminator(language) {
83
+ return /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|mjs|cjs|mts|cts|php|java|c|cpp|c\+\+|csharp|cs|rust|rs|dart|zig|glsl|hlsl|scala)$/i.test(language)
84
+ ? ';'
85
+ : null;
86
+ }
@@ -0,0 +1,6 @@
1
+ import type { EditSource } from './edit-source.ts';
2
+ export interface PostfixOperand {
3
+ readonly from: number;
4
+ readonly statement: boolean;
5
+ }
6
+ export declare function postfixOperands(source: EditSource, dot: number, language: string): PostfixOperand[];
@@ -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,21 @@
1
+ import type { EditPlan, EditSource } from './edit-source.ts';
2
+ import type { FoldingRange } from './structure.ts';
3
+ import type { Selection } from './types.ts';
4
+ export interface MoveStatementOptions {
5
+ language: string;
6
+ unit: string;
7
+ tabSize: number;
8
+ structure: readonly FoldingRange[];
9
+ lineSpan?: (line: number) => {
10
+ first: number;
11
+ last: number;
12
+ };
13
+ }
14
+ export interface StatementMove extends EditPlan {
15
+ moves: {
16
+ from: number;
17
+ to: number;
18
+ }[];
19
+ }
20
+ export declare function statementMoveMode(language: string): 'braced' | 'indented' | null;
21
+ export declare function planMoveStatement(source: EditSource, selections: readonly Selection[], direction: -1 | 1, options: MoveStatementOptions): StatementMove | null;