@adecore/editor 0.23.1 → 0.24.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/controller.js +7 -2
- package/dist/fake.d.ts +14 -4
- package/dist/fake.js +257 -96
- package/dist/layout.d.ts +7 -0
- package/dist/layout.js +228 -40
- package/dist/tokens.js +2 -1
- package/dist/view.d.ts +2 -3
- package/dist/view.js +35 -26
- package/package.json +2 -2
- package/src/controller.ts +9 -2
- package/src/fake.ts +301 -97
- package/src/layout.ts +262 -40
- package/src/tokens.ts +2 -1
- package/src/view.ts +36 -28
package/dist/layout.js
CHANGED
|
@@ -2,6 +2,7 @@ import { offsetOfStop, rowOfStop, stopAtOffset, stopCount, xOfStop } from './geo
|
|
|
2
2
|
import { BIDI_COLUMNS, bidiControl, visibleBidi } from './bidi.js';
|
|
3
3
|
import { uniformGeometry } from './uniform-geometry.js';
|
|
4
4
|
import { lineChangeMapper } from './layout-changes.js';
|
|
5
|
+
import { offsetMapper } from './offsets.js';
|
|
5
6
|
export function subRowBand(top, subRows, lineHeight, window, block = 1) {
|
|
6
7
|
const first = Math.max(0, Math.min(subRows - 1, Math.floor((window.top - top) / lineHeight)));
|
|
7
8
|
const last = Math.max(first, Math.min(subRows - 1, Math.ceil((window.bottom - top) / lineHeight) - 1));
|
|
@@ -21,7 +22,7 @@ const WRAP_INDENT_STEP = 2;
|
|
|
21
22
|
const GEOMETRY_CACHE_SIZE = 400;
|
|
22
23
|
const PLAIN = /^[\x20-\x7e\t]*$/;
|
|
23
24
|
const GRAPHEME_LIMIT = 20000;
|
|
24
|
-
|
|
25
|
+
let segmenter;
|
|
25
26
|
/* Where each character starts, with the length last. A very long line is cut by code unit, since splitting it by grapheme would cost more than it shows. */
|
|
26
27
|
function characterOffsets(text) {
|
|
27
28
|
const offsets = [];
|
|
@@ -31,7 +32,8 @@ function characterOffsets(text) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
else {
|
|
34
|
-
|
|
35
|
+
segmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
36
|
+
for (const part of segmenter.segment(text)) {
|
|
35
37
|
offsets.push(part.index);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -190,6 +192,47 @@ export function scanLine(line, metrics, limit, lineInlays) {
|
|
|
190
192
|
}
|
|
191
193
|
return scanDenseLine(line, metrics, limit, lineInlays);
|
|
192
194
|
}
|
|
195
|
+
/* A text row whose key follows its line, so an edit above it moves the row without a new key. */
|
|
196
|
+
class LineRow {
|
|
197
|
+
kind = 'text';
|
|
198
|
+
index;
|
|
199
|
+
line;
|
|
200
|
+
lastLine;
|
|
201
|
+
tail;
|
|
202
|
+
head;
|
|
203
|
+
subRows;
|
|
204
|
+
top = 0;
|
|
205
|
+
height;
|
|
206
|
+
constructor(index, line, lastLine, tail, head, subRows, height) {
|
|
207
|
+
this.index = index;
|
|
208
|
+
this.line = line;
|
|
209
|
+
this.lastLine = lastLine;
|
|
210
|
+
this.tail = tail;
|
|
211
|
+
this.head = head;
|
|
212
|
+
this.subRows = subRows;
|
|
213
|
+
this.height = height;
|
|
214
|
+
}
|
|
215
|
+
get key() {
|
|
216
|
+
return `line:${this.line}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/* Sorted, with the ranges that overlap or touch joined. */
|
|
220
|
+
function mergeRanges(ranges) {
|
|
221
|
+
ranges.sort((left, right) => left.first - right.first);
|
|
222
|
+
const merged = [];
|
|
223
|
+
for (const range of ranges) {
|
|
224
|
+
const previous = merged.at(-1);
|
|
225
|
+
if (previous !== undefined && range.first <= previous.last + 1) {
|
|
226
|
+
previous.last = Math.max(previous.last, range.last);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
merged.push({ ...range });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return merged;
|
|
233
|
+
}
|
|
234
|
+
/* Past this many lines to lay out again, building every row costs about as much, and it keeps the arguments of a splice few. */
|
|
235
|
+
const PATCH_LIMIT = 20000;
|
|
193
236
|
/*
|
|
194
237
|
* The rows of the document as they are drawn: one per line, taller for a wrapped line, one for a
|
|
195
238
|
* widget, none for a line a fold hides. Only the rows that are on screen are ever measured; the
|
|
@@ -202,6 +245,9 @@ export class EditorLayout {
|
|
|
202
245
|
/* The widest line measured since the text last changed, and the room past it; what a horizontal scroll bar needs. */
|
|
203
246
|
width = 0;
|
|
204
247
|
lineRows = [];
|
|
248
|
+
/* The rows of widgets and replacements by key, which their measured heights come in by. */
|
|
249
|
+
keyed = new Map();
|
|
250
|
+
special = [];
|
|
205
251
|
geometryCache = new Map();
|
|
206
252
|
measuredRows = new Map();
|
|
207
253
|
measuredHeights = new Map();
|
|
@@ -246,6 +292,7 @@ export class EditorLayout {
|
|
|
246
292
|
this.replacements = config.replacements;
|
|
247
293
|
this.lineHeightsVersion++;
|
|
248
294
|
}
|
|
295
|
+
let edit;
|
|
249
296
|
if (measureAgain || (config.textChanged && !config.changes?.length)) {
|
|
250
297
|
this.lineHeightsVersion++;
|
|
251
298
|
this.geometryCache.clear();
|
|
@@ -254,6 +301,7 @@ export class EditorLayout {
|
|
|
254
301
|
}
|
|
255
302
|
else {
|
|
256
303
|
if (config.changes?.length) {
|
|
304
|
+
edit = this.lineEdit(config.changes);
|
|
257
305
|
this.retainGeometry(config.changes);
|
|
258
306
|
this.lineHeightsVersion++;
|
|
259
307
|
}
|
|
@@ -278,7 +326,31 @@ export class EditorLayout {
|
|
|
278
326
|
}
|
|
279
327
|
}
|
|
280
328
|
}
|
|
281
|
-
this.rebuild();
|
|
329
|
+
this.rebuild(edit);
|
|
330
|
+
}
|
|
331
|
+
/* The lines the transactions changed, in the coordinates of the text before and after them. */
|
|
332
|
+
lineEdit(batches) {
|
|
333
|
+
let from = Number.POSITIVE_INFINITY;
|
|
334
|
+
let to = Number.NEGATIVE_INFINITY;
|
|
335
|
+
for (const changes of batches) {
|
|
336
|
+
if (Number.isFinite(to)) {
|
|
337
|
+
to = offsetMapper(changes)(to);
|
|
338
|
+
}
|
|
339
|
+
let delta = 0;
|
|
340
|
+
for (const change of changes) {
|
|
341
|
+
from = Math.min(from, change.from);
|
|
342
|
+
to = Math.max(to, change.from + delta + change.insertedLength);
|
|
343
|
+
delta += change.insertedLength - (change.to - change.from);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (!Number.isFinite(from)) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
// The line above too, since an edit at the start of a line can turn the `\r` that ends it into a `\r\n`.
|
|
350
|
+
const first = Math.max(0, this.document.positionAt(from).line - 1);
|
|
351
|
+
const newLast = this.document.positionAt(to).line;
|
|
352
|
+
const oldLast = newLast - (this.document.getLineCount() - this.lineRows.length);
|
|
353
|
+
return oldLast < first ? undefined : { first, oldLast, newLast };
|
|
282
354
|
}
|
|
283
355
|
retainGeometry(batches) {
|
|
284
356
|
const map = lineChangeMapper(batches);
|
|
@@ -318,7 +390,18 @@ export class EditorLayout {
|
|
|
318
390
|
}
|
|
319
391
|
return Math.max(1, Math.ceil((length * this.metrics.charWidth) / Math.max(limit, this.metrics.charWidth)));
|
|
320
392
|
}
|
|
321
|
-
rebuild() {
|
|
393
|
+
rebuild(edit) {
|
|
394
|
+
const structure = this.structure();
|
|
395
|
+
if (edit === undefined || !this.patch(edit, structure)) {
|
|
396
|
+
this.keyed.clear();
|
|
397
|
+
const built = this.buildRows(structure, 0, this.document.getLineCount() - 1, 0);
|
|
398
|
+
this.rows = built.rows;
|
|
399
|
+
this.lineRows = built.lineRows;
|
|
400
|
+
this.reflow(0);
|
|
401
|
+
}
|
|
402
|
+
this.special = structure.special;
|
|
403
|
+
}
|
|
404
|
+
structure() {
|
|
322
405
|
this.inlaysByLine.clear();
|
|
323
406
|
for (const inlay of this.inlays) {
|
|
324
407
|
const line = this.document.positionAt(inlay.at).line;
|
|
@@ -351,15 +434,115 @@ export class EditorLayout {
|
|
|
351
434
|
replaced.set(startLine, { replacement, endLine });
|
|
352
435
|
}
|
|
353
436
|
}
|
|
437
|
+
const special = [];
|
|
438
|
+
for (const line of [...this.inlaysByLine.keys(), ...above.keys(), ...below.keys()]) {
|
|
439
|
+
special.push({ first: line, last: line });
|
|
440
|
+
}
|
|
441
|
+
for (const fold of folds.values()) {
|
|
442
|
+
special.push({ first: fold.startLine, last: Math.max(fold.startLine, fold.endLine) });
|
|
443
|
+
}
|
|
444
|
+
for (const [startLine, entry] of replaced) {
|
|
445
|
+
special.push({ first: startLine, last: entry.endLine });
|
|
446
|
+
}
|
|
447
|
+
return { above, below, folds, replaced, special: mergeRanges(special) };
|
|
448
|
+
}
|
|
449
|
+
/*
|
|
450
|
+
* Lays out again only the lines an edit changed and the rows that stand on more than their line, and moves the
|
|
451
|
+
* rest. False when that would cost no less than building every row.
|
|
452
|
+
*/
|
|
453
|
+
patch(edit, structure) {
|
|
354
454
|
const count = this.document.getLineCount();
|
|
355
|
-
|
|
356
|
-
this.lineRows
|
|
455
|
+
const delta = edit.newLast - edit.oldLast;
|
|
456
|
+
if (this.rows.length === 0 || this.lineRows.length + delta !== count) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
const moved = (range) => {
|
|
460
|
+
if (range.last < edit.first) {
|
|
461
|
+
return range;
|
|
462
|
+
}
|
|
463
|
+
if (range.first > edit.oldLast) {
|
|
464
|
+
return { first: range.first + delta, last: range.last + delta };
|
|
465
|
+
}
|
|
466
|
+
return { first: Math.min(range.first, edit.first), last: range.last > edit.oldLast ? range.last + delta : edit.newLast };
|
|
467
|
+
};
|
|
468
|
+
const ranges = mergeRanges([{ first: edit.first, last: edit.newLast }, ...structure.special, ...this.special.map(moved)]).filter((range) => range.first < count);
|
|
469
|
+
let lines = 0;
|
|
470
|
+
for (const range of ranges) {
|
|
471
|
+
range.last = Math.min(range.last, count - 1);
|
|
472
|
+
lines += range.last - range.first + 1;
|
|
473
|
+
}
|
|
474
|
+
if (lines > Math.min(PATCH_LIMIT, count / 2)) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const rows = this.rows;
|
|
478
|
+
const built = new Set();
|
|
479
|
+
let first = rows.length;
|
|
480
|
+
let resized = delta !== 0;
|
|
481
|
+
// From the bottom up, so the rows and lines of the ranges above keep their indices. Rows not built again keep their old lines until the end.
|
|
482
|
+
for (let index = ranges.length - 1; index >= 0; index--) {
|
|
483
|
+
const range = ranges[index];
|
|
484
|
+
const oldFirst = range.first > edit.newLast ? range.first - delta : range.first;
|
|
485
|
+
const oldLast = range.last >= edit.newLast ? range.last - delta : range.last;
|
|
486
|
+
let start = this.lineRows[oldFirst];
|
|
487
|
+
while (start > 0 && rows[start - 1].kind === 'block' && rows[start - 1].line >= oldFirst) {
|
|
488
|
+
start--;
|
|
489
|
+
}
|
|
490
|
+
let end = this.lineRows[oldLast];
|
|
491
|
+
while (end + 1 < rows.length && rows[end + 1].kind === 'block' && rows[end + 1].line <= oldLast) {
|
|
492
|
+
end++;
|
|
493
|
+
}
|
|
494
|
+
const fresh = this.buildRows(structure, range.first, range.last, start);
|
|
495
|
+
const removed = rows.splice(start, end - start + 1, ...fresh.rows);
|
|
496
|
+
this.lineRows.splice(oldFirst, oldLast - oldFirst + 1, ...fresh.lineRows);
|
|
497
|
+
let height = 0;
|
|
498
|
+
for (const row of removed) {
|
|
499
|
+
height -= row.height;
|
|
500
|
+
if (this.keyed.get(row.key) === row) {
|
|
501
|
+
this.keyed.delete(row.key);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
let top = start > 0 ? rows[start - 1].top + rows[start - 1].height : 0;
|
|
505
|
+
for (const row of fresh.rows) {
|
|
506
|
+
built.add(row);
|
|
507
|
+
height += row.height;
|
|
508
|
+
row.top = top;
|
|
509
|
+
top += row.height;
|
|
510
|
+
}
|
|
511
|
+
resized ||= height !== 0 || removed.length !== fresh.rows.length;
|
|
512
|
+
first = start;
|
|
513
|
+
}
|
|
514
|
+
if (resized) {
|
|
515
|
+
for (let index = first; index < rows.length; index++) {
|
|
516
|
+
const row = rows[index];
|
|
517
|
+
row.index = index;
|
|
518
|
+
if (!built.has(row) && row.line > edit.oldLast) {
|
|
519
|
+
row.line += delta;
|
|
520
|
+
if (row.kind === 'text') {
|
|
521
|
+
row.lastLine += delta;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (row.kind === 'text') {
|
|
525
|
+
for (let covered = row.line; covered <= row.lastLine; covered++) {
|
|
526
|
+
this.lineRows[covered] = index;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
this.reflow(first);
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
/* The rows of lines `from` through `to`, the first of them at `index`, and the row each of the lines is in. */
|
|
535
|
+
buildRows(structure, from, to, index) {
|
|
536
|
+
const { above, below, folds, replaced } = structure;
|
|
537
|
+
const count = this.document.getLineCount();
|
|
538
|
+
const rows = [];
|
|
539
|
+
const lineRows = new Array(to - from + 1);
|
|
357
540
|
const addBlock = (widget, line) => {
|
|
358
541
|
const key = `block:${widget.id}`;
|
|
359
|
-
|
|
542
|
+
const row = {
|
|
360
543
|
key,
|
|
361
544
|
kind: 'block',
|
|
362
|
-
index:
|
|
545
|
+
index: index + rows.length,
|
|
363
546
|
line,
|
|
364
547
|
widget,
|
|
365
548
|
top: 0,
|
|
@@ -368,9 +551,11 @@ export class EditorLayout {
|
|
|
368
551
|
: widget.fixed
|
|
369
552
|
? Math.max(1, widget.height ?? DEFAULT_BLOCK_HEIGHT)
|
|
370
553
|
: (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
|
|
371
|
-
}
|
|
554
|
+
};
|
|
555
|
+
rows.push(row);
|
|
556
|
+
this.keyed.set(key, row);
|
|
372
557
|
};
|
|
373
|
-
for (let line =
|
|
558
|
+
for (let line = from; line <= to; line++) {
|
|
374
559
|
for (const block of above.get(line) ?? []) {
|
|
375
560
|
addBlock(block, line);
|
|
376
561
|
}
|
|
@@ -378,11 +563,11 @@ export class EditorLayout {
|
|
|
378
563
|
if (standIn !== undefined) {
|
|
379
564
|
const last = Math.min(count - 1, standIn.endLine);
|
|
380
565
|
const key = `replaced:${standIn.replacement.id}`;
|
|
381
|
-
const
|
|
382
|
-
|
|
566
|
+
const rowIndex = index + rows.length;
|
|
567
|
+
const row = {
|
|
383
568
|
key,
|
|
384
569
|
kind: 'text',
|
|
385
|
-
index,
|
|
570
|
+
index: rowIndex,
|
|
386
571
|
line,
|
|
387
572
|
lastLine: last,
|
|
388
573
|
tail: null,
|
|
@@ -391,9 +576,11 @@ export class EditorLayout {
|
|
|
391
576
|
top: 0,
|
|
392
577
|
height: this.measuredHeights.get(key) ?? standIn.replacement.height ?? DEFAULT_BLOCK_HEIGHT,
|
|
393
578
|
replacement: standIn.replacement
|
|
394
|
-
}
|
|
579
|
+
};
|
|
580
|
+
rows.push(row);
|
|
581
|
+
this.keyed.set(key, row);
|
|
395
582
|
for (let covered = line; covered <= last; covered++) {
|
|
396
|
-
|
|
583
|
+
lineRows[covered - from] = rowIndex;
|
|
397
584
|
}
|
|
398
585
|
for (const block of below.get(last) ?? []) {
|
|
399
586
|
addBlock(block, last);
|
|
@@ -405,59 +592,59 @@ export class EditorLayout {
|
|
|
405
592
|
const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
|
|
406
593
|
const tail = fold?.closer !== undefined && last > line ? this.clampToLine(fold.closer, last) : null;
|
|
407
594
|
const head = fold?.head !== undefined && last > line ? this.clampToLine(fold.head, line) : null;
|
|
408
|
-
const
|
|
595
|
+
const rowIndex = index + rows.length;
|
|
409
596
|
const bounds = this.wrapWidth === null ? null : this.document.getLine(line);
|
|
410
597
|
const subRows = this.rowCountOf(line, bounds === null ? 0 : bounds.end - bounds.start);
|
|
411
|
-
|
|
412
|
-
key: `line:${line}`,
|
|
413
|
-
kind: 'text',
|
|
414
|
-
index,
|
|
415
|
-
line,
|
|
416
|
-
lastLine: last,
|
|
417
|
-
tail,
|
|
418
|
-
head,
|
|
419
|
-
subRows,
|
|
420
|
-
top: 0,
|
|
421
|
-
height: subRows * this.metrics.lineHeight
|
|
422
|
-
});
|
|
598
|
+
rows.push(new LineRow(rowIndex, line, last, tail, head, subRows, subRows * this.metrics.lineHeight));
|
|
423
599
|
for (let covered = line; covered <= last; covered++) {
|
|
424
|
-
|
|
600
|
+
lineRows[covered - from] = rowIndex;
|
|
425
601
|
}
|
|
426
602
|
for (const block of below.get(line) ?? []) {
|
|
427
603
|
addBlock(block, line);
|
|
428
604
|
}
|
|
429
605
|
line = last;
|
|
430
606
|
}
|
|
431
|
-
|
|
607
|
+
return { rows, lineRows };
|
|
432
608
|
}
|
|
433
609
|
/* An offset of a fold clamped to its line, since the fold may be a moment older than an edit. */
|
|
434
610
|
clampToLine(offset, line) {
|
|
435
611
|
const bounds = this.document.getLine(line);
|
|
436
612
|
return Math.min(bounds.end, Math.max(bounds.start, offset));
|
|
437
613
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
614
|
+
/* The tops of the rows from `from` on. */
|
|
615
|
+
reflow(from) {
|
|
616
|
+
const previous = this.rows[from - 1];
|
|
617
|
+
let top = previous === undefined ? 0 : previous.top + previous.height;
|
|
618
|
+
for (let index = from; index < this.rows.length; index++) {
|
|
619
|
+
const row = this.rows[index];
|
|
441
620
|
row.top = top;
|
|
442
621
|
top += row.height;
|
|
443
622
|
}
|
|
444
623
|
this.height = top + BOTTOM_PADDING;
|
|
445
624
|
}
|
|
625
|
+
/* The row of a widget, a replacement or a line by its key. */
|
|
626
|
+
rowByKey(key) {
|
|
627
|
+
if (key.startsWith('line:')) {
|
|
628
|
+
const row = this.rowForLine(Number(key.slice(5)));
|
|
629
|
+
return row.key === key ? row : undefined;
|
|
630
|
+
}
|
|
631
|
+
return this.keyed.get(key);
|
|
632
|
+
}
|
|
446
633
|
/* A widget's measured height. Returns whether it moved anything. */
|
|
447
634
|
setMeasuredHeight(key, height) {
|
|
448
|
-
const row = this.
|
|
635
|
+
const row = this.rowByKey(key);
|
|
449
636
|
const next = Math.max(1, Math.round(height));
|
|
450
637
|
if (!row || (row.kind === 'block' && row.widget.fixed) || !Number.isFinite(next) || row.height === next) {
|
|
451
638
|
return false;
|
|
452
639
|
}
|
|
453
640
|
this.measuredHeights.set(key, next);
|
|
454
641
|
row.height = next;
|
|
455
|
-
this.reflow();
|
|
642
|
+
this.reflow(row.index);
|
|
456
643
|
return true;
|
|
457
644
|
}
|
|
458
645
|
/* The visual lines a drawn row really has, which its estimate may have missed. Returns whether any row changed. */
|
|
459
646
|
syncRows(rows) {
|
|
460
|
-
let
|
|
647
|
+
let first = this.rows.length;
|
|
461
648
|
for (const row of rows) {
|
|
462
649
|
if (row.kind !== 'text' || row.replacement !== undefined) {
|
|
463
650
|
continue;
|
|
@@ -476,14 +663,15 @@ export class EditorLayout {
|
|
|
476
663
|
if (subRows !== row.subRows) {
|
|
477
664
|
row.subRows = subRows;
|
|
478
665
|
row.height = subRows * this.metrics.lineHeight;
|
|
479
|
-
|
|
666
|
+
first = Math.min(first, row.index);
|
|
480
667
|
}
|
|
481
668
|
}
|
|
482
|
-
if (
|
|
669
|
+
if (first < this.rows.length) {
|
|
483
670
|
this.lineHeightsVersion++;
|
|
484
|
-
this.reflow();
|
|
671
|
+
this.reflow(first);
|
|
672
|
+
return true;
|
|
485
673
|
}
|
|
486
|
-
return
|
|
674
|
+
return false;
|
|
487
675
|
}
|
|
488
676
|
rowAt(y) {
|
|
489
677
|
let low = 0;
|
package/dist/tokens.js
CHANGED
|
@@ -109,7 +109,8 @@ export class TokenCache {
|
|
|
109
109
|
const old = this.entries[lastOld];
|
|
110
110
|
this.pivotLine = old ? lastNew : -1;
|
|
111
111
|
this.pivotState = old?.state;
|
|
112
|
-
|
|
112
|
+
// Not a spread into splice: a paste of a few hundred thousand lines would overflow the call stack.
|
|
113
|
+
this.entries = this.entries.slice(0, firstLine).concat(new Array(lastNew - firstLine + 1).fill(undefined), this.entries.slice(lastOld + 1));
|
|
113
114
|
this.reusableFrom = lastNew + 1;
|
|
114
115
|
}
|
|
115
116
|
/* Old colors stay visible until the rebuilt grammar reaches each line. */
|
package/dist/view.d.ts
CHANGED
|
@@ -157,11 +157,11 @@ export declare class EditorView {
|
|
|
157
157
|
private defaultRoles;
|
|
158
158
|
private settled;
|
|
159
159
|
private foldTimer;
|
|
160
|
+
private bracketTimer;
|
|
160
161
|
private colorTimer;
|
|
161
162
|
private noticeTimer;
|
|
162
163
|
private liveTimer;
|
|
163
164
|
private lineLabelProvider;
|
|
164
|
-
private bracketCache;
|
|
165
165
|
private wrapWidth;
|
|
166
166
|
private rowEndCaret;
|
|
167
167
|
private topAnchor;
|
|
@@ -277,7 +277,6 @@ export declare class EditorView {
|
|
|
277
277
|
private anchorScroll;
|
|
278
278
|
private syncWrap;
|
|
279
279
|
private bracketAt;
|
|
280
|
-
private brackets;
|
|
281
280
|
matchingBracket(selection: {
|
|
282
281
|
anchor: number;
|
|
283
282
|
head: number;
|
|
@@ -285,7 +284,7 @@ export declare class EditorView {
|
|
|
285
284
|
at: number;
|
|
286
285
|
mate: number | undefined;
|
|
287
286
|
} | null;
|
|
288
|
-
|
|
287
|
+
private scheduleBrackets;
|
|
289
288
|
contentPoint(clientX: number, clientY: number): {
|
|
290
289
|
x: number;
|
|
291
290
|
y: number;
|
package/dist/view.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { offsetOfStop, xOfStop } from './geometry-stops.js';
|
|
2
2
|
import { visibleBidi } from './bidi.js';
|
|
3
|
-
import { indentationColumn, replacementText
|
|
3
|
+
import { indentationColumn, replacementText } from '@adecore/editor-core';
|
|
4
4
|
import { createSearchClient } from './search-client.js';
|
|
5
5
|
import { FindController } from './find.js';
|
|
6
6
|
import { EditorLayout, RIGHT_PADDING, subRowBand } from './layout.js';
|
|
@@ -39,6 +39,8 @@ const MIN_OVERSCAN = 400;
|
|
|
39
39
|
const COLOR_SLICE_MS = 8;
|
|
40
40
|
const FIRST_PAINT_MS = 12;
|
|
41
41
|
const FOLD_DELAY_MS = 300;
|
|
42
|
+
/* How much text one frame lexes to pair the brackets at the carets; a large document edited all over takes a few frames. */
|
|
43
|
+
const BRACKET_FRAME_LIMIT = 256 * 1024;
|
|
42
44
|
/* The space between the last character of a line and what the host puts after it. */
|
|
43
45
|
const LINE_ACTION_GAP = 28;
|
|
44
46
|
const NOTICE_MS = 3000;
|
|
@@ -181,11 +183,11 @@ export class EditorView {
|
|
|
181
183
|
/* The folds somebody already decided on, a person or the defaults, so a later answer of a server leaves them as they are. */
|
|
182
184
|
settled = new Set();
|
|
183
185
|
foldTimer;
|
|
186
|
+
bracketTimer;
|
|
184
187
|
colorTimer;
|
|
185
188
|
noticeTimer;
|
|
186
189
|
liveTimer;
|
|
187
190
|
lineLabelProvider = null;
|
|
188
|
-
bracketCache;
|
|
189
191
|
wrapWidth = null;
|
|
190
192
|
/* The offset a click past the end of a wrapped visual line chose, which its caret is drawn at the end of that line for. */
|
|
191
193
|
rowEndCaret = null;
|
|
@@ -766,7 +768,7 @@ export class EditorView {
|
|
|
766
768
|
...(this.foldHints === null ? {} : { hints: this.foldHints })
|
|
767
769
|
});
|
|
768
770
|
this.foldRanges = this.withCustomFolds(ranges);
|
|
769
|
-
this.outline.setStructure(structuralEntries(ranges, this.model, (from, to) => this.model.slice(from, to), (offset) => this.
|
|
771
|
+
this.outline.setStructure(structuralEntries(ranges, this.model, (from, to) => this.model.slice(from, to), (offset) => this.model.matchBracket(offset, this.settings.language)?.mate));
|
|
770
772
|
const anchors = new Set(this.foldRanges.map((range) => range.from));
|
|
771
773
|
this.collapsed = new Set([...this.collapsed].filter((anchor) => anchors.has(anchor)));
|
|
772
774
|
this.refoldLayout();
|
|
@@ -1079,7 +1081,7 @@ export class EditorView {
|
|
|
1079
1081
|
const anchor = this.layout.rowAt(top);
|
|
1080
1082
|
const offset = top - anchor.top;
|
|
1081
1083
|
action();
|
|
1082
|
-
const next = this.layout.
|
|
1084
|
+
const next = this.layout.rowByKey(anchor.key);
|
|
1083
1085
|
if (next) {
|
|
1084
1086
|
// Unwrapping can shrink the anchor below its old offset.
|
|
1085
1087
|
this.viewport.scrollTop = Math.max(0, next.top + Math.min(offset, Math.max(0, next.height - 1)));
|
|
@@ -1100,23 +1102,15 @@ export class EditorView {
|
|
|
1100
1102
|
const candidates = from !== to ? [from] : [head, head - 1];
|
|
1101
1103
|
return candidates.find((at) => at >= 0 && /[()[\]{}]/.test(this.model.slice(at, at + 1)));
|
|
1102
1104
|
}
|
|
1103
|
-
brackets() {
|
|
1104
|
-
if (this.bracketCache?.revision !== this.revision || this.bracketCache.language !== this.settings.language) {
|
|
1105
|
-
this.bracketCache = {
|
|
1106
|
-
revision: this.revision,
|
|
1107
|
-
language: this.settings.language,
|
|
1108
|
-
index: scanBrackets(this.model.getText(), this.settings.language)
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
return this.bracketCache.index;
|
|
1112
|
-
}
|
|
1113
1105
|
/* The offset a bracket at the caret pairs with, if it is one. */
|
|
1114
1106
|
matchingBracket(selection) {
|
|
1115
1107
|
const at = this.bracketAt(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
|
|
1116
|
-
return at === undefined ? null : { at, mate: this.
|
|
1108
|
+
return at === undefined ? null : { at, mate: this.model.matchBracket(at, this.settings.language)?.mate };
|
|
1117
1109
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1110
|
+
/* Draws the highlights of the brackets at the carets that the last frame ran out of lexing for. */
|
|
1111
|
+
scheduleBrackets() {
|
|
1112
|
+
clearTimeout(this.bracketTimer);
|
|
1113
|
+
this.bracketTimer = setTimeout(() => this.requestRender(), 0);
|
|
1120
1114
|
}
|
|
1121
1115
|
/* A point of the screen in the coordinates of the content, undoing the scale a canvas puts on a node. */
|
|
1122
1116
|
contentPoint(clientX, clientY) {
|
|
@@ -1813,12 +1807,16 @@ export class EditorView {
|
|
|
1813
1807
|
const firstLine = rows[0]?.line ?? 0;
|
|
1814
1808
|
const lastLine = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
|
|
1815
1809
|
const highlighted = this.highlightedLines(firstLine, lastLine);
|
|
1810
|
+
const shownFrom = this.model.offsetAt({ line: firstLine, column: 0 });
|
|
1811
|
+
const shownTo = lastLine + 1 < this.model.getLineCount() ? this.model.offsetAt({ line: lastLine + 1, column: 0 }) : this.model.getLength();
|
|
1816
1812
|
paintGutter(this.gutterLines, this.layout, rows, {
|
|
1817
1813
|
lineNumber: (line) => this.lineNumberLabel(line),
|
|
1818
1814
|
changes: this.changedLines(rows),
|
|
1819
1815
|
attribution: this.attributedLines(firstLine, lastLine),
|
|
1820
1816
|
highlights: highlighted,
|
|
1821
|
-
activeLines: new Set(selections
|
|
1817
|
+
activeLines: new Set(selections
|
|
1818
|
+
.filter((selection) => selection.head >= shownFrom && selection.head <= shownTo)
|
|
1819
|
+
.map((selection) => this.model.positionAt(selection.head).line)),
|
|
1822
1820
|
foldable,
|
|
1823
1821
|
lineLabel: (line) => this.lineLabelProvider?.(line) ?? null,
|
|
1824
1822
|
foldName: (line, collapsed) => this.foldName(line, collapsed),
|
|
@@ -1890,18 +1888,28 @@ export class EditorView {
|
|
|
1890
1888
|
}
|
|
1891
1889
|
}
|
|
1892
1890
|
const seen = new Set();
|
|
1893
|
-
for (const selection of selections) {
|
|
1894
|
-
|
|
1895
|
-
if (
|
|
1891
|
+
for (const [index, selection] of selections.entries()) {
|
|
1892
|
+
// A caret off screen has no bracket to draw, except the primary one, whose mate may be on screen.
|
|
1893
|
+
if (index < selections.length - 1 && (selection.head < visibleFrom - 1 || selection.head > visibleTo + 1)) {
|
|
1896
1894
|
continue;
|
|
1897
1895
|
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1896
|
+
const at = this.bracketAt(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
|
|
1897
|
+
if (at === undefined) {
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
const match = this.model.matchBracket(at, this.settings.language, BRACKET_FRAME_LIMIT);
|
|
1901
|
+
if (match === null) {
|
|
1902
|
+
this.scheduleBrackets();
|
|
1903
|
+
break;
|
|
1904
|
+
}
|
|
1905
|
+
if (match === undefined) {
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
if (match.mate === undefined) {
|
|
1909
|
+
marks.push({ className: 'se-bracket-unmatched', rects: this.paintRectangles(at, at + 1, rows) });
|
|
1902
1910
|
continue;
|
|
1903
1911
|
}
|
|
1904
|
-
for (const offset of [
|
|
1912
|
+
for (const offset of [at, match.mate]) {
|
|
1905
1913
|
if (!seen.has(offset)) {
|
|
1906
1914
|
seen.add(offset);
|
|
1907
1915
|
marks.push({ className: 'se-bracket-match', rects: this.paintRectangles(offset, offset + 1, rows) });
|
|
@@ -2430,6 +2438,7 @@ export class EditorView {
|
|
|
2430
2438
|
this.subscription.dispose();
|
|
2431
2439
|
this.resizeObserver?.disconnect();
|
|
2432
2440
|
this.blockObserver?.disconnect();
|
|
2441
|
+
clearTimeout(this.bracketTimer);
|
|
2433
2442
|
clearTimeout(this.foldTimer);
|
|
2434
2443
|
clearTimeout(this.colorTimer);
|
|
2435
2444
|
clearTimeout(this.noticeTimer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"test": "bun test --conditions=source"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@adecore/editor-core": "0.
|
|
57
|
+
"@adecore/editor-core": "0.24.0",
|
|
58
58
|
"linkedom": "^0.18.13",
|
|
59
59
|
"shiki": "^4.4.3"
|
|
60
60
|
},
|
package/src/controller.ts
CHANGED
|
@@ -202,7 +202,9 @@ export class InputController {
|
|
|
202
202
|
});
|
|
203
203
|
this.listen(input, 'keyup', (event) => this.gesture.keyup(event));
|
|
204
204
|
this.listen(root, 'focusout', (event) => {
|
|
205
|
-
|
|
205
|
+
// A node by its type, as `instanceof Node` fails for one of another window, such as a window the opener mounted the editor in.
|
|
206
|
+
const related = event.relatedTarget as Node | null;
|
|
207
|
+
if (typeof related?.nodeType !== 'number' || !root.contains(related)) {
|
|
206
208
|
this.options.blur();
|
|
207
209
|
}
|
|
208
210
|
});
|
|
@@ -790,7 +792,12 @@ export class InputController {
|
|
|
790
792
|
private pointerDown(event: PointerEvent): void {
|
|
791
793
|
this.view.cancelScroll();
|
|
792
794
|
const target = event.target as HTMLElement;
|
|
793
|
-
|
|
795
|
+
// A press a part took already, such as a pinned header, which draws itself anew and so takes its target off the page before the press bubbles here.
|
|
796
|
+
if (
|
|
797
|
+
event.defaultPrevented ||
|
|
798
|
+
event.button !== 0 ||
|
|
799
|
+
target.closest('button, a, input, textarea, [contenteditable], .se-block, .se-gutter, .se-sticky')
|
|
800
|
+
) {
|
|
794
801
|
return;
|
|
795
802
|
}
|
|
796
803
|
// Cancels the compatibility mouse events, whose focus change would close the keyboard a tap opened; scrolling is not affected.
|