@adecore/editor 0.23.0 → 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/engine.js +5 -0
- package/dist/fake.d.ts +16 -4
- package/dist/fake.js +261 -96
- package/dist/layout.d.ts +7 -0
- package/dist/layout.js +228 -40
- package/dist/tokens.js +2 -1
- package/dist/types.d.ts +2 -0
- package/dist/view.d.ts +3 -3
- package/dist/view.js +38 -26
- package/package.json +2 -2
- package/src/controller.ts +9 -2
- package/src/engine.ts +6 -0
- package/src/fake.ts +306 -97
- package/src/layout.ts +262 -40
- package/src/tokens.ts +2 -1
- package/src/types.ts +4 -0
- package/src/view.ts +41 -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/types.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface EditorOptions {
|
|
|
63
63
|
readonly smartKeys?: Partial<EditorSmartKeys>;
|
|
64
64
|
/* A line at each indentation level, the one of the scope around the caret stronger; on unless false. */
|
|
65
65
|
readonly guides?: boolean;
|
|
66
|
+
/* The headers of the blocks scrolled out of sight stay pinned on top; on unless false. */
|
|
67
|
+
readonly stickyScroll?: boolean;
|
|
66
68
|
/* Spaces drawn as dots and tabs as arrows; off unless true. */
|
|
67
69
|
readonly whitespace?: boolean;
|
|
68
70
|
/* The column to draw a line at, such as the `max_line_length` of a project; none by default. */
|
|
@@ -615,6 +617,8 @@ export interface Editor {
|
|
|
615
617
|
/* Changes what it does as a person types; the keys left out stay as they are. */
|
|
616
618
|
setSmartKeys(keys: Partial<EditorSmartKeys>): void;
|
|
617
619
|
setGuides(guides: boolean): void;
|
|
620
|
+
/* Optional so an editor from before it still fits. */
|
|
621
|
+
setStickyScroll?(sticky: boolean): void;
|
|
618
622
|
setWhitespace(whitespace: boolean): void;
|
|
619
623
|
/* The column to draw a line at; null takes it away. */
|
|
620
624
|
setRightMargin(column: number | null): void;
|
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';
|
|
@@ -114,6 +112,8 @@ export interface ViewSettings {
|
|
|
114
112
|
/* When the arrow that folds a block shows in the gutter. */
|
|
115
113
|
foldOutline: 'off' | 'hover' | 'always';
|
|
116
114
|
folding?: boolean;
|
|
115
|
+
/* The headers of the blocks scrolled out of sight stay pinned on top; on unless false. */
|
|
116
|
+
stickyScroll?: boolean;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/* The lines past the visible ones that are colored ahead of the scroll, and how long one slice of that work may take. */
|
|
@@ -122,6 +122,8 @@ const MIN_OVERSCAN = 400;
|
|
|
122
122
|
const COLOR_SLICE_MS = 8;
|
|
123
123
|
const FIRST_PAINT_MS = 12;
|
|
124
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;
|
|
125
127
|
|
|
126
128
|
/* The space between the last character of a line and what the host puts after it. */
|
|
127
129
|
const LINE_ACTION_GAP = 28;
|
|
@@ -306,11 +308,11 @@ export class EditorView {
|
|
|
306
308
|
/* The folds somebody already decided on, a person or the defaults, so a later answer of a server leaves them as they are. */
|
|
307
309
|
private settled = new Set<number>();
|
|
308
310
|
private foldTimer: ReturnType<typeof setTimeout> | undefined;
|
|
311
|
+
private bracketTimer: ReturnType<typeof setTimeout> | undefined;
|
|
309
312
|
private colorTimer: ReturnType<typeof setTimeout> | undefined;
|
|
310
313
|
private noticeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
311
314
|
private liveTimer: ReturnType<typeof setTimeout> | undefined;
|
|
312
315
|
private lineLabelProvider: ((line: number) => string | null) | null = null;
|
|
313
|
-
private bracketCache: { revision: number; language: string | undefined; index: BracketIndex } | undefined;
|
|
314
316
|
private wrapWidth: number | null = null;
|
|
315
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. */
|
|
316
318
|
private rowEndCaret: number | null = null;
|
|
@@ -966,7 +968,7 @@ export class EditorView {
|
|
|
966
968
|
ranges,
|
|
967
969
|
this.model,
|
|
968
970
|
(from, to) => this.model.slice(from, to),
|
|
969
|
-
(offset) => this.
|
|
971
|
+
(offset) => this.model.matchBracket(offset, this.settings.language)?.mate
|
|
970
972
|
)
|
|
971
973
|
);
|
|
972
974
|
const anchors = new Set(this.foldRanges.map((range) => range.from));
|
|
@@ -1304,7 +1306,7 @@ export class EditorView {
|
|
|
1304
1306
|
const anchor = this.layout.rowAt(top);
|
|
1305
1307
|
const offset = top - anchor.top;
|
|
1306
1308
|
action();
|
|
1307
|
-
const next = this.layout.
|
|
1309
|
+
const next = this.layout.rowByKey(anchor.key);
|
|
1308
1310
|
if (next) {
|
|
1309
1311
|
// Unwrapping can shrink the anchor below its old offset.
|
|
1310
1312
|
this.viewport.scrollTop = Math.max(0, next.top + Math.min(offset, Math.max(0, next.height - 1)));
|
|
@@ -1329,25 +1331,16 @@ export class EditorView {
|
|
|
1329
1331
|
return candidates.find((at) => at >= 0 && /[()[\]{}]/.test(this.model.slice(at, at + 1)));
|
|
1330
1332
|
}
|
|
1331
1333
|
|
|
1332
|
-
private brackets(): BracketIndex {
|
|
1333
|
-
if (this.bracketCache?.revision !== this.revision || this.bracketCache.language !== this.settings.language) {
|
|
1334
|
-
this.bracketCache = {
|
|
1335
|
-
revision: this.revision,
|
|
1336
|
-
language: this.settings.language,
|
|
1337
|
-
index: scanBrackets(this.model.getText(), this.settings.language)
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
|
-
return this.bracketCache.index;
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
1334
|
/* The offset a bracket at the caret pairs with, if it is one. */
|
|
1344
1335
|
matchingBracket(selection: { anchor: number; head: number }): { at: number; mate: number | undefined } | null {
|
|
1345
1336
|
const at = this.bracketAt(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
|
|
1346
|
-
return at === undefined ? null : { at, mate: this.
|
|
1337
|
+
return at === undefined ? null : { at, mate: this.model.matchBracket(at, this.settings.language)?.mate };
|
|
1347
1338
|
}
|
|
1348
1339
|
|
|
1349
|
-
|
|
1350
|
-
|
|
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);
|
|
1351
1344
|
}
|
|
1352
1345
|
|
|
1353
1346
|
/* A point of the screen in the coordinates of the content, undoing the scale a canvas puts on a node. */
|
|
@@ -2118,12 +2111,18 @@ export class EditorView {
|
|
|
2118
2111
|
const firstLine = rows[0]?.line ?? 0;
|
|
2119
2112
|
const lastLine = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
|
|
2120
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();
|
|
2121
2116
|
paintGutter(this.gutterLines, this.layout, rows, {
|
|
2122
2117
|
lineNumber: (line) => this.lineNumberLabel(line),
|
|
2123
2118
|
changes: this.changedLines(rows),
|
|
2124
2119
|
attribution: this.attributedLines(firstLine, lastLine),
|
|
2125
2120
|
highlights: highlighted,
|
|
2126
|
-
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
|
+
),
|
|
2127
2126
|
foldable,
|
|
2128
2127
|
lineLabel: (line) => this.lineLabelProvider?.(line) ?? null,
|
|
2129
2128
|
foldName: (line, collapsed) => this.foldName(line, collapsed),
|
|
@@ -2197,18 +2196,28 @@ export class EditorView {
|
|
|
2197
2196
|
}
|
|
2198
2197
|
}
|
|
2199
2198
|
const seen = new Set<number>();
|
|
2200
|
-
for (const selection of selections) {
|
|
2201
|
-
|
|
2202
|
-
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)) {
|
|
2203
2202
|
continue;
|
|
2204
2203
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
marks.push({ className: 'se-bracket-unmatched', rects: this.paintRectangles(bracket.at, bracket.at + 1, rows) });
|
|
2208
|
-
}
|
|
2204
|
+
const at = this.bracketAt(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
|
|
2205
|
+
if (at === undefined) {
|
|
2209
2206
|
continue;
|
|
2210
2207
|
}
|
|
2211
|
-
|
|
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) });
|
|
2218
|
+
continue;
|
|
2219
|
+
}
|
|
2220
|
+
for (const offset of [at, match.mate]) {
|
|
2212
2221
|
if (!seen.has(offset)) {
|
|
2213
2222
|
seen.add(offset);
|
|
2214
2223
|
marks.push({ className: 'se-bracket-match', rects: this.paintRectangles(offset, offset + 1, rows) });
|
|
@@ -2654,6 +2663,9 @@ export class EditorView {
|
|
|
2654
2663
|
}
|
|
2655
2664
|
|
|
2656
2665
|
private stickyAt(scrollTop: number): StickyPlacement[] {
|
|
2666
|
+
if (this.settings.stickyScroll === false) {
|
|
2667
|
+
return [];
|
|
2668
|
+
}
|
|
2657
2669
|
const { layout } = this;
|
|
2658
2670
|
const bottomOf = (line: number): number => {
|
|
2659
2671
|
const row = layout.rowForLine(line);
|
|
@@ -2806,6 +2818,7 @@ export class EditorView {
|
|
|
2806
2818
|
this.subscription.dispose();
|
|
2807
2819
|
this.resizeObserver?.disconnect();
|
|
2808
2820
|
this.blockObserver?.disconnect();
|
|
2821
|
+
clearTimeout(this.bracketTimer);
|
|
2809
2822
|
clearTimeout(this.foldTimer);
|
|
2810
2823
|
clearTimeout(this.colorTimer);
|
|
2811
2824
|
clearTimeout(this.noticeTimer);
|