@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.
Files changed (53) hide show
  1. package/dist/controller.d.ts +1 -0
  2. package/dist/controller.js +17 -28
  3. package/dist/engine.js +2 -9
  4. package/dist/find.js +12 -13
  5. package/dist/layout.js +7 -2
  6. package/dist/paint.d.ts +2 -0
  7. package/dist/paint.js +23 -20
  8. package/dist/view.d.ts +3 -2
  9. package/dist/view.js +46 -59
  10. package/package.json +6 -6
  11. package/src/attribution.ts +14 -0
  12. package/src/code-block.ts +25 -0
  13. package/src/column-selection.ts +10 -0
  14. package/src/controller.ts +189 -28
  15. package/src/engine.ts +87 -9
  16. package/src/fake.ts +79 -0
  17. package/src/find-search.ts +20 -0
  18. package/src/find.ts +135 -14
  19. package/src/geometry-stops.ts +10 -0
  20. package/src/grammar-preparation.ts +23 -0
  21. package/src/index.ts +1 -0
  22. package/src/input.ts +13 -0
  23. package/src/keymap-table.ts +9 -0
  24. package/src/keymap.ts +25 -0
  25. package/src/layout-changes.ts +7 -0
  26. package/src/layout.ts +219 -3
  27. package/src/lens.ts +3 -0
  28. package/src/listeners.ts +1 -0
  29. package/src/metrics.ts +14 -0
  30. package/src/modifier-gesture.ts +17 -0
  31. package/src/native-input.ts +27 -0
  32. package/src/offsets.ts +10 -0
  33. package/src/outline.ts +38 -0
  34. package/src/overview.ts +13 -0
  35. package/src/paint.ts +140 -20
  36. package/src/php-html.ts +1 -0
  37. package/src/pointer.ts +50 -0
  38. package/src/scroll-animation.ts +10 -0
  39. package/src/scroll.ts +7 -0
  40. package/src/search-client.ts +32 -0
  41. package/src/search-worker.ts +38 -0
  42. package/src/semantic.ts +12 -0
  43. package/src/shiki-document.ts +19 -0
  44. package/src/shiki-worker.ts +68 -0
  45. package/src/shiki.ts +37 -0
  46. package/src/testing.ts +17 -0
  47. package/src/theme-scopes.ts +14 -0
  48. package/src/tokens.ts +31 -0
  49. package/src/touch.ts +16 -0
  50. package/src/tracked-range.ts +5 -0
  51. package/src/uniform-geometry.ts +10 -0
  52. package/src/view.ts +481 -75
  53. 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).start;
603
- const end = this.model.getLine(line).end;
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(from: number, to: number, head: number): number | undefined {
1327
- if (to - from > 1) {
1494
+ private bracketAt({ anchor, head }: EditorSelection): number | undefined {
1495
+ if (Math.abs(head - anchor) > 1) {
1328
1496
  return undefined;
1329
1497
  }
1330
- const candidates = from !== to ? [from] : [head, head - 1];
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(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
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 as HTMLElement | null)?.closest?.('.se-inlay, .se-block, .se-line-action, .se-gutter, .se-sticky, .se-fold-chip, .se-overview')) {
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
- const nearest = (direction: -1 | 1): number => {
1546
- for (let at = line + direction, steps = 0; at >= 0 && at <= last && steps < GUIDE_SCAN_LINES; at += direction, steps++) {
1547
- const indent = this.indentOf(at);
1548
- if (indent !== null) {
1549
- return indent;
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
- return 0;
1553
- };
1554
- return Math.min(nearest(-1), nearest(1));
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
- const next = (() => {
1574
- for (let at = line + 1; at < count && at < line + GUIDE_SCAN_LINES; at++) {
1575
- if (this.indentOf(at) !== null) {
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 as HTMLElement | null)?.closest?.<HTMLElement>('.se-inlay');
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.highlightedLines(firstLine, lastLine);
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(rows),
2119
- attribution: this.attributedLines(firstLine, lastLine),
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
- const at = this.bracketAt(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), selection.head);
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
- const selected = selections.flatMap((selection) =>
2228
- selection.anchor === selection.head
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
- /* How each line on screen differs. */
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 highlightedLines(first: number, last: number): Map<number, AttributedLines> {
2440
- return this.highlights.linesIn(
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
- const bar = this.attributedLines(row.line, row.line).get(row.line);
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 as HTMLElement | null)?.closest?.<HTMLElement>('.se-tick[data-offset]') ?? null;
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
- const button = (target as HTMLElement | null)?.closest?.<HTMLElement>('[data-gutter-marker]');
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 as HTMLElement | null)?.closest?.('[data-gutter-action]');
2798
- return button ? Number((button as HTMLElement).dataset.gutterAction) : null;
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
- /* Whether a click at this point of the gutter is on a fold control, and which line it folds. */
3207
+ /* The line a fold control in the gutter folds. */
2806
3208
  foldLineOf(target: EventTarget | null): number | null {
2807
- const button = (target as HTMLElement | null)?.closest?.('[data-fold-line]');
2808
- return button ? Number((button as HTMLElement).dataset.foldLine) : null;
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();