@adecore/editor 0.21.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/testing.ts CHANGED
@@ -43,6 +43,10 @@ export function createPage(): { document: Document; window: Window & typeof glob
43
43
  };
44
44
  }
45
45
 
46
+ function createEvent(window: Window, type: string): Event {
47
+ return new (window as unknown as { Event: typeof Event }).Event(type, { bubbles: true, cancelable: true });
48
+ }
49
+
46
50
  export interface KeyOptions {
47
51
  ctrlKey?: boolean;
48
52
  metaKey?: boolean;
@@ -52,7 +56,7 @@ export interface KeyOptions {
52
56
 
53
57
  /* Fires a keydown at the element and says whether the editor took the key. */
54
58
  export function press(window: Window, target: Element, key: string, options: KeyOptions = {}): boolean {
55
- const event = new (window as unknown as { Event: typeof Event }).Event('keydown', { bubbles: true, cancelable: true });
59
+ const event = createEvent(window, 'keydown');
56
60
  Object.assign(event, { key, ctrlKey: false, metaKey: false, altKey: false, shiftKey: false, isComposing: false, ...options });
57
61
  target.dispatchEvent(event);
58
62
  return event.defaultPrevented;
@@ -60,14 +64,14 @@ export function press(window: Window, target: Element, key: string, options: Key
60
64
 
61
65
  /* Fires a keyup at the element, the other half of `press`. */
62
66
  export function release(window: Window, target: Element, key: string, options: KeyOptions = {}): void {
63
- const event = new (window as unknown as { Event: typeof Event }).Event('keyup', { bubbles: true, cancelable: true });
67
+ const event = createEvent(window, 'keyup');
64
68
  Object.assign(event, { key, ctrlKey: false, metaKey: false, altKey: false, shiftKey: false, isComposing: false, ...options });
65
69
  target.dispatchEvent(event);
66
70
  }
67
71
 
68
72
  /* What a browser sends before it puts text in a textarea. */
69
73
  export function typeInto(window: Window, target: Element, text: string): void {
70
- const event = new (window as unknown as { Event: typeof Event }).Event('beforeinput', { bubbles: true, cancelable: true });
74
+ const event = createEvent(window, 'beforeinput');
71
75
  Object.assign(event, { inputType: 'insertText', data: text, isComposing: false });
72
76
  target.dispatchEvent(event);
73
77
  }
@@ -79,7 +83,7 @@ export function clipboard(
79
83
  type: 'copy' | 'cut' | 'paste',
80
84
  data: { text: string } = { text: '' }
81
85
  ): { prevented: boolean; text: string } {
82
- const event = new (window as unknown as { Event: typeof Event }).Event(type, { bubbles: true, cancelable: true });
86
+ const event = createEvent(window, type);
83
87
  Object.assign(event, {
84
88
  clipboardData: {
85
89
  getData: () => data.text,
@@ -101,7 +105,7 @@ export function pointer(
101
105
  y: number,
102
106
  options: KeyOptions = {}
103
107
  ): void {
104
- const event = new (window as unknown as { Event: typeof Event }).Event(type, { bubbles: true, cancelable: true });
108
+ const event = createEvent(window, type);
105
109
  Object.assign(event, {
106
110
  clientX: x,
107
111
  clientY: y,
package/src/tokens.ts CHANGED
@@ -49,8 +49,7 @@ export class TokenCache {
49
49
  this.lines = this.source.getLineCount();
50
50
  this.entries = [];
51
51
  this.frontier = 0;
52
- this.reusableFrom = Number.POSITIVE_INFINITY;
53
- this.pivotLine = -1;
52
+ this.forgetPivot();
54
53
  }
55
54
 
56
55
  /* How far down the document has colors that are right for the text. */
@@ -92,8 +91,7 @@ export class TokenCache {
92
91
  if (first === undefined) {
93
92
  this.entries = [];
94
93
  this.frontier = 0;
95
- this.reusableFrom = Number.POSITIVE_INFINITY;
96
- this.pivotLine = -1;
94
+ this.forgetPivot();
97
95
  return;
98
96
  }
99
97
  const firstLine = lineOf(first.from);
@@ -108,8 +106,7 @@ export class TokenCache {
108
106
  this.frontier = Math.min(this.frontier, firstLine);
109
107
  if (last === undefined || lastOld < firstLine || this.entries.length <= firstLine) {
110
108
  this.entries.length = Math.min(this.entries.length, firstLine);
111
- this.reusableFrom = Number.POSITIVE_INFINITY;
112
- this.pivotLine = -1;
109
+ this.forgetPivot();
113
110
  return;
114
111
  }
115
112
  const old = this.entries[lastOld];
@@ -131,8 +128,7 @@ export class TokenCache {
131
128
  }
132
129
  if (tokenizer.stale?.() === true) {
133
130
  this.frontier = 0;
134
- this.reusableFrom = Number.POSITIVE_INFINITY;
135
- this.pivotLine = -1;
131
+ this.forgetPivot();
136
132
  }
137
133
  const count = this.source.getLineCount();
138
134
  const limit = Math.min(until, count - 1);
@@ -157,8 +153,7 @@ export class TokenCache {
157
153
  (previous !== undefined && line >= this.reusableFrom && tokenizer.sameState(previous.state, next.state));
158
154
  if (settled) {
159
155
  this.frontier = this.entries.length;
160
- this.reusableFrom = Number.POSITIVE_INFINITY;
161
- this.pivotLine = -1;
156
+ this.forgetPivot();
162
157
  break;
163
158
  }
164
159
  if (now() > deadline) {
@@ -168,6 +163,11 @@ export class TokenCache {
168
163
  return this.frontier > from ? { from, to: this.frontier - 1 } : null;
169
164
  }
170
165
 
166
+ private forgetPivot(): void {
167
+ this.reusableFrom = Number.POSITIVE_INFINITY;
168
+ this.pivotLine = -1;
169
+ }
170
+
171
171
  /* Whether lines through `line` are colored for the text as it is now. */
172
172
  covers(line: number): boolean {
173
173
  return this.frontier > line || this.frontier >= this.source.getLineCount();
package/src/types.ts CHANGED
@@ -45,7 +45,7 @@ export type EditorTheme = string;
45
45
 
46
46
  export interface EditorOptions {
47
47
  readonly text: string;
48
- /* The Shiki id `fs.read` answers with. Without one, or with one Shiki does not know, it is plain text. */
48
+ /* A Shiki language id. Without one, or with one Shiki does not know, it is plain text. */
49
49
  readonly language?: string;
50
50
  /* The file's path, absolute or only a name. A language service reads the dialect off its extension, a `.tsx` from a `.ts`. */
51
51
  readonly path?: string;
@@ -459,8 +459,10 @@ export interface EditorTrackedRange {
459
459
 
460
460
  export interface Editor {
461
461
  getText(): string;
462
- /* A change from outside, such as a reload after `fs.changed` or another surface's edit. It is never
463
- reported as a change, and the cursor and the scroll stay put wherever the text around them did. */
462
+ /*
463
+ * A change from outside, such as a reload after the file changed on disk. It is never reported as a
464
+ * change, and the cursor and the scroll stay put wherever the text around them did.
465
+ */
464
466
  setText(text: string): void;
465
467
  onChange(listener: () => void): () => void;
466
468
  /* Every change of the text, `setText` and undo included, with what changed in the positions a language server expects. */
@@ -470,14 +472,16 @@ export interface Editor {
470
472
  offsetAt(position: EditorPosition): number;
471
473
  /* The text of a range, such as the word before the caret. */
472
474
  textInRange(range: EditorRange): string;
473
- /* Mod+S from inside the editor; what happens then is the client's. */
475
+ /* Mod+S from inside the editor; what happens then is the host's. */
474
476
  onSave(listener: () => void): () => void;
475
477
  /* The focus left the editor and every widget of its own, such as its suggestions. */
476
478
  onBlur(listener: () => void): () => void;
477
479
  /* Puts the cursor at the start of a one-based line and scrolls it into view, a third from the top when it was out of view. */
478
480
  revealLine(line: number, reveal?: EditorReveal): void;
479
- /* Marks every match and moves to the first one from the cursor on; null takes the marks away. The
480
- count comes back through `onFind`, and again whenever an edit changes it. */
481
+ /*
482
+ * Marks every match and moves to the first one from the cursor on; null takes the marks away. The
483
+ * count comes back through `onFind`, and again whenever an edit changes it.
484
+ */
481
485
  find(query: EditorFindQuery | null): void;
482
486
  findStep(direction: 1 | -1): void;
483
487
  /* Replaces the match the find is on, with `$1` and the like expanded for a regular expression, and moves to the next one. False when there is none or the editor is read only. */
@@ -507,8 +511,10 @@ export interface Editor {
507
511
  setFoldHints(hints: EditorFoldHints | null): void;
508
512
  /* Marks that follow their lines through edits until the host sets them again. */
509
513
  setChangeMarks(marks: readonly EditorChangeMark[]): void;
510
- /* The blocks sticky scroll and the breadcrumb go by. The editor reads them from brackets and
511
- indentation until the host has better, such as a language server's symbols; null goes back. */
514
+ /*
515
+ * The blocks sticky scroll and the breadcrumb go by. The editor reads them from brackets and
516
+ * indentation until the host has better, such as a language server's symbols; null goes back.
517
+ */
512
518
  setBlocks(blocks: readonly EditorBlock[] | null): void;
513
519
  /* The named blocks around the caret, outermost first, said again only when they change. */
514
520
  onScope(listener: (scope: readonly EditorBlock[]) => void): () => void;
package/src/view.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  type FoldHints,
6
6
  type FoldRole,
7
7
  type FoldingRange,
8
+ indentationColumn,
8
9
  replacementText,
9
10
  scanBrackets,
10
11
  type BracketIndex,
@@ -15,6 +16,7 @@ import { type BlockWidget, EditorLayout, type FoldState, type Inlay, type Layout
15
16
  import { createMetrics, type EditorFont, readEditorFont } from './metrics.ts';
16
17
  import { type AttributedLines, AttributionRuns, colorValue, tintValue } from './attribution.ts';
17
18
  import { renderCodeBlock } from './code-block.ts';
19
+ import { emit, subscribe } from './listeners.ts';
18
20
  import { mapOffset } from './offsets.ts';
19
21
  import { mapTrackedRange } from './tracked-range.ts';
20
22
  import { type ScrollKind, scrollPosition } from './scroll.ts';
@@ -35,7 +37,6 @@ import {
35
37
  type WrapSign
36
38
  } from './paint.ts';
37
39
  import { overlayTokens, type SemanticSpan } from './semantic.ts';
38
- import { indentationColumn } from '@adecore/editor-core';
39
40
  import { TokenCache } from './tokens.ts';
40
41
  import type {
41
42
  EditorBlock,
@@ -58,6 +59,22 @@ function sameRect(left: EditorRect, right: EditorRect): boolean {
58
59
  return left.left === right.left && left.top === right.top && left.right === right.right && left.bottom === right.bottom;
59
60
  }
60
61
 
62
+ function mapRange<Range extends { from: number; to: number }>(range: Range, changes: readonly DocumentChange[]): Range {
63
+ return { ...range, from: mapOffset(range.from, changes), to: mapOffset(range.to, changes) };
64
+ }
65
+
66
+ function mapFoldHints(hints: FoldHints, changes: readonly DocumentChange[]): FoldHints {
67
+ return {
68
+ symbols: hints.symbols?.map((hint) => mapRange(hint, changes)) ?? [],
69
+ ranges:
70
+ hints.ranges?.map((hint) => ({
71
+ ...mapRange(hint, changes),
72
+ ...(hint.head === undefined ? {} : { head: mapOffset(hint.head, changes) }),
73
+ ...(hint.tail === undefined ? {} : { tail: mapOffset(hint.tail, changes) })
74
+ })) ?? []
75
+ };
76
+ }
77
+
61
78
  export interface ViewSettings {
62
79
  language: string | undefined;
63
80
  tabSize: number;
@@ -556,45 +573,7 @@ export class EditorView {
556
573
  this.layoutGhostParts();
557
574
  }
558
575
  for (const changes of batches) {
559
- this.inlays = this.inlays.map((inlay) => ({ ...inlay, at: mapOffset(inlay.at, changes) }));
560
- for (const [owner, blocks] of this.blocks) {
561
- this.blocks.set(
562
- owner,
563
- blocks.map((block) => ({ ...block, at: mapOffset(block.at, changes) }))
564
- );
565
- }
566
- this.lenses = this.lenses.map((lens) => ({ ...lens, at: mapOffset(lens.at, changes) }));
567
- this.collapsed = new Set([...this.collapsed].map((anchor) => mapOffset(anchor, changes)));
568
- this.settled = new Set([...this.settled].map((anchor) => mapOffset(anchor, changes)));
569
- if (this.foldHints !== null) {
570
- this.foldHints = {
571
- symbols:
572
- this.foldHints.symbols?.map((hint) => ({ ...hint, from: mapOffset(hint.from, changes), to: mapOffset(hint.to, changes) })) ?? [],
573
- ranges:
574
- this.foldHints.ranges?.map((hint) => ({
575
- ...hint,
576
- from: mapOffset(hint.from, changes),
577
- to: mapOffset(hint.to, changes),
578
- ...(hint.head === undefined ? {} : { head: mapOffset(hint.head, changes) }),
579
- ...(hint.tail === undefined ? {} : { tail: mapOffset(hint.tail, changes) })
580
- })) ?? []
581
- };
582
- }
583
- this.customFolds = this.customFolds.map((fold) => ({ from: mapOffset(fold.from, changes), to: mapOffset(fold.to, changes) }));
584
- this.find.mapBounds(changes);
585
- this.changeMarks = this.changeMarks.map((mark) => ({ ...mark, from: mapOffset(mark.from, changes), to: mapOffset(mark.to, changes) }));
586
- this.markers = this.markers.map((marker) => ({ ...marker, from: mapOffset(marker.from, changes), to: mapOffset(marker.to, changes) }));
587
- this.mapTracked(changes);
588
- this.attribution.map(changes);
589
- this.highlights.map(changes);
590
- this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at: mapOffset(caret.at, changes) }));
591
- for (const [owner, entries] of this.lineActions) {
592
- this.lineActions.set(
593
- owner,
594
- entries.map((entry) => ({ ...entry, at: mapOffset(entry.at, changes) }))
595
- );
596
- }
597
- this.semantic = this.semantic.map((token) => ({ ...token, from: mapOffset(token.from, changes), to: mapOffset(token.to, changes) }));
576
+ this.followEdit(changes);
598
577
  }
599
578
  this.foldRanges = this.mappedFolds(batches);
600
579
  this.semanticVersion++;
@@ -625,6 +604,38 @@ export class EditorView {
625
604
  this.requestRender();
626
605
  }
627
606
 
607
+ /* Moves everything anchored to an offset along with one batch of changes. */
608
+ private followEdit(changes: readonly DocumentChange[]): void {
609
+ this.inlays = this.inlays.map((inlay) => ({ ...inlay, at: mapOffset(inlay.at, changes) }));
610
+ for (const [owner, blocks] of this.blocks) {
611
+ this.blocks.set(
612
+ owner,
613
+ blocks.map((block) => ({ ...block, at: mapOffset(block.at, changes) }))
614
+ );
615
+ }
616
+ this.lenses = this.lenses.map((lens) => ({ ...lens, at: mapOffset(lens.at, changes) }));
617
+ this.collapsed = new Set([...this.collapsed].map((anchor) => mapOffset(anchor, changes)));
618
+ this.settled = new Set([...this.settled].map((anchor) => mapOffset(anchor, changes)));
619
+ if (this.foldHints !== null) {
620
+ this.foldHints = mapFoldHints(this.foldHints, changes);
621
+ }
622
+ this.customFolds = this.customFolds.map((fold) => mapRange(fold, changes));
623
+ this.find.mapBounds(changes);
624
+ this.changeMarks = this.changeMarks.map((mark) => mapRange(mark, changes));
625
+ this.markers = this.markers.map((marker) => mapRange(marker, changes));
626
+ this.mapTracked(changes);
627
+ this.attribution.map(changes);
628
+ this.highlights.map(changes);
629
+ this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at: mapOffset(caret.at, changes) }));
630
+ for (const [owner, entries] of this.lineActions) {
631
+ this.lineActions.set(
632
+ owner,
633
+ entries.map((entry) => ({ ...entry, at: mapOffset(entry.at, changes) }))
634
+ );
635
+ }
636
+ this.semantic = this.semantic.map((token) => mapRange(token, changes));
637
+ }
638
+
628
639
  private mapTracked(changes: readonly DocumentChange[]): void {
629
640
  for (const entry of this.tracked) {
630
641
  const mapped = mapTrackedRange(entry.from, entry.to, changes);
@@ -773,9 +784,7 @@ export class EditorView {
773
784
  );
774
785
  const anchors = new Set(this.foldRanges.map((range) => range.from));
775
786
  this.collapsed = new Set([...this.collapsed].filter((anchor) => anchors.has(anchor)));
776
- this.anchorScroll(() => this.layout.configure({ folds: this.foldStates() }));
777
- this.moveHiddenCarets();
778
- this.render();
787
+ this.refoldLayout();
779
788
  }
780
789
 
781
790
  /* The folds a person made of a selection, which the document's own structure does not know, among the ones it does. */
@@ -1149,9 +1158,11 @@ export class EditorView {
1149
1158
  if (!rect) {
1150
1159
  return { x: clientX - this.gutterWidth + this.viewport.scrollLeft, y: clientY + this.viewport.scrollTop };
1151
1160
  }
1152
- const scaleX = this.viewport.offsetWidth > 0 ? rect.width / this.viewport.offsetWidth : 1;
1153
- const scaleY = this.viewport.offsetHeight > 0 ? rect.height / this.viewport.offsetHeight : 1;
1154
- return { x: (clientX - rect.left) / scaleX - this.gutterWidth + this.viewport.scrollLeft, y: (clientY - rect.top) / scaleY + this.viewport.scrollTop };
1161
+ const scale = this.screenScale(rect);
1162
+ return {
1163
+ x: (clientX - rect.left) / scale.x - this.gutterWidth + this.viewport.scrollLeft,
1164
+ y: (clientY - rect.top) / scale.y + this.viewport.scrollTop
1165
+ };
1155
1166
  }
1156
1167
 
1157
1168
  offsetAtPoint(clientX: number, clientY: number): number {
@@ -1250,13 +1261,9 @@ export class EditorView {
1250
1261
  }
1251
1262
 
1252
1263
  onHover(listener: (offset: number | null) => void): () => void {
1253
- this.hoverListeners.add(listener);
1254
- return () => {
1255
- this.hoverListeners.delete(listener);
1256
- };
1264
+ return subscribe(this.hoverListeners, listener);
1257
1265
  }
1258
1266
 
1259
- /* The pointer moved over the editor; a drag in progress is not a hover. */
1260
1267
  /*
1261
1268
  * The other places the selected text is, found without a language server: only for one selection that
1262
1269
  * holds no line break and is not blank, and not at all when there are more than a few of them.
@@ -1480,6 +1487,7 @@ export class EditorView {
1480
1487
  this.render();
1481
1488
  }
1482
1489
 
1490
+ /* The pointer moved over the editor; a drag in progress is not a hover. */
1483
1491
  hoverMoved(event: PointerEvent): void {
1484
1492
  this.content.classList.toggle(
1485
1493
  'se-over-selection',
@@ -1493,16 +1501,11 @@ export class EditorView {
1493
1501
  return;
1494
1502
  }
1495
1503
  this.hoverOffset = offset;
1496
- for (const listener of [...this.hoverListeners]) {
1497
- listener(offset);
1498
- }
1504
+ emit(this.hoverListeners, offset);
1499
1505
  }
1500
1506
 
1501
1507
  onViewChange(listener: () => void): () => void {
1502
- this.viewListeners.add(listener);
1503
- return () => {
1504
- this.viewListeners.delete(listener);
1505
- };
1508
+ return subscribe(this.viewListeners, listener);
1506
1509
  }
1507
1510
 
1508
1511
  private announceView(): void {
@@ -1511,9 +1514,7 @@ export class EditorView {
1511
1514
  return;
1512
1515
  }
1513
1516
  this.viewKey = key;
1514
- for (const listener of [...this.viewListeners]) {
1515
- listener();
1516
- }
1517
+ emit(this.viewListeners);
1517
1518
  }
1518
1519
 
1519
1520
  private caretOf(head: number): LayoutRect {
@@ -1525,7 +1526,6 @@ export class EditorView {
1525
1526
  return this.scrollRun?.to ?? { x: this.viewport.scrollLeft, y: this.viewport.scrollTop };
1526
1527
  }
1527
1528
 
1528
- /* Stops a scroll on its way where it is. */
1529
1529
  cancelScroll(): void {
1530
1530
  if (this.scrollRun !== null) {
1531
1531
  this.document.defaultView?.cancelAnimationFrame?.(this.scrollRun.frame);
@@ -1533,7 +1533,6 @@ export class EditorView {
1533
1533
  }
1534
1534
  }
1535
1535
 
1536
- /* Whether a person asked their system for less motion. */
1537
1536
  private reducedMotion(): boolean {
1538
1537
  return this.document.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;
1539
1538
  }
@@ -1672,16 +1671,11 @@ export class EditorView {
1672
1671
  }
1673
1672
 
1674
1673
  onFind(listener: (state: EditorFindState) => void): () => void {
1675
- this.findListeners.add(listener);
1676
- return () => {
1677
- this.findListeners.delete(listener);
1678
- };
1674
+ return subscribe(this.findListeners, listener);
1679
1675
  }
1680
1676
 
1681
1677
  private announceFind(): void {
1682
- for (const listener of [...this.findListeners]) {
1683
- listener(this.find.state);
1684
- }
1678
+ emit(this.findListeners, this.find.state);
1685
1679
  }
1686
1680
 
1687
1681
  revealFind(): void {
@@ -2114,10 +2108,7 @@ export class EditorView {
2114
2108
  }
2115
2109
 
2116
2110
  onAttributionHover(listener: (hover: { id: string; rect: EditorRect } | null) => void): () => void {
2117
- this.attributionListeners.add(listener);
2118
- return () => {
2119
- this.attributionListeners.delete(listener);
2120
- };
2111
+ return subscribe(this.attributionListeners, listener);
2121
2112
  }
2122
2113
 
2123
2114
  /* The bar is three pixels wide, so the pointer finds it a few pixels to either side. */
@@ -2139,9 +2130,7 @@ export class EditorView {
2139
2130
  return;
2140
2131
  }
2141
2132
  this.attributionHover = next;
2142
- for (const listener of [...this.attributionListeners]) {
2143
- listener(next);
2144
- }
2133
+ emit(this.attributionListeners, next);
2145
2134
  }
2146
2135
 
2147
2136
  private attributionUnder(clientX: number, clientY: number): { id: string; rect: EditorRect } | null {
@@ -2259,10 +2248,7 @@ export class EditorView {
2259
2248
  }
2260
2249
 
2261
2250
  onScope(listener: (scope: readonly EditorBlock[]) => void): () => void {
2262
- this.scopeListeners.add(listener);
2263
- return () => {
2264
- this.scopeListeners.delete(listener);
2265
- };
2251
+ return subscribe(this.scopeListeners, listener);
2266
2252
  }
2267
2253
 
2268
2254
  private announceScope(): void {
@@ -2278,9 +2264,7 @@ export class EditorView {
2278
2264
  return;
2279
2265
  }
2280
2266
  this.scopeKey = key;
2281
- for (const listener of [...this.scopeListeners]) {
2282
- listener(scope);
2283
- }
2267
+ emit(this.scopeListeners, scope);
2284
2268
  }
2285
2269
 
2286
2270
  private stickyLimit(): number {
@@ -2356,10 +2340,7 @@ export class EditorView {
2356
2340
  }
2357
2341
 
2358
2342
  onGutterAction(listener: (line: number) => void): () => void {
2359
- this.gutterActionListeners.add(listener);
2360
- return () => {
2361
- this.gutterActionListeners.delete(listener);
2362
- };
2343
+ return subscribe(this.gutterActionListeners, listener);
2363
2344
  }
2364
2345
 
2365
2346
  setGutterMarkers(owner: string, markers: readonly { id: string; line: number; label: string }[]): void {
@@ -2379,10 +2360,7 @@ export class EditorView {
2379
2360
  }
2380
2361
 
2381
2362
  onGutterMarker(listener: (id: string) => void): () => void {
2382
- this.gutterMarkerListeners.add(listener);
2383
- return () => {
2384
- this.gutterMarkerListeners.delete(listener);
2385
- };
2363
+ return subscribe(this.gutterMarkerListeners, listener);
2386
2364
  }
2387
2365
 
2388
2366
  /* The id of the host's gutter marker when the target is it. */
@@ -2392,12 +2370,10 @@ export class EditorView {
2392
2370
  }
2393
2371
 
2394
2372
  pressGutterMarker(id: string): void {
2395
- for (const listener of [...this.gutterMarkerListeners]) {
2396
- listener(id);
2397
- }
2373
+ emit(this.gutterMarkerListeners, id);
2398
2374
  }
2399
2375
 
2400
- /* One marker per line: where two owners mark the same line, the owner named first stays. */
2376
+ /* One marker per line: where two owners mark the same line, the owner that sorts first stays. */
2401
2377
  private markersByLine(): ReadonlyMap<number, { id: string; label: string }> {
2402
2378
  const byLine = new Map<number, { id: string; label: string }>();
2403
2379
  for (const owner of [...this.gutterMarkers.keys()].sort()) {
@@ -2417,9 +2393,7 @@ export class EditorView {
2417
2393
  }
2418
2394
 
2419
2395
  pressGutterAction(line: number): void {
2420
- for (const listener of [...this.gutterActionListeners]) {
2421
- listener(line);
2422
- }
2396
+ emit(this.gutterActionListeners, line);
2423
2397
  }
2424
2398
 
2425
2399
  /* Whether a click at this point of the gutter is on a fold control, and which line it folds. */