@adecore/editor 0.0.1 → 0.17.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +82 -0
  3. package/dist/attribution.d.ts +28 -0
  4. package/dist/attribution.js +55 -0
  5. package/dist/code-block.d.ts +2 -0
  6. package/dist/code-block.js +89 -0
  7. package/dist/column-selection.d.ts +9 -0
  8. package/dist/column-selection.js +52 -0
  9. package/dist/controller.d.ts +64 -0
  10. package/dist/controller.js +772 -0
  11. package/dist/editor.css +813 -0
  12. package/dist/engine.d.ts +2 -0
  13. package/dist/engine.js +604 -0
  14. package/dist/fake.d.ts +190 -0
  15. package/dist/fake.js +658 -0
  16. package/dist/find.d.ts +30 -0
  17. package/dist/find.js +108 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.js +4 -0
  20. package/dist/input.d.ts +9 -0
  21. package/dist/input.js +57 -0
  22. package/dist/keymap-table.d.ts +102 -0
  23. package/dist/keymap-table.js +130 -0
  24. package/dist/keymap.d.ts +36 -0
  25. package/dist/keymap.js +163 -0
  26. package/dist/layout.d.ts +167 -0
  27. package/dist/layout.js +583 -0
  28. package/dist/lens.d.ts +3 -0
  29. package/dist/lens.js +29 -0
  30. package/dist/listeners.d.ts +3 -0
  31. package/dist/listeners.js +12 -0
  32. package/dist/metrics.d.ts +8 -0
  33. package/dist/metrics.js +54 -0
  34. package/dist/modifier-gesture.d.ts +24 -0
  35. package/dist/modifier-gesture.js +98 -0
  36. package/dist/native-input.d.ts +26 -0
  37. package/dist/native-input.js +147 -0
  38. package/dist/offsets.d.ts +2 -0
  39. package/dist/offsets.js +14 -0
  40. package/dist/outline.d.ts +55 -0
  41. package/dist/outline.js +181 -0
  42. package/dist/overview.d.ts +17 -0
  43. package/dist/overview.js +55 -0
  44. package/dist/paint.d.ts +86 -0
  45. package/dist/paint.js +481 -0
  46. package/dist/php-html.d.ts +9 -0
  47. package/dist/php-html.js +36 -0
  48. package/dist/pointer.d.ts +49 -0
  49. package/dist/pointer.js +243 -0
  50. package/dist/scroll-animation.d.ts +3 -0
  51. package/dist/scroll-animation.js +45 -0
  52. package/dist/scroll.d.ts +28 -0
  53. package/dist/scroll.js +76 -0
  54. package/dist/semantic.d.ts +8 -0
  55. package/dist/semantic.js +47 -0
  56. package/dist/shiki.d.ts +7 -0
  57. package/dist/shiki.js +106 -0
  58. package/dist/smart-keys.d.ts +3 -0
  59. package/dist/smart-keys.js +14 -0
  60. package/dist/testing.d.ts +34 -0
  61. package/dist/testing.js +117 -0
  62. package/dist/theme-scopes.d.ts +9 -0
  63. package/dist/theme-scopes.js +61 -0
  64. package/dist/tokens.d.ts +28 -0
  65. package/dist/tokens.js +152 -0
  66. package/dist/tracked-range.d.ts +5 -0
  67. package/dist/tracked-range.js +20 -0
  68. package/dist/types.d.ts +342 -0
  69. package/dist/types.js +1 -0
  70. package/dist/view.d.ts +382 -0
  71. package/dist/view.js +2134 -0
  72. package/package.json +63 -3
  73. package/src/attribution.ts +85 -0
  74. package/src/code-block.ts +100 -0
  75. package/src/column-selection.ts +63 -0
  76. package/src/controller.ts +838 -0
  77. package/src/editor.css +813 -0
  78. package/src/engine.ts +757 -0
  79. package/src/fake.ts +831 -0
  80. package/src/find.ts +128 -0
  81. package/src/index.ts +69 -0
  82. package/src/input.ts +63 -0
  83. package/src/keymap-table.ts +163 -0
  84. package/src/keymap.ts +204 -0
  85. package/src/layout.ts +754 -0
  86. package/src/lens.ts +32 -0
  87. package/src/listeners.ts +15 -0
  88. package/src/metrics.ts +65 -0
  89. package/src/modifier-gesture.ts +115 -0
  90. package/src/native-input.ts +179 -0
  91. package/src/offsets.ts +16 -0
  92. package/src/outline.ts +248 -0
  93. package/src/overview.ts +80 -0
  94. package/src/paint.ts +608 -0
  95. package/src/php-html.ts +43 -0
  96. package/src/pointer.ts +326 -0
  97. package/src/scroll-animation.ts +48 -0
  98. package/src/scroll.ts +102 -0
  99. package/src/semantic.ts +54 -0
  100. package/src/shiki.ts +118 -0
  101. package/src/smart-keys.ts +17 -0
  102. package/src/testing.ts +157 -0
  103. package/src/theme-scopes.ts +75 -0
  104. package/src/tokens.ts +175 -0
  105. package/src/tracked-range.ts +22 -0
  106. package/src/types.ts +656 -0
  107. package/src/view.ts +2436 -0
@@ -0,0 +1,80 @@
1
+ export type OverviewKind = 'added' | 'modified' | 'deleted' | 'find' | 'find-current' | 'occurrence' | 'info' | 'warning' | 'error';
2
+
3
+ /* A stretch of the content, in the pixels the content is laid out in. */
4
+ export interface OverviewSpan {
5
+ kind: OverviewKind;
6
+ top: number;
7
+ bottom: number;
8
+ /* Where a press on the tick goes, and what it says: a problem has both. */
9
+ offset?: number;
10
+ title?: string;
11
+ }
12
+
13
+ export interface OverviewTick {
14
+ kind: OverviewKind;
15
+ y: number;
16
+ height: number;
17
+ offset?: number;
18
+ title?: string;
19
+ }
20
+
21
+ const MIN_TICK_HEIGHT = 3;
22
+
23
+ /*
24
+ * The spans of the content as ticks in a track as tall as the view. A short document is not
25
+ * stretched over the track, so a tick is where its line is. Ticks of one kind that touch are one.
26
+ */
27
+ export function overviewTicks(spans: readonly OverviewSpan[], contentHeight: number, trackHeight: number): OverviewTick[] {
28
+ if (trackHeight <= 0) {
29
+ return [];
30
+ }
31
+ const scale = trackHeight / Math.max(contentHeight, trackHeight);
32
+ const ticks = spans
33
+ .map((span): OverviewTick => ({
34
+ kind: span.kind,
35
+ y: Math.round(span.top * scale),
36
+ height: Math.max(MIN_TICK_HEIGHT, Math.round((span.bottom - span.top) * scale)),
37
+ ...(span.offset === undefined ? {} : { offset: span.offset }),
38
+ ...(span.title === undefined ? {} : { title: span.title })
39
+ }))
40
+ .sort((left, right) => left.y - right.y);
41
+ const merged: OverviewTick[] = [];
42
+ const last = new Map<OverviewKind, OverviewTick>();
43
+ for (const tick of ticks) {
44
+ const before = last.get(tick.kind);
45
+ if (before && tick.y <= before.y + before.height) {
46
+ before.height = Math.max(before.height, tick.y + tick.height - before.y);
47
+ continue;
48
+ }
49
+ const next = { ...tick };
50
+ last.set(tick.kind, next);
51
+ merged.push(next);
52
+ }
53
+ return merged;
54
+ }
55
+
56
+ /* What is painted later is on top: the current find match, and the worse a problem is the later it goes. */
57
+ const PAINT_ORDER: Partial<Record<OverviewKind, number>> = { warning: 1, error: 2, 'find-current': 3 };
58
+
59
+ /* Draws the ticks. */
60
+ export function paintOverview(container: HTMLElement, ticks: readonly OverviewTick[]): void {
61
+ const document = container.ownerDocument;
62
+ const order = (tick: OverviewTick): number => PAINT_ORDER[tick.kind] ?? 0;
63
+ container.replaceChildren(
64
+ ...[...ticks]
65
+ .sort((left, right) => order(left) - order(right))
66
+ .map((tick) => {
67
+ const element = document.createElement('span');
68
+ element.className = `se-tick se-tick-${tick.kind}`;
69
+ element.style.top = `${tick.y}px`;
70
+ element.style.height = `${tick.height}px`;
71
+ if (tick.offset !== undefined) {
72
+ element.setAttribute('data-offset', String(tick.offset));
73
+ }
74
+ if (tick.title !== undefined) {
75
+ element.setAttribute('data-title', tick.title);
76
+ }
77
+ return element;
78
+ })
79
+ );
80
+ }
package/src/paint.ts ADDED
@@ -0,0 +1,608 @@
1
+ import { type AttributedLines, colorValue } from './attribution.ts';
2
+ import { type BlockWidget, type EditorLayout, type LayoutRect, type LayoutRow, type LineGeometry, scanLine, type TextRow } from './layout.ts';
3
+ import type { EditorChangeKind, LineToken } from './types.ts';
4
+
5
+ const ITALIC = 1;
6
+ const BOLD = 2;
7
+ const UNDERLINE = 4;
8
+ const STRIKETHROUGH = 8;
9
+
10
+ interface PaintedRow {
11
+ element: HTMLElement;
12
+ text: string;
13
+ tokens: readonly LineToken[] | null;
14
+ layoutVersion: number;
15
+ whitespace: boolean;
16
+ folded: boolean;
17
+ /* What a collapsed fold keeps drawn after its placeholder, and the colors of the line it is from. */
18
+ tail: string;
19
+ tailTokens: readonly LineToken[] | null;
20
+ /* The revision of the widget a block row was drawn from. */
21
+ revision: number;
22
+ }
23
+
24
+ /* The gap around a collapsed fold's placeholder, so it reads as `{…}` and not as three separate things. */
25
+ const FOLD_GAP = 4;
26
+ const FOLD_CHIP_PADDING = 12;
27
+
28
+ export interface RowPaint {
29
+ /* Changes whenever the metrics, the wrapping or the inlays do, which moves every character. */
30
+ layoutVersion: number;
31
+ /* Spaces are drawn as dots and tabs as arrows. */
32
+ whitespace: boolean;
33
+ tokensOf(line: number): readonly LineToken[] | null;
34
+ viewportWidth: number;
35
+ /* Where a row of the host's own DOM starts and how wide it is: the whole width of the editor, over the gutter, wherever the text is scrolled to. */
36
+ hostLeft: number;
37
+ hostWidth: number;
38
+ /* A collapsed fold's row draws a chip after its line. */
39
+ onUnfold(line: number): void;
40
+ }
41
+
42
+ export function styleSpan(span: HTMLElement, token: LineToken): void {
43
+ if (token.color !== '') {
44
+ span.style.color = token.color;
45
+ }
46
+ if (token.fontStyle & ITALIC) {
47
+ span.style.fontStyle = 'italic';
48
+ }
49
+ if (token.fontStyle & BOLD) {
50
+ span.style.fontWeight = 'bold';
51
+ }
52
+ const decorations = [token.fontStyle & UNDERLINE ? 'underline' : '', token.fontStyle & STRIKETHROUGH ? 'line-through' : ''].filter(Boolean);
53
+ if (decorations.length > 0) {
54
+ span.style.textDecoration = decorations.join(' ');
55
+ }
56
+ }
57
+
58
+ /* Text in an element, with each space drawn as a faint dot when whitespace shows. A dot is as wide as a space in the code face, so nothing moves. */
59
+ function setText(element: HTMLElement, text: string, whitespace: boolean): void {
60
+ if (!whitespace || !text.includes(' ')) {
61
+ element.textContent = text;
62
+ return;
63
+ }
64
+ const document = element.ownerDocument;
65
+ for (const part of text.split(/( +)/)) {
66
+ if (part.startsWith(' ')) {
67
+ const dots = document.createElement('span');
68
+ dots.className = 'se-ws';
69
+ dots.textContent = '·'.repeat(part.length);
70
+ element.append(dots);
71
+ } else if (part !== '') {
72
+ element.append(part);
73
+ }
74
+ }
75
+ }
76
+
77
+ /* The text of one run, split along the colors of the line. `tokensFrom` is the offset the first token starts at, which is the geometry's own start unless it is a slice of a line. */
78
+ function fillRun(
79
+ run: HTMLElement,
80
+ geometry: LineGeometry,
81
+ from: number,
82
+ to: number,
83
+ tokens: readonly LineToken[] | null,
84
+ whitespace: boolean,
85
+ tokensFrom = geometry.start
86
+ ): void {
87
+ if (tokens === null) {
88
+ setText(run, geometry.text.slice(from - geometry.start, to - geometry.start), whitespace);
89
+ return;
90
+ }
91
+ let tokenStart = tokensFrom;
92
+ for (const token of tokens) {
93
+ const tokenEnd = tokenStart + token.length;
94
+ const start = Math.max(from, tokenStart);
95
+ const end = Math.min(to, tokenEnd);
96
+ if (start < end) {
97
+ const span = run.ownerDocument.createElement('span');
98
+ setText(span, geometry.text.slice(start - geometry.start, end - geometry.start), whitespace);
99
+ styleSpan(span, token);
100
+ run.append(span);
101
+ }
102
+ tokenStart = tokenEnd;
103
+ if (tokenStart >= to) {
104
+ break;
105
+ }
106
+ }
107
+ }
108
+
109
+ /* One line of text as absolutely placed runs and inlays. `firstRowOnly` leaves out what a wrapped line carries onto later visual lines. */
110
+ export function lineElement(
111
+ document: Document,
112
+ geometry: LineGeometry,
113
+ tokens: readonly LineToken[] | null,
114
+ lineHeight: number,
115
+ firstRowOnly: boolean,
116
+ whitespace = false
117
+ ): HTMLElement {
118
+ const element = document.createElement('div');
119
+ element.className = 'se-line';
120
+ element.style.width = `${geometry.width}px`;
121
+ for (const run of geometry.runs) {
122
+ if (firstRowOnly && run.subRow > 0) {
123
+ continue;
124
+ }
125
+ const span = document.createElement('span');
126
+ span.className = 'se-run';
127
+ span.style.left = `${run.x}px`;
128
+ span.style.top = `${run.subRow * lineHeight}px`;
129
+ fillRun(span, geometry, run.from, run.to, tokens, whitespace);
130
+ element.append(span);
131
+ }
132
+ if (whitespace) {
133
+ appendTabSigns(element, geometry, lineHeight, firstRowOnly);
134
+ }
135
+ for (const box of geometry.inlays) {
136
+ if (firstRowOnly && box.subRow > 0) {
137
+ continue;
138
+ }
139
+ const span = document.createElement('span');
140
+ span.className = box.inlay.ghost ? 'se-ghost' : box.inlay.composition ? 'se-composition' : 'se-inlay';
141
+ span.dataset.inlayId = box.inlay.id;
142
+ span.textContent = box.inlay.text;
143
+ span.style.left = `${box.x}px`;
144
+ span.style.top = `${box.subRow * lineHeight}px`;
145
+ span.style.width = `${box.width}px`;
146
+ element.append(span);
147
+ }
148
+ return element;
149
+ }
150
+
151
+ /*
152
+ * The text layer: one element per visible row, kept while what it shows stays the same. A line is
153
+ * drawn as absolutely placed runs of text, so the layout alone decides where each character is and
154
+ * the browser only has to paint it.
155
+ */
156
+ export class RowPainter {
157
+ private readonly rows = new Map<string, PaintedRow>();
158
+ private readonly code: HTMLElement;
159
+ private readonly layout: EditorLayout;
160
+ private readonly observer: ResizeObserver | undefined;
161
+
162
+ constructor(code: HTMLElement, layout: EditorLayout, observer: ResizeObserver | undefined) {
163
+ this.code = code;
164
+ this.layout = layout;
165
+ this.observer = observer;
166
+ }
167
+
168
+ /* Draws the rows and takes the rest away. Returns whether a widget's height moved the layout. */
169
+ paint(rows: readonly LayoutRow[], paint: RowPaint): boolean {
170
+ const visible = new Set(rows.map((row) => row.key));
171
+ for (const [key, entry] of this.rows) {
172
+ if (!visible.has(key)) {
173
+ this.observer?.unobserve(entry.element);
174
+ entry.element.remove();
175
+ this.rows.delete(key);
176
+ }
177
+ }
178
+ let measured = false;
179
+ for (const row of rows) {
180
+ if (row.kind === 'text') {
181
+ this.paintText(row, paint);
182
+ } else {
183
+ measured = this.paintBlock(row, paint) || measured;
184
+ }
185
+ }
186
+ return measured;
187
+ }
188
+
189
+ private paintText(row: TextRow, paint: RowPaint): void {
190
+ const geometry = this.layout.geometry(row);
191
+ const tokens = paint.tokensOf(row.line);
192
+ const folded = row.lastLine > row.line;
193
+ const lastLine = folded && row.tail !== null ? this.layout.document.getLine(row.lastLine) : null;
194
+ const tail = lastLine === null ? '' : lastLine.text.slice(row.tail! - lastLine.start);
195
+ const tailTokens = tail === '' ? null : paint.tokensOf(row.lastLine);
196
+ let entry = this.rows.get(row.key);
197
+ if (
198
+ !entry ||
199
+ entry.text !== geometry.text ||
200
+ entry.tokens !== tokens ||
201
+ entry.layoutVersion !== paint.layoutVersion ||
202
+ entry.whitespace !== paint.whitespace ||
203
+ entry.folded !== folded ||
204
+ entry.tail !== tail ||
205
+ entry.tailTokens !== tailTokens
206
+ ) {
207
+ const element = this.buildText(row, geometry, tokens, paint, tail, tailTokens);
208
+ if (entry) {
209
+ entry.element.replaceWith(element);
210
+ } else {
211
+ this.code.append(element);
212
+ }
213
+ entry = {
214
+ element,
215
+ text: geometry.text,
216
+ tokens,
217
+ layoutVersion: paint.layoutVersion,
218
+ whitespace: paint.whitespace,
219
+ folded,
220
+ tail,
221
+ tailTokens,
222
+ revision: 0
223
+ };
224
+ this.rows.set(row.key, entry);
225
+ }
226
+ entry.element.style.top = `${row.top}px`;
227
+ entry.element.style.height = `${row.height}px`;
228
+ entry.element.dataset.line = String(row.line);
229
+ }
230
+
231
+ private buildText(
232
+ row: TextRow,
233
+ geometry: LineGeometry,
234
+ tokens: readonly LineToken[] | null,
235
+ paint: RowPaint,
236
+ tail: string,
237
+ tailTokens: readonly LineToken[] | null
238
+ ): HTMLElement {
239
+ const document = this.code.ownerDocument;
240
+ const lineHeight = this.layout.metrics.lineHeight;
241
+ const element = lineElement(document, geometry, tokens, lineHeight, false, paint.whitespace);
242
+ if (row.lastLine > row.line) {
243
+ const chip = document.createElement('button');
244
+ chip.type = 'button';
245
+ chip.className = 'se-fold-chip';
246
+ chip.textContent = '…';
247
+ chip.tabIndex = -1;
248
+ const chipLeft = geometry.rowEnds[geometry.rowEnds.length - 1]! + FOLD_GAP;
249
+ const chipWidth = Math.ceil(this.layout.metrics.charWidth) + FOLD_CHIP_PADDING;
250
+ chip.style.left = `${chipLeft}px`;
251
+ chip.style.top = `${(geometry.subRows - 1) * lineHeight}px`;
252
+ chip.style.width = `${chipWidth}px`;
253
+ chip.addEventListener('pointerdown', (event) => event.stopPropagation());
254
+ chip.addEventListener('click', () => paint.onUnfold(row.line));
255
+ element.append(chip);
256
+ if (tail !== '' && row.tail !== null) {
257
+ this.appendTail(element, row, geometry, tail, tailTokens, chipLeft + chipWidth + FOLD_GAP);
258
+ }
259
+ }
260
+ return element;
261
+ }
262
+
263
+ /* The closing delimiter of a collapsed fold, laid out as a line of its own that starts after the placeholder. */
264
+ private appendTail(element: HTMLElement, row: TextRow, header: LineGeometry, tail: string, tokens: readonly LineToken[] | null, left: number): void {
265
+ const document = this.code.ownerDocument;
266
+ const lineStart = this.layout.document.getLine(row.lastLine).start;
267
+ const geometry = scanLine({ start: row.tail!, end: row.tail! + tail.length, text: tail }, this.layout.metrics, null, []);
268
+ for (const run of geometry.runs) {
269
+ const span = document.createElement('span');
270
+ span.className = 'se-run';
271
+ span.style.left = `${left + run.x}px`;
272
+ span.style.top = `${(header.subRows - 1) * this.layout.metrics.lineHeight}px`;
273
+ fillRun(span, geometry, run.from, run.to, tokens, false, lineStart);
274
+ element.append(span);
275
+ }
276
+ }
277
+
278
+ private paintBlock(row: Extract<LayoutRow, { kind: 'block' }>, paint: RowPaint): boolean {
279
+ const { widget } = row;
280
+ const revision = widget.revision ?? 0;
281
+ let entry = this.rows.get(row.key);
282
+ if (!entry) {
283
+ const element = this.code.ownerDocument.createElement('div');
284
+ element.className = 'se-block';
285
+ element.dataset.rowKey = row.key;
286
+ element.dataset.widgetId = widget.id;
287
+ this.fillBlock(element, widget);
288
+ this.code.append(element);
289
+ if (!widget.lens) {
290
+ this.observer?.observe(element);
291
+ }
292
+ entry = { element, text: '', tokens: null, layoutVersion: 0, whitespace: false, folded: false, tail: '', tailTokens: null, revision };
293
+ this.rows.set(row.key, entry);
294
+ } else if (entry.revision !== revision) {
295
+ entry.element.replaceChildren();
296
+ this.fillBlock(entry.element, widget);
297
+ entry.revision = revision;
298
+ }
299
+ entry.element.style.top = `${row.top}px`;
300
+ if (widget.lens) {
301
+ entry.element.style.left = `${this.layout.indentX(row.line)}px`;
302
+ entry.element.style.height = `${row.height}px`;
303
+ return false;
304
+ }
305
+ if (widget.render) {
306
+ entry.element.style.left = `${paint.hostLeft}px`;
307
+ entry.element.style.width = `${paint.hostWidth}px`;
308
+ } else {
309
+ entry.element.style.width = `${Math.max(80, paint.viewportWidth)}px`;
310
+ }
311
+ const height = entry.element.getBoundingClientRect?.().height ?? 0;
312
+ return height > 0 && this.layout.setMeasuredHeight(row.key, height);
313
+ }
314
+
315
+ private fillBlock(element: HTMLElement, widget: BlockWidget): void {
316
+ if (widget.render) {
317
+ widget.render(element);
318
+ } else {
319
+ element.textContent = widget.text ?? '';
320
+ }
321
+ }
322
+
323
+ clear(): void {
324
+ for (const entry of this.rows.values()) {
325
+ this.observer?.unobserve(entry.element);
326
+ entry.element.remove();
327
+ }
328
+ this.rows.clear();
329
+ }
330
+ }
331
+
332
+ export interface GutterPaint {
333
+ activeLines: ReadonlySet<number>;
334
+ /* How the lines the host marked differ from what it compares against. */
335
+ changes: ReadonlyMap<number, EditorChangeKind>;
336
+ /* The bar of the agent that wrote each line, which takes the place of the change mark beside it. */
337
+ attribution: ReadonlyMap<number, AttributedLines>;
338
+ /* The line highlights of the host, whose sign stands in the gutter. */
339
+ highlights: ReadonlyMap<number, AttributedLines>;
340
+ /* Lines a fold can start at, and whether each is collapsed. */
341
+ foldable: ReadonlyMap<number, boolean>;
342
+ /* The line that carries the host's button, and what it is called. */
343
+ action: { line: number; label: string } | null;
344
+ /* The host's markers by line. */
345
+ markers: ReadonlyMap<number, { id: string; label: string }>;
346
+ }
347
+
348
+ /* The line numbers of the visible rows, and the fold control of the lines that have one. */
349
+ export function paintGutter(container: HTMLElement, layout: EditorLayout, rows: readonly LayoutRow[], paint: GutterPaint): void {
350
+ const document = container.ownerDocument;
351
+ const fragment = document.createDocumentFragment();
352
+ for (const row of rows) {
353
+ if (row.kind !== 'text') {
354
+ continue;
355
+ }
356
+ const item = document.createElement('div');
357
+ item.className = paint.activeLines.has(row.line) ? 'se-line-number se-active-number' : 'se-line-number';
358
+ item.style.top = `${row.top}px`;
359
+ item.style.height = `${layout.metrics.lineHeight}px`;
360
+ const fill = paint.highlights.get(row.line)?.fill;
361
+ if (fill !== undefined) {
362
+ item.style.background = colorValue(fill);
363
+ }
364
+ const number = document.createElement('span');
365
+ number.textContent = String(row.line + 1);
366
+ item.append(number);
367
+ if (paint.action?.line === row.line) {
368
+ const button = document.createElement('button');
369
+ button.type = 'button';
370
+ button.className = 'se-gutter-action';
371
+ button.dataset.gutterAction = String(row.line);
372
+ button.tabIndex = -1;
373
+ button.setAttribute('aria-label', paint.action.label);
374
+ item.append(button);
375
+ }
376
+ const marker = paint.markers.get(row.line);
377
+ if (marker !== undefined) {
378
+ const button = document.createElement('button');
379
+ button.type = 'button';
380
+ button.className = 'se-gutter-marker';
381
+ button.dataset.gutterMarker = marker.id;
382
+ button.tabIndex = -1;
383
+ button.setAttribute('aria-label', marker.label);
384
+ item.append(button);
385
+ }
386
+ const sign = paint.highlights.get(row.line)?.sign;
387
+ if (sign) {
388
+ const mark = document.createElement('span');
389
+ mark.className = 'se-line-sign';
390
+ mark.textContent = sign;
391
+ mark.style.color = colorValue(paint.highlights.get(row.line)!.color);
392
+ item.append(mark);
393
+ }
394
+ const bar = paint.attribution.get(row.line);
395
+ if (bar) {
396
+ const mark = document.createElement('span');
397
+ mark.className = 'se-attribution';
398
+ mark.dataset.attributionId = bar.id;
399
+ mark.style.background = colorValue(bar.color);
400
+ mark.style.height = `${row.height}px`;
401
+ item.append(mark);
402
+ }
403
+ const change = paint.changes.get(row.line);
404
+ // The bar of an agent stands where the mark of a change would, and a removal is a triangle between lines that the bar never hides.
405
+ if (change && (!bar || change === 'deleted')) {
406
+ const mark = document.createElement('span');
407
+ mark.className = `se-change se-change-${change}`;
408
+ if (change !== 'deleted') {
409
+ mark.style.height = `${row.height}px`;
410
+ }
411
+ item.append(mark);
412
+ }
413
+ const collapsed = paint.foldable.get(row.line);
414
+ if (collapsed !== undefined) {
415
+ const button = document.createElement('button');
416
+ button.type = 'button';
417
+ button.className = collapsed ? 'se-fold-toggle se-folded' : 'se-fold-toggle';
418
+ button.dataset.foldLine = String(row.line);
419
+ button.tabIndex = -1;
420
+ button.setAttribute('aria-label', collapsed ? 'Expand' : 'Collapse');
421
+ item.append(button);
422
+ }
423
+ fragment.append(item);
424
+ }
425
+ container.replaceChildren(fragment);
426
+ }
427
+
428
+ export interface OverlayPaint {
429
+ currentLine: LayoutRect | null;
430
+ selections: readonly LayoutRect[];
431
+ focused: boolean;
432
+ marks: readonly { className: string; rects: readonly LayoutRect[]; background?: string }[];
433
+ }
434
+
435
+ function box(document: Document, className: string, rect: LayoutRect): HTMLElement {
436
+ const element = document.createElement('div');
437
+ element.className = className;
438
+ element.style.left = `${rect.x}px`;
439
+ element.style.top = `${rect.y}px`;
440
+ element.style.width = `${Math.max(1, rect.width)}px`;
441
+ element.style.height = `${rect.height}px`;
442
+ return element;
443
+ }
444
+
445
+ /* What sits behind the text: the current line, the selections and the marks of find and brackets. */
446
+ export function paintOverlays(container: HTMLElement, paint: OverlayPaint): void {
447
+ const document = container.ownerDocument;
448
+ const fragment = document.createDocumentFragment();
449
+ if (paint.currentLine) {
450
+ fragment.append(box(document, 'se-current-line', paint.currentLine));
451
+ }
452
+ for (const mark of paint.marks) {
453
+ for (const rect of mark.rects) {
454
+ const element = box(document, mark.className, rect);
455
+ if (mark.background) {
456
+ element.style.background = mark.background;
457
+ }
458
+ fragment.append(element);
459
+ }
460
+ }
461
+ for (const rect of paint.selections) {
462
+ fragment.append(box(document, paint.focused ? 'se-selection' : 'se-selection se-selection-inactive', rect));
463
+ }
464
+ container.replaceChildren(fragment);
465
+ }
466
+
467
+ /* What is drawn over the text, such as the fading of code nothing uses. */
468
+ export function paintOver(container: HTMLElement, layers: readonly { className: string; rects: readonly LayoutRect[] }[]): void {
469
+ const document = container.ownerDocument;
470
+ const fragment = document.createDocumentFragment();
471
+ for (const layer of layers) {
472
+ for (const rect of layer.rects) {
473
+ fragment.append(box(document, layer.className, rect));
474
+ }
475
+ }
476
+ container.replaceChildren(fragment);
477
+ }
478
+
479
+ /* The carets, above the text. */
480
+ export function paintCarets(container: HTMLElement, carets: readonly (LayoutRect & { primary: boolean; drop?: boolean })[]): void {
481
+ const document = container.ownerDocument;
482
+ container.replaceChildren(
483
+ ...carets.map((rect) => box(document, rect.drop ? 'se-drop-caret' : rect.primary ? 'se-caret se-primary-caret' : 'se-caret', { ...rect, width: 2 }))
484
+ );
485
+ }
486
+
487
+ export interface RemoteCaret {
488
+ id: string;
489
+ name: string;
490
+ color: string;
491
+ x: number;
492
+ y: number;
493
+ height: number;
494
+ /* The name goes under the caret, when there is no room above it. */
495
+ below: boolean;
496
+ }
497
+
498
+ /* The carets of agents with their names, above the text; they take no pointer and never move the real caret. */
499
+ export function paintRemoteCarets(container: HTMLElement, carets: readonly RemoteCaret[]): void {
500
+ const document = container.ownerDocument;
501
+ container.replaceChildren(
502
+ ...carets.map((caret) => {
503
+ const element = box(document, 'se-remote-caret', { x: caret.x, y: caret.y, width: 2, height: caret.height });
504
+ element.dataset.remoteId = caret.id;
505
+ element.style.background = colorValue(caret.color);
506
+ const label = document.createElement('span');
507
+ label.className = 'se-remote-label';
508
+ label.textContent = caret.name;
509
+ label.style.background = colorValue(caret.color);
510
+ if (caret.below) {
511
+ label.dataset.below = 'true';
512
+ }
513
+ element.append(label);
514
+ return element;
515
+ })
516
+ );
517
+ }
518
+
519
+ export interface StickyEntry {
520
+ line: number;
521
+ geometry: LineGeometry;
522
+ tokens: readonly LineToken[] | null;
523
+ }
524
+
525
+ /* The headers pinned at the top: a line number in the gutter's column, then the header's own text. */
526
+ export function paintSticky(container: HTMLElement, entries: readonly StickyEntry[], lineHeight: number): void {
527
+ const document = container.ownerDocument;
528
+ container.replaceChildren(
529
+ ...entries.map((entry, depth) => {
530
+ const row = document.createElement('div');
531
+ row.className = 'se-sticky-row';
532
+ row.dataset.line = String(entry.line);
533
+ row.dataset.depth = String(depth);
534
+ const number = document.createElement('span');
535
+ number.className = 'se-sticky-number';
536
+ number.textContent = String(entry.line + 1);
537
+ const code = document.createElement('div');
538
+ code.className = 'se-sticky-code';
539
+ code.append(lineElement(document, entry.geometry, entry.tokens, lineHeight, true));
540
+ row.append(number, code);
541
+ return row;
542
+ })
543
+ );
544
+ }
545
+
546
+ const SVG = 'http://www.w3.org/2000/svg';
547
+
548
+ /*
549
+ * An arrow as the platform draws a tab or a soft wrap: a line across the middle of half a line's height,
550
+ * ending in a head that points right with a bar behind it, in as much room as it has.
551
+ */
552
+ export function arrowSign(document: Document, width: number, lineHeight: number, className: string): SVGSVGElement {
553
+ const svg = document.createElementNS(SVG, 'svg');
554
+ svg.setAttribute('class', className);
555
+ svg.setAttribute('width', String(Math.max(1, Math.round(width))));
556
+ svg.setAttribute('height', String(lineHeight));
557
+ const height = Math.round(lineHeight / 2);
558
+ const stop = Math.max(1, Math.round(width)) - 1;
559
+ const bottom = Math.round((lineHeight + height) / 2);
560
+ const top = bottom - height;
561
+ const middle = bottom - height / 2;
562
+ const head = Math.max(1, Math.round(height / 2));
563
+ const path = document.createElementNS(SVG, 'path');
564
+ path.setAttribute('d', `M0 ${middle}H${stop}M${stop} ${top}V${bottom}M${stop - head} ${top}L${stop} ${middle}L${stop - head} ${bottom}Z`);
565
+ path.setAttribute('fill', 'currentColor');
566
+ path.setAttribute('stroke', 'currentColor');
567
+ path.setAttribute('stroke-width', '1');
568
+ svg.append(path);
569
+ return svg as SVGSVGElement;
570
+ }
571
+
572
+ /* An arrow over each tab of a line. */
573
+ function appendTabSigns(element: HTMLElement, geometry: LineGeometry, lineHeight: number, firstRowOnly: boolean): void {
574
+ const document = element.ownerDocument;
575
+ for (let stop = 0; stop < geometry.offsets.length - 1; stop++) {
576
+ if (geometry.text[geometry.offsets[stop]!] !== '\t' || (firstRowOnly && geometry.rowOf[stop]! > 0)) {
577
+ continue;
578
+ }
579
+ const left = geometry.before[stop]!;
580
+ const right = geometry.rowOf[stop + 1] === geometry.rowOf[stop] ? geometry.before[stop + 1]! : geometry.rowEnds[geometry.rowOf[stop]!]!;
581
+ if (right - left < 3) {
582
+ continue;
583
+ }
584
+ const sign = arrowSign(document, right - left - 2, lineHeight, 'se-tab-sign');
585
+ sign.style.left = `${left + 1}px`;
586
+ sign.style.top = `${geometry.rowOf[stop]! * lineHeight}px`;
587
+ element.append(sign);
588
+ }
589
+ }
590
+
591
+ export interface WrapSign {
592
+ x: number;
593
+ y: number;
594
+ width: number;
595
+ }
596
+
597
+ /* The signs of the soft wraps: an arrow where a row ends in the middle of a line, and one at the start of the row that goes on. */
598
+ export function paintSigns(container: HTMLElement, signs: readonly WrapSign[], lineHeight: number): void {
599
+ const document = container.ownerDocument;
600
+ container.replaceChildren(
601
+ ...signs.map((sign) => {
602
+ const arrow = arrowSign(document, sign.width, lineHeight, 'se-wrap-sign');
603
+ arrow.style.left = `${sign.x}px`;
604
+ arrow.style.top = `${sign.y}px`;
605
+ return arrow;
606
+ })
607
+ );
608
+ }