@adecore/editor 0.23.1 → 0.24.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/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/src/layout.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { offsetOfStop, rowOfStop, stopAtOffset, stopCount, xOfStop } from './geo
|
|
|
4
4
|
import { BIDI_COLUMNS, bidiControl, visibleBidi } from './bidi.ts';
|
|
5
5
|
import { uniformGeometry } from './uniform-geometry.ts';
|
|
6
6
|
import { lineChangeMapper } from './layout-changes.ts';
|
|
7
|
+
import { offsetMapper } from './offsets.ts';
|
|
7
8
|
|
|
8
9
|
export type Affinity = 'before' | 'after';
|
|
9
10
|
|
|
@@ -193,7 +194,7 @@ const WRAP_INDENT_STEP = 2;
|
|
|
193
194
|
const GEOMETRY_CACHE_SIZE = 400;
|
|
194
195
|
const PLAIN = /^[\x20-\x7e\t]*$/;
|
|
195
196
|
const GRAPHEME_LIMIT = 20000;
|
|
196
|
-
|
|
197
|
+
let segmenter: Intl.Segmenter | undefined;
|
|
197
198
|
|
|
198
199
|
/* 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. */
|
|
199
200
|
function characterOffsets(text: string): number[] {
|
|
@@ -203,7 +204,8 @@ function characterOffsets(text: string): number[] {
|
|
|
203
204
|
offsets.push(i);
|
|
204
205
|
}
|
|
205
206
|
} else {
|
|
206
|
-
|
|
207
|
+
segmenter ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
208
|
+
for (const part of segmenter.segment(text)) {
|
|
207
209
|
offsets.push(part.index);
|
|
208
210
|
}
|
|
209
211
|
}
|
|
@@ -393,6 +395,74 @@ export interface LayoutConfig {
|
|
|
393
395
|
replacements?: readonly BlockReplacement[];
|
|
394
396
|
}
|
|
395
397
|
|
|
398
|
+
/* Lines `first` through `last`, both included. */
|
|
399
|
+
interface LineRange {
|
|
400
|
+
first: number;
|
|
401
|
+
last: number;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* The lines an edit replaced: `first` through `oldLast` before it, `first` through `newLast` after it. */
|
|
405
|
+
interface LineEdit {
|
|
406
|
+
first: number;
|
|
407
|
+
oldLast: number;
|
|
408
|
+
newLast: number;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* What the rows stand on besides the text, by the line it is at. */
|
|
412
|
+
interface RowStructure {
|
|
413
|
+
above: Map<number, BlockWidget[]>;
|
|
414
|
+
below: Map<number, BlockWidget[]>;
|
|
415
|
+
folds: Map<number, FoldState>;
|
|
416
|
+
replaced: Map<number, { replacement: BlockReplacement; endLine: number }>;
|
|
417
|
+
/* The lines whose rows are more than a plain line: with a widget, a collapsed fold, a replacement or an inlay. */
|
|
418
|
+
special: LineRange[];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* A text row whose key follows its line, so an edit above it moves the row without a new key. */
|
|
422
|
+
class LineRow implements TextRow {
|
|
423
|
+
readonly kind = 'text';
|
|
424
|
+
index: number;
|
|
425
|
+
line: number;
|
|
426
|
+
lastLine: number;
|
|
427
|
+
tail: number | null;
|
|
428
|
+
head: number | null;
|
|
429
|
+
subRows: number;
|
|
430
|
+
top = 0;
|
|
431
|
+
height: number;
|
|
432
|
+
|
|
433
|
+
constructor(index: number, line: number, lastLine: number, tail: number | null, head: number | null, subRows: number, height: number) {
|
|
434
|
+
this.index = index;
|
|
435
|
+
this.line = line;
|
|
436
|
+
this.lastLine = lastLine;
|
|
437
|
+
this.tail = tail;
|
|
438
|
+
this.head = head;
|
|
439
|
+
this.subRows = subRows;
|
|
440
|
+
this.height = height;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
get key(): string {
|
|
444
|
+
return `line:${this.line}`;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Sorted, with the ranges that overlap or touch joined. */
|
|
449
|
+
function mergeRanges(ranges: LineRange[]): LineRange[] {
|
|
450
|
+
ranges.sort((left, right) => left.first - right.first);
|
|
451
|
+
const merged: LineRange[] = [];
|
|
452
|
+
for (const range of ranges) {
|
|
453
|
+
const previous = merged.at(-1);
|
|
454
|
+
if (previous !== undefined && range.first <= previous.last + 1) {
|
|
455
|
+
previous.last = Math.max(previous.last, range.last);
|
|
456
|
+
} else {
|
|
457
|
+
merged.push({ ...range });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return merged;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* Past this many lines to lay out again, building every row costs about as much, and it keeps the arguments of a splice few. */
|
|
464
|
+
const PATCH_LIMIT = 20000;
|
|
465
|
+
|
|
396
466
|
/*
|
|
397
467
|
* The rows of the document as they are drawn: one per line, taller for a wrapped line, one for a
|
|
398
468
|
* widget, none for a line a fold hides. Only the rows that are on screen are ever measured; the
|
|
@@ -405,6 +475,9 @@ export class EditorLayout {
|
|
|
405
475
|
/* The widest line measured since the text last changed, and the room past it; what a horizontal scroll bar needs. */
|
|
406
476
|
width = 0;
|
|
407
477
|
private lineRows: number[] = [];
|
|
478
|
+
/* The rows of widgets and replacements by key, which their measured heights come in by. */
|
|
479
|
+
private keyed = new Map<string, LayoutRow>();
|
|
480
|
+
private special: LineRange[] = [];
|
|
408
481
|
private geometryCache = new Map<number, LineGeometry>();
|
|
409
482
|
private measuredRows = new Map<number, { start: number; end: number; next: number; subRows: number }>();
|
|
410
483
|
private measuredHeights = new Map<string, number>();
|
|
@@ -453,6 +526,7 @@ export class EditorLayout {
|
|
|
453
526
|
this.replacements = config.replacements;
|
|
454
527
|
this.lineHeightsVersion++;
|
|
455
528
|
}
|
|
529
|
+
let edit: LineEdit | undefined;
|
|
456
530
|
if (measureAgain || (config.textChanged && !config.changes?.length)) {
|
|
457
531
|
this.lineHeightsVersion++;
|
|
458
532
|
this.geometryCache.clear();
|
|
@@ -460,6 +534,7 @@ export class EditorLayout {
|
|
|
460
534
|
this.width = 0;
|
|
461
535
|
} else {
|
|
462
536
|
if (config.changes?.length) {
|
|
537
|
+
edit = this.lineEdit(config.changes);
|
|
463
538
|
this.retainGeometry(config.changes);
|
|
464
539
|
this.lineHeightsVersion++;
|
|
465
540
|
}
|
|
@@ -484,7 +559,32 @@ export class EditorLayout {
|
|
|
484
559
|
}
|
|
485
560
|
}
|
|
486
561
|
}
|
|
487
|
-
this.rebuild();
|
|
562
|
+
this.rebuild(edit);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/* The lines the transactions changed, in the coordinates of the text before and after them. */
|
|
566
|
+
private lineEdit(batches: readonly (readonly DocumentChange[])[]): LineEdit | undefined {
|
|
567
|
+
let from = Number.POSITIVE_INFINITY;
|
|
568
|
+
let to = Number.NEGATIVE_INFINITY;
|
|
569
|
+
for (const changes of batches) {
|
|
570
|
+
if (Number.isFinite(to)) {
|
|
571
|
+
to = offsetMapper(changes)(to);
|
|
572
|
+
}
|
|
573
|
+
let delta = 0;
|
|
574
|
+
for (const change of changes) {
|
|
575
|
+
from = Math.min(from, change.from);
|
|
576
|
+
to = Math.max(to, change.from + delta + change.insertedLength);
|
|
577
|
+
delta += change.insertedLength - (change.to - change.from);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (!Number.isFinite(from)) {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
// The line above too, since an edit at the start of a line can turn the `\r` that ends it into a `\r\n`.
|
|
584
|
+
const first = Math.max(0, this.document.positionAt(from).line - 1);
|
|
585
|
+
const newLast = this.document.positionAt(to).line;
|
|
586
|
+
const oldLast = newLast - (this.document.getLineCount() - this.lineRows.length);
|
|
587
|
+
return oldLast < first ? undefined : { first, oldLast, newLast };
|
|
488
588
|
}
|
|
489
589
|
|
|
490
590
|
private retainGeometry(batches: readonly (readonly DocumentChange[])[]): void {
|
|
@@ -527,7 +627,19 @@ export class EditorLayout {
|
|
|
527
627
|
return Math.max(1, Math.ceil((length * this.metrics.charWidth) / Math.max(limit, this.metrics.charWidth)));
|
|
528
628
|
}
|
|
529
629
|
|
|
530
|
-
private rebuild(): void {
|
|
630
|
+
private rebuild(edit?: LineEdit): void {
|
|
631
|
+
const structure = this.structure();
|
|
632
|
+
if (edit === undefined || !this.patch(edit, structure)) {
|
|
633
|
+
this.keyed.clear();
|
|
634
|
+
const built = this.buildRows(structure, 0, this.document.getLineCount() - 1, 0);
|
|
635
|
+
this.rows = built.rows;
|
|
636
|
+
this.lineRows = built.lineRows;
|
|
637
|
+
this.reflow(0);
|
|
638
|
+
}
|
|
639
|
+
this.special = structure.special;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private structure(): RowStructure {
|
|
531
643
|
this.inlaysByLine.clear();
|
|
532
644
|
for (const inlay of this.inlays) {
|
|
533
645
|
const line = this.document.positionAt(inlay.at).line;
|
|
@@ -560,15 +672,119 @@ export class EditorLayout {
|
|
|
560
672
|
replaced.set(startLine, { replacement, endLine });
|
|
561
673
|
}
|
|
562
674
|
}
|
|
675
|
+
const special: LineRange[] = [];
|
|
676
|
+
for (const line of [...this.inlaysByLine.keys(), ...above.keys(), ...below.keys()]) {
|
|
677
|
+
special.push({ first: line, last: line });
|
|
678
|
+
}
|
|
679
|
+
for (const fold of folds.values()) {
|
|
680
|
+
special.push({ first: fold.startLine, last: Math.max(fold.startLine, fold.endLine) });
|
|
681
|
+
}
|
|
682
|
+
for (const [startLine, entry] of replaced) {
|
|
683
|
+
special.push({ first: startLine, last: entry.endLine });
|
|
684
|
+
}
|
|
685
|
+
return { above, below, folds, replaced, special: mergeRanges(special) };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/*
|
|
689
|
+
* Lays out again only the lines an edit changed and the rows that stand on more than their line, and moves the
|
|
690
|
+
* rest. False when that would cost no less than building every row.
|
|
691
|
+
*/
|
|
692
|
+
private patch(edit: LineEdit, structure: RowStructure): boolean {
|
|
693
|
+
const count = this.document.getLineCount();
|
|
694
|
+
const delta = edit.newLast - edit.oldLast;
|
|
695
|
+
if (this.rows.length === 0 || this.lineRows.length + delta !== count) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
const moved = (range: LineRange): LineRange => {
|
|
699
|
+
if (range.last < edit.first) {
|
|
700
|
+
return range;
|
|
701
|
+
}
|
|
702
|
+
if (range.first > edit.oldLast) {
|
|
703
|
+
return { first: range.first + delta, last: range.last + delta };
|
|
704
|
+
}
|
|
705
|
+
return { first: Math.min(range.first, edit.first), last: range.last > edit.oldLast ? range.last + delta : edit.newLast };
|
|
706
|
+
};
|
|
707
|
+
const ranges = mergeRanges([{ first: edit.first, last: edit.newLast }, ...structure.special, ...this.special.map(moved)]).filter(
|
|
708
|
+
(range) => range.first < count
|
|
709
|
+
);
|
|
710
|
+
let lines = 0;
|
|
711
|
+
for (const range of ranges) {
|
|
712
|
+
range.last = Math.min(range.last, count - 1);
|
|
713
|
+
lines += range.last - range.first + 1;
|
|
714
|
+
}
|
|
715
|
+
if (lines > Math.min(PATCH_LIMIT, count / 2)) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
const rows = this.rows;
|
|
719
|
+
const built = new Set<LayoutRow>();
|
|
720
|
+
let first = rows.length;
|
|
721
|
+
let resized = delta !== 0;
|
|
722
|
+
// 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.
|
|
723
|
+
for (let index = ranges.length - 1; index >= 0; index--) {
|
|
724
|
+
const range = ranges[index]!;
|
|
725
|
+
const oldFirst = range.first > edit.newLast ? range.first - delta : range.first;
|
|
726
|
+
const oldLast = range.last >= edit.newLast ? range.last - delta : range.last;
|
|
727
|
+
let start = this.lineRows[oldFirst]!;
|
|
728
|
+
while (start > 0 && rows[start - 1]!.kind === 'block' && rows[start - 1]!.line >= oldFirst) {
|
|
729
|
+
start--;
|
|
730
|
+
}
|
|
731
|
+
let end = this.lineRows[oldLast]!;
|
|
732
|
+
while (end + 1 < rows.length && rows[end + 1]!.kind === 'block' && rows[end + 1]!.line <= oldLast) {
|
|
733
|
+
end++;
|
|
734
|
+
}
|
|
735
|
+
const fresh = this.buildRows(structure, range.first, range.last, start);
|
|
736
|
+
const removed = rows.splice(start, end - start + 1, ...fresh.rows);
|
|
737
|
+
this.lineRows.splice(oldFirst, oldLast - oldFirst + 1, ...fresh.lineRows);
|
|
738
|
+
let height = 0;
|
|
739
|
+
for (const row of removed) {
|
|
740
|
+
height -= row.height;
|
|
741
|
+
if (this.keyed.get(row.key) === row) {
|
|
742
|
+
this.keyed.delete(row.key);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
let top = start > 0 ? rows[start - 1]!.top + rows[start - 1]!.height : 0;
|
|
746
|
+
for (const row of fresh.rows) {
|
|
747
|
+
built.add(row);
|
|
748
|
+
height += row.height;
|
|
749
|
+
row.top = top;
|
|
750
|
+
top += row.height;
|
|
751
|
+
}
|
|
752
|
+
resized ||= height !== 0 || removed.length !== fresh.rows.length;
|
|
753
|
+
first = start;
|
|
754
|
+
}
|
|
755
|
+
if (resized) {
|
|
756
|
+
for (let index = first; index < rows.length; index++) {
|
|
757
|
+
const row = rows[index]!;
|
|
758
|
+
row.index = index;
|
|
759
|
+
if (!built.has(row) && row.line > edit.oldLast) {
|
|
760
|
+
row.line += delta;
|
|
761
|
+
if (row.kind === 'text') {
|
|
762
|
+
row.lastLine += delta;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (row.kind === 'text') {
|
|
766
|
+
for (let covered = row.line; covered <= row.lastLine; covered++) {
|
|
767
|
+
this.lineRows[covered] = index;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
this.reflow(first);
|
|
772
|
+
}
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/* The rows of lines `from` through `to`, the first of them at `index`, and the row each of the lines is in. */
|
|
777
|
+
private buildRows(structure: RowStructure, from: number, to: number, index: number): { rows: LayoutRow[]; lineRows: number[] } {
|
|
778
|
+
const { above, below, folds, replaced } = structure;
|
|
563
779
|
const count = this.document.getLineCount();
|
|
564
|
-
|
|
565
|
-
|
|
780
|
+
const rows: LayoutRow[] = [];
|
|
781
|
+
const lineRows = new Array<number>(to - from + 1);
|
|
566
782
|
const addBlock = (widget: BlockWidget, line: number): void => {
|
|
567
783
|
const key = `block:${widget.id}`;
|
|
568
|
-
|
|
784
|
+
const row: BlockRow = {
|
|
569
785
|
key,
|
|
570
786
|
kind: 'block',
|
|
571
|
-
index:
|
|
787
|
+
index: index + rows.length,
|
|
572
788
|
line,
|
|
573
789
|
widget,
|
|
574
790
|
top: 0,
|
|
@@ -577,9 +793,11 @@ export class EditorLayout {
|
|
|
577
793
|
: widget.fixed
|
|
578
794
|
? Math.max(1, widget.height ?? DEFAULT_BLOCK_HEIGHT)
|
|
579
795
|
: (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
|
|
580
|
-
}
|
|
796
|
+
};
|
|
797
|
+
rows.push(row);
|
|
798
|
+
this.keyed.set(key, row);
|
|
581
799
|
};
|
|
582
|
-
for (let line =
|
|
800
|
+
for (let line = from; line <= to; line++) {
|
|
583
801
|
for (const block of above.get(line) ?? []) {
|
|
584
802
|
addBlock(block, line);
|
|
585
803
|
}
|
|
@@ -587,11 +805,11 @@ export class EditorLayout {
|
|
|
587
805
|
if (standIn !== undefined) {
|
|
588
806
|
const last = Math.min(count - 1, standIn.endLine);
|
|
589
807
|
const key = `replaced:${standIn.replacement.id}`;
|
|
590
|
-
const
|
|
591
|
-
|
|
808
|
+
const rowIndex = index + rows.length;
|
|
809
|
+
const row: TextRow = {
|
|
592
810
|
key,
|
|
593
811
|
kind: 'text',
|
|
594
|
-
index,
|
|
812
|
+
index: rowIndex,
|
|
595
813
|
line,
|
|
596
814
|
lastLine: last,
|
|
597
815
|
tail: null,
|
|
@@ -600,9 +818,11 @@ export class EditorLayout {
|
|
|
600
818
|
top: 0,
|
|
601
819
|
height: this.measuredHeights.get(key) ?? standIn.replacement.height ?? DEFAULT_BLOCK_HEIGHT,
|
|
602
820
|
replacement: standIn.replacement
|
|
603
|
-
}
|
|
821
|
+
};
|
|
822
|
+
rows.push(row);
|
|
823
|
+
this.keyed.set(key, row);
|
|
604
824
|
for (let covered = line; covered <= last; covered++) {
|
|
605
|
-
|
|
825
|
+
lineRows[covered - from] = rowIndex;
|
|
606
826
|
}
|
|
607
827
|
for (const block of below.get(last) ?? []) {
|
|
608
828
|
addBlock(block, last);
|
|
@@ -614,30 +834,19 @@ export class EditorLayout {
|
|
|
614
834
|
const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
|
|
615
835
|
const tail = fold?.closer !== undefined && last > line ? this.clampToLine(fold.closer, last) : null;
|
|
616
836
|
const head = fold?.head !== undefined && last > line ? this.clampToLine(fold.head, line) : null;
|
|
617
|
-
const
|
|
837
|
+
const rowIndex = index + rows.length;
|
|
618
838
|
const bounds = this.wrapWidth === null ? null : this.document.getLine(line);
|
|
619
839
|
const subRows = this.rowCountOf(line, bounds === null ? 0 : bounds.end - bounds.start);
|
|
620
|
-
|
|
621
|
-
key: `line:${line}`,
|
|
622
|
-
kind: 'text',
|
|
623
|
-
index,
|
|
624
|
-
line,
|
|
625
|
-
lastLine: last,
|
|
626
|
-
tail,
|
|
627
|
-
head,
|
|
628
|
-
subRows,
|
|
629
|
-
top: 0,
|
|
630
|
-
height: subRows * this.metrics.lineHeight
|
|
631
|
-
});
|
|
840
|
+
rows.push(new LineRow(rowIndex, line, last, tail, head, subRows, subRows * this.metrics.lineHeight));
|
|
632
841
|
for (let covered = line; covered <= last; covered++) {
|
|
633
|
-
|
|
842
|
+
lineRows[covered - from] = rowIndex;
|
|
634
843
|
}
|
|
635
844
|
for (const block of below.get(line) ?? []) {
|
|
636
845
|
addBlock(block, line);
|
|
637
846
|
}
|
|
638
847
|
line = last;
|
|
639
848
|
}
|
|
640
|
-
|
|
849
|
+
return { rows, lineRows };
|
|
641
850
|
}
|
|
642
851
|
|
|
643
852
|
/* An offset of a fold clamped to its line, since the fold may be a moment older than an edit. */
|
|
@@ -646,31 +855,43 @@ export class EditorLayout {
|
|
|
646
855
|
return Math.min(bounds.end, Math.max(bounds.start, offset));
|
|
647
856
|
}
|
|
648
857
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
858
|
+
/* The tops of the rows from `from` on. */
|
|
859
|
+
private reflow(from: number): void {
|
|
860
|
+
const previous = this.rows[from - 1];
|
|
861
|
+
let top = previous === undefined ? 0 : previous.top + previous.height;
|
|
862
|
+
for (let index = from; index < this.rows.length; index++) {
|
|
863
|
+
const row = this.rows[index]!;
|
|
652
864
|
row.top = top;
|
|
653
865
|
top += row.height;
|
|
654
866
|
}
|
|
655
867
|
this.height = top + BOTTOM_PADDING;
|
|
656
868
|
}
|
|
657
869
|
|
|
870
|
+
/* The row of a widget, a replacement or a line by its key. */
|
|
871
|
+
rowByKey(key: string): LayoutRow | undefined {
|
|
872
|
+
if (key.startsWith('line:')) {
|
|
873
|
+
const row = this.rowForLine(Number(key.slice(5)));
|
|
874
|
+
return row.key === key ? row : undefined;
|
|
875
|
+
}
|
|
876
|
+
return this.keyed.get(key);
|
|
877
|
+
}
|
|
878
|
+
|
|
658
879
|
/* A widget's measured height. Returns whether it moved anything. */
|
|
659
880
|
setMeasuredHeight(key: string, height: number): boolean {
|
|
660
|
-
const row = this.
|
|
881
|
+
const row = this.rowByKey(key);
|
|
661
882
|
const next = Math.max(1, Math.round(height));
|
|
662
883
|
if (!row || (row.kind === 'block' && row.widget.fixed) || !Number.isFinite(next) || row.height === next) {
|
|
663
884
|
return false;
|
|
664
885
|
}
|
|
665
886
|
this.measuredHeights.set(key, next);
|
|
666
887
|
row.height = next;
|
|
667
|
-
this.reflow();
|
|
888
|
+
this.reflow(row.index);
|
|
668
889
|
return true;
|
|
669
890
|
}
|
|
670
891
|
|
|
671
892
|
/* The visual lines a drawn row really has, which its estimate may have missed. Returns whether any row changed. */
|
|
672
893
|
syncRows(rows: readonly LayoutRow[]): boolean {
|
|
673
|
-
let
|
|
894
|
+
let first = this.rows.length;
|
|
674
895
|
for (const row of rows) {
|
|
675
896
|
if (row.kind !== 'text' || row.replacement !== undefined) {
|
|
676
897
|
continue;
|
|
@@ -688,14 +909,15 @@ export class EditorLayout {
|
|
|
688
909
|
if (subRows !== row.subRows) {
|
|
689
910
|
row.subRows = subRows;
|
|
690
911
|
row.height = subRows * this.metrics.lineHeight;
|
|
691
|
-
|
|
912
|
+
first = Math.min(first, row.index);
|
|
692
913
|
}
|
|
693
914
|
}
|
|
694
|
-
if (
|
|
915
|
+
if (first < this.rows.length) {
|
|
695
916
|
this.lineHeightsVersion++;
|
|
696
|
-
this.reflow();
|
|
917
|
+
this.reflow(first);
|
|
918
|
+
return true;
|
|
697
919
|
}
|
|
698
|
-
return
|
|
920
|
+
return false;
|
|
699
921
|
}
|
|
700
922
|
|
|
701
923
|
rowAt(y: number): LayoutRow {
|
package/src/tokens.ts
CHANGED
|
@@ -128,7 +128,8 @@ export class TokenCache {
|
|
|
128
128
|
const old = this.entries[lastOld];
|
|
129
129
|
this.pivotLine = old ? lastNew : -1;
|
|
130
130
|
this.pivotState = old?.state;
|
|
131
|
-
|
|
131
|
+
// Not a spread into splice: a paste of a few hundred thousand lines would overflow the call stack.
|
|
132
|
+
this.entries = this.entries.slice(0, firstLine).concat(new Array<undefined>(lastNew - firstLine + 1).fill(undefined), this.entries.slice(lastOld + 1));
|
|
132
133
|
this.reusableFrom = lastNew + 1;
|
|
133
134
|
}
|
|
134
135
|
|
package/src/view.ts
CHANGED
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
type FoldingRange,
|
|
10
10
|
indentationColumn,
|
|
11
11
|
replacementText,
|
|
12
|
-
scanBrackets,
|
|
13
|
-
type BracketIndex,
|
|
14
12
|
type Selection as EditorSelection
|
|
15
13
|
} from '@adecore/editor-core';
|
|
16
14
|
import { createSearchClient, type EditorSearchClientOptions } from './search-client.ts';
|
|
@@ -124,6 +122,8 @@ const MIN_OVERSCAN = 400;
|
|
|
124
122
|
const COLOR_SLICE_MS = 8;
|
|
125
123
|
const FIRST_PAINT_MS = 12;
|
|
126
124
|
const FOLD_DELAY_MS = 300;
|
|
125
|
+
/* How much text one frame lexes to pair the brackets at the carets; a large document edited all over takes a few frames. */
|
|
126
|
+
const BRACKET_FRAME_LIMIT = 256 * 1024;
|
|
127
127
|
|
|
128
128
|
/* The space between the last character of a line and what the host puts after it. */
|
|
129
129
|
const LINE_ACTION_GAP = 28;
|
|
@@ -308,11 +308,11 @@ export class EditorView {
|
|
|
308
308
|
/* The folds somebody already decided on, a person or the defaults, so a later answer of a server leaves them as they are. */
|
|
309
309
|
private settled = new Set<number>();
|
|
310
310
|
private foldTimer: ReturnType<typeof setTimeout> | undefined;
|
|
311
|
+
private bracketTimer: ReturnType<typeof setTimeout> | undefined;
|
|
311
312
|
private colorTimer: ReturnType<typeof setTimeout> | undefined;
|
|
312
313
|
private noticeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
313
314
|
private liveTimer: ReturnType<typeof setTimeout> | undefined;
|
|
314
315
|
private lineLabelProvider: ((line: number) => string | null) | null = null;
|
|
315
|
-
private bracketCache: { revision: number; language: string | undefined; index: BracketIndex } | undefined;
|
|
316
316
|
private wrapWidth: number | null = null;
|
|
317
317
|
/* 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. */
|
|
318
318
|
private rowEndCaret: number | null = null;
|
|
@@ -968,7 +968,7 @@ export class EditorView {
|
|
|
968
968
|
ranges,
|
|
969
969
|
this.model,
|
|
970
970
|
(from, to) => this.model.slice(from, to),
|
|
971
|
-
(offset) => this.
|
|
971
|
+
(offset) => this.model.matchBracket(offset, this.settings.language)?.mate
|
|
972
972
|
)
|
|
973
973
|
);
|
|
974
974
|
const anchors = new Set(this.foldRanges.map((range) => range.from));
|
|
@@ -1306,7 +1306,7 @@ export class EditorView {
|
|
|
1306
1306
|
const anchor = this.layout.rowAt(top);
|
|
1307
1307
|
const offset = top - anchor.top;
|
|
1308
1308
|
action();
|
|
1309
|
-
const next = this.layout.
|
|
1309
|
+
const next = this.layout.rowByKey(anchor.key);
|
|
1310
1310
|
if (next) {
|
|
1311
1311
|
// Unwrapping can shrink the anchor below its old offset.
|
|
1312
1312
|
this.viewport.scrollTop = Math.max(0, next.top + Math.min(offset, Math.max(0, next.height - 1)));
|
|
@@ -1331,25 +1331,16 @@ export class EditorView {
|
|
|
1331
1331
|
return candidates.find((at) => at >= 0 && /[()[\]{}]/.test(this.model.slice(at, at + 1)));
|
|
1332
1332
|
}
|
|
1333
1333
|
|
|
1334
|
-
private brackets(): BracketIndex {
|
|
1335
|
-
if (this.bracketCache?.revision !== this.revision || this.bracketCache.language !== this.settings.language) {
|
|
1336
|
-
this.bracketCache = {
|
|
1337
|
-
revision: this.revision,
|
|
1338
|
-
language: this.settings.language,
|
|
1339
|
-
index: scanBrackets(this.model.getText(), this.settings.language)
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
return this.bracketCache.index;
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
1334
|
/* The offset a bracket at the caret pairs with, if it is one. */
|
|
1346
1335
|
matchingBracket(selection: { anchor: number; head: number }): { at: number; mate: number | undefined } | null {
|
|
1347
1336
|
const at = this.bracketAt(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
|
|
1348
|
-
return at === undefined ? null : { at, mate: this.
|
|
1337
|
+
return at === undefined ? null : { at, mate: this.model.matchBracket(at, this.settings.language)?.mate };
|
|
1349
1338
|
}
|
|
1350
1339
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1340
|
+
/* Draws the highlights of the brackets at the carets that the last frame ran out of lexing for. */
|
|
1341
|
+
private scheduleBrackets(): void {
|
|
1342
|
+
clearTimeout(this.bracketTimer);
|
|
1343
|
+
this.bracketTimer = setTimeout(() => this.requestRender(), 0);
|
|
1353
1344
|
}
|
|
1354
1345
|
|
|
1355
1346
|
/* A point of the screen in the coordinates of the content, undoing the scale a canvas puts on a node. */
|
|
@@ -2120,12 +2111,18 @@ export class EditorView {
|
|
|
2120
2111
|
const firstLine = rows[0]?.line ?? 0;
|
|
2121
2112
|
const lastLine = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
|
|
2122
2113
|
const highlighted = this.highlightedLines(firstLine, lastLine);
|
|
2114
|
+
const shownFrom = this.model.offsetAt({ line: firstLine, column: 0 });
|
|
2115
|
+
const shownTo = lastLine + 1 < this.model.getLineCount() ? this.model.offsetAt({ line: lastLine + 1, column: 0 }) : this.model.getLength();
|
|
2123
2116
|
paintGutter(this.gutterLines, this.layout, rows, {
|
|
2124
2117
|
lineNumber: (line) => this.lineNumberLabel(line),
|
|
2125
2118
|
changes: this.changedLines(rows),
|
|
2126
2119
|
attribution: this.attributedLines(firstLine, lastLine),
|
|
2127
2120
|
highlights: highlighted,
|
|
2128
|
-
activeLines: new Set(
|
|
2121
|
+
activeLines: new Set(
|
|
2122
|
+
selections
|
|
2123
|
+
.filter((selection) => selection.head >= shownFrom && selection.head <= shownTo)
|
|
2124
|
+
.map((selection) => this.model.positionAt(selection.head).line)
|
|
2125
|
+
),
|
|
2129
2126
|
foldable,
|
|
2130
2127
|
lineLabel: (line) => this.lineLabelProvider?.(line) ?? null,
|
|
2131
2128
|
foldName: (line, collapsed) => this.foldName(line, collapsed),
|
|
@@ -2199,18 +2196,28 @@ export class EditorView {
|
|
|
2199
2196
|
}
|
|
2200
2197
|
}
|
|
2201
2198
|
const seen = new Set<number>();
|
|
2202
|
-
for (const selection of selections) {
|
|
2203
|
-
|
|
2204
|
-
if (
|
|
2199
|
+
for (const [index, selection] of selections.entries()) {
|
|
2200
|
+
// A caret off screen has no bracket to draw, except the primary one, whose mate may be on screen.
|
|
2201
|
+
if (index < selections.length - 1 && (selection.head < visibleFrom - 1 || selection.head > visibleTo + 1)) {
|
|
2205
2202
|
continue;
|
|
2206
2203
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2204
|
+
const at = this.bracketAt(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
|
|
2205
|
+
if (at === undefined) {
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
const match = this.model.matchBracket(at, this.settings.language, BRACKET_FRAME_LIMIT);
|
|
2209
|
+
if (match === null) {
|
|
2210
|
+
this.scheduleBrackets();
|
|
2211
|
+
break;
|
|
2212
|
+
}
|
|
2213
|
+
if (match === undefined) {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
if (match.mate === undefined) {
|
|
2217
|
+
marks.push({ className: 'se-bracket-unmatched', rects: this.paintRectangles(at, at + 1, rows) });
|
|
2211
2218
|
continue;
|
|
2212
2219
|
}
|
|
2213
|
-
for (const offset of [
|
|
2220
|
+
for (const offset of [at, match.mate]) {
|
|
2214
2221
|
if (!seen.has(offset)) {
|
|
2215
2222
|
seen.add(offset);
|
|
2216
2223
|
marks.push({ className: 'se-bracket-match', rects: this.paintRectangles(offset, offset + 1, rows) });
|
|
@@ -2811,6 +2818,7 @@ export class EditorView {
|
|
|
2811
2818
|
this.subscription.dispose();
|
|
2812
2819
|
this.resizeObserver?.disconnect();
|
|
2813
2820
|
this.blockObserver?.disconnect();
|
|
2821
|
+
clearTimeout(this.bracketTimer);
|
|
2814
2822
|
clearTimeout(this.foldTimer);
|
|
2815
2823
|
clearTimeout(this.colorTimer);
|
|
2816
2824
|
clearTimeout(this.noticeTimer);
|