@adecore/editor-core 0.38.0 → 0.40.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/block-folds.js +5 -2
- package/dist/bracket-tracker.js +4 -3
- package/dist/brackets.d.ts +2 -12
- package/dist/brackets.js +3 -15
- package/dist/closer-tracker.js +0 -1
- package/dist/doc-comments.js +0 -1
- package/dist/document.d.ts +2 -0
- package/dist/document.js +33 -35
- package/dist/edit-regions.js +3 -4
- package/dist/enter.d.ts +2 -0
- package/dist/enter.js +11 -9
- package/dist/fold-roles.js +2 -2
- package/dist/lexical-folds.d.ts +0 -1
- package/dist/lexical-folds.js +1 -4
- package/dist/lexical.d.ts +11 -5
- package/dist/lexical.js +13 -17
- package/dist/line-commands.js +7 -15
- package/dist/line-text.d.ts +2 -0
- package/dist/line-text.js +4 -0
- package/dist/markup-regions.js +2 -3
- package/dist/paste.js +2 -7
- package/dist/structure.js +2 -1
- package/dist/typing-context.js +5 -15
- package/package.json +2 -2
- package/src/block-folds.ts +39 -2
- package/src/bracket-tracker.ts +55 -12
- package/src/brackets.ts +35 -42
- package/src/closer-tracker.ts +6 -1
- package/src/comments.ts +103 -0
- package/src/doc-comments.ts +19 -1
- package/src/document.ts +505 -36
- package/src/edit-regions.ts +26 -4
- package/src/enter.ts +104 -9
- package/src/fold-roles.ts +34 -2
- package/src/index.ts +2 -0
- package/src/languages.ts +11 -0
- package/src/lexical-folds.ts +21 -5
- package/src/lexical.ts +31 -23
- package/src/line-commands.ts +81 -17
- package/src/line-text.ts +7 -0
- package/src/markup-regions.ts +101 -3
- package/src/offsets.ts +10 -0
- package/src/paste.ts +20 -7
- package/src/php-markup.ts +23 -0
- package/src/preserve-case.ts +12 -0
- package/src/rope.ts +70 -0
- package/src/script-source.ts +10 -0
- package/src/search.ts +38 -0
- package/src/structure.ts +29 -1
- package/src/text-span.ts +53 -0
- package/src/typing-context.ts +65 -41
- package/src/typing-handlers.ts +28 -0
- package/src/words.ts +10 -0
package/src/markup-regions.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
import { whitespaceOf } from './line-text.ts';
|
|
2
3
|
|
|
3
4
|
const JSX_LANGUAGES = /^(typescriptreact|javascriptreact|tsx|jsx|javascript|js|mjs|cjs)$/i;
|
|
4
5
|
const NAME = /[\w$.:-]/;
|
|
@@ -39,93 +40,128 @@ class JsxScanner {
|
|
|
39
40
|
let at = from;
|
|
40
41
|
let previous = '';
|
|
41
42
|
let word = '';
|
|
43
|
+
|
|
42
44
|
while (at < text.length) {
|
|
43
45
|
if (--this.budget < 0) {
|
|
44
46
|
return -1;
|
|
45
47
|
}
|
|
48
|
+
|
|
46
49
|
const char = text[at]!;
|
|
47
50
|
const next = text[at + 1];
|
|
51
|
+
|
|
48
52
|
if (/\s/.test(char)) {
|
|
49
53
|
at++;
|
|
54
|
+
|
|
50
55
|
continue;
|
|
51
56
|
}
|
|
57
|
+
|
|
52
58
|
if (char === '/' && next === '/') {
|
|
53
59
|
const end = text.indexOf('\n', at);
|
|
54
60
|
at = end < 0 ? text.length : end;
|
|
61
|
+
|
|
55
62
|
continue;
|
|
56
63
|
}
|
|
64
|
+
|
|
57
65
|
if (char === '/' && next === '*') {
|
|
58
66
|
const end = text.indexOf('*/', at + 2);
|
|
59
67
|
at = end < 0 ? text.length : end + 2;
|
|
68
|
+
|
|
60
69
|
continue;
|
|
61
70
|
}
|
|
71
|
+
|
|
62
72
|
if (char === '"' || char === "'") {
|
|
63
73
|
at = this.quoted(at);
|
|
64
74
|
previous = char;
|
|
65
75
|
word = '';
|
|
76
|
+
|
|
66
77
|
continue;
|
|
67
78
|
}
|
|
79
|
+
|
|
68
80
|
if (char === '`') {
|
|
69
81
|
at = this.template(at);
|
|
82
|
+
|
|
70
83
|
if (at < 0) {
|
|
71
84
|
return -1;
|
|
72
85
|
}
|
|
86
|
+
|
|
73
87
|
previous = char;
|
|
74
88
|
word = '';
|
|
89
|
+
|
|
75
90
|
continue;
|
|
76
91
|
}
|
|
92
|
+
|
|
77
93
|
const expressionStart = previous === '' || EXPRESSION_STARTS.includes(previous) || EXPRESSION_KEYWORDS.test(word);
|
|
94
|
+
|
|
78
95
|
if (char === '/' && expressionStart) {
|
|
79
96
|
at = this.regex(at);
|
|
80
97
|
previous = '/';
|
|
81
98
|
word = '';
|
|
99
|
+
|
|
82
100
|
continue;
|
|
83
101
|
}
|
|
102
|
+
|
|
84
103
|
if (char === '<' && expressionStart && next !== undefined && START_OF_NAME.test(next)) {
|
|
85
104
|
const end = this.element(at, true);
|
|
105
|
+
|
|
86
106
|
if (end >= 0) {
|
|
87
107
|
at = end;
|
|
88
108
|
previous = ')';
|
|
89
109
|
word = '';
|
|
110
|
+
|
|
90
111
|
continue;
|
|
91
112
|
}
|
|
92
113
|
}
|
|
114
|
+
|
|
93
115
|
const closes = CLOSERS[char];
|
|
116
|
+
|
|
94
117
|
if (closes !== undefined) {
|
|
95
118
|
const end = this.code(at + 1, closes);
|
|
119
|
+
|
|
96
120
|
if (end < 0) {
|
|
97
121
|
return -1;
|
|
98
122
|
}
|
|
123
|
+
|
|
99
124
|
at = end;
|
|
100
125
|
previous = closes;
|
|
101
126
|
word = '';
|
|
127
|
+
|
|
102
128
|
continue;
|
|
103
129
|
}
|
|
130
|
+
|
|
104
131
|
if (char === '}' || char === ')' || char === ']') {
|
|
105
132
|
if (closer === char) {
|
|
106
133
|
return at + 1;
|
|
107
134
|
}
|
|
135
|
+
|
|
108
136
|
if (closer !== null) {
|
|
109
137
|
return -1;
|
|
110
138
|
}
|
|
139
|
+
|
|
111
140
|
at++;
|
|
112
141
|
previous = char;
|
|
113
142
|
word = '';
|
|
143
|
+
|
|
114
144
|
continue;
|
|
115
145
|
}
|
|
146
|
+
|
|
116
147
|
if (/[\w$]/.test(char)) {
|
|
117
148
|
const start = at;
|
|
149
|
+
|
|
118
150
|
while (at < text.length && /[\w$]/.test(text[at]!)) {
|
|
119
151
|
at++;
|
|
120
152
|
}
|
|
153
|
+
|
|
121
154
|
word = text.slice(start, at);
|
|
122
155
|
previous = word.slice(-1);
|
|
156
|
+
|
|
123
157
|
continue;
|
|
124
158
|
}
|
|
159
|
+
|
|
125
160
|
previous = char;
|
|
126
161
|
word = '';
|
|
127
162
|
at++;
|
|
128
163
|
}
|
|
164
|
+
|
|
129
165
|
return closer === null ? text.length : -1;
|
|
130
166
|
}
|
|
131
167
|
|
|
@@ -133,15 +169,18 @@ class JsxScanner {
|
|
|
133
169
|
const { text } = this;
|
|
134
170
|
const quote = text[from]!;
|
|
135
171
|
let at = from + 1;
|
|
172
|
+
|
|
136
173
|
while (at < text.length && text[at] !== quote && text[at] !== '\n') {
|
|
137
174
|
at += text[at] === '\\' ? 2 : 1;
|
|
138
175
|
}
|
|
176
|
+
|
|
139
177
|
return Math.min(text.length, at + 1);
|
|
140
178
|
}
|
|
141
179
|
|
|
142
180
|
private template(from: number): number {
|
|
143
181
|
const { text } = this;
|
|
144
182
|
let at = from + 1;
|
|
183
|
+
|
|
145
184
|
while (at < text.length) {
|
|
146
185
|
if (text[at] === '\\') {
|
|
147
186
|
at += 2;
|
|
@@ -149,6 +188,7 @@ class JsxScanner {
|
|
|
149
188
|
return at + 1;
|
|
150
189
|
} else if (text[at] === '$' && text[at + 1] === '{') {
|
|
151
190
|
at = this.code(at + 2, '}');
|
|
191
|
+
|
|
152
192
|
if (at < 0) {
|
|
153
193
|
return -1;
|
|
154
194
|
}
|
|
@@ -156,6 +196,7 @@ class JsxScanner {
|
|
|
156
196
|
at++;
|
|
157
197
|
}
|
|
158
198
|
}
|
|
199
|
+
|
|
159
200
|
return text.length;
|
|
160
201
|
}
|
|
161
202
|
|
|
@@ -163,8 +204,10 @@ class JsxScanner {
|
|
|
163
204
|
const { text } = this;
|
|
164
205
|
let at = from + 1;
|
|
165
206
|
let inClass = false;
|
|
207
|
+
|
|
166
208
|
while (at < text.length && text[at] !== '\n') {
|
|
167
209
|
const char = text[at]!;
|
|
210
|
+
|
|
168
211
|
if (char === '\\') {
|
|
169
212
|
at++;
|
|
170
213
|
} else if (char === '[') {
|
|
@@ -174,8 +217,10 @@ class JsxScanner {
|
|
|
174
217
|
} else if (char === '/' && !inClass) {
|
|
175
218
|
return at + 1;
|
|
176
219
|
}
|
|
220
|
+
|
|
177
221
|
at++;
|
|
178
222
|
}
|
|
223
|
+
|
|
179
224
|
return from + 1;
|
|
180
225
|
}
|
|
181
226
|
|
|
@@ -186,15 +231,19 @@ class JsxScanner {
|
|
|
186
231
|
this.depth++;
|
|
187
232
|
const end = this.parseElement(from);
|
|
188
233
|
this.depth--;
|
|
234
|
+
|
|
189
235
|
if (end < 0) {
|
|
190
236
|
this.children.length = children;
|
|
237
|
+
|
|
191
238
|
if (previousEnd !== undefined) {
|
|
192
239
|
this.children[children - 1]!.to = previousEnd;
|
|
193
240
|
}
|
|
194
241
|
}
|
|
242
|
+
|
|
195
243
|
if (end >= 0 && outermost && this.depth === 0) {
|
|
196
244
|
this.ranges.push({ from, to: end });
|
|
197
245
|
}
|
|
246
|
+
|
|
198
247
|
return end;
|
|
199
248
|
}
|
|
200
249
|
|
|
@@ -202,7 +251,9 @@ class JsxScanner {
|
|
|
202
251
|
if (!this.collectChildren || from >= to) {
|
|
203
252
|
return;
|
|
204
253
|
}
|
|
254
|
+
|
|
205
255
|
const previous = this.children.at(-1);
|
|
256
|
+
|
|
206
257
|
if (previous !== undefined && previous.to === from) {
|
|
207
258
|
previous.to = to;
|
|
208
259
|
} else {
|
|
@@ -212,72 +263,100 @@ class JsxScanner {
|
|
|
212
263
|
|
|
213
264
|
private skipSpace(from: number): number {
|
|
214
265
|
let at = from;
|
|
266
|
+
|
|
215
267
|
while (at < this.text.length && /\s/.test(this.text[at]!)) {
|
|
216
268
|
at++;
|
|
217
269
|
}
|
|
270
|
+
|
|
218
271
|
return at;
|
|
219
272
|
}
|
|
220
273
|
|
|
221
274
|
private parseElement(from: number): number {
|
|
222
275
|
const { text } = this;
|
|
223
276
|
let at = from + 1;
|
|
277
|
+
|
|
224
278
|
while (at < text.length && NAME.test(text[at]!)) {
|
|
225
279
|
at++;
|
|
226
280
|
}
|
|
281
|
+
|
|
227
282
|
const name = text.slice(from + 1, at);
|
|
283
|
+
|
|
228
284
|
for (;;) {
|
|
229
285
|
at = this.skipSpace(at);
|
|
230
286
|
const char = text[at];
|
|
287
|
+
|
|
231
288
|
if (char === undefined || --this.budget < 0) {
|
|
232
289
|
return -1;
|
|
233
290
|
}
|
|
291
|
+
|
|
234
292
|
if (char === '/' && text[at + 1] === '/') {
|
|
235
293
|
const end = text.indexOf('\n', at + 2);
|
|
294
|
+
|
|
236
295
|
if (end < 0) {
|
|
237
296
|
return -1;
|
|
238
297
|
}
|
|
298
|
+
|
|
239
299
|
at = end + 1;
|
|
300
|
+
|
|
240
301
|
continue;
|
|
241
302
|
}
|
|
303
|
+
|
|
242
304
|
if (char === '/' && text[at + 1] === '*') {
|
|
243
305
|
const end = text.indexOf('*/', at + 2);
|
|
306
|
+
|
|
244
307
|
if (end < 0) {
|
|
245
308
|
return -1;
|
|
246
309
|
}
|
|
310
|
+
|
|
247
311
|
at = end + 2;
|
|
312
|
+
|
|
248
313
|
continue;
|
|
249
314
|
}
|
|
315
|
+
|
|
250
316
|
if (char === '/' && text[at + 1] === '>') {
|
|
251
317
|
return at + 2;
|
|
252
318
|
}
|
|
319
|
+
|
|
253
320
|
if (char === '>') {
|
|
254
321
|
at++;
|
|
322
|
+
|
|
255
323
|
break;
|
|
256
324
|
}
|
|
325
|
+
|
|
257
326
|
if (char === '{') {
|
|
258
327
|
at = this.code(at + 1, '}');
|
|
328
|
+
|
|
259
329
|
if (at < 0) {
|
|
260
330
|
return -1;
|
|
261
331
|
}
|
|
332
|
+
|
|
262
333
|
continue;
|
|
263
334
|
}
|
|
335
|
+
|
|
264
336
|
if (!/[A-Za-z_$]/.test(char)) {
|
|
265
337
|
return -1;
|
|
266
338
|
}
|
|
339
|
+
|
|
267
340
|
while (at < text.length && NAME.test(text[at]!)) {
|
|
268
341
|
at++;
|
|
269
342
|
}
|
|
343
|
+
|
|
270
344
|
at = this.skipSpace(at);
|
|
345
|
+
|
|
271
346
|
if (text[at] !== '=') {
|
|
272
347
|
continue;
|
|
273
348
|
}
|
|
349
|
+
|
|
274
350
|
at = this.skipSpace(at + 1);
|
|
275
351
|
const value = text[at];
|
|
352
|
+
|
|
276
353
|
if (value === '"' || value === "'") {
|
|
277
354
|
const close = text.indexOf(value, at + 1);
|
|
355
|
+
|
|
278
356
|
if (close < 0) {
|
|
279
357
|
return -1;
|
|
280
358
|
}
|
|
359
|
+
|
|
281
360
|
this.values.push({ from: at, to: close + 1 });
|
|
282
361
|
at = close + 1;
|
|
283
362
|
} else if (value === '{') {
|
|
@@ -287,34 +366,43 @@ class JsxScanner {
|
|
|
287
366
|
} else {
|
|
288
367
|
return -1;
|
|
289
368
|
}
|
|
369
|
+
|
|
290
370
|
if (at < 0) {
|
|
291
371
|
return -1;
|
|
292
372
|
}
|
|
293
373
|
}
|
|
374
|
+
|
|
294
375
|
for (;;) {
|
|
295
376
|
const start = at;
|
|
377
|
+
|
|
296
378
|
while (at < text.length && text[at] !== '<' && text[at] !== '{') {
|
|
297
379
|
at++;
|
|
298
380
|
}
|
|
381
|
+
|
|
299
382
|
this.child(start, text[at] === '<' ? at + 1 : at);
|
|
383
|
+
|
|
300
384
|
if (at >= text.length || --this.budget < 0) {
|
|
301
385
|
return -1;
|
|
302
386
|
}
|
|
387
|
+
|
|
303
388
|
if (text[at] === '{') {
|
|
304
389
|
this.child(at, at + 1);
|
|
305
390
|
const from = at;
|
|
306
391
|
at = this.code(at + 1, '}');
|
|
392
|
+
|
|
307
393
|
if (at >= 0 && /^\{\s*\/\*/.test(text.slice(from, from + 20))) {
|
|
308
394
|
this.child(from + 1, at);
|
|
309
395
|
}
|
|
310
396
|
} else if (text[at + 1] === '/') {
|
|
311
397
|
const closing = /^<\/\s*([\w$.:-]*)\s*>/.exec(text.slice(at, at + 200));
|
|
398
|
+
|
|
312
399
|
return closing !== null && closing[1] === name ? at + closing[0].length : -1;
|
|
313
400
|
} else if (START_OF_NAME.test(text[at + 1] ?? '')) {
|
|
314
401
|
at = this.parseElement(at);
|
|
315
402
|
} else {
|
|
316
403
|
return -1;
|
|
317
404
|
}
|
|
405
|
+
|
|
318
406
|
if (at < 0) {
|
|
319
407
|
return -1;
|
|
320
408
|
}
|
|
@@ -326,6 +414,7 @@ class JsxScanner {
|
|
|
326
414
|
export function jsxRanges(text: string): { from: number; to: number }[] {
|
|
327
415
|
const scanner = new JsxScanner(text);
|
|
328
416
|
scanner.scan();
|
|
417
|
+
|
|
329
418
|
return scanner.ranges;
|
|
330
419
|
}
|
|
331
420
|
|
|
@@ -333,9 +422,11 @@ export function jsxRanges(text: string): { from: number; to: number }[] {
|
|
|
333
422
|
function insideAny(ranges: readonly { from: number; to: number }[], offset: number): boolean {
|
|
334
423
|
let low = 0;
|
|
335
424
|
let high = ranges.length - 1;
|
|
425
|
+
|
|
336
426
|
while (low <= high) {
|
|
337
427
|
const middle = (low + high) >> 1;
|
|
338
428
|
const range = ranges[middle]!;
|
|
429
|
+
|
|
339
430
|
if (offset < range.from) {
|
|
340
431
|
high = middle - 1;
|
|
341
432
|
} else if (offset >= range.to) {
|
|
@@ -344,6 +435,7 @@ function insideAny(ranges: readonly { from: number; to: number }[], offset: numb
|
|
|
344
435
|
return true;
|
|
345
436
|
}
|
|
346
437
|
}
|
|
438
|
+
|
|
347
439
|
return false;
|
|
348
440
|
}
|
|
349
441
|
|
|
@@ -351,10 +443,10 @@ function insideAny(ranges: readonly { from: number; to: number }[], offset: numb
|
|
|
351
443
|
export function jsxChildren(text: string): (offset: number) => boolean {
|
|
352
444
|
const scanner = new JsxScanner(text, true);
|
|
353
445
|
scanner.scan();
|
|
446
|
+
|
|
354
447
|
return (offset) => insideAny(scanner.children, offset);
|
|
355
448
|
}
|
|
356
449
|
|
|
357
|
-
/* Whether a language is one whose scripts can hold JSX. */
|
|
358
450
|
export function hasJsx(language: string): boolean {
|
|
359
451
|
return JSX_LANGUAGES.test(language);
|
|
360
452
|
}
|
|
@@ -363,6 +455,7 @@ export function hasJsx(language: string): boolean {
|
|
|
363
455
|
export function isJsxAttributeValue(text: string, offset: number): boolean {
|
|
364
456
|
const scanner = new JsxScanner(text);
|
|
365
457
|
scanner.scan();
|
|
458
|
+
|
|
366
459
|
return scanner.values.some((value) => value.from === offset);
|
|
367
460
|
}
|
|
368
461
|
|
|
@@ -373,19 +466,24 @@ export function isJsxAttributeValue(text: string, offset: number): boolean {
|
|
|
373
466
|
*/
|
|
374
467
|
export function markupGuard(source: EditSource, language: string): (line: number) => boolean {
|
|
375
468
|
const id = language.toLowerCase();
|
|
469
|
+
|
|
376
470
|
if (id === 'vue') {
|
|
377
471
|
return (line) => {
|
|
378
472
|
const region = source.region(line);
|
|
473
|
+
|
|
379
474
|
return (region !== 'script' && region !== 'style') || /^\s*<\/?(?:script|style|template)\b/.test(source.line(line).text);
|
|
380
475
|
};
|
|
381
476
|
}
|
|
477
|
+
|
|
382
478
|
if (hasJsx(id)) {
|
|
383
479
|
const ranges = jsxRanges(source.slice(0, source.line(source.lineCount - 1).end));
|
|
480
|
+
|
|
384
481
|
return (line) => {
|
|
385
482
|
const bounds = source.line(line);
|
|
386
|
-
|
|
387
|
-
return insideAny(ranges, start);
|
|
483
|
+
|
|
484
|
+
return insideAny(ranges, bounds.start + whitespaceOf(bounds.text).length);
|
|
388
485
|
};
|
|
389
486
|
}
|
|
487
|
+
|
|
390
488
|
return () => false;
|
|
391
489
|
}
|
package/src/offsets.ts
CHANGED
|
@@ -3,38 +3,48 @@ import type { TextEdit } from './types.ts';
|
|
|
3
3
|
/* An offset after simultaneous edits. An edit that inserts at it leaves it in front of the new text when `stayBefore`. */
|
|
4
4
|
export function mapOffset(offset: number, edits: readonly TextEdit[], stayBefore = false): number {
|
|
5
5
|
let delta = 0;
|
|
6
|
+
|
|
6
7
|
for (const edit of edits) {
|
|
7
8
|
if (offset < edit.from || (stayBefore && offset === edit.from && edit.to === edit.from)) {
|
|
8
9
|
break;
|
|
9
10
|
}
|
|
11
|
+
|
|
10
12
|
if (offset < edit.to) {
|
|
11
13
|
return edit.from + delta + edit.text.length;
|
|
12
14
|
}
|
|
15
|
+
|
|
13
16
|
delta += edit.text.length - (edit.to - edit.from);
|
|
14
17
|
}
|
|
18
|
+
|
|
15
19
|
return offset + delta;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
/* Reuse this index across selections while retaining both endpoint affinities. */
|
|
19
23
|
export function offsetMapper(edits: readonly TextEdit[]): (offset: number, stayBefore?: boolean) => number {
|
|
20
24
|
const deltas = new Float64Array(edits.length + 1);
|
|
25
|
+
|
|
21
26
|
for (let index = 0; index < edits.length; index++) {
|
|
22
27
|
const edit = edits[index]!;
|
|
23
28
|
deltas[index + 1] = deltas[index]! + edit.text.length - (edit.to - edit.from);
|
|
24
29
|
}
|
|
30
|
+
|
|
25
31
|
return (offset, stayBefore = false) => {
|
|
26
32
|
let low = 0,
|
|
27
33
|
high = edits.length;
|
|
34
|
+
|
|
28
35
|
while (low < high) {
|
|
29
36
|
const middle = (low + high) >>> 1;
|
|
30
37
|
const edit = edits[middle]!;
|
|
38
|
+
|
|
31
39
|
if (edit.to < offset || (edit.to === offset && !(stayBefore && edit.from === edit.to))) {
|
|
32
40
|
low = middle + 1;
|
|
33
41
|
} else {
|
|
34
42
|
high = middle;
|
|
35
43
|
}
|
|
36
44
|
}
|
|
45
|
+
|
|
37
46
|
const edit = edits[low];
|
|
47
|
+
|
|
38
48
|
return edit !== undefined && edit.from <= offset && offset < edit.to ? edit.from + deltas[low]! + edit.text.length : offset + deltas[low]!;
|
|
39
49
|
};
|
|
40
50
|
}
|
package/src/paste.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
|
-
import { indentText, isBlank, whitespaceOf } from './line-text.ts';
|
|
2
|
+
import { indentText, isBlank, rangeOf, whitespaceOf } from './line-text.ts';
|
|
3
3
|
import { mapOffset } from './offsets.ts';
|
|
4
4
|
import { indentationColumn } from './structure.ts';
|
|
5
5
|
import type { Selection, TextEdit } from './types.ts';
|
|
@@ -23,7 +23,9 @@ function perCaretLines(text: string, carets: number): string[] | null {
|
|
|
23
23
|
if (carets < 2) {
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
|
+
|
|
26
27
|
const lines = linesOf(text);
|
|
28
|
+
|
|
27
29
|
return lines.length === carets ? lines : null;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -36,9 +38,11 @@ function perCaretLines(text: string, carets: number): string[] | null {
|
|
|
36
38
|
function reindent(source: EditSource, from: number, text: string, options: PasteOptions): string {
|
|
37
39
|
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
38
40
|
const lines = text.split(/\r?\n/);
|
|
41
|
+
|
|
39
42
|
if (lines.length < 2) {
|
|
40
43
|
return text;
|
|
41
44
|
}
|
|
45
|
+
|
|
42
46
|
const line = source.line(source.lineAt(from));
|
|
43
47
|
const before = source.slice(line.start, from);
|
|
44
48
|
const caretColumn = indentationColumn(before, options.tabSize);
|
|
@@ -50,25 +54,32 @@ function reindent(source: EditSource, from: number, text: string, options: Paste
|
|
|
50
54
|
const widths = lines.map((entry) => (isBlank(entry) ? null : indentationColumn(whitespaceOf(entry), options.tabSize)));
|
|
51
55
|
const measured = widths.filter((width, index): width is number => width !== null && !(anchored && index === 0));
|
|
52
56
|
const trailingBreak = isBlank(lines.at(-1)!);
|
|
57
|
+
|
|
53
58
|
if (measured.length > 0) {
|
|
54
59
|
// A reduce, not a spread: a paste of a few hundred thousand lines is more arguments than V8 takes in one call.
|
|
55
60
|
const delta = target - measured.reduce((least, width) => Math.min(least, width));
|
|
61
|
+
|
|
56
62
|
for (const [index, entry] of lines.entries()) {
|
|
57
63
|
const width = widths[index];
|
|
64
|
+
|
|
58
65
|
if (width === null || width === undefined || (anchored && index === 0)) {
|
|
59
66
|
continue;
|
|
60
67
|
}
|
|
68
|
+
|
|
61
69
|
// The first line follows the indentation already in front of the caret.
|
|
62
70
|
const indent = Math.max(0, width + delta - (index === 0 ? caretColumn : 0));
|
|
71
|
+
|
|
63
72
|
if (indent !== width) {
|
|
64
73
|
lines[index] = indentText(indent, options) + entry.slice(whitespaceOf(entry).length);
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
}
|
|
77
|
+
|
|
68
78
|
if (trailingBreak) {
|
|
69
79
|
// What follows the paste stays under the indentation of the line it was on.
|
|
70
80
|
lines[lines.length - 1] = whitespaceOf(before);
|
|
71
81
|
}
|
|
82
|
+
|
|
72
83
|
return lines.join(newline);
|
|
73
84
|
}
|
|
74
85
|
|
|
@@ -78,29 +89,30 @@ function reindent(source: EditSource, from: number, text: string, options: Paste
|
|
|
78
89
|
* the line it lands on, when it lands in code and not in a string, a comment or a heredoc.
|
|
79
90
|
*/
|
|
80
91
|
export function planPaste(source: EditSource, selections: readonly Selection[], text: string, options: PasteOptions): EditPlan {
|
|
81
|
-
const ordered = selections.map((selection, index) => ({
|
|
82
|
-
from: Math.min(selection.anchor, selection.head),
|
|
83
|
-
to: Math.max(selection.anchor, selection.head),
|
|
84
|
-
index
|
|
85
|
-
}));
|
|
86
|
-
ordered.sort((left, right) => left.from - right.from);
|
|
92
|
+
const ordered = selections.map((selection, index) => ({ ...rangeOf(selection), index })).sort((left, right) => left.from - right.from);
|
|
87
93
|
const segments = perCaretLines(text, selections.length);
|
|
88
94
|
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
89
95
|
const edits: TextEdit[] = [];
|
|
96
|
+
|
|
90
97
|
if (options.wholeLines && selections.every((selection) => selection.anchor === selection.head)) {
|
|
91
98
|
const lineEdits = new Map<number, string>();
|
|
99
|
+
|
|
92
100
|
for (const [position, { from }] of ordered.entries()) {
|
|
93
101
|
const start = source.line(source.lineAt(from)).start;
|
|
94
102
|
const piece = segments?.[position] ?? text;
|
|
95
103
|
lineEdits.set(start, (lineEdits.get(start) ?? '') + (/\r?\n$/.test(piece) ? piece : piece + newline));
|
|
96
104
|
}
|
|
105
|
+
|
|
97
106
|
for (const [start, insert] of lineEdits) {
|
|
98
107
|
edits.push({ from: start, to: start, text: insert });
|
|
99
108
|
}
|
|
109
|
+
|
|
100
110
|
return { edits, selections: selections.map((selection) => ({ anchor: mapOffset(selection.anchor, edits), head: mapOffset(selection.head, edits) })) };
|
|
101
111
|
}
|
|
112
|
+
|
|
102
113
|
const carets: Selection[] = new Array<Selection>(selections.length);
|
|
103
114
|
let shift = 0;
|
|
115
|
+
|
|
104
116
|
for (const [position, { from, to, index }] of ordered.entries()) {
|
|
105
117
|
const piece = segments?.[position] ?? text;
|
|
106
118
|
const inserted = options.reindent && segments === null && source.context(from).mode === 'code' ? reindent(source, from, piece, options) : piece;
|
|
@@ -109,5 +121,6 @@ export function planPaste(source: EditSource, selections: readonly Selection[],
|
|
|
109
121
|
carets[index] = { anchor: head, head };
|
|
110
122
|
shift += inserted.length - (to - from);
|
|
111
123
|
}
|
|
124
|
+
|
|
112
125
|
return { edits, selections: carets };
|
|
113
126
|
}
|
package/src/php-markup.ts
CHANGED
|
@@ -12,9 +12,11 @@ function finishTag(state: TypingContext, selfClosing: boolean, script: ScriptLex
|
|
|
12
12
|
state.markup = !selfClosing && state.element !== undefined ? 'raw' : undefined;
|
|
13
13
|
state.markupQuote = undefined;
|
|
14
14
|
state.embed = state.markup === 'raw' && state.element === 'script' ? script.initial() : undefined;
|
|
15
|
+
|
|
15
16
|
if (state.embed !== undefined) {
|
|
16
17
|
state.embed.embedStart = start;
|
|
17
18
|
}
|
|
19
|
+
|
|
18
20
|
if (selfClosing) {
|
|
19
21
|
state.element = undefined;
|
|
20
22
|
}
|
|
@@ -23,33 +25,42 @@ function finishTag(state: TypingContext, selfClosing: boolean, script: ScriptLex
|
|
|
23
25
|
export function scanPhpMarkup(state: TypingContext, text: string, from: number, end: number, offset: number, script: ScriptLexer): number {
|
|
24
26
|
for (let at = from; at < end; at++) {
|
|
25
27
|
const char = text[at]!;
|
|
28
|
+
|
|
26
29
|
if (char === '<' && text[at + 1] === '?') {
|
|
27
30
|
PHP.lastIndex = at;
|
|
28
31
|
const open = PHP.exec(text);
|
|
32
|
+
|
|
29
33
|
if (open !== null) {
|
|
30
34
|
if (state.markup === 'equals') {
|
|
31
35
|
state.markup = 'value';
|
|
32
36
|
}
|
|
37
|
+
|
|
33
38
|
state.mode = 'code';
|
|
34
39
|
state.expression = true;
|
|
35
40
|
state.word = '';
|
|
41
|
+
|
|
36
42
|
return at + open[0].length - 1;
|
|
37
43
|
}
|
|
38
44
|
}
|
|
45
|
+
|
|
39
46
|
if (state.markup === 'comment') {
|
|
40
47
|
if (text.startsWith('-->', at) && at + 3 <= end) {
|
|
41
48
|
state.markup = undefined;
|
|
42
49
|
at += 2;
|
|
43
50
|
}
|
|
51
|
+
|
|
44
52
|
continue;
|
|
45
53
|
}
|
|
54
|
+
|
|
46
55
|
if (state.markup === 'value' && state.markupQuote !== undefined) {
|
|
47
56
|
if (char === state.markupQuote) {
|
|
48
57
|
state.markup = 'tag';
|
|
49
58
|
state.markupQuote = undefined;
|
|
50
59
|
}
|
|
60
|
+
|
|
51
61
|
continue;
|
|
52
62
|
}
|
|
63
|
+
|
|
53
64
|
if (char === '<') {
|
|
54
65
|
if (state.markup !== 'raw' && text.startsWith('<!--', at)) {
|
|
55
66
|
// HTML also closes these two comments immediately, without a later -->.
|
|
@@ -57,33 +68,44 @@ export function scanPhpMarkup(state: TypingContext, text: string, from: number,
|
|
|
57
68
|
state.markup = empty > 0 && at + empty <= end ? undefined : 'comment';
|
|
58
69
|
state.element = undefined;
|
|
59
70
|
at += (empty || 4) - 1;
|
|
71
|
+
|
|
60
72
|
continue;
|
|
61
73
|
}
|
|
74
|
+
|
|
62
75
|
TAG.lastIndex = at;
|
|
63
76
|
const tag = TAG.exec(text);
|
|
64
77
|
const name = tag?.[2]?.toLowerCase();
|
|
78
|
+
|
|
65
79
|
if (state.markup === 'raw' && (tag?.[1] !== '/' || name !== state.element)) {
|
|
66
80
|
if (state.embed !== undefined) {
|
|
67
81
|
at = script.step(state.embed, text, at, end, offset);
|
|
68
82
|
}
|
|
83
|
+
|
|
69
84
|
continue;
|
|
70
85
|
}
|
|
86
|
+
|
|
71
87
|
state.embed = undefined;
|
|
72
88
|
state.element = tag?.[1] !== '/' && (name === 'script' || name === 'style' || name === 'textarea' || name === 'title') ? name : undefined;
|
|
73
89
|
state.markup = tag === null ? undefined : 'tag';
|
|
74
90
|
state.markupQuote = undefined;
|
|
91
|
+
|
|
75
92
|
if (tag !== null) {
|
|
76
93
|
at += tag[0].length - 1;
|
|
77
94
|
}
|
|
95
|
+
|
|
78
96
|
continue;
|
|
79
97
|
}
|
|
98
|
+
|
|
80
99
|
if (state.embed !== undefined) {
|
|
81
100
|
at = script.step(state.embed, text, at, end, offset);
|
|
101
|
+
|
|
82
102
|
continue;
|
|
83
103
|
}
|
|
104
|
+
|
|
84
105
|
if (state.markup === undefined || state.markup === 'raw') {
|
|
85
106
|
continue;
|
|
86
107
|
}
|
|
108
|
+
|
|
87
109
|
if (char === '>') {
|
|
88
110
|
finishTag(state, false, script, offset + at + 1);
|
|
89
111
|
} else if (char === '/' && text[at + 1] === '>' && at + 2 <= end) {
|
|
@@ -100,5 +122,6 @@ export function scanPhpMarkup(state: TypingContext, text: string, from: number,
|
|
|
100
122
|
state.markup = 'equals';
|
|
101
123
|
}
|
|
102
124
|
}
|
|
125
|
+
|
|
103
126
|
return end - 1;
|
|
104
127
|
}
|