@adecore/editor 0.39.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/controller.d.ts +1 -0
- package/dist/controller.js +17 -28
- package/dist/engine.js +2 -9
- package/dist/find.js +12 -13
- package/dist/layout.js +7 -2
- package/dist/paint.d.ts +2 -0
- package/dist/paint.js +23 -20
- package/dist/view.d.ts +3 -2
- package/dist/view.js +46 -59
- package/package.json +6 -6
- package/src/attribution.ts +14 -0
- package/src/code-block.ts +25 -0
- package/src/column-selection.ts +10 -0
- package/src/controller.ts +189 -28
- package/src/engine.ts +87 -9
- package/src/fake.ts +79 -0
- package/src/find-search.ts +20 -0
- package/src/find.ts +135 -14
- package/src/geometry-stops.ts +10 -0
- package/src/grammar-preparation.ts +23 -0
- package/src/index.ts +1 -0
- package/src/input.ts +13 -0
- package/src/keymap-table.ts +9 -0
- package/src/keymap.ts +25 -0
- package/src/layout-changes.ts +7 -0
- package/src/layout.ts +219 -3
- package/src/lens.ts +3 -0
- package/src/listeners.ts +1 -0
- package/src/metrics.ts +14 -0
- package/src/modifier-gesture.ts +17 -0
- package/src/native-input.ts +27 -0
- package/src/offsets.ts +10 -0
- package/src/outline.ts +38 -0
- package/src/overview.ts +13 -0
- package/src/paint.ts +140 -20
- package/src/php-html.ts +1 -0
- package/src/pointer.ts +50 -0
- package/src/scroll-animation.ts +10 -0
- package/src/scroll.ts +7 -0
- package/src/search-client.ts +32 -0
- package/src/search-worker.ts +38 -0
- package/src/semantic.ts +12 -0
- package/src/shiki-document.ts +19 -0
- package/src/shiki-worker.ts +68 -0
- package/src/shiki.ts +37 -0
- package/src/testing.ts +17 -0
- package/src/theme-scopes.ts +14 -0
- package/src/tokens.ts +31 -0
- package/src/touch.ts +16 -0
- package/src/tracked-range.ts +5 -0
- package/src/uniform-geometry.ts +10 -0
- package/src/view.ts +481 -75
- package/src/worker-client.ts +54 -0
package/src/view.ts
CHANGED
|
@@ -76,6 +76,10 @@ function sameRect(left: EditorRect, right: EditorRect): boolean {
|
|
|
76
76
|
return left.left === right.left && left.top === right.top && left.right === right.right && left.bottom === right.bottom;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function closestElement(target: EventTarget | null, selector: string): HTMLElement | null {
|
|
80
|
+
return (target as HTMLElement | null)?.closest?.<HTMLElement>(selector) ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
function mapRange<Range extends { from: number; to: number }>(range: Range, map: (offset: number) => number): Range {
|
|
80
84
|
return { ...range, from: map(range.from), to: map(range.to) };
|
|
81
85
|
}
|
|
@@ -246,11 +250,13 @@ export class EditorView {
|
|
|
246
250
|
private scopeKey = '';
|
|
247
251
|
private readonly changeLayers = new Map<string, { kind: EditorChangeKind; from: number; to: number }[]>();
|
|
248
252
|
private changeVersion = 0;
|
|
253
|
+
|
|
249
254
|
private changeCache: {
|
|
250
255
|
version: number;
|
|
251
256
|
lines: Map<number, EditorChangeKind>;
|
|
252
257
|
runs: { kind: EditorChangeKind; start: number; end: number }[];
|
|
253
258
|
} | null = null;
|
|
259
|
+
|
|
254
260
|
private readonly attribution = new AttributionRuns();
|
|
255
261
|
private readonly highlights = new AttributionRuns();
|
|
256
262
|
private remoteCarets: { id: string; name: string; color: string; at: number }[] = [];
|
|
@@ -327,11 +333,14 @@ export class EditorView {
|
|
|
327
333
|
this.settings = settings;
|
|
328
334
|
const document = container.ownerDocument;
|
|
329
335
|
this.document = document;
|
|
336
|
+
|
|
330
337
|
const make = <Tag extends keyof HTMLElementTagNameMap>(tag: Tag, className: string): HTMLElementTagNameMap[Tag] => {
|
|
331
338
|
const element = document.createElement(tag);
|
|
332
339
|
element.className = className;
|
|
340
|
+
|
|
333
341
|
return element;
|
|
334
342
|
};
|
|
343
|
+
|
|
335
344
|
this.root = make('div', 'se-editor');
|
|
336
345
|
this.gutter = make('div', 'se-gutter');
|
|
337
346
|
this.gutterLines = make('div', 'se-gutter-lines');
|
|
@@ -349,9 +358,11 @@ export class EditorView {
|
|
|
349
358
|
this.preview = make('div', 'se-preview');
|
|
350
359
|
this.preview.setAttribute('aria-hidden', 'true');
|
|
351
360
|
this.preview.hidden = true;
|
|
361
|
+
|
|
352
362
|
for (const layer of [this.overlays, this.over, this.carets, this.remote]) {
|
|
353
363
|
layer.setAttribute('aria-hidden', 'true');
|
|
354
364
|
}
|
|
365
|
+
|
|
355
366
|
code.setAttribute('aria-hidden', 'true');
|
|
356
367
|
this.content.append(this.overlays, this.signs, code, this.over, this.carets, this.remote, this.actions, this.preview);
|
|
357
368
|
// The gutter is in the scroller with the text and sticks to its left, so the browser moves both in the same frame.
|
|
@@ -365,9 +376,11 @@ export class EditorView {
|
|
|
365
376
|
this.input = make('textarea', 'se-input');
|
|
366
377
|
this.input.wrap = 'off';
|
|
367
378
|
this.input.spellcheck = false;
|
|
379
|
+
|
|
368
380
|
for (const name of ['autocapitalize', 'autocomplete', 'autocorrect']) {
|
|
369
381
|
this.input.setAttribute(name, 'off');
|
|
370
382
|
}
|
|
383
|
+
|
|
371
384
|
this.input.setAttribute('enterkeyhint', 'enter');
|
|
372
385
|
this.input.setAttribute('inputmode', 'text');
|
|
373
386
|
this.input.setAttribute('aria-multiline', 'true');
|
|
@@ -391,36 +404,45 @@ export class EditorView {
|
|
|
391
404
|
|
|
392
405
|
this.font = readEditorFont(this.root);
|
|
393
406
|
this.layout = new EditorLayout(model, createMetrics(this.font, settings.tabSize, document));
|
|
407
|
+
|
|
394
408
|
this.find = new FindController(model, {
|
|
395
409
|
createClient: () => (settings.searchWorker === undefined ? null : createSearchClient({ workerFactory: settings.searchWorker })),
|
|
396
410
|
changed: (reveal) => {
|
|
397
411
|
if (this.disposed) {
|
|
398
412
|
return;
|
|
399
413
|
}
|
|
414
|
+
|
|
400
415
|
this.announceFind();
|
|
416
|
+
|
|
401
417
|
if (reveal) {
|
|
402
418
|
this.revealFind();
|
|
403
419
|
}
|
|
420
|
+
|
|
404
421
|
this.paintPreview();
|
|
405
422
|
this.requestRender();
|
|
406
423
|
}
|
|
407
424
|
});
|
|
425
|
+
|
|
408
426
|
this.tokens = new TokenCache({ getLineCount: () => model.getLineCount(), getLineText: (line) => model.getLine(line).text });
|
|
409
427
|
const ResizeObserverClass = document.defaultView?.ResizeObserver;
|
|
428
|
+
|
|
410
429
|
this.blockObserver = ResizeObserverClass
|
|
411
430
|
? new ResizeObserverClass((entries) => {
|
|
412
431
|
let changed = false;
|
|
432
|
+
|
|
413
433
|
this.anchorScroll(() => {
|
|
414
434
|
for (const entry of entries) {
|
|
415
435
|
const element = entry.target as HTMLElement;
|
|
416
436
|
changed = this.layout.setMeasuredHeight(element.dataset.rowKey!, element.getBoundingClientRect().height) || changed;
|
|
417
437
|
}
|
|
418
438
|
});
|
|
439
|
+
|
|
419
440
|
if (changed) {
|
|
420
441
|
this.render();
|
|
421
442
|
}
|
|
422
443
|
})
|
|
423
444
|
: undefined;
|
|
445
|
+
|
|
424
446
|
this.resizeObserver = ResizeObserverClass ? new ResizeObserverClass(() => this.requestRender()) : undefined;
|
|
425
447
|
this.resizeObserver?.observe(this.viewport);
|
|
426
448
|
this.painter = new RowPainter(code, this.layout, this.blockObserver);
|
|
@@ -489,6 +511,7 @@ export class EditorView {
|
|
|
489
511
|
text: inlay.text.replace(/[\r\n]/g, ' '),
|
|
490
512
|
...(inlay.parts === undefined ? {} : { parts: inlay.parts.map((part) => part.replace(/[\r\n]/g, ' ')) })
|
|
491
513
|
}));
|
|
514
|
+
|
|
492
515
|
this.configureText();
|
|
493
516
|
}
|
|
494
517
|
|
|
@@ -498,6 +521,7 @@ export class EditorView {
|
|
|
498
521
|
} else {
|
|
499
522
|
this.blocks.set(owner, [...blocks]);
|
|
500
523
|
}
|
|
524
|
+
|
|
501
525
|
this.anchorScroll(() => this.layout.configure({ blocks: this.allBlocks() }));
|
|
502
526
|
this.render();
|
|
503
527
|
}
|
|
@@ -507,11 +531,13 @@ export class EditorView {
|
|
|
507
531
|
if (replacements.length === 0 && !this.replacements.has(owner)) {
|
|
508
532
|
return;
|
|
509
533
|
}
|
|
534
|
+
|
|
510
535
|
if (replacements.length === 0) {
|
|
511
536
|
this.replacements.delete(owner);
|
|
512
537
|
} else {
|
|
513
538
|
this.replacements.set(owner, [...replacements]);
|
|
514
539
|
}
|
|
540
|
+
|
|
515
541
|
this.anchorScroll(() => this.layout.configure({ replacements: this.allReplacements() }));
|
|
516
542
|
this.render();
|
|
517
543
|
}
|
|
@@ -530,6 +556,7 @@ export class EditorView {
|
|
|
530
556
|
/* Rows under the same line go by owner name and then by the order the owner gave them, so they never swap places when another owner sets its own. */
|
|
531
557
|
private allBlocks(): BlockWidget[] {
|
|
532
558
|
const owners = [...this.blocks.keys()].sort();
|
|
559
|
+
|
|
533
560
|
return [...owners.flatMap((owner) => this.blocks.get(owner)!), ...this.lenses];
|
|
534
561
|
}
|
|
535
562
|
|
|
@@ -537,6 +564,7 @@ export class EditorView {
|
|
|
537
564
|
if (occurrences.length === 0 && this.occurrences.length === 0) {
|
|
538
565
|
return;
|
|
539
566
|
}
|
|
567
|
+
|
|
540
568
|
this.occurrences = [...occurrences];
|
|
541
569
|
this.render();
|
|
542
570
|
}
|
|
@@ -566,52 +594,69 @@ export class EditorView {
|
|
|
566
594
|
/* The grammar's colors of a line with the servers' laid over them, the same object for as long as neither changed. */
|
|
567
595
|
private styledTokensOf(line: number): readonly LineToken[] | null {
|
|
568
596
|
const base = this.lineTokenProvider === null ? this.tokens.tokensOf(line) : (this.lineTokenProvider(line) ?? null);
|
|
597
|
+
|
|
569
598
|
if (base === null || this.semantic.length === 0 || this.scopeColors === null) {
|
|
570
599
|
return base;
|
|
571
600
|
}
|
|
601
|
+
|
|
572
602
|
const spans = this.semanticSpans().get(line);
|
|
603
|
+
|
|
573
604
|
if (spans === undefined) {
|
|
574
605
|
return base;
|
|
575
606
|
}
|
|
607
|
+
|
|
576
608
|
const signature = spans.map((span) => `${span.from}:${span.to}:${span.color}:${span.fontStyle}:${span.within ?? ''}`).join('|');
|
|
577
609
|
const cached = this.semanticLines.get(line);
|
|
610
|
+
|
|
578
611
|
if (cached?.base === base && cached.signature === signature) {
|
|
579
612
|
return cached.result;
|
|
580
613
|
}
|
|
614
|
+
|
|
581
615
|
if (this.semanticLines.size > 600) {
|
|
582
616
|
this.semanticLines.clear();
|
|
583
617
|
}
|
|
618
|
+
|
|
584
619
|
const result = overlayTokens(base, spans);
|
|
585
620
|
this.semanticLines.set(line, { base, signature, result });
|
|
621
|
+
|
|
586
622
|
return result;
|
|
587
623
|
}
|
|
588
624
|
|
|
589
625
|
private semanticSpans(): Map<number, SemanticSpan[]> {
|
|
590
626
|
const key = `${this.revision}|${this.semanticVersion}`;
|
|
627
|
+
|
|
591
628
|
if (this.semanticIndex?.key === key) {
|
|
592
629
|
return this.semanticIndex.lines;
|
|
593
630
|
}
|
|
631
|
+
|
|
594
632
|
const lines = new Map<number, SemanticSpan[]>();
|
|
595
633
|
const colors = this.scopeColors;
|
|
634
|
+
|
|
596
635
|
for (const token of this.semantic) {
|
|
597
636
|
const style = colors?.(token.scopes);
|
|
637
|
+
|
|
598
638
|
if (style === undefined || token.to <= token.from) {
|
|
599
639
|
continue;
|
|
600
640
|
}
|
|
641
|
+
|
|
601
642
|
const line = this.model.positionAt(token.from).line;
|
|
602
|
-
const start = this.model.getLine(line)
|
|
603
|
-
|
|
643
|
+
const { start, end } = this.model.getLine(line);
|
|
644
|
+
|
|
604
645
|
if (token.to > end) {
|
|
605
646
|
continue;
|
|
606
647
|
}
|
|
648
|
+
|
|
607
649
|
const list = lines.get(line) ?? [];
|
|
608
650
|
list.push({ from: token.from - start, to: token.to - start, color: style.color, fontStyle: style.fontStyle, within: token.within });
|
|
609
651
|
lines.set(line, list);
|
|
610
652
|
}
|
|
653
|
+
|
|
611
654
|
for (const list of lines.values()) {
|
|
612
655
|
list.sort((left, right) => left.from - right.from);
|
|
613
656
|
}
|
|
657
|
+
|
|
614
658
|
this.semanticIndex = { key, lines };
|
|
659
|
+
|
|
615
660
|
return lines;
|
|
616
661
|
}
|
|
617
662
|
|
|
@@ -631,16 +676,32 @@ export class EditorView {
|
|
|
631
676
|
this.notice.textContent = message;
|
|
632
677
|
this.notice.hidden = false;
|
|
633
678
|
clearTimeout(this.noticeTimer);
|
|
679
|
+
|
|
634
680
|
this.noticeTimer = setTimeout(() => {
|
|
635
681
|
this.notice.hidden = true;
|
|
636
682
|
}, NOTICE_MS);
|
|
637
683
|
}
|
|
638
684
|
|
|
685
|
+
/* Tells a person why nothing happened, when the editor is read only and has said why. */
|
|
686
|
+
blockedByReadOnly(): boolean {
|
|
687
|
+
if (!this.settings.readOnly) {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (this.settings.readOnlyReason) {
|
|
692
|
+
this.notify(this.settings.readOnlyReason);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
|
|
639
698
|
private foldName(line: number, collapsed: boolean): string {
|
|
640
699
|
const template =
|
|
641
700
|
(collapsed ? this.settings.messages.unfold : this.settings.messages.fold) ??
|
|
642
701
|
(collapsed ? 'Unfold lines {start} to {end}' : 'Fold lines {start} to {end}');
|
|
702
|
+
|
|
643
703
|
const end = this.foldAt(line)?.endLine ?? line;
|
|
704
|
+
|
|
644
705
|
return template.replace('{start}', String(line + 1)).replace('{end}', String(end + 1));
|
|
645
706
|
}
|
|
646
707
|
|
|
@@ -658,11 +719,14 @@ export class EditorView {
|
|
|
658
719
|
for (const name of ['aria-expanded', 'aria-controls', 'aria-autocomplete', 'aria-activedescendant']) {
|
|
659
720
|
this.input.removeAttribute(name);
|
|
660
721
|
}
|
|
722
|
+
|
|
661
723
|
return;
|
|
662
724
|
}
|
|
725
|
+
|
|
663
726
|
this.input.setAttribute('aria-expanded', 'true');
|
|
664
727
|
this.input.setAttribute('aria-autocomplete', 'list');
|
|
665
728
|
this.input.setAttribute('aria-controls', popup.controls);
|
|
729
|
+
|
|
666
730
|
if (popup.activeDescendant === undefined) {
|
|
667
731
|
this.input.removeAttribute('aria-activedescendant');
|
|
668
732
|
} else {
|
|
@@ -734,13 +798,16 @@ export class EditorView {
|
|
|
734
798
|
this.rowEndCaret = null;
|
|
735
799
|
this.hoverOffset = null;
|
|
736
800
|
const batches = snapshot.changes ?? [];
|
|
801
|
+
|
|
737
802
|
if (this.ghost !== null && batches.length > 0) {
|
|
738
803
|
this.ghost = null;
|
|
739
804
|
this.layoutGhostParts();
|
|
740
805
|
}
|
|
806
|
+
|
|
741
807
|
for (const changes of batches) {
|
|
742
808
|
this.followEdit(changes);
|
|
743
809
|
}
|
|
810
|
+
|
|
744
811
|
this.foldRanges = this.mappedFolds(batches);
|
|
745
812
|
this.semanticVersion++;
|
|
746
813
|
this.markerVersion++;
|
|
@@ -751,6 +818,7 @@ export class EditorView {
|
|
|
751
818
|
this.outline.edited(batches);
|
|
752
819
|
this.changeVersion++;
|
|
753
820
|
this.tokens.edited(batches, (offset) => this.model.positionAt(offset).line);
|
|
821
|
+
|
|
754
822
|
this.layout.configure({
|
|
755
823
|
textChanged: true,
|
|
756
824
|
changes: batches,
|
|
@@ -759,21 +827,27 @@ export class EditorView {
|
|
|
759
827
|
folds: this.foldStates(),
|
|
760
828
|
replacements: this.allReplacements()
|
|
761
829
|
});
|
|
830
|
+
|
|
762
831
|
if (anchor && this.viewport.scrollTop > 0) {
|
|
763
832
|
let offset = anchor.offset;
|
|
833
|
+
|
|
764
834
|
for (const changes of batches) {
|
|
765
835
|
offset = mapOffset(offset, changes);
|
|
766
836
|
}
|
|
837
|
+
|
|
767
838
|
this.viewport.scrollTop = Math.max(0, this.layout.rowForLine(this.model.positionAt(offset).line).top + anchor.delta);
|
|
768
839
|
}
|
|
840
|
+
|
|
769
841
|
this.find.refresh();
|
|
770
842
|
this.announceFind();
|
|
771
843
|
this.scheduleFolds();
|
|
772
844
|
this.colorAhead(true);
|
|
773
845
|
}
|
|
846
|
+
|
|
774
847
|
if (this.rowEndCaret !== null && this.model.getPrimary().head !== this.rowEndCaret) {
|
|
775
848
|
this.rowEndCaret = null;
|
|
776
849
|
}
|
|
850
|
+
|
|
777
851
|
this.requestRender();
|
|
778
852
|
}
|
|
779
853
|
|
|
@@ -781,49 +855,60 @@ export class EditorView {
|
|
|
781
855
|
private followEdit(changes: readonly DocumentChange[]): void {
|
|
782
856
|
const map = offsetMapper(changes);
|
|
783
857
|
this.inlays = this.inlays.map((inlay) => ({ ...inlay, at: map(inlay.at) }));
|
|
858
|
+
|
|
784
859
|
for (const [owner, blocks] of this.blocks) {
|
|
785
860
|
this.blocks.set(
|
|
786
861
|
owner,
|
|
787
862
|
blocks.map((block) => ({ ...block, at: map(block.at) }))
|
|
788
863
|
);
|
|
789
864
|
}
|
|
865
|
+
|
|
790
866
|
this.lenses = this.lenses.map((lens) => ({ ...lens, at: map(lens.at) }));
|
|
867
|
+
|
|
791
868
|
for (const [owner, entries] of this.replacements) {
|
|
792
869
|
this.replacements.set(
|
|
793
870
|
owner,
|
|
794
871
|
entries.map((entry) => ({ ...entry, from: map(entry.from), to: map(entry.to) }))
|
|
795
872
|
);
|
|
796
873
|
}
|
|
874
|
+
|
|
797
875
|
this.collapsed = new Set([...this.collapsed].map((anchor) => map(anchor)));
|
|
798
876
|
this.settled = new Set([...this.settled].map((anchor) => map(anchor)));
|
|
877
|
+
|
|
799
878
|
if (this.foldHints !== null) {
|
|
800
879
|
this.foldHints = mapFoldHints(this.foldHints, map);
|
|
801
880
|
}
|
|
881
|
+
|
|
802
882
|
this.customFolds = this.customFolds.map((fold) => mapRange(fold, map));
|
|
803
883
|
this.find.mapBounds(changes);
|
|
884
|
+
|
|
804
885
|
for (const [layer, marks] of this.changeLayers) {
|
|
805
886
|
this.changeLayers.set(
|
|
806
887
|
layer,
|
|
807
888
|
marks.map((mark) => mapRange(mark, map))
|
|
808
889
|
);
|
|
809
890
|
}
|
|
891
|
+
|
|
810
892
|
this.markers = this.markers.map((marker) => mapRange(marker, map));
|
|
811
893
|
this.mapTracked(changes);
|
|
812
894
|
this.attribution.map(changes, map);
|
|
813
895
|
this.highlights.map(changes, map);
|
|
814
896
|
this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at: map(caret.at) }));
|
|
897
|
+
|
|
815
898
|
for (const [owner, entries] of this.lineActions) {
|
|
816
899
|
this.lineActions.set(
|
|
817
900
|
owner,
|
|
818
901
|
entries.map((entry) => ({ ...entry, at: map(entry.at) }))
|
|
819
902
|
);
|
|
820
903
|
}
|
|
904
|
+
|
|
821
905
|
this.semantic = this.semantic.map((token) => mapRange(token, map));
|
|
822
906
|
}
|
|
823
907
|
|
|
824
908
|
private mapTracked(changes: readonly DocumentChange[]): void {
|
|
825
909
|
for (const entry of this.tracked) {
|
|
826
910
|
const mapped = mapTrackedRange(entry.from, entry.to, changes);
|
|
911
|
+
|
|
827
912
|
if (mapped === null) {
|
|
828
913
|
entry.lost = true;
|
|
829
914
|
this.tracked.delete(entry);
|
|
@@ -838,6 +923,7 @@ export class EditorView {
|
|
|
838
923
|
trackRange(from: number, to: number): { get(): { from: number; to: number } | null; dispose(): void } {
|
|
839
924
|
const entry = { from, to, lost: false };
|
|
840
925
|
this.tracked.add(entry);
|
|
926
|
+
|
|
841
927
|
return {
|
|
842
928
|
get: () => (entry.lost ? null : { from: entry.from, to: entry.to }),
|
|
843
929
|
dispose: () => {
|
|
@@ -851,19 +937,24 @@ export class EditorView {
|
|
|
851
937
|
private mappedFolds(batches: readonly (readonly DocumentChange[])[]): ViewFold[] {
|
|
852
938
|
const mappers = batches.map(offsetMapper);
|
|
853
939
|
const mapped: ViewFold[] = [];
|
|
940
|
+
|
|
854
941
|
for (const range of this.foldRanges) {
|
|
855
942
|
let from = range.from;
|
|
856
943
|
let to = range.to;
|
|
944
|
+
|
|
857
945
|
for (const map of mappers) {
|
|
858
946
|
from = map(from);
|
|
859
947
|
to = map(to);
|
|
860
948
|
}
|
|
949
|
+
|
|
861
950
|
const startLine = this.model.positionAt(from).line;
|
|
862
951
|
const endLine = this.model.positionAt(to).line;
|
|
952
|
+
|
|
863
953
|
if (endLine > startLine) {
|
|
864
954
|
mapped.push({ ...range, from, to, startLine, endLine });
|
|
865
955
|
}
|
|
866
956
|
}
|
|
957
|
+
|
|
867
958
|
return mapped;
|
|
868
959
|
}
|
|
869
960
|
|
|
@@ -871,7 +962,9 @@ export class EditorView {
|
|
|
871
962
|
const inlays = this.composition
|
|
872
963
|
? [...this.inlays, { id: '__composition', at: this.model.getPrimary().head, text: this.composition, composition: true }]
|
|
873
964
|
: this.inlays;
|
|
965
|
+
|
|
874
966
|
const [first = ''] = this.ghost?.text.split(/\r?\n/) ?? [];
|
|
967
|
+
|
|
875
968
|
return this.ghost === null || first === '' ? inlays : [...inlays, { id: '__ghost', at: this.ghost.at, text: first, ghost: true }];
|
|
876
969
|
}
|
|
877
970
|
|
|
@@ -886,6 +979,7 @@ export class EditorView {
|
|
|
886
979
|
const ghost = this.ghost;
|
|
887
980
|
const [, ...rest] = ghost?.text.split(/\r?\n/) ?? [];
|
|
888
981
|
const owner = '__ghost';
|
|
982
|
+
|
|
889
983
|
this.setBlockWidgets(
|
|
890
984
|
owner,
|
|
891
985
|
ghost === null || rest.length === 0
|
|
@@ -903,6 +997,7 @@ export class EditorView {
|
|
|
903
997
|
}
|
|
904
998
|
]
|
|
905
999
|
);
|
|
1000
|
+
|
|
906
1001
|
this.setLineActions(
|
|
907
1002
|
owner,
|
|
908
1003
|
ghost?.accessory === undefined
|
|
@@ -929,6 +1024,7 @@ export class EditorView {
|
|
|
929
1024
|
private foldStates(): FoldState[] {
|
|
930
1025
|
return this.foldRanges.map((range) => {
|
|
931
1026
|
const collapsed = this.collapsed.has(range.from);
|
|
1027
|
+
|
|
932
1028
|
return {
|
|
933
1029
|
startLine: range.startLine,
|
|
934
1030
|
endLine: range.endLine,
|
|
@@ -942,9 +1038,11 @@ export class EditorView {
|
|
|
942
1038
|
/* The folds of the document, a moment after the last edit since they read all of it. */
|
|
943
1039
|
private scheduleFolds(): void {
|
|
944
1040
|
clearTimeout(this.foldTimer);
|
|
1041
|
+
|
|
945
1042
|
if (this.settings.folding === false) {
|
|
946
1043
|
return;
|
|
947
1044
|
}
|
|
1045
|
+
|
|
948
1046
|
this.foldTimer = setTimeout(() => this.refreshFolds(), FOLD_DELAY_MS);
|
|
949
1047
|
}
|
|
950
1048
|
|
|
@@ -953,6 +1051,7 @@ export class EditorView {
|
|
|
953
1051
|
if (this.disposed || this.settings.folding === false) {
|
|
954
1052
|
return;
|
|
955
1053
|
}
|
|
1054
|
+
|
|
956
1055
|
const ranges =
|
|
957
1056
|
this.model.getLength() > FOLD_LIMIT
|
|
958
1057
|
? []
|
|
@@ -962,7 +1061,9 @@ export class EditorView {
|
|
|
962
1061
|
...(this.settings.language === undefined ? {} : { language: this.settings.language }),
|
|
963
1062
|
...(this.foldHints === null ? {} : { hints: this.foldHints })
|
|
964
1063
|
});
|
|
1064
|
+
|
|
965
1065
|
this.foldRanges = this.withCustomFolds(ranges);
|
|
1066
|
+
|
|
966
1067
|
this.outline.setStructure(
|
|
967
1068
|
structuralEntries(
|
|
968
1069
|
ranges,
|
|
@@ -971,6 +1072,7 @@ export class EditorView {
|
|
|
971
1072
|
(offset) => this.model.matchBracket(offset, this.settings.language)?.mate
|
|
972
1073
|
)
|
|
973
1074
|
);
|
|
1075
|
+
|
|
974
1076
|
const anchors = new Set(this.foldRanges.map((range) => range.from));
|
|
975
1077
|
this.collapsed = new Set([...this.collapsed].filter((anchor) => anchors.has(anchor)));
|
|
976
1078
|
this.refoldLayout();
|
|
@@ -981,8 +1083,10 @@ export class EditorView {
|
|
|
981
1083
|
const custom = this.customFolds.flatMap((fold): ViewFold[] => {
|
|
982
1084
|
const startLine = this.model.positionAt(fold.from).line;
|
|
983
1085
|
const endLine = this.model.positionAt(fold.to).line;
|
|
1086
|
+
|
|
984
1087
|
return endLine > startLine ? [{ startLine, endLine, from: fold.from, to: fold.to, kind: 'custom' }] : [];
|
|
985
1088
|
});
|
|
1089
|
+
|
|
986
1090
|
return [...ranges, ...custom].sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
987
1091
|
}
|
|
988
1092
|
|
|
@@ -991,14 +1095,18 @@ export class EditorView {
|
|
|
991
1095
|
if (range.kind === 'comment') {
|
|
992
1096
|
return range.to - 2;
|
|
993
1097
|
}
|
|
1098
|
+
|
|
994
1099
|
if (range.kind !== 'bracket') {
|
|
995
1100
|
return undefined;
|
|
996
1101
|
}
|
|
1102
|
+
|
|
997
1103
|
const lineStart = this.model.getLine(range.endLine).start;
|
|
998
1104
|
let start = range.to - 1;
|
|
1105
|
+
|
|
999
1106
|
while (start > lineStart && /[)\]}]/.test(this.model.slice(start - 1, start))) {
|
|
1000
1107
|
start--;
|
|
1001
1108
|
}
|
|
1109
|
+
|
|
1002
1110
|
return start;
|
|
1003
1111
|
}
|
|
1004
1112
|
|
|
@@ -1010,6 +1118,7 @@ export class EditorView {
|
|
|
1010
1118
|
/* The range that starts on a line when it is the only one, as the platform's fold commands read a line. */
|
|
1011
1119
|
private soleFoldAt(line: number): ViewFold | undefined {
|
|
1012
1120
|
const starting = this.foldRanges.filter((range) => range.startLine === line);
|
|
1121
|
+
|
|
1013
1122
|
return starting.length === 1 ? starting[0] : undefined;
|
|
1014
1123
|
}
|
|
1015
1124
|
|
|
@@ -1024,6 +1133,7 @@ export class EditorView {
|
|
|
1024
1133
|
|
|
1025
1134
|
private setCollapsed(range: ViewFold, collapse: boolean): void {
|
|
1026
1135
|
this.settled.add(range.from);
|
|
1136
|
+
|
|
1027
1137
|
if (collapse) {
|
|
1028
1138
|
this.collapsed.add(range.from);
|
|
1029
1139
|
} else {
|
|
@@ -1034,16 +1144,21 @@ export class EditorView {
|
|
|
1034
1144
|
/* A click on a line's control: the outermost range there folds, and unfolding opens every range that starts there. */
|
|
1035
1145
|
toggleFold(line: number, collapse?: boolean): boolean {
|
|
1036
1146
|
const range = this.foldAt(line);
|
|
1147
|
+
|
|
1037
1148
|
if (!range) {
|
|
1038
1149
|
return false;
|
|
1039
1150
|
}
|
|
1151
|
+
|
|
1040
1152
|
const next = collapse ?? !this.foldRanges.some((candidate) => candidate.startLine === line && this.isCollapsed(candidate));
|
|
1153
|
+
|
|
1041
1154
|
for (const candidate of this.foldRanges) {
|
|
1042
1155
|
if (candidate.startLine === line && (next ? candidate === range : true)) {
|
|
1043
1156
|
this.setCollapsed(candidate, next);
|
|
1044
1157
|
}
|
|
1045
1158
|
}
|
|
1159
|
+
|
|
1046
1160
|
this.refoldLayout();
|
|
1161
|
+
|
|
1047
1162
|
return true;
|
|
1048
1163
|
}
|
|
1049
1164
|
|
|
@@ -1051,17 +1166,21 @@ export class EditorView {
|
|
|
1051
1166
|
collapseRegion(): void {
|
|
1052
1167
|
const carets = this.model.getSelections();
|
|
1053
1168
|
const primary = this.model.getPrimary().head;
|
|
1169
|
+
|
|
1054
1170
|
for (const caret of carets) {
|
|
1055
1171
|
const range = this.soleFoldAt(this.model.positionAt(caret.head).line);
|
|
1172
|
+
|
|
1056
1173
|
if (range !== undefined && !this.isCollapsed(range)) {
|
|
1057
1174
|
this.setCollapsed(range, true);
|
|
1058
1175
|
} else {
|
|
1059
1176
|
const open = this.foldsAt(primary).find((candidate) => !this.isCollapsed(candidate));
|
|
1177
|
+
|
|
1060
1178
|
if (open !== undefined) {
|
|
1061
1179
|
this.setCollapsed(open, true);
|
|
1062
1180
|
}
|
|
1063
1181
|
}
|
|
1064
1182
|
}
|
|
1183
|
+
|
|
1065
1184
|
this.refoldLayout();
|
|
1066
1185
|
}
|
|
1067
1186
|
|
|
@@ -1069,15 +1188,18 @@ export class EditorView {
|
|
|
1069
1188
|
expandRegion(): void {
|
|
1070
1189
|
for (const caret of this.model.getSelections()) {
|
|
1071
1190
|
const range = this.soleFoldAt(this.model.positionAt(caret.head).line);
|
|
1191
|
+
|
|
1072
1192
|
if (range !== undefined && this.isCollapsed(range)) {
|
|
1073
1193
|
this.setCollapsed(range, false);
|
|
1074
1194
|
} else {
|
|
1075
1195
|
const closed = this.foldsAt(caret.head).findLast((candidate) => this.isCollapsed(candidate));
|
|
1196
|
+
|
|
1076
1197
|
if (closed !== undefined) {
|
|
1077
1198
|
this.setCollapsed(closed, false);
|
|
1078
1199
|
}
|
|
1079
1200
|
}
|
|
1080
1201
|
}
|
|
1202
|
+
|
|
1081
1203
|
this.refoldLayout();
|
|
1082
1204
|
}
|
|
1083
1205
|
|
|
@@ -1085,9 +1207,11 @@ export class EditorView {
|
|
|
1085
1207
|
private foldTreeAtCaret(collapse: boolean): ViewFold[] {
|
|
1086
1208
|
const head = this.model.getPrimary().head;
|
|
1087
1209
|
let root = this.soleFoldAt(this.model.positionAt(head).line);
|
|
1210
|
+
|
|
1088
1211
|
if (root === undefined || (collapse && this.isCollapsed(root))) {
|
|
1089
1212
|
root = this.foldsAt(head).find((candidate) => !this.isCollapsed(candidate) === collapse);
|
|
1090
1213
|
}
|
|
1214
|
+
|
|
1091
1215
|
return root === undefined ? [] : this.foldRanges.filter((range) => root.from <= range.from && range.to <= root.to);
|
|
1092
1216
|
}
|
|
1093
1217
|
|
|
@@ -1095,6 +1219,7 @@ export class EditorView {
|
|
|
1095
1219
|
for (const range of this.foldTreeAtCaret(true)) {
|
|
1096
1220
|
this.setCollapsed(range, true);
|
|
1097
1221
|
}
|
|
1222
|
+
|
|
1098
1223
|
this.refoldLayout();
|
|
1099
1224
|
}
|
|
1100
1225
|
|
|
@@ -1102,6 +1227,7 @@ export class EditorView {
|
|
|
1102
1227
|
for (const range of this.foldTreeAtCaret(false)) {
|
|
1103
1228
|
this.setCollapsed(range, false);
|
|
1104
1229
|
}
|
|
1230
|
+
|
|
1105
1231
|
this.refoldLayout();
|
|
1106
1232
|
}
|
|
1107
1233
|
|
|
@@ -1111,9 +1237,11 @@ export class EditorView {
|
|
|
1111
1237
|
const from = Math.min(anchor, head);
|
|
1112
1238
|
const to = Math.max(anchor, head);
|
|
1113
1239
|
const inside = from === to ? [] : this.foldRanges.filter((range) => from <= range.from && range.to <= to);
|
|
1240
|
+
|
|
1114
1241
|
for (const range of inside.length > 0 ? inside : this.foldRanges) {
|
|
1115
1242
|
this.setCollapsed(range, collapse);
|
|
1116
1243
|
}
|
|
1244
|
+
|
|
1117
1245
|
this.refoldLayout();
|
|
1118
1246
|
}
|
|
1119
1247
|
|
|
@@ -1126,29 +1254,39 @@ export class EditorView {
|
|
|
1126
1254
|
if (this.settings.folding === false) {
|
|
1127
1255
|
return;
|
|
1128
1256
|
}
|
|
1257
|
+
|
|
1129
1258
|
const { anchor, head } = this.model.getPrimary();
|
|
1130
1259
|
let from = Math.min(anchor, head);
|
|
1131
1260
|
let to = Math.max(anchor, head);
|
|
1261
|
+
|
|
1132
1262
|
if (from === to) {
|
|
1133
1263
|
const around = this.foldsAt(head)[0];
|
|
1264
|
+
|
|
1134
1265
|
if (around?.kind === 'custom') {
|
|
1135
1266
|
this.customFolds = this.customFolds.filter((fold) => fold.from !== around.from);
|
|
1136
1267
|
this.collapsed.delete(around.from);
|
|
1137
1268
|
} else if (around !== undefined) {
|
|
1138
1269
|
this.setCollapsed(around, !this.isCollapsed(around));
|
|
1139
1270
|
}
|
|
1271
|
+
|
|
1140
1272
|
this.refreshFolds();
|
|
1273
|
+
|
|
1141
1274
|
return;
|
|
1142
1275
|
}
|
|
1276
|
+
|
|
1143
1277
|
if (this.model.slice(to - 1, to) === '\n') {
|
|
1144
1278
|
to--;
|
|
1145
1279
|
}
|
|
1280
|
+
|
|
1146
1281
|
const startLine = this.model.positionAt(from).line;
|
|
1282
|
+
|
|
1147
1283
|
if (this.model.positionAt(to).line <= startLine) {
|
|
1148
1284
|
return;
|
|
1149
1285
|
}
|
|
1286
|
+
|
|
1150
1287
|
from = this.model.getLine(startLine).start;
|
|
1151
1288
|
const existing = this.foldRanges.find((range) => range.kind === 'custom' && range.from === from && range.to === to);
|
|
1289
|
+
|
|
1152
1290
|
if (existing !== undefined) {
|
|
1153
1291
|
this.customFolds = this.customFolds.filter((fold) => fold.from !== from);
|
|
1154
1292
|
this.collapsed.delete(from);
|
|
@@ -1156,12 +1294,14 @@ export class EditorView {
|
|
|
1156
1294
|
this.customFolds = [...this.customFolds, { from, to }];
|
|
1157
1295
|
this.collapsed.add(from);
|
|
1158
1296
|
}
|
|
1297
|
+
|
|
1159
1298
|
this.refreshFolds();
|
|
1160
1299
|
}
|
|
1161
1300
|
|
|
1162
1301
|
/* The lines that are folded and the ones a selection folded, for a host that opens the file again where it left it. */
|
|
1163
1302
|
getFolds(): { collapsed: { startLine: number; endLine: number }[]; custom: { startLine: number; endLine: number }[] } {
|
|
1164
1303
|
const lines = (range: ViewFold): { startLine: number; endLine: number } => ({ startLine: range.startLine, endLine: range.endLine });
|
|
1304
|
+
|
|
1165
1305
|
return {
|
|
1166
1306
|
collapsed: this.foldRanges.filter((range) => this.isCollapsed(range)).map(lines),
|
|
1167
1307
|
custom: this.foldRanges.filter((range) => range.kind === 'custom').map(lines)
|
|
@@ -1180,22 +1320,28 @@ export class EditorView {
|
|
|
1180
1320
|
if (this.settings.folding === false) {
|
|
1181
1321
|
return;
|
|
1182
1322
|
}
|
|
1323
|
+
|
|
1183
1324
|
const last = this.model.getLineCount() - 1;
|
|
1325
|
+
|
|
1184
1326
|
if (state !== null) {
|
|
1185
1327
|
this.customFolds = state.custom
|
|
1186
1328
|
.filter((fold) => fold.startLine < fold.endLine && fold.endLine <= last)
|
|
1187
1329
|
.map((fold) => ({ from: this.model.getLine(fold.startLine).start, to: this.model.getLine(fold.endLine).end }));
|
|
1188
1330
|
}
|
|
1331
|
+
|
|
1189
1332
|
this.refreshFolds();
|
|
1190
1333
|
this.defaultRoles = state === null && defaults.length > 0 ? new Set(defaults) : null;
|
|
1334
|
+
|
|
1191
1335
|
for (const remembered of state?.collapsed ?? []) {
|
|
1192
1336
|
const match =
|
|
1193
1337
|
this.foldRanges.find((range) => range.startLine === remembered.startLine && range.endLine === remembered.endLine) ??
|
|
1194
1338
|
this.foldRanges.find((range) => range.startLine === remembered.startLine);
|
|
1339
|
+
|
|
1195
1340
|
if (match !== undefined) {
|
|
1196
1341
|
this.setCollapsed(match, true);
|
|
1197
1342
|
}
|
|
1198
1343
|
}
|
|
1344
|
+
|
|
1199
1345
|
this.applyFoldDefaults();
|
|
1200
1346
|
this.refoldLayout();
|
|
1201
1347
|
}
|
|
@@ -1211,14 +1357,18 @@ export class EditorView {
|
|
|
1211
1357
|
/* Folds the ranges whose role is a default, except a range somebody decided on or one that has the caret or a selection in it. */
|
|
1212
1358
|
private applyFoldDefaults(): void {
|
|
1213
1359
|
const roles = this.defaultRoles;
|
|
1360
|
+
|
|
1214
1361
|
if (roles === null) {
|
|
1215
1362
|
return;
|
|
1216
1363
|
}
|
|
1364
|
+
|
|
1217
1365
|
const lines = this.model
|
|
1218
1366
|
.getSelections()
|
|
1219
1367
|
.flatMap((selection) => [this.model.positionAt(selection.anchor).line, this.model.positionAt(selection.head).line]);
|
|
1368
|
+
|
|
1220
1369
|
for (const range of this.foldRanges) {
|
|
1221
1370
|
const hidesCaret = lines.some((line) => line > range.startLine && line <= range.endLine);
|
|
1371
|
+
|
|
1222
1372
|
if (range.role !== undefined && roles.has(range.role) && !this.settled.has(range.from) && !hidesCaret) {
|
|
1223
1373
|
this.setCollapsed(range, true);
|
|
1224
1374
|
}
|
|
@@ -1232,6 +1382,7 @@ export class EditorView {
|
|
|
1232
1382
|
this.setCollapsed(range, collapse);
|
|
1233
1383
|
}
|
|
1234
1384
|
}
|
|
1385
|
+
|
|
1235
1386
|
this.refoldLayout();
|
|
1236
1387
|
}
|
|
1237
1388
|
|
|
@@ -1241,17 +1392,21 @@ export class EditorView {
|
|
|
1241
1392
|
*/
|
|
1242
1393
|
expandAllToLevel(level: number): void {
|
|
1243
1394
|
const open: ViewFold[] = [];
|
|
1395
|
+
|
|
1244
1396
|
for (const range of this.foldRanges) {
|
|
1245
1397
|
while (open.length > 0 && !(open.at(-1)!.from <= range.from && range.to <= open.at(-1)!.to)) {
|
|
1246
1398
|
open.pop();
|
|
1247
1399
|
}
|
|
1400
|
+
|
|
1248
1401
|
if (open.length < level) {
|
|
1249
1402
|
this.setCollapsed(range, false);
|
|
1250
1403
|
} else if (open.length === level) {
|
|
1251
1404
|
this.setCollapsed(range, true);
|
|
1252
1405
|
}
|
|
1406
|
+
|
|
1253
1407
|
open.push(range);
|
|
1254
1408
|
}
|
|
1409
|
+
|
|
1255
1410
|
this.refoldLayout();
|
|
1256
1411
|
}
|
|
1257
1412
|
|
|
@@ -1267,11 +1422,15 @@ export class EditorView {
|
|
|
1267
1422
|
if (!this.layout.hides(offset)) {
|
|
1268
1423
|
return offset;
|
|
1269
1424
|
}
|
|
1425
|
+
|
|
1270
1426
|
const row = this.layout.rowForLine(this.model.positionAt(offset).line);
|
|
1427
|
+
|
|
1271
1428
|
return row.kind === 'text' && row.head !== null ? row.head : this.model.getLine(row.line).end;
|
|
1272
1429
|
};
|
|
1430
|
+
|
|
1273
1431
|
const selections = this.model.getSelections();
|
|
1274
1432
|
const moved = selections.map((selection) => ({ anchor: visible(selection.anchor), head: visible(selection.head) }));
|
|
1433
|
+
|
|
1275
1434
|
if (moved.some((selection, index) => selection.anchor !== selections[index]!.anchor || selection.head !== selections[index]!.head)) {
|
|
1276
1435
|
this.model.setSelections(moved);
|
|
1277
1436
|
}
|
|
@@ -1281,32 +1440,40 @@ export class EditorView {
|
|
|
1281
1440
|
ensureVisible(offset: number): void {
|
|
1282
1441
|
const line = this.model.positionAt(offset).line;
|
|
1283
1442
|
const row = this.layout.rowForLine(line);
|
|
1443
|
+
|
|
1284
1444
|
if (row.kind === 'text') {
|
|
1285
1445
|
row.replacement?.onReveal?.();
|
|
1286
1446
|
}
|
|
1447
|
+
|
|
1287
1448
|
if (!this.layout.hides(offset)) {
|
|
1288
1449
|
return;
|
|
1289
1450
|
}
|
|
1451
|
+
|
|
1290
1452
|
for (const range of this.foldRanges) {
|
|
1291
1453
|
if ((range.startLine < line && range.endLine >= line) || (range.startLine === line && range.head !== undefined && offset > range.head)) {
|
|
1292
1454
|
this.collapsed.delete(range.from);
|
|
1293
1455
|
this.settled.add(range.from);
|
|
1294
1456
|
}
|
|
1295
1457
|
}
|
|
1458
|
+
|
|
1296
1459
|
this.anchorScroll(() => this.layout.configure({ folds: this.foldStates() }));
|
|
1297
1460
|
}
|
|
1298
1461
|
|
|
1299
1462
|
/* Keeps the first row on screen where it is while the rows above it change height. */
|
|
1300
1463
|
private anchorScroll(action: () => void): void {
|
|
1301
1464
|
const top = this.viewport.scrollTop;
|
|
1465
|
+
|
|
1302
1466
|
if (top <= 0) {
|
|
1303
1467
|
action();
|
|
1468
|
+
|
|
1304
1469
|
return;
|
|
1305
1470
|
}
|
|
1471
|
+
|
|
1306
1472
|
const anchor = this.layout.rowAt(top);
|
|
1307
1473
|
const offset = top - anchor.top;
|
|
1308
1474
|
action();
|
|
1309
1475
|
const next = this.layout.rowByKey(anchor.key);
|
|
1476
|
+
|
|
1310
1477
|
if (next) {
|
|
1311
1478
|
// Unwrapping can shrink the anchor below its old offset.
|
|
1312
1479
|
this.viewport.scrollTop = Math.max(0, next.top + Math.min(offset, Math.max(0, next.height - 1)));
|
|
@@ -1316,6 +1483,7 @@ export class EditorView {
|
|
|
1316
1483
|
private syncWrap(): void {
|
|
1317
1484
|
const width =
|
|
1318
1485
|
this.settings.wrap && this.viewport.clientWidth > 0 ? Math.max(this.layout.metrics.charWidth * 8, this.viewportWidth - RIGHT_PADDING) : null;
|
|
1486
|
+
|
|
1319
1487
|
if (width !== this.wrapWidth) {
|
|
1320
1488
|
this.wrapWidth = width;
|
|
1321
1489
|
this.layoutVersion++;
|
|
@@ -1323,17 +1491,20 @@ export class EditorView {
|
|
|
1323
1491
|
}
|
|
1324
1492
|
}
|
|
1325
1493
|
|
|
1326
|
-
private bracketAt(
|
|
1327
|
-
if (
|
|
1494
|
+
private bracketAt({ anchor, head }: EditorSelection): number | undefined {
|
|
1495
|
+
if (Math.abs(head - anchor) > 1) {
|
|
1328
1496
|
return undefined;
|
|
1329
1497
|
}
|
|
1330
|
-
|
|
1498
|
+
|
|
1499
|
+
const candidates = anchor !== head ? [Math.min(anchor, head)] : [head, head - 1];
|
|
1500
|
+
|
|
1331
1501
|
return candidates.find((at) => at >= 0 && /[()[\]{}]/.test(this.model.slice(at, at + 1)));
|
|
1332
1502
|
}
|
|
1333
1503
|
|
|
1334
1504
|
/* The offset a bracket at the caret pairs with, if it is one. */
|
|
1335
1505
|
matchingBracket(selection: { anchor: number; head: number }): { at: number; mate: number | undefined } | null {
|
|
1336
|
-
const at = this.bracketAt(
|
|
1506
|
+
const at = this.bracketAt(selection);
|
|
1507
|
+
|
|
1337
1508
|
return at === undefined ? null : { at, mate: this.model.matchBracket(at, this.settings.language)?.mate };
|
|
1338
1509
|
}
|
|
1339
1510
|
|
|
@@ -1346,10 +1517,13 @@ export class EditorView {
|
|
|
1346
1517
|
/* A point of the screen in the coordinates of the content, undoing the scale a canvas puts on a node. */
|
|
1347
1518
|
contentPoint(clientX: number, clientY: number): { x: number; y: number } {
|
|
1348
1519
|
const rect = this.viewport.getBoundingClientRect?.();
|
|
1520
|
+
|
|
1349
1521
|
if (!rect) {
|
|
1350
1522
|
return { x: clientX - this.gutterWidth + this.viewport.scrollLeft, y: clientY + this.viewport.scrollTop };
|
|
1351
1523
|
}
|
|
1524
|
+
|
|
1352
1525
|
const scale = this.screenScale(rect);
|
|
1526
|
+
|
|
1353
1527
|
return {
|
|
1354
1528
|
x: (clientX - rect.left) / scale.x - this.gutterWidth + this.viewport.scrollLeft,
|
|
1355
1529
|
y: (clientY - rect.top) / scale.y + this.viewport.scrollTop
|
|
@@ -1360,6 +1534,7 @@ export class EditorView {
|
|
|
1360
1534
|
const point = this.contentPoint(clientX, clientY);
|
|
1361
1535
|
const hit = this.layout.hitTestRow(point.x, point.y);
|
|
1362
1536
|
this.rowEndCaret = hit.rowEnd ? hit.offset : null;
|
|
1537
|
+
|
|
1363
1538
|
return hit.offset;
|
|
1364
1539
|
}
|
|
1365
1540
|
|
|
@@ -1374,49 +1549,61 @@ export class EditorView {
|
|
|
1374
1549
|
/* The character cell at an offset in the page's pixels, undoing nothing: a popup placed by it is right at any zoom. */
|
|
1375
1550
|
clientRectOf(offset: number): EditorRect | null {
|
|
1376
1551
|
const rect = this.viewport.getBoundingClientRect?.();
|
|
1552
|
+
|
|
1377
1553
|
if (!rect) {
|
|
1378
1554
|
return null;
|
|
1379
1555
|
}
|
|
1556
|
+
|
|
1380
1557
|
const scale = this.screenScale(rect);
|
|
1381
1558
|
const caret = this.layout.caret(offset);
|
|
1382
1559
|
const left = rect.left + (caret.x + this.gutterWidth - this.viewport.scrollLeft) * scale.x;
|
|
1383
1560
|
const top = rect.top + (caret.y - this.viewport.scrollTop) * scale.y;
|
|
1561
|
+
|
|
1384
1562
|
return { left, top, right: left + this.layout.metrics.charWidth * scale.x, bottom: top + caret.height * scale.y };
|
|
1385
1563
|
}
|
|
1386
1564
|
|
|
1387
1565
|
/* The offset of the character under a point of the page, or null over anything that is not text. */
|
|
1388
1566
|
characterAtPoint(clientX: number, clientY: number, target: EventTarget | null): number | null {
|
|
1389
|
-
if ((target
|
|
1567
|
+
if (closestElement(target, '.se-inlay, .se-block, .se-line-action, .se-gutter, .se-sticky, .se-fold-chip, .se-overview')) {
|
|
1390
1568
|
return null;
|
|
1391
1569
|
}
|
|
1570
|
+
|
|
1392
1571
|
const point = this.contentPoint(clientX, clientY);
|
|
1572
|
+
|
|
1393
1573
|
if (point.y < 0 || point.y > this.layout.height) {
|
|
1394
1574
|
return null;
|
|
1395
1575
|
}
|
|
1576
|
+
|
|
1396
1577
|
const row = this.layout.rowAt(point.y);
|
|
1578
|
+
|
|
1397
1579
|
if (row.kind !== 'text') {
|
|
1398
1580
|
return null;
|
|
1399
1581
|
}
|
|
1582
|
+
|
|
1400
1583
|
const hit = this.layout.hitTestRow(point.x, point.y);
|
|
1401
1584
|
const caret = this.layout.caret(hit.offset);
|
|
1402
1585
|
const line = this.model.getLine(row.line);
|
|
1403
1586
|
const offset = point.x >= caret.x ? hit.offset : hit.offset - 1;
|
|
1587
|
+
|
|
1404
1588
|
// Past the last character of a line the nearest stop is its end, which no character is under.
|
|
1405
1589
|
if (offset < line.start || offset >= line.end || Math.abs(point.x - caret.x) > this.layout.metrics.charWidth * 1.5) {
|
|
1406
1590
|
return null;
|
|
1407
1591
|
}
|
|
1592
|
+
|
|
1408
1593
|
return offset;
|
|
1409
1594
|
}
|
|
1410
1595
|
|
|
1411
1596
|
/* The visual line an offset is on, where a caret drawn at the end of a wrapped row counts as being on that row. */
|
|
1412
1597
|
rowOf(offset: number): { start: number; end: number } {
|
|
1413
1598
|
const { start, end } = this.layout.visualLine(offset === this.rowEndCaret ? offset - 1 : offset);
|
|
1599
|
+
|
|
1414
1600
|
return { start, end };
|
|
1415
1601
|
}
|
|
1416
1602
|
|
|
1417
1603
|
/* The lines that stand on one row of the screen: a collapsed fold is a row, from its first line to its last. */
|
|
1418
1604
|
lineSpan(line: number): { first: number; last: number } {
|
|
1419
1605
|
const row = this.layout.rowForLine(line);
|
|
1606
|
+
|
|
1420
1607
|
return row.kind === 'text' ? { first: row.line, last: row.lastLine } : { first: line, last: line };
|
|
1421
1608
|
}
|
|
1422
1609
|
|
|
@@ -1424,27 +1611,33 @@ export class EditorView {
|
|
|
1424
1611
|
refoldMoved(folded: readonly { startLine: number; endLine: number }[], moves: readonly { from: number; to: number }[]): void {
|
|
1425
1612
|
const moved = new Map(moves.map((move) => [move.from, move.to]));
|
|
1426
1613
|
this.refreshFolds();
|
|
1614
|
+
|
|
1427
1615
|
for (const range of this.foldRanges) {
|
|
1428
1616
|
if (moved.size > 0 && [...moved.values()].includes(range.startLine)) {
|
|
1429
1617
|
this.collapsed.delete(range.from);
|
|
1430
1618
|
}
|
|
1431
1619
|
}
|
|
1620
|
+
|
|
1432
1621
|
for (const fold of folded) {
|
|
1433
1622
|
const start = moved.get(fold.startLine);
|
|
1623
|
+
|
|
1434
1624
|
const target =
|
|
1435
1625
|
start === undefined
|
|
1436
1626
|
? undefined
|
|
1437
1627
|
: this.foldRanges.find((range) => range.startLine === start && range.endLine === start + fold.endLine - fold.startLine);
|
|
1628
|
+
|
|
1438
1629
|
if (target !== undefined) {
|
|
1439
1630
|
this.setCollapsed(target, true);
|
|
1440
1631
|
}
|
|
1441
1632
|
}
|
|
1633
|
+
|
|
1442
1634
|
this.refoldLayout();
|
|
1443
1635
|
}
|
|
1444
1636
|
|
|
1445
1637
|
/* A caret that went to the end of a wrapped row stays drawn there, though its offset is where the next row starts. */
|
|
1446
1638
|
markRowEnd(offset: number): void {
|
|
1447
1639
|
const line = this.model.getLine(this.model.positionAt(offset).line);
|
|
1640
|
+
|
|
1448
1641
|
if (offset > line.start && offset < line.end && this.layout.visualLine(offset).start === offset) {
|
|
1449
1642
|
this.rowEndCaret = offset;
|
|
1450
1643
|
this.requestRender();
|
|
@@ -1465,19 +1658,25 @@ export class EditorView {
|
|
|
1465
1658
|
const from = only === null ? 0 : Math.min(only.anchor, only.head);
|
|
1466
1659
|
const to = only === null ? 0 : Math.max(only.anchor, only.head);
|
|
1467
1660
|
const key = `${this.revision}|${from}|${to}`;
|
|
1661
|
+
|
|
1468
1662
|
if (this.selectionOccurrenceCache.key === key) {
|
|
1469
1663
|
return this.selectionOccurrenceCache.ranges;
|
|
1470
1664
|
}
|
|
1665
|
+
|
|
1471
1666
|
let ranges: { from: number; to: number }[] = [];
|
|
1472
1667
|
const text = to - from > 0 && to - from <= OCCURRENCE_TEXT_LIMIT ? this.model.slice(from, to) : '';
|
|
1668
|
+
|
|
1473
1669
|
if (text.trim() !== '' && !/[\r\n]/.test(text) && this.model.getLength() <= FOLD_LIMIT) {
|
|
1474
1670
|
const found = this.model.find(text, { maxResults: OCCURRENCE_LIMIT + 1 });
|
|
1671
|
+
|
|
1475
1672
|
ranges =
|
|
1476
1673
|
found.length > OCCURRENCE_LIMIT
|
|
1477
1674
|
? []
|
|
1478
1675
|
: found.filter((match) => match.from !== from || match.to !== to).map((match) => ({ from: match.from, to: match.to }));
|
|
1479
1676
|
}
|
|
1677
|
+
|
|
1480
1678
|
this.selectionOccurrenceCache = { key, ranges };
|
|
1679
|
+
|
|
1481
1680
|
return ranges;
|
|
1482
1681
|
}
|
|
1483
1682
|
|
|
@@ -1492,6 +1691,7 @@ export class EditorView {
|
|
|
1492
1691
|
private visibleTextBounds(row: TextRow): { from: number; to: number } {
|
|
1493
1692
|
const geometry = this.layout.geometry(row);
|
|
1494
1693
|
const band = subRowBand(row.top, geometry.subRows, this.layout.metrics.lineHeight, this.paintWindow);
|
|
1694
|
+
|
|
1495
1695
|
return {
|
|
1496
1696
|
from: geometry.start + offsetOfStop(geometry, geometry.rowStarts[band.first]!),
|
|
1497
1697
|
to:
|
|
@@ -1506,25 +1706,32 @@ export class EditorView {
|
|
|
1506
1706
|
if (!this.layout.wrapping) {
|
|
1507
1707
|
return [];
|
|
1508
1708
|
}
|
|
1709
|
+
|
|
1509
1710
|
const lineHeight = this.layout.metrics.lineHeight;
|
|
1510
1711
|
const signs: WrapSign[] = [];
|
|
1712
|
+
|
|
1511
1713
|
for (const row of rows) {
|
|
1512
1714
|
if (row.kind !== 'text' || row.subRows < 2) {
|
|
1513
1715
|
continue;
|
|
1514
1716
|
}
|
|
1717
|
+
|
|
1515
1718
|
const geometry = this.layout.geometry(row);
|
|
1516
1719
|
const band = subRowBand(row.top, geometry.subRows, lineHeight, this.paintWindow);
|
|
1720
|
+
|
|
1517
1721
|
for (let subRow = band.first; subRow <= band.last; subRow++) {
|
|
1518
1722
|
const y = row.top + subRow * lineHeight;
|
|
1723
|
+
|
|
1519
1724
|
if (subRow + 1 < geometry.subRows) {
|
|
1520
1725
|
const x = geometry.rowEnds[subRow]!;
|
|
1521
1726
|
signs.push({ x, y, width: this.viewportWidth - x });
|
|
1522
1727
|
}
|
|
1728
|
+
|
|
1523
1729
|
if (subRow > 0) {
|
|
1524
1730
|
signs.push({ x: 0, y, width: xOfStop(geometry, geometry.rowStarts[subRow]!) });
|
|
1525
1731
|
}
|
|
1526
1732
|
}
|
|
1527
1733
|
}
|
|
1734
|
+
|
|
1528
1735
|
return signs.filter((sign) => sign.width >= 3);
|
|
1529
1736
|
}
|
|
1530
1737
|
|
|
@@ -1532,35 +1739,45 @@ export class EditorView {
|
|
|
1532
1739
|
private indentOf(line: number): number | null {
|
|
1533
1740
|
const text = this.model.getLine(line).text;
|
|
1534
1741
|
const lead = /^[\t ]*/.exec(text)![0];
|
|
1742
|
+
|
|
1535
1743
|
return lead.length === text.length ? null : indentationColumn(lead, this.settings.tabSize);
|
|
1536
1744
|
}
|
|
1537
1745
|
|
|
1538
1746
|
/* The indentation a line is read at: a blank one takes the lesser of the lines around it, so a guide runs through it. */
|
|
1539
1747
|
private effectiveIndent(line: number): number {
|
|
1540
1748
|
const own = this.indentOf(line);
|
|
1749
|
+
|
|
1541
1750
|
if (own !== null) {
|
|
1542
1751
|
return own;
|
|
1543
1752
|
}
|
|
1753
|
+
|
|
1754
|
+
return Math.min(this.nearestIndent(line, -1, GUIDE_SCAN_LINES), this.nearestIndent(line, 1, GUIDE_SCAN_LINES));
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/* The indentation of the first line that is not blank within `reach` lines of `line` in `direction`, or 0. */
|
|
1758
|
+
private nearestIndent(line: number, direction: -1 | 1, reach: number): number {
|
|
1544
1759
|
const last = this.model.getLineCount() - 1;
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1760
|
+
|
|
1761
|
+
for (let at = line + direction, steps = 0; at >= 0 && at <= last && steps < reach; at += direction, steps++) {
|
|
1762
|
+
const indent = this.indentOf(at);
|
|
1763
|
+
|
|
1764
|
+
if (indent !== null) {
|
|
1765
|
+
return indent;
|
|
1551
1766
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
return
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
return 0;
|
|
1555
1770
|
}
|
|
1556
1771
|
|
|
1557
1772
|
/* The scope guide of the caret, worked out again only when the text or the caret's line is not the one it was for. */
|
|
1558
1773
|
private activeGuide(): { column: number; first: number; last: number } | null {
|
|
1559
1774
|
const line = this.model.positionAt(this.model.getPrimary().head).line;
|
|
1560
1775
|
const key = `${this.revision}|${line}|${this.settings.tabSize}`;
|
|
1776
|
+
|
|
1561
1777
|
if (this.activeGuideCache?.key !== key) {
|
|
1562
1778
|
this.activeGuideCache = { key, guide: this.scopeGuide(line) };
|
|
1563
1779
|
}
|
|
1780
|
+
|
|
1564
1781
|
return this.activeGuideCache.guide;
|
|
1565
1782
|
}
|
|
1566
1783
|
|
|
@@ -1569,39 +1786,33 @@ export class EditorView {
|
|
|
1569
1786
|
const size = this.settings.tabSize;
|
|
1570
1787
|
const indent = this.effectiveIndent(line);
|
|
1571
1788
|
const count = this.model.getLineCount();
|
|
1789
|
+
|
|
1572
1790
|
const deeper = (at: number, column: number): boolean => at >= 0 && at < count && this.effectiveIndent(at) > column;
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
return this.indentOf(at)!;
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
return 0;
|
|
1580
|
-
})();
|
|
1581
|
-
const previous = (() => {
|
|
1582
|
-
for (let at = line - 1; at >= 0 && at > line - GUIDE_SCAN_LINES; at--) {
|
|
1583
|
-
if (this.indentOf(at) !== null) {
|
|
1584
|
-
return this.indentOf(at)!;
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
return 0;
|
|
1588
|
-
})();
|
|
1791
|
+
|
|
1792
|
+
const next = this.nearestIndent(line, 1, GUIDE_SCAN_LINES - 1);
|
|
1793
|
+
const previous = this.nearestIndent(line, -1, GUIDE_SCAN_LINES - 1);
|
|
1589
1794
|
// A line that opens a block, or closes one, is at the edge of its guide: the one that sits at its own indentation.
|
|
1590
1795
|
const opens = indent % size === 0 && (next > indent || previous > indent) && this.indentOf(line) !== null;
|
|
1591
1796
|
const column = opens ? indent : indent > 0 ? Math.ceil(indent / size - 1) * size : -1;
|
|
1797
|
+
|
|
1592
1798
|
if (column < 0) {
|
|
1593
1799
|
return null;
|
|
1594
1800
|
}
|
|
1801
|
+
|
|
1595
1802
|
const reaches = (from: number, direction: -1 | 1): number => {
|
|
1596
1803
|
let at = from;
|
|
1804
|
+
|
|
1597
1805
|
while (deeper(at + direction, column) && Math.abs(at - line) < GUIDE_RUN_LINES) {
|
|
1598
1806
|
at += direction;
|
|
1599
1807
|
}
|
|
1808
|
+
|
|
1600
1809
|
return at;
|
|
1601
1810
|
};
|
|
1811
|
+
|
|
1602
1812
|
const inside = deeper(line, column);
|
|
1603
1813
|
const first = inside ? reaches(line, -1) : next > indent ? line + 1 : reaches(line - 1, -1);
|
|
1604
1814
|
const last = inside ? reaches(line, 1) : next > indent ? reaches(line + 1, 1) : line - 1;
|
|
1815
|
+
|
|
1605
1816
|
return { column, first, last };
|
|
1606
1817
|
}
|
|
1607
1818
|
|
|
@@ -1609,9 +1820,11 @@ export class EditorView {
|
|
|
1609
1820
|
private caretOnGuide(): { line: number; column: number } | null {
|
|
1610
1821
|
const position = this.model.positionAt(this.model.getPrimary().head);
|
|
1611
1822
|
const lead = this.model.getLine(position.line).text.slice(0, position.column);
|
|
1823
|
+
|
|
1612
1824
|
if (position.column === 0 || !/^[\t ]*$/.test(lead)) {
|
|
1613
1825
|
return null;
|
|
1614
1826
|
}
|
|
1827
|
+
|
|
1615
1828
|
return { line: position.line, column: indentationColumn(lead, this.settings.tabSize) };
|
|
1616
1829
|
}
|
|
1617
1830
|
|
|
@@ -1626,13 +1839,16 @@ export class EditorView {
|
|
|
1626
1839
|
const caret = this.caretOnGuide();
|
|
1627
1840
|
const plain: LayoutRect[] = [];
|
|
1628
1841
|
const marked: LayoutRect[] = [];
|
|
1842
|
+
|
|
1629
1843
|
interface Open {
|
|
1630
1844
|
top: number;
|
|
1631
1845
|
bottom: number;
|
|
1632
1846
|
first: number;
|
|
1633
1847
|
last: number;
|
|
1634
1848
|
}
|
|
1849
|
+
|
|
1635
1850
|
const open = new Map<number, Open>();
|
|
1851
|
+
|
|
1636
1852
|
const close = (column: number): void => {
|
|
1637
1853
|
const segment = open.get(column)!;
|
|
1638
1854
|
open.delete(column);
|
|
@@ -1641,21 +1857,27 @@ export class EditorView {
|
|
|
1641
1857
|
const under = caret !== null && caret.column === column && segment.first <= caret.line && caret.line <= segment.last;
|
|
1642
1858
|
(scope || under ? marked : plain).push(rect);
|
|
1643
1859
|
};
|
|
1860
|
+
|
|
1644
1861
|
for (const row of rows) {
|
|
1645
1862
|
if (row.kind !== 'text') {
|
|
1646
1863
|
for (const segment of open.values()) {
|
|
1647
1864
|
segment.bottom = row.top + row.height;
|
|
1648
1865
|
}
|
|
1866
|
+
|
|
1649
1867
|
continue;
|
|
1650
1868
|
}
|
|
1869
|
+
|
|
1651
1870
|
const indent = this.effectiveIndent(row.line);
|
|
1871
|
+
|
|
1652
1872
|
for (const column of [...open.keys()]) {
|
|
1653
1873
|
if (column >= indent) {
|
|
1654
1874
|
close(column);
|
|
1655
1875
|
}
|
|
1656
1876
|
}
|
|
1877
|
+
|
|
1657
1878
|
for (let column = 0; column < indent; column += size) {
|
|
1658
1879
|
const segment = open.get(column);
|
|
1880
|
+
|
|
1659
1881
|
if (segment === undefined) {
|
|
1660
1882
|
open.set(column, { top: row.top, bottom: row.top + row.height, first: row.line, last: row.lastLine });
|
|
1661
1883
|
} else {
|
|
@@ -1664,9 +1886,11 @@ export class EditorView {
|
|
|
1664
1886
|
}
|
|
1665
1887
|
}
|
|
1666
1888
|
}
|
|
1889
|
+
|
|
1667
1890
|
for (const column of [...open.keys()]) {
|
|
1668
1891
|
close(column);
|
|
1669
1892
|
}
|
|
1893
|
+
|
|
1670
1894
|
return { plain, active: marked };
|
|
1671
1895
|
}
|
|
1672
1896
|
|
|
@@ -1674,10 +1898,12 @@ export class EditorView {
|
|
|
1674
1898
|
selectionAtPoint(clientX: number, clientY: number): EditorSelection | null {
|
|
1675
1899
|
const point = this.contentPoint(clientX, clientY);
|
|
1676
1900
|
const rows = this.layout.visibleRows(this.viewport.scrollTop, this.viewportHeight, 0);
|
|
1901
|
+
|
|
1677
1902
|
return (
|
|
1678
1903
|
this.model.getSelections().find((selection) => {
|
|
1679
1904
|
const from = Math.min(selection.anchor, selection.head);
|
|
1680
1905
|
const to = Math.max(selection.anchor, selection.head);
|
|
1906
|
+
|
|
1681
1907
|
return (
|
|
1682
1908
|
from !== to &&
|
|
1683
1909
|
this.layout
|
|
@@ -1705,26 +1931,32 @@ export class EditorView {
|
|
|
1705
1931
|
'se-over-selection',
|
|
1706
1932
|
!this.settings.readOnly && event.buttons === 0 && this.selectionAtPoint(event.clientX, event.clientY) !== null
|
|
1707
1933
|
);
|
|
1934
|
+
|
|
1708
1935
|
this.setHover(event.buttons === 0 ? this.characterAtPoint(event.clientX, event.clientY, event.target) : null);
|
|
1709
1936
|
this.setInlayHover(event.buttons === 0 ? event.target : null);
|
|
1710
1937
|
}
|
|
1711
1938
|
|
|
1712
1939
|
/* The hint, and the part of its label, an event happened on, or null anywhere else. */
|
|
1713
1940
|
inlayAt(target: EventTarget | null): { id: string; part: number | null; element: HTMLElement } | null {
|
|
1714
|
-
const element = (target
|
|
1941
|
+
const element = closestElement(target, '.se-inlay');
|
|
1942
|
+
|
|
1715
1943
|
if (!element?.dataset.inlayId) {
|
|
1716
1944
|
return null;
|
|
1717
1945
|
}
|
|
1946
|
+
|
|
1718
1947
|
const part = (target as HTMLElement).closest<HTMLElement>('[data-part]');
|
|
1948
|
+
|
|
1719
1949
|
return { id: element.dataset.inlayId, part: part === null ? null : Number(part.dataset.part), element: part ?? element };
|
|
1720
1950
|
}
|
|
1721
1951
|
|
|
1722
1952
|
/* Says which hint the pointer is on, or that it left them all. */
|
|
1723
1953
|
setInlayHover(target: EventTarget | null): void {
|
|
1724
1954
|
const found = this.inlayAt(target);
|
|
1955
|
+
|
|
1725
1956
|
if (found?.id === this.inlayHover?.id && found?.part === this.inlayHover?.part) {
|
|
1726
1957
|
return;
|
|
1727
1958
|
}
|
|
1959
|
+
|
|
1728
1960
|
this.inlayHover = found === null ? null : { id: found.id, part: found.part };
|
|
1729
1961
|
emit(this.inlayHoverListeners, found === null ? null : { id: found.id, part: found.part, rect: found.element.getBoundingClientRect() });
|
|
1730
1962
|
}
|
|
@@ -1741,20 +1973,26 @@ export class EditorView {
|
|
|
1741
1973
|
|
|
1742
1974
|
private applyInlayLink(): void {
|
|
1743
1975
|
const target = this.inlayLink;
|
|
1976
|
+
|
|
1744
1977
|
if (target === null && this.inlayLinkElement === null) {
|
|
1745
1978
|
return;
|
|
1746
1979
|
}
|
|
1980
|
+
|
|
1747
1981
|
let element: HTMLElement | undefined;
|
|
1982
|
+
|
|
1748
1983
|
if (target !== null) {
|
|
1749
1984
|
const pill = Array.from(this.content.querySelectorAll<HTMLElement>('[data-inlay-id]')).find((candidate) => candidate.dataset.inlayId === target.id);
|
|
1985
|
+
|
|
1750
1986
|
element =
|
|
1751
1987
|
target.part === null
|
|
1752
1988
|
? pill
|
|
1753
1989
|
: (Array.from(pill?.querySelectorAll<HTMLElement>('[data-part]') ?? []).find((part) => part.dataset.part === String(target.part)) ?? pill);
|
|
1754
1990
|
}
|
|
1991
|
+
|
|
1755
1992
|
if (element === this.inlayLinkElement) {
|
|
1756
1993
|
return;
|
|
1757
1994
|
}
|
|
1995
|
+
|
|
1758
1996
|
this.inlayLinkElement?.classList.remove('se-inlay-link');
|
|
1759
1997
|
element?.classList.add('se-inlay-link');
|
|
1760
1998
|
this.inlayLinkElement = element ?? null;
|
|
@@ -1764,6 +2002,7 @@ export class EditorView {
|
|
|
1764
2002
|
if (offset === this.hoverOffset) {
|
|
1765
2003
|
return;
|
|
1766
2004
|
}
|
|
2005
|
+
|
|
1767
2006
|
this.hoverOffset = offset;
|
|
1768
2007
|
emit(this.hoverListeners, offset);
|
|
1769
2008
|
}
|
|
@@ -1772,6 +2011,7 @@ export class EditorView {
|
|
|
1772
2011
|
if (!Number.isInteger(line) || line < 0 || line >= this.model.getLineCount() || this.layout.isHidden(line)) {
|
|
1773
2012
|
return 0;
|
|
1774
2013
|
}
|
|
2014
|
+
|
|
1775
2015
|
return this.layout.rowForLine(line).height;
|
|
1776
2016
|
}
|
|
1777
2017
|
|
|
@@ -1783,11 +2023,14 @@ export class EditorView {
|
|
|
1783
2023
|
if (!Number.isFinite(top)) {
|
|
1784
2024
|
return;
|
|
1785
2025
|
}
|
|
2026
|
+
|
|
1786
2027
|
this.cancelScroll();
|
|
1787
2028
|
const next = Math.max(0, top);
|
|
2029
|
+
|
|
1788
2030
|
if (this.viewport.scrollTop === next) {
|
|
1789
2031
|
return;
|
|
1790
2032
|
}
|
|
2033
|
+
|
|
1791
2034
|
this.viewport.scrollTop = next;
|
|
1792
2035
|
this.render();
|
|
1793
2036
|
}
|
|
@@ -1798,9 +2041,11 @@ export class EditorView {
|
|
|
1798
2041
|
|
|
1799
2042
|
private announceView(): void {
|
|
1800
2043
|
const key = `${this.scrollTop}|${this.scrollLeft}|${this.clientWidth}|${this.clientHeight}|${this.layout.height}`;
|
|
2044
|
+
|
|
1801
2045
|
if (key === this.viewKey) {
|
|
1802
2046
|
return;
|
|
1803
2047
|
}
|
|
2048
|
+
|
|
1804
2049
|
this.viewKey = key;
|
|
1805
2050
|
emit(this.viewListeners);
|
|
1806
2051
|
}
|
|
@@ -1834,27 +2079,36 @@ export class EditorView {
|
|
|
1834
2079
|
const view = this.document.defaultView;
|
|
1835
2080
|
const distance = Math.hypot(x - this.viewport.scrollLeft, y - this.viewport.scrollTop);
|
|
1836
2081
|
const duration = scrollDuration(distance, this.layout.metrics.lineHeight);
|
|
2082
|
+
|
|
1837
2083
|
if (!animate || duration === 0 || view?.requestAnimationFrame === undefined || this.reducedMotion()) {
|
|
1838
2084
|
this.viewport.scrollTop = y;
|
|
1839
2085
|
this.viewport.scrollLeft = x;
|
|
2086
|
+
|
|
1840
2087
|
return;
|
|
1841
2088
|
}
|
|
2089
|
+
|
|
1842
2090
|
const run = { frame: 0, from: { x: this.viewport.scrollLeft, y: this.viewport.scrollTop }, to: { x, y }, start: null as number | null, duration };
|
|
2091
|
+
|
|
1843
2092
|
const step = (time: number): void => {
|
|
1844
2093
|
if (this.scrollRun !== run || this.disposed) {
|
|
1845
2094
|
return;
|
|
1846
2095
|
}
|
|
2096
|
+
|
|
1847
2097
|
run.start ??= time;
|
|
1848
2098
|
const fraction = Math.min(1, (time - run.start) / run.duration);
|
|
1849
2099
|
const eased = easeOut(fraction);
|
|
1850
2100
|
this.viewport.scrollTop = Math.round(run.from.y + (run.to.y - run.from.y) * eased);
|
|
1851
2101
|
this.viewport.scrollLeft = Math.round(run.from.x + (run.to.x - run.from.x) * eased);
|
|
2102
|
+
|
|
1852
2103
|
if (fraction >= 1) {
|
|
1853
2104
|
this.scrollRun = null;
|
|
2105
|
+
|
|
1854
2106
|
return;
|
|
1855
2107
|
}
|
|
2108
|
+
|
|
1856
2109
|
run.frame = view.requestAnimationFrame(step);
|
|
1857
2110
|
};
|
|
2111
|
+
|
|
1858
2112
|
this.scrollRun = run;
|
|
1859
2113
|
run.frame = view.requestAnimationFrame(step);
|
|
1860
2114
|
}
|
|
@@ -1873,6 +2127,7 @@ export class EditorView {
|
|
|
1873
2127
|
const place = this.scrollPlace;
|
|
1874
2128
|
let cover = this.stickyHeightAt(place.y);
|
|
1875
2129
|
let position = place;
|
|
2130
|
+
|
|
1876
2131
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1877
2132
|
position = scrollPosition(
|
|
1878
2133
|
{
|
|
@@ -1889,12 +2144,16 @@ export class EditorView {
|
|
|
1889
2144
|
},
|
|
1890
2145
|
kind
|
|
1891
2146
|
);
|
|
2147
|
+
|
|
1892
2148
|
const next = this.stickyHeightAt(position.y);
|
|
2149
|
+
|
|
1893
2150
|
if (next === cover) {
|
|
1894
2151
|
break;
|
|
1895
2152
|
}
|
|
2153
|
+
|
|
1896
2154
|
cover = next;
|
|
1897
2155
|
}
|
|
2156
|
+
|
|
1898
2157
|
this.scrollTo(position.x, position.y, animate);
|
|
1899
2158
|
this.requestRender();
|
|
1900
2159
|
}
|
|
@@ -1915,21 +2174,26 @@ export class EditorView {
|
|
|
1915
2174
|
const match = this.find.currentMatch;
|
|
1916
2175
|
const wanted = this.replacePreview;
|
|
1917
2176
|
let shown: string | null = null;
|
|
2177
|
+
|
|
1918
2178
|
if (wanted !== null && match?.regex === true) {
|
|
1919
2179
|
try {
|
|
1920
2180
|
if (wanted.text.length > 10000) {
|
|
1921
2181
|
throw new RangeError('The replacement preview is too large.');
|
|
1922
2182
|
}
|
|
2183
|
+
|
|
1923
2184
|
const text = replacementText(this.model.getText(), match, wanted.text, false, wanted.preserveCase, 10000);
|
|
1924
2185
|
shown = text === wanted.text ? null : text;
|
|
1925
2186
|
} catch {
|
|
1926
2187
|
shown = null;
|
|
1927
2188
|
}
|
|
1928
2189
|
}
|
|
2190
|
+
|
|
1929
2191
|
this.preview.hidden = shown === null;
|
|
2192
|
+
|
|
1930
2193
|
if (shown === null || match === null) {
|
|
1931
2194
|
return;
|
|
1932
2195
|
}
|
|
2196
|
+
|
|
1933
2197
|
const start = this.layout.caret(match.from);
|
|
1934
2198
|
const end = this.layout.caret(match.to);
|
|
1935
2199
|
this.preview.textContent = shown;
|
|
@@ -1940,9 +2204,11 @@ export class EditorView {
|
|
|
1940
2204
|
setFind(query: EditorFindQuery | null, reveal: boolean): void {
|
|
1941
2205
|
this.find.set(query);
|
|
1942
2206
|
this.announceFind();
|
|
2207
|
+
|
|
1943
2208
|
if (reveal) {
|
|
1944
2209
|
this.revealFind();
|
|
1945
2210
|
}
|
|
2211
|
+
|
|
1946
2212
|
this.render();
|
|
1947
2213
|
}
|
|
1948
2214
|
|
|
@@ -1963,6 +2229,7 @@ export class EditorView {
|
|
|
1963
2229
|
|
|
1964
2230
|
revealFind(): void {
|
|
1965
2231
|
const mark = this.find.currentMark;
|
|
2232
|
+
|
|
1966
2233
|
if (mark) {
|
|
1967
2234
|
this.revealOffset(mark.from, 'center');
|
|
1968
2235
|
}
|
|
@@ -1973,17 +2240,22 @@ export class EditorView {
|
|
|
1973
2240
|
if (this.disposed || this.lineTokenProvider !== null || !this.tokens.ready) {
|
|
1974
2241
|
return;
|
|
1975
2242
|
}
|
|
2243
|
+
|
|
1976
2244
|
clearTimeout(this.colorTimer);
|
|
1977
2245
|
const target = this.layout.rowAt(this.scrollTop + this.viewportHeight).line + COLOR_MARGIN;
|
|
2246
|
+
|
|
1978
2247
|
if (this.tokens.covers(target)) {
|
|
1979
2248
|
return;
|
|
1980
2249
|
}
|
|
2250
|
+
|
|
1981
2251
|
this.colorTimer = setTimeout(
|
|
1982
2252
|
() => {
|
|
1983
2253
|
const changed = this.tokens.advance(target, COLOR_SLICE_MS);
|
|
2254
|
+
|
|
1984
2255
|
if (changed) {
|
|
1985
2256
|
this.render();
|
|
1986
2257
|
}
|
|
2258
|
+
|
|
1987
2259
|
this.colorAhead();
|
|
1988
2260
|
},
|
|
1989
2261
|
immediate ? 0 : 1
|
|
@@ -1999,8 +2271,10 @@ export class EditorView {
|
|
|
1999
2271
|
this.lineNumberRevision = this.model.getRevision();
|
|
2000
2272
|
this.stickyEntries = [];
|
|
2001
2273
|
}
|
|
2274
|
+
|
|
2002
2275
|
const digits = this.lineNumberProvider === null ? String(this.model.getLineCount()).length : this.lineNumberWidth;
|
|
2003
2276
|
const width = Math.max(MIN_GUTTER_WIDTH, Math.ceil(digits * this.layout.metrics.charWidth) + 40);
|
|
2277
|
+
|
|
2004
2278
|
if (width !== this.gutterWidth) {
|
|
2005
2279
|
this.gutterWidth = width;
|
|
2006
2280
|
this.root.style.setProperty('--se-gutter-width', `${width}px`);
|
|
@@ -2012,11 +2286,15 @@ export class EditorView {
|
|
|
2012
2286
|
if (this.disposed || this.frame !== undefined) {
|
|
2013
2287
|
return;
|
|
2014
2288
|
}
|
|
2289
|
+
|
|
2015
2290
|
const view = this.document.defaultView;
|
|
2291
|
+
|
|
2016
2292
|
if (!view?.requestAnimationFrame) {
|
|
2017
2293
|
this.render();
|
|
2294
|
+
|
|
2018
2295
|
return;
|
|
2019
2296
|
}
|
|
2297
|
+
|
|
2020
2298
|
this.frame = view.requestAnimationFrame(() => {
|
|
2021
2299
|
this.frame = undefined;
|
|
2022
2300
|
this.render();
|
|
@@ -2038,26 +2316,34 @@ export class EditorView {
|
|
|
2038
2316
|
if (this.disposed || this.rendering) {
|
|
2039
2317
|
return;
|
|
2040
2318
|
}
|
|
2319
|
+
|
|
2041
2320
|
this.rendering = true;
|
|
2321
|
+
|
|
2042
2322
|
try {
|
|
2043
2323
|
this.syncGutter();
|
|
2044
2324
|
this.syncWrap();
|
|
2325
|
+
|
|
2045
2326
|
this.frameGeometry = {
|
|
2046
2327
|
top: this.viewport.scrollTop,
|
|
2047
2328
|
left: this.viewport.scrollLeft,
|
|
2048
2329
|
width: this.viewport.clientWidth,
|
|
2049
2330
|
height: this.viewport.clientHeight
|
|
2050
2331
|
};
|
|
2332
|
+
|
|
2051
2333
|
let rows = this.layout.visibleRows(this.scrollTop, this.viewportHeight, this.overscan);
|
|
2334
|
+
|
|
2052
2335
|
if (this.layout.syncRows(rows)) {
|
|
2053
2336
|
rows = this.layout.visibleRows(this.scrollTop, this.viewportHeight, this.overscan);
|
|
2054
2337
|
}
|
|
2338
|
+
|
|
2055
2339
|
const lastLine = rows.at(-1)?.line ?? 0;
|
|
2056
2340
|
const firstRow = this.layout.rowAt(this.scrollTop);
|
|
2057
2341
|
this.topAnchor = { offset: this.model.getLine(firstRow.line).start, delta: this.scrollTop - firstRow.top };
|
|
2342
|
+
|
|
2058
2343
|
if (this.lineTokenProvider === null && lastLine - this.tokens.colored <= COLOR_MARGIN * 4) {
|
|
2059
2344
|
this.tokens.advance(lastLine, FIRST_PAINT_MS);
|
|
2060
2345
|
}
|
|
2346
|
+
|
|
2061
2347
|
const paint = {
|
|
2062
2348
|
layoutVersion: this.layoutVersion,
|
|
2063
2349
|
whitespace: this.settings.whitespace,
|
|
@@ -2068,10 +2354,12 @@ export class EditorView {
|
|
|
2068
2354
|
hostWidth: this.viewportWidth + this.gutterWidth,
|
|
2069
2355
|
onUnfold: (line: number) => this.toggleFold(line, false)
|
|
2070
2356
|
};
|
|
2357
|
+
|
|
2071
2358
|
if (this.painter.paint(rows, paint)) {
|
|
2072
2359
|
rows = this.layout.visibleRows(this.scrollTop, this.viewportHeight, this.overscan);
|
|
2073
2360
|
this.painter.paint(rows, paint);
|
|
2074
2361
|
}
|
|
2362
|
+
|
|
2075
2363
|
this.applyInlayLink();
|
|
2076
2364
|
this.content.style.height = `${this.layout.height}px`;
|
|
2077
2365
|
this.content.style.width = `${Math.max(this.layout.width, this.viewportWidth)}px`;
|
|
@@ -2082,9 +2370,11 @@ export class EditorView {
|
|
|
2082
2370
|
this.paintOverview();
|
|
2083
2371
|
this.paintRemote();
|
|
2084
2372
|
this.paintLineActions();
|
|
2373
|
+
|
|
2085
2374
|
if (this.attributionPointer !== null) {
|
|
2086
2375
|
this.refreshAttributionHover();
|
|
2087
2376
|
}
|
|
2377
|
+
|
|
2088
2378
|
this.announceScope();
|
|
2089
2379
|
this.announceView();
|
|
2090
2380
|
this.colorAhead();
|
|
@@ -2092,6 +2382,7 @@ export class EditorView {
|
|
|
2092
2382
|
this.frameGeometry = null;
|
|
2093
2383
|
this.rendering = false;
|
|
2094
2384
|
}
|
|
2385
|
+
|
|
2095
2386
|
if (!this.disposed && this.announcedLineHeights !== this.layout.lineHeightsVersion) {
|
|
2096
2387
|
this.announcedLineHeights = this.layout.lineHeightsVersion;
|
|
2097
2388
|
// Hosts may adjust rows in response; rendering must already have finished.
|
|
@@ -2104,19 +2395,22 @@ export class EditorView {
|
|
|
2104
2395
|
const primary = this.model.getPrimary();
|
|
2105
2396
|
const focused = this.focused;
|
|
2106
2397
|
const foldable = new Map<number, boolean>();
|
|
2398
|
+
|
|
2107
2399
|
for (const range of this.foldRanges) {
|
|
2108
2400
|
foldable.set(range.startLine, (foldable.get(range.startLine) ?? false) || this.collapsed.has(range.from));
|
|
2109
2401
|
}
|
|
2402
|
+
|
|
2110
2403
|
const lastRow = rows.at(-1);
|
|
2111
2404
|
const firstLine = rows[0]?.line ?? 0;
|
|
2112
2405
|
const lastLine = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
|
|
2113
|
-
const highlighted = this.
|
|
2406
|
+
const highlighted = this.runLines(this.highlights, firstLine, lastLine);
|
|
2114
2407
|
const shownFrom = this.model.offsetAt({ line: firstLine, column: 0 });
|
|
2115
2408
|
const shownTo = lastLine + 1 < this.model.getLineCount() ? this.model.offsetAt({ line: lastLine + 1, column: 0 }) : this.model.getLength();
|
|
2409
|
+
|
|
2116
2410
|
paintGutter(this.gutterLines, this.layout, rows, {
|
|
2117
2411
|
lineNumber: (line) => this.lineNumberLabel(line),
|
|
2118
|
-
changes: this.changedLines(
|
|
2119
|
-
attribution: this.
|
|
2412
|
+
changes: this.changedLines(firstLine, lastLine),
|
|
2413
|
+
attribution: this.runLines(this.attribution, firstLine, lastLine),
|
|
2120
2414
|
highlights: highlighted,
|
|
2121
2415
|
activeLines: new Set(
|
|
2122
2416
|
selections
|
|
@@ -2129,14 +2423,18 @@ export class EditorView {
|
|
|
2129
2423
|
action: this.gutterAction,
|
|
2130
2424
|
markers: this.markersByLine()
|
|
2131
2425
|
});
|
|
2426
|
+
|
|
2132
2427
|
const marks: { className: string; rects: LayoutRect[]; background?: string }[] = this.highlightMarks(rows, highlighted);
|
|
2133
2428
|
const row = this.layout.rowForLine(this.model.positionAt(primary.head).line);
|
|
2429
|
+
|
|
2134
2430
|
const currentLine =
|
|
2135
2431
|
primary.anchor === primary.head && row.kind === 'text'
|
|
2136
2432
|
? { x: 0, y: row.top, width: Math.max(this.layout.width, this.viewportWidth), height: row.height }
|
|
2137
2433
|
: null;
|
|
2434
|
+
|
|
2138
2435
|
for (const kind of ['text', 'read', 'write'] as const) {
|
|
2139
2436
|
const group = this.occurrences.filter((mark) => (mark.kind ?? 'text') === kind);
|
|
2437
|
+
|
|
2140
2438
|
if (group.length > 0) {
|
|
2141
2439
|
marks.push({
|
|
2142
2440
|
className: kind === 'write' ? 'se-occurrence se-occurrence-write' : 'se-occurrence',
|
|
@@ -2144,42 +2442,55 @@ export class EditorView {
|
|
|
2144
2442
|
});
|
|
2145
2443
|
}
|
|
2146
2444
|
}
|
|
2445
|
+
|
|
2147
2446
|
const marginX = this.settings.rightMargin === null ? null : this.settings.rightMargin * this.layout.metrics.charWidth;
|
|
2447
|
+
|
|
2148
2448
|
// A margin past the content would stretch the scroll extent to itself, so it shows only where the text or the view reaches it.
|
|
2149
2449
|
if (marginX !== null && marginX < Math.max(this.layout.width, this.viewportWidth)) {
|
|
2150
2450
|
marks.push({ className: 'se-margin', rects: [{ x: marginX, y: 0, width: 1, height: this.layout.height }] });
|
|
2151
2451
|
}
|
|
2452
|
+
|
|
2152
2453
|
if (this.settings.guides) {
|
|
2153
2454
|
const guides = this.guideRects(rows);
|
|
2154
2455
|
marks.push({ className: 'se-guide', rects: guides.plain }, { className: 'se-guide se-guide-active', rects: guides.active });
|
|
2155
2456
|
}
|
|
2457
|
+
|
|
2156
2458
|
paintSigns(this.signs, this.wrapSigns(rows), this.layout.metrics.lineHeight);
|
|
2157
2459
|
const alike = this.selectionOccurrences();
|
|
2460
|
+
|
|
2158
2461
|
if (alike.length > 0) {
|
|
2159
2462
|
marks.push({ className: 'se-occurrence', rects: alike.flatMap((range) => this.paintRectangles(range.from, range.to, rows)) });
|
|
2160
2463
|
}
|
|
2464
|
+
|
|
2161
2465
|
const faded: LayoutRect[] = [];
|
|
2162
2466
|
const struck: LayoutRect[] = [];
|
|
2467
|
+
|
|
2163
2468
|
for (const marker of this.markers) {
|
|
2164
2469
|
if (marker.to <= marker.from) {
|
|
2165
2470
|
continue;
|
|
2166
2471
|
}
|
|
2472
|
+
|
|
2167
2473
|
const rects = this.paintRectangles(marker.from, marker.to, rows);
|
|
2474
|
+
|
|
2168
2475
|
if (marker.severity !== 'hint') {
|
|
2169
2476
|
marks.push({ className: `se-squiggle se-squiggle-${marker.severity}`, rects });
|
|
2170
2477
|
}
|
|
2478
|
+
|
|
2171
2479
|
if (marker.unnecessary) {
|
|
2172
2480
|
faded.push(...rects);
|
|
2173
2481
|
}
|
|
2482
|
+
|
|
2174
2483
|
if (marker.deprecated) {
|
|
2175
2484
|
struck.push(...rects);
|
|
2176
2485
|
}
|
|
2177
2486
|
}
|
|
2487
|
+
|
|
2178
2488
|
const current = this.find.currentMark;
|
|
2179
2489
|
const textRows = rows.filter((candidate) => candidate.kind === 'text');
|
|
2180
2490
|
const visibleFrom = textRows.length > 0 ? this.visibleTextBounds(textRows[0]!).from : 0;
|
|
2181
2491
|
const visibleTo = textRows.length > 0 ? this.visibleTextBounds(textRows.at(-1)!).to : 0;
|
|
2182
2492
|
const found = this.find.matchesBetween(visibleFrom, visibleTo);
|
|
2493
|
+
|
|
2183
2494
|
const spans =
|
|
2184
2495
|
current !== null &&
|
|
2185
2496
|
!found.some((mark) => mark.from === current.from && mark.to === current.to) &&
|
|
@@ -2187,6 +2498,7 @@ export class EditorView {
|
|
|
2187
2498
|
current.from <= visibleTo
|
|
2188
2499
|
? [...found, current]
|
|
2189
2500
|
: found;
|
|
2501
|
+
|
|
2190
2502
|
for (const mark of spans) {
|
|
2191
2503
|
if (mark.to > mark.from) {
|
|
2192
2504
|
marks.push({
|
|
@@ -2195,28 +2507,39 @@ export class EditorView {
|
|
|
2195
2507
|
});
|
|
2196
2508
|
}
|
|
2197
2509
|
}
|
|
2510
|
+
|
|
2198
2511
|
const seen = new Set<number>();
|
|
2512
|
+
|
|
2199
2513
|
for (const [index, selection] of selections.entries()) {
|
|
2200
2514
|
// A caret off screen has no bracket to draw, except the primary one, whose mate may be on screen.
|
|
2201
2515
|
if (index < selections.length - 1 && (selection.head < visibleFrom - 1 || selection.head > visibleTo + 1)) {
|
|
2202
2516
|
continue;
|
|
2203
2517
|
}
|
|
2204
|
-
|
|
2518
|
+
|
|
2519
|
+
const at = this.bracketAt(selection);
|
|
2520
|
+
|
|
2205
2521
|
if (at === undefined) {
|
|
2206
2522
|
continue;
|
|
2207
2523
|
}
|
|
2524
|
+
|
|
2208
2525
|
const match = this.model.matchBracket(at, this.settings.language, BRACKET_FRAME_LIMIT);
|
|
2526
|
+
|
|
2209
2527
|
if (match === null) {
|
|
2210
2528
|
this.scheduleBrackets();
|
|
2529
|
+
|
|
2211
2530
|
break;
|
|
2212
2531
|
}
|
|
2532
|
+
|
|
2213
2533
|
if (match === undefined) {
|
|
2214
2534
|
continue;
|
|
2215
2535
|
}
|
|
2536
|
+
|
|
2216
2537
|
if (match.mate === undefined) {
|
|
2217
2538
|
marks.push({ className: 'se-bracket-unmatched', rects: this.paintRectangles(at, at + 1, rows) });
|
|
2539
|
+
|
|
2218
2540
|
continue;
|
|
2219
2541
|
}
|
|
2542
|
+
|
|
2220
2543
|
for (const offset of [at, match.mate]) {
|
|
2221
2544
|
if (!seen.has(offset)) {
|
|
2222
2545
|
seen.add(offset);
|
|
@@ -2224,30 +2547,36 @@ export class EditorView {
|
|
|
2224
2547
|
}
|
|
2225
2548
|
}
|
|
2226
2549
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
: this.paintRectangles(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), rows)
|
|
2550
|
+
|
|
2551
|
+
const selected = selections.flatMap(({ anchor, head }) =>
|
|
2552
|
+
anchor === head ? [] : this.paintRectangles(Math.min(anchor, head), Math.max(anchor, head), rows)
|
|
2231
2553
|
);
|
|
2554
|
+
|
|
2232
2555
|
paintOverlays(this.overlays, { currentLine, selections: selected, focused, marks });
|
|
2556
|
+
|
|
2233
2557
|
paintOver(this.over, [
|
|
2234
2558
|
{ className: 'se-faded', rects: faded },
|
|
2235
2559
|
{ className: 'se-struck', rects: struck },
|
|
2236
2560
|
{ className: 'se-link', rects: this.link === null ? [] : this.paintRectangles(this.link.from, this.link.to, rows) }
|
|
2237
2561
|
]);
|
|
2562
|
+
|
|
2238
2563
|
const top = this.scrollTop;
|
|
2564
|
+
|
|
2239
2565
|
const caretRects = focused
|
|
2240
2566
|
? selections
|
|
2241
2567
|
.map((selection) => ({ ...this.caretOf(selection.head), primary: selection === selections.at(-1) }))
|
|
2242
2568
|
.filter((rect) => rect.y + rect.height >= top - rect.height && rect.y <= top + this.viewportHeight)
|
|
2243
2569
|
: [];
|
|
2570
|
+
|
|
2244
2571
|
const caretKey = caretRects.map((rect) => `${rect.x},${rect.y}`).join(' ');
|
|
2572
|
+
|
|
2245
2573
|
if (caretKey !== this.caretKey) {
|
|
2246
2574
|
this.caretKey = caretKey;
|
|
2247
2575
|
this.carets.dataset.moving = 'true';
|
|
2248
2576
|
clearTimeout(this.caretTimer);
|
|
2249
2577
|
this.caretTimer = setTimeout(() => delete this.carets.dataset.moving, CARET_SOLID_MS);
|
|
2250
2578
|
}
|
|
2579
|
+
|
|
2251
2580
|
paintCarets(this.carets, [...caretRects, ...(this.dropCaret === null ? [] : [{ ...this.layout.caret(this.dropCaret), primary: false, drop: true }])]);
|
|
2252
2581
|
const caret = this.caretOf(primary.head);
|
|
2253
2582
|
this.input.style.left = `${this.gutterWidth + Math.max(0, Math.min(this.viewportWidth - 2, caret.x - this.scrollLeft))}px`;
|
|
@@ -2262,7 +2591,9 @@ export class EditorView {
|
|
|
2262
2591
|
*/
|
|
2263
2592
|
setChangeMarks(marks: readonly EditorChangeMark[], layer = ''): void {
|
|
2264
2593
|
const last = this.model.getLineCount();
|
|
2594
|
+
|
|
2265
2595
|
const clamp = (line: number): number => Math.min(last, Math.max(1, Math.trunc(line) || 1)) - 1;
|
|
2596
|
+
|
|
2266
2597
|
if (marks.length === 0) {
|
|
2267
2598
|
this.changeLayers.delete(layer);
|
|
2268
2599
|
} else {
|
|
@@ -2270,6 +2601,7 @@ export class EditorView {
|
|
|
2270
2601
|
layer,
|
|
2271
2602
|
marks.map((mark) => {
|
|
2272
2603
|
const from = this.model.getLine(clamp(mark.startLine)).start;
|
|
2604
|
+
|
|
2273
2605
|
return {
|
|
2274
2606
|
kind: mark.kind,
|
|
2275
2607
|
from,
|
|
@@ -2278,6 +2610,7 @@ export class EditorView {
|
|
|
2278
2610
|
})
|
|
2279
2611
|
);
|
|
2280
2612
|
}
|
|
2613
|
+
|
|
2281
2614
|
this.changeVersion++;
|
|
2282
2615
|
this.render();
|
|
2283
2616
|
}
|
|
@@ -2290,49 +2623,58 @@ export class EditorView {
|
|
|
2290
2623
|
if (this.changeCache?.version === this.changeVersion) {
|
|
2291
2624
|
return this.changeCache;
|
|
2292
2625
|
}
|
|
2626
|
+
|
|
2293
2627
|
const lines = new Map<number, EditorChangeKind>();
|
|
2628
|
+
|
|
2294
2629
|
for (const marks of this.changeLayers.values()) {
|
|
2295
2630
|
const layer = new Map<number, EditorChangeKind>();
|
|
2631
|
+
|
|
2296
2632
|
for (const mark of marks) {
|
|
2297
2633
|
const start = this.model.positionAt(mark.from).line;
|
|
2298
2634
|
const end = mark.kind === 'deleted' ? start : this.model.positionAt(Math.max(mark.from, mark.to)).line;
|
|
2635
|
+
|
|
2299
2636
|
for (let line = start; line <= end; line++) {
|
|
2300
2637
|
if (mark.kind !== 'deleted' || !layer.has(line)) {
|
|
2301
2638
|
layer.set(line, mark.kind);
|
|
2302
2639
|
}
|
|
2303
2640
|
}
|
|
2304
2641
|
}
|
|
2642
|
+
|
|
2305
2643
|
for (const [line, kind] of layer) {
|
|
2306
2644
|
lines.set(line, kind);
|
|
2307
2645
|
}
|
|
2308
2646
|
}
|
|
2647
|
+
|
|
2309
2648
|
const runs: { kind: EditorChangeKind; start: number; end: number }[] = [];
|
|
2649
|
+
|
|
2310
2650
|
for (const line of [...lines.keys()].sort((left, right) => left - right)) {
|
|
2311
2651
|
const kind = lines.get(line)!;
|
|
2312
2652
|
const run = runs.at(-1);
|
|
2653
|
+
|
|
2313
2654
|
if (run !== undefined && kind !== 'deleted' && run.kind === kind && run.end === line - 1) {
|
|
2314
2655
|
run.end = line;
|
|
2315
2656
|
} else {
|
|
2316
2657
|
runs.push({ kind, start: line, end: line });
|
|
2317
2658
|
}
|
|
2318
2659
|
}
|
|
2660
|
+
|
|
2319
2661
|
this.changeCache = { version: this.changeVersion, lines, runs };
|
|
2662
|
+
|
|
2320
2663
|
return this.changeCache;
|
|
2321
2664
|
}
|
|
2322
2665
|
|
|
2323
|
-
|
|
2324
|
-
private changedLines(rows: readonly LayoutRow[]): Map<number, EditorChangeKind> {
|
|
2666
|
+
private changedLines(first: number, last: number): Map<number, EditorChangeKind> {
|
|
2325
2667
|
const { lines } = this.resolvedChanges();
|
|
2326
|
-
const first = rows[0]?.line ?? 0;
|
|
2327
|
-
const lastRow = rows.at(-1);
|
|
2328
|
-
const last = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
|
|
2329
2668
|
const visible = new Map<number, EditorChangeKind>();
|
|
2669
|
+
|
|
2330
2670
|
for (let line = first; line <= last; line++) {
|
|
2331
2671
|
const kind = lines.get(line);
|
|
2672
|
+
|
|
2332
2673
|
if (kind !== undefined) {
|
|
2333
2674
|
visible.set(line, kind);
|
|
2334
2675
|
}
|
|
2335
2676
|
}
|
|
2677
|
+
|
|
2336
2678
|
return visible;
|
|
2337
2679
|
}
|
|
2338
2680
|
|
|
@@ -2348,21 +2690,28 @@ export class EditorView {
|
|
|
2348
2690
|
const top = this.scrollTop;
|
|
2349
2691
|
const cover = this.stickyHeightAt(top);
|
|
2350
2692
|
const carets: RemoteCaret[] = [];
|
|
2693
|
+
|
|
2351
2694
|
for (const remote of this.remoteCarets) {
|
|
2352
2695
|
const line = this.model.positionAt(remote.at).line;
|
|
2353
2696
|
const row = this.layout.rowForLine(line);
|
|
2697
|
+
|
|
2354
2698
|
if (row.kind !== 'text' || row.line !== line) {
|
|
2355
2699
|
continue;
|
|
2356
2700
|
}
|
|
2701
|
+
|
|
2357
2702
|
const place = this.layout.caret(remote.at);
|
|
2358
2703
|
const x = Math.round(place.x);
|
|
2359
2704
|
const y = Math.round(place.y);
|
|
2705
|
+
|
|
2360
2706
|
if (y + lineHeight < top - REMOTE_LABEL_HEIGHT || y > top + this.viewportHeight) {
|
|
2361
2707
|
continue;
|
|
2362
2708
|
}
|
|
2709
|
+
|
|
2363
2710
|
carets.push({ id: remote.id, name: remote.name, color: remote.color, x, y, height: lineHeight, below: y - top - cover < REMOTE_LABEL_HEIGHT });
|
|
2364
2711
|
}
|
|
2712
|
+
|
|
2365
2713
|
const key = carets.map((caret) => `${caret.id}|${caret.name}|${caret.color}|${caret.x},${caret.y},${caret.below}`).join('\n');
|
|
2714
|
+
|
|
2366
2715
|
if (key !== this.remoteKey) {
|
|
2367
2716
|
this.remoteKey = key;
|
|
2368
2717
|
paintRemoteCarets(this.remote, carets);
|
|
@@ -2375,27 +2724,33 @@ export class EditorView {
|
|
|
2375
2724
|
if (!this.lineActions.has(owner)) {
|
|
2376
2725
|
return;
|
|
2377
2726
|
}
|
|
2727
|
+
|
|
2378
2728
|
this.lineActions.delete(owner);
|
|
2379
2729
|
} else {
|
|
2380
2730
|
this.lineActions.set(owner, [...actions]);
|
|
2381
2731
|
}
|
|
2732
|
+
|
|
2382
2733
|
const wanted = new Set([...this.lineActions].flatMap(([name, entries]) => entries.map((entry) => `${name}\0${entry.id}`)));
|
|
2734
|
+
|
|
2383
2735
|
for (const [key, element] of [...this.actionElements]) {
|
|
2384
2736
|
if (!wanted.has(key)) {
|
|
2385
2737
|
element.remove();
|
|
2386
2738
|
this.actionElements.delete(key);
|
|
2387
2739
|
}
|
|
2388
2740
|
}
|
|
2741
|
+
|
|
2389
2742
|
this.render();
|
|
2390
2743
|
}
|
|
2391
2744
|
|
|
2392
2745
|
private paintLineActions(): void {
|
|
2393
2746
|
const lineHeight = this.layout.metrics.lineHeight;
|
|
2394
2747
|
const top = this.scrollTop;
|
|
2748
|
+
|
|
2395
2749
|
for (const [owner, entries] of this.lineActions) {
|
|
2396
2750
|
for (const entry of entries) {
|
|
2397
2751
|
const key = `${owner}\0${entry.id}`;
|
|
2398
2752
|
let element = this.actionElements.get(key);
|
|
2753
|
+
|
|
2399
2754
|
if (element === undefined) {
|
|
2400
2755
|
element = this.document.createElement('div');
|
|
2401
2756
|
element.className = 'se-line-action';
|
|
@@ -2405,13 +2760,17 @@ export class EditorView {
|
|
|
2405
2760
|
this.actionElements.set(key, element);
|
|
2406
2761
|
entry.render(element);
|
|
2407
2762
|
}
|
|
2763
|
+
|
|
2408
2764
|
const line = this.model.positionAt(entry.at).line;
|
|
2409
2765
|
const row = this.layout.rowForLine(line);
|
|
2410
2766
|
const place = row.kind === 'text' && row.line === line ? this.layout.caret(this.model.getLine(line).end) : null;
|
|
2767
|
+
|
|
2411
2768
|
if (place === null || place.y + lineHeight < top - this.viewportHeight || place.y > top + 2 * this.viewportHeight) {
|
|
2412
2769
|
element.hidden = true;
|
|
2770
|
+
|
|
2413
2771
|
continue;
|
|
2414
2772
|
}
|
|
2773
|
+
|
|
2415
2774
|
element.hidden = false;
|
|
2416
2775
|
element.style.left = `${Math.round(place.x) + LINE_ACTION_GAP}px`;
|
|
2417
2776
|
element.style.top = `${Math.round(place.y)}px`;
|
|
@@ -2433,17 +2792,12 @@ export class EditorView {
|
|
|
2433
2792
|
this.model.getLineCount(),
|
|
2434
2793
|
(line) => this.model.getLine(line)
|
|
2435
2794
|
);
|
|
2795
|
+
|
|
2436
2796
|
this.render();
|
|
2437
2797
|
}
|
|
2438
2798
|
|
|
2439
|
-
private
|
|
2440
|
-
return this.
|
|
2441
|
-
first,
|
|
2442
|
-
last,
|
|
2443
|
-
this.model.getLine(first).start,
|
|
2444
|
-
this.model.getLine(last).end,
|
|
2445
|
-
(offset) => this.model.positionAt(offset).line
|
|
2446
|
-
);
|
|
2799
|
+
private runLines(runs: AttributionRuns, first: number, last: number): Map<number, AttributedLines> {
|
|
2800
|
+
return runs.linesIn(first, last, this.model.getLine(first).start, this.model.getLine(last).end, (offset) => this.model.positionAt(offset).line);
|
|
2447
2801
|
}
|
|
2448
2802
|
|
|
2449
2803
|
/* One box per row, so a row of the host's between two tinted lines stays clear. */
|
|
@@ -2454,24 +2808,30 @@ export class EditorView {
|
|
|
2454
2808
|
const byBackground = new Map<string, LayoutRect[]>();
|
|
2455
2809
|
const width = Math.max(this.layout.width, this.viewportWidth);
|
|
2456
2810
|
const emphasisByColor = new Map<string, LayoutRect[]>();
|
|
2811
|
+
|
|
2457
2812
|
for (const row of rows) {
|
|
2458
2813
|
const highlight = row.kind === 'text' ? lines.get(row.line) : undefined;
|
|
2814
|
+
|
|
2459
2815
|
if (highlight !== undefined) {
|
|
2460
2816
|
const background = highlight.fill === undefined ? tintValue(highlight.color) : colorValue(highlight.fill);
|
|
2461
2817
|
const rects = byBackground.get(background) ?? [];
|
|
2462
2818
|
rects.push({ x: 0, y: row.top, width, height: row.height });
|
|
2463
2819
|
byBackground.set(background, rects);
|
|
2820
|
+
|
|
2464
2821
|
if (row.kind === 'text' && highlight.emphasis !== undefined) {
|
|
2465
2822
|
const bounds = this.model.getLine(row.line);
|
|
2466
2823
|
const emphasisBackground = `color-mix(in srgb, ${colorValue(highlight.color)} 22%, transparent)`;
|
|
2467
2824
|
const emphasized = emphasisByColor.get(emphasisBackground) ?? [];
|
|
2825
|
+
|
|
2468
2826
|
for (const range of highlight.emphasis) {
|
|
2469
2827
|
emphasized.push(...this.paintRectangles(Math.max(bounds.start, range.from), Math.min(bounds.end, range.to), [row]));
|
|
2470
2828
|
}
|
|
2829
|
+
|
|
2471
2830
|
emphasisByColor.set(emphasisBackground, emphasized);
|
|
2472
2831
|
}
|
|
2473
2832
|
}
|
|
2474
2833
|
}
|
|
2834
|
+
|
|
2475
2835
|
return [
|
|
2476
2836
|
...[...byBackground].map(([background, rects]) => ({ className: 'se-line-highlight', rects, background })),
|
|
2477
2837
|
...[...emphasisByColor].map(([background, rects]) => ({ className: 'se-line-emphasis', rects, background }))
|
|
@@ -2489,16 +2849,6 @@ export class EditorView {
|
|
|
2489
2849
|
);
|
|
2490
2850
|
}
|
|
2491
2851
|
|
|
2492
|
-
private attributedLines(first: number, last: number): Map<number, AttributedLines> {
|
|
2493
|
-
return this.attribution.linesIn(
|
|
2494
|
-
first,
|
|
2495
|
-
last,
|
|
2496
|
-
this.model.getLine(first).start,
|
|
2497
|
-
this.model.getLine(last).end,
|
|
2498
|
-
(offset) => this.model.positionAt(offset).line
|
|
2499
|
-
);
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
2852
|
onAttributionHover(listener: (hover: { id: string; rect: EditorRect } | null) => void): () => void {
|
|
2503
2853
|
return subscribe(this.attributionListeners, listener);
|
|
2504
2854
|
}
|
|
@@ -2509,6 +2859,7 @@ export class EditorView {
|
|
|
2509
2859
|
this.attributionPointer = { x: event.clientX, y: event.clientY };
|
|
2510
2860
|
this.refreshAttributionHover();
|
|
2511
2861
|
});
|
|
2862
|
+
|
|
2512
2863
|
this.gutter.addEventListener('pointerleave', () => {
|
|
2513
2864
|
this.attributionPointer = null;
|
|
2514
2865
|
this.refreshAttributionHover();
|
|
@@ -2518,9 +2869,11 @@ export class EditorView {
|
|
|
2518
2869
|
private refreshAttributionHover(): void {
|
|
2519
2870
|
const next = this.attributionPointer === null ? null : this.attributionUnder(this.attributionPointer.x, this.attributionPointer.y);
|
|
2520
2871
|
const before = this.attributionHover;
|
|
2872
|
+
|
|
2521
2873
|
if (before === next || (before !== null && next !== null && before.id === next.id && sameRect(before.rect, next.rect))) {
|
|
2522
2874
|
return;
|
|
2523
2875
|
}
|
|
2876
|
+
|
|
2524
2877
|
this.attributionHover = next;
|
|
2525
2878
|
emit(this.attributionListeners, next);
|
|
2526
2879
|
}
|
|
@@ -2529,51 +2882,67 @@ export class EditorView {
|
|
|
2529
2882
|
if (this.attribution.size === 0) {
|
|
2530
2883
|
return null;
|
|
2531
2884
|
}
|
|
2885
|
+
|
|
2532
2886
|
const point = this.contentPoint(clientX, clientY);
|
|
2533
2887
|
const fromEdge = this.gutterWidth - (point.x + this.gutterWidth - this.viewport.scrollLeft);
|
|
2888
|
+
|
|
2534
2889
|
if (fromEdge < -ATTRIBUTION_REACH || fromEdge > ATTRIBUTION_REACH + ATTRIBUTION_WIDTH || point.y < 0 || point.y > this.layout.height) {
|
|
2535
2890
|
return null;
|
|
2536
2891
|
}
|
|
2892
|
+
|
|
2537
2893
|
const row = this.layout.rowAt(point.y);
|
|
2894
|
+
|
|
2538
2895
|
if (row.kind !== 'text') {
|
|
2539
2896
|
return null;
|
|
2540
2897
|
}
|
|
2541
|
-
|
|
2898
|
+
|
|
2899
|
+
const bar = this.runLines(this.attribution, row.line, row.line).get(row.line);
|
|
2900
|
+
|
|
2542
2901
|
if (bar === undefined) {
|
|
2543
2902
|
return null;
|
|
2544
2903
|
}
|
|
2904
|
+
|
|
2545
2905
|
const origin = this.viewport.getBoundingClientRect?.();
|
|
2546
2906
|
const scale = origin ? this.screenScale(origin) : { x: 1, y: 1 };
|
|
2547
2907
|
const right = (origin?.left ?? 0) + this.gutterWidth * scale.x;
|
|
2548
2908
|
const top = (origin?.top ?? 0) + (row.top - this.viewport.scrollTop) * scale.y;
|
|
2909
|
+
|
|
2549
2910
|
return { id: bar.id, rect: { left: right - ATTRIBUTION_WIDTH * scale.x, top, right, bottom: top + row.height * scale.y } };
|
|
2550
2911
|
}
|
|
2551
2912
|
|
|
2552
2913
|
/* The first and last line in view, zero-based. */
|
|
2553
2914
|
visibleLines(): { first: number; last: number } {
|
|
2554
2915
|
const top = this.scrollTop;
|
|
2916
|
+
|
|
2555
2917
|
return { first: this.layout.rowAt(top).line, last: this.layout.rowAt(top + this.viewportHeight).line };
|
|
2556
2918
|
}
|
|
2557
2919
|
|
|
2558
2920
|
/* A problem's tick says what the problem is when the pointer rests on it, and a press goes to it. The rest of the track lets the pointer through. */
|
|
2559
2921
|
private wireOverview(): void {
|
|
2560
|
-
const tickOf = (event: Event): HTMLElement | null => (event.target
|
|
2922
|
+
const tickOf = (event: Event): HTMLElement | null => closestElement(event.target, '.se-tick[data-offset]');
|
|
2923
|
+
|
|
2561
2924
|
this.overview.addEventListener('pointerover', (event) => {
|
|
2562
2925
|
const tick = tickOf(event);
|
|
2563
2926
|
const title = tick?.getAttribute('data-title');
|
|
2927
|
+
|
|
2564
2928
|
if (tick === null || !title) {
|
|
2565
2929
|
this.tickTip.hidden = true;
|
|
2930
|
+
|
|
2566
2931
|
return;
|
|
2567
2932
|
}
|
|
2933
|
+
|
|
2568
2934
|
this.tickTip.textContent = title;
|
|
2569
2935
|
this.tickTip.style.top = tick.style.top;
|
|
2570
2936
|
this.tickTip.hidden = false;
|
|
2571
2937
|
});
|
|
2938
|
+
|
|
2572
2939
|
this.overview.addEventListener('pointerout', () => {
|
|
2573
2940
|
this.tickTip.hidden = true;
|
|
2574
2941
|
});
|
|
2942
|
+
|
|
2575
2943
|
this.overview.addEventListener('pointerdown', (event) => {
|
|
2576
2944
|
const tick = tickOf(event);
|
|
2945
|
+
|
|
2577
2946
|
if (tick !== null) {
|
|
2578
2947
|
event.preventDefault();
|
|
2579
2948
|
this.jumpTo(Number(tick.getAttribute('data-offset')));
|
|
@@ -2595,23 +2964,31 @@ export class EditorView {
|
|
|
2595
2964
|
const current = this.find.current;
|
|
2596
2965
|
const alike = this.selectionOccurrences();
|
|
2597
2966
|
const key = `${this.revision}|${this.changeVersion}|${this.markerVersion}|${this.layout.height}|${trackHeight}|${matches.length}|${current}|${matches[0]?.from}|${matches.at(-1)?.from}|${this.selectionOccurrenceCache.key}`;
|
|
2967
|
+
|
|
2598
2968
|
if (key === this.overviewKey) {
|
|
2599
2969
|
return;
|
|
2600
2970
|
}
|
|
2971
|
+
|
|
2601
2972
|
this.overviewKey = key;
|
|
2602
2973
|
const spans: OverviewSpan[] = [];
|
|
2974
|
+
|
|
2603
2975
|
const rowTop = (line: number): number => this.layout.rowForLine(line).top;
|
|
2976
|
+
|
|
2604
2977
|
const rowBottom = (line: number): number => {
|
|
2605
2978
|
const row = this.layout.rowForLine(line);
|
|
2979
|
+
|
|
2606
2980
|
return row.top + row.height;
|
|
2607
2981
|
};
|
|
2982
|
+
|
|
2608
2983
|
for (const { kind, start, end } of this.resolvedChanges().runs) {
|
|
2609
2984
|
spans.push({ kind, top: rowTop(start), bottom: kind === 'deleted' ? rowTop(start) : rowBottom(end) });
|
|
2610
2985
|
}
|
|
2986
|
+
|
|
2611
2987
|
for (const marker of this.markers) {
|
|
2612
2988
|
if (marker.severity !== 'hint' && marker.to > marker.from) {
|
|
2613
2989
|
const start = this.model.positionAt(marker.from).line;
|
|
2614
2990
|
const end = this.model.positionAt(marker.to).line;
|
|
2991
|
+
|
|
2615
2992
|
spans.push({
|
|
2616
2993
|
kind: marker.severity,
|
|
2617
2994
|
top: rowTop(start),
|
|
@@ -2621,14 +2998,17 @@ export class EditorView {
|
|
|
2621
2998
|
});
|
|
2622
2999
|
}
|
|
2623
3000
|
}
|
|
3001
|
+
|
|
2624
3002
|
for (const range of alike) {
|
|
2625
3003
|
const line = this.model.positionAt(range.from).line;
|
|
2626
3004
|
spans.push({ kind: 'occurrence', top: rowTop(line), bottom: rowBottom(line) });
|
|
2627
3005
|
}
|
|
3006
|
+
|
|
2628
3007
|
matches.forEach((match, index) => {
|
|
2629
3008
|
const line = this.model.positionAt(match.from).line;
|
|
2630
3009
|
spans.push({ kind: index === current ? 'find-current' : 'find', top: rowTop(line), bottom: rowBottom(line) });
|
|
2631
3010
|
});
|
|
3011
|
+
|
|
2632
3012
|
paintOverview(this.overview, overviewTicks(spans, this.layout.height, trackHeight));
|
|
2633
3013
|
}
|
|
2634
3014
|
|
|
@@ -2644,16 +3024,20 @@ export class EditorView {
|
|
|
2644
3024
|
|
|
2645
3025
|
private announceScope(): void {
|
|
2646
3026
|
const line = this.model.positionAt(this.model.getPrimary().head).line;
|
|
3027
|
+
|
|
2647
3028
|
const scope = scopeChain(this.outline.blocks(this.model), line).map((block): EditorBlock => ({
|
|
2648
3029
|
startLine: block.startLine + 1,
|
|
2649
3030
|
endLine: block.endLine + 1,
|
|
2650
3031
|
name: block.name!,
|
|
2651
3032
|
...(block.kind === undefined ? {} : { kind: block.kind })
|
|
2652
3033
|
}));
|
|
3034
|
+
|
|
2653
3035
|
const key = scope.map((block) => `${block.startLine}:${block.endLine}:${block.name}:${block.kind ?? ''}`).join('|');
|
|
3036
|
+
|
|
2654
3037
|
if (key === this.scopeKey) {
|
|
2655
3038
|
return;
|
|
2656
3039
|
}
|
|
3040
|
+
|
|
2657
3041
|
this.scopeKey = key;
|
|
2658
3042
|
emit(this.scopeListeners, scope);
|
|
2659
3043
|
}
|
|
@@ -2666,16 +3050,21 @@ export class EditorView {
|
|
|
2666
3050
|
if (this.settings.stickyScroll === false) {
|
|
2667
3051
|
return [];
|
|
2668
3052
|
}
|
|
3053
|
+
|
|
2669
3054
|
const { layout } = this;
|
|
3055
|
+
|
|
2670
3056
|
const bottomOf = (line: number): number => {
|
|
2671
3057
|
const row = layout.rowForLine(line);
|
|
3058
|
+
|
|
2672
3059
|
return row.top + row.height;
|
|
2673
3060
|
};
|
|
3061
|
+
|
|
2674
3062
|
return stickyPlacements(this.outline.blocks(this.model), scrollTop, layout.metrics.lineHeight, this.stickyLimit(), {
|
|
2675
3063
|
top: (line) => layout.rowForLine(line).top,
|
|
2676
3064
|
bottom: bottomOf,
|
|
2677
3065
|
startsRow: (line) => {
|
|
2678
3066
|
const row = layout.rowForLine(line);
|
|
3067
|
+
|
|
2679
3068
|
return row.kind === 'text' && row.line === line;
|
|
2680
3069
|
}
|
|
2681
3070
|
});
|
|
@@ -2689,8 +3078,10 @@ export class EditorView {
|
|
|
2689
3078
|
private paintSticky(): void {
|
|
2690
3079
|
const lineHeight = this.layout.metrics.lineHeight;
|
|
2691
3080
|
const placements = this.scrollTop <= 0 ? [] : this.stickyAt(this.scrollTop);
|
|
3081
|
+
|
|
2692
3082
|
const entries = placements.map((placement): StickyEntry => {
|
|
2693
3083
|
const row = this.layout.rowForLine(placement.block.startLine) as TextRow;
|
|
3084
|
+
|
|
2694
3085
|
return {
|
|
2695
3086
|
line: placement.block.startLine,
|
|
2696
3087
|
label: this.lineNumberLabel(placement.block.startLine),
|
|
@@ -2698,22 +3089,29 @@ export class EditorView {
|
|
|
2698
3089
|
tokens: this.styledTokensOf(placement.block.startLine)
|
|
2699
3090
|
};
|
|
2700
3091
|
});
|
|
3092
|
+
|
|
2701
3093
|
const changed =
|
|
2702
3094
|
entries.length !== this.stickyEntries.length ||
|
|
2703
3095
|
entries.some((entry, index) => {
|
|
2704
3096
|
const before = this.stickyEntries[index]!;
|
|
3097
|
+
|
|
2705
3098
|
return entry.line !== before.line || entry.label !== before.label || entry.geometry !== before.geometry || entry.tokens !== before.tokens;
|
|
2706
3099
|
});
|
|
3100
|
+
|
|
2707
3101
|
this.sticky.hidden = entries.length === 0;
|
|
2708
3102
|
this.sticky.style.setProperty('--se-scroll-left', `${this.scrollLeft}px`);
|
|
2709
3103
|
this.sticky.style.width = `${this.clientWidth}px`;
|
|
3104
|
+
|
|
2710
3105
|
if (changed) {
|
|
2711
3106
|
this.stickyEntries = entries;
|
|
2712
3107
|
paintSticky(this.sticky, entries, lineHeight);
|
|
2713
3108
|
}
|
|
3109
|
+
|
|
2714
3110
|
this.sticky.style.height = `${stickyCover(placements, lineHeight)}px`;
|
|
3111
|
+
|
|
2715
3112
|
placements.forEach((placement, index) => {
|
|
2716
3113
|
const row = this.sticky.children[index] as HTMLElement | undefined;
|
|
3114
|
+
|
|
2717
3115
|
if (row) {
|
|
2718
3116
|
row.style.transform = placement.offset === 0 ? '' : `translateY(${placement.offset}px)`;
|
|
2719
3117
|
row.style.zIndex = String(placements.length - index);
|
|
@@ -2734,6 +3132,7 @@ export class EditorView {
|
|
|
2734
3132
|
if (action?.line === this.gutterAction?.line && action?.label === this.gutterAction?.label) {
|
|
2735
3133
|
return;
|
|
2736
3134
|
}
|
|
3135
|
+
|
|
2737
3136
|
this.gutterAction = action;
|
|
2738
3137
|
this.render();
|
|
2739
3138
|
}
|
|
@@ -2744,6 +3143,7 @@ export class EditorView {
|
|
|
2744
3143
|
|
|
2745
3144
|
setGutterMarkers(owner: string, markers: readonly GutterMarker[]): void {
|
|
2746
3145
|
const before = this.gutterMarkers.get(owner) ?? [];
|
|
3146
|
+
|
|
2747
3147
|
if (
|
|
2748
3148
|
before.length === markers.length &&
|
|
2749
3149
|
before.every(
|
|
@@ -2757,11 +3157,13 @@ export class EditorView {
|
|
|
2757
3157
|
) {
|
|
2758
3158
|
return;
|
|
2759
3159
|
}
|
|
3160
|
+
|
|
2760
3161
|
if (markers.length === 0) {
|
|
2761
3162
|
this.gutterMarkers.delete(owner);
|
|
2762
3163
|
} else {
|
|
2763
3164
|
this.gutterMarkers.set(owner, markers);
|
|
2764
3165
|
}
|
|
3166
|
+
|
|
2765
3167
|
this.render();
|
|
2766
3168
|
}
|
|
2767
3169
|
|
|
@@ -2769,10 +3171,8 @@ export class EditorView {
|
|
|
2769
3171
|
return subscribe(this.gutterMarkerListeners, listener);
|
|
2770
3172
|
}
|
|
2771
3173
|
|
|
2772
|
-
/* The id of the host's gutter marker when the target is it. */
|
|
2773
3174
|
gutterMarkerIdOf(target: EventTarget | null): string | null {
|
|
2774
|
-
|
|
2775
|
-
return button?.dataset.gutterMarker ?? null;
|
|
3175
|
+
return closestElement(target, '[data-gutter-marker]')?.dataset.gutterMarker ?? null;
|
|
2776
3176
|
}
|
|
2777
3177
|
|
|
2778
3178
|
pressGutterMarker(id: string): void {
|
|
@@ -2782,6 +3182,7 @@ export class EditorView {
|
|
|
2782
3182
|
/* One marker per line: where two owners mark the same line, the owner that sorts first stays. */
|
|
2783
3183
|
private markersByLine(): ReadonlyMap<number, GutterMarker> {
|
|
2784
3184
|
const byLine = new Map<number, GutterMarker>();
|
|
3185
|
+
|
|
2785
3186
|
for (const owner of [...this.gutterMarkers.keys()].sort()) {
|
|
2786
3187
|
for (const marker of this.gutterMarkers.get(owner)!) {
|
|
2787
3188
|
if (!byLine.has(marker.line)) {
|
|
@@ -2789,29 +3190,32 @@ export class EditorView {
|
|
|
2789
3190
|
}
|
|
2790
3191
|
}
|
|
2791
3192
|
}
|
|
3193
|
+
|
|
2792
3194
|
return byLine;
|
|
2793
3195
|
}
|
|
2794
3196
|
|
|
2795
|
-
/* The line of the host's gutter button when the target is it. */
|
|
2796
3197
|
gutterActionLineOf(target: EventTarget | null): number | null {
|
|
2797
|
-
const button = (target
|
|
2798
|
-
|
|
3198
|
+
const button = closestElement(target, '[data-gutter-action]');
|
|
3199
|
+
|
|
3200
|
+
return button ? Number(button.dataset.gutterAction) : null;
|
|
2799
3201
|
}
|
|
2800
3202
|
|
|
2801
3203
|
pressGutterAction(line: number): void {
|
|
2802
3204
|
emit(this.gutterActionListeners, line);
|
|
2803
3205
|
}
|
|
2804
3206
|
|
|
2805
|
-
/*
|
|
3207
|
+
/* The line a fold control in the gutter folds. */
|
|
2806
3208
|
foldLineOf(target: EventTarget | null): number | null {
|
|
2807
|
-
const button = (target
|
|
2808
|
-
|
|
3209
|
+
const button = closestElement(target, '[data-fold-line]');
|
|
3210
|
+
|
|
3211
|
+
return button ? Number(button.dataset.foldLine) : null;
|
|
2809
3212
|
}
|
|
2810
3213
|
|
|
2811
3214
|
dispose(): void {
|
|
2812
3215
|
if (this.disposed) {
|
|
2813
3216
|
return;
|
|
2814
3217
|
}
|
|
3218
|
+
|
|
2815
3219
|
this.disposed = true;
|
|
2816
3220
|
this.cancelScroll();
|
|
2817
3221
|
this.find.dispose();
|
|
@@ -2824,9 +3228,11 @@ export class EditorView {
|
|
|
2824
3228
|
clearTimeout(this.noticeTimer);
|
|
2825
3229
|
clearTimeout(this.liveTimer);
|
|
2826
3230
|
clearTimeout(this.caretTimer);
|
|
3231
|
+
|
|
2827
3232
|
if (this.frame !== undefined) {
|
|
2828
3233
|
this.document.defaultView?.cancelAnimationFrame?.(this.frame);
|
|
2829
3234
|
}
|
|
3235
|
+
|
|
2830
3236
|
this.findListeners.clear();
|
|
2831
3237
|
this.scopeListeners.clear();
|
|
2832
3238
|
this.viewListeners.clear();
|