@adecore/editor 0.0.1 → 0.17.0-beta.2

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 +57 -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 +610 -0
  14. package/dist/fake.d.ts +192 -0
  15. package/dist/fake.js +663 -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 +344 -0
  69. package/dist/types.js +1 -0
  70. package/dist/view.d.ts +383 -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 +764 -0
  79. package/src/fake.ts +838 -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 +659 -0
  107. package/src/view.ts +2437 -0
package/dist/layout.js ADDED
@@ -0,0 +1,583 @@
1
+ export const TEXT_PADDING = 0;
2
+ export const BOTTOM_PADDING = 8;
3
+ export const RIGHT_PADDING = 16;
4
+ /* The room the platform keeps past the widest line, in columns of a space. */
5
+ const ADDITIONAL_COLUMNS = 3;
6
+ const DEFAULT_BLOCK_HEIGHT = 40;
7
+ /* How many characters a wrapped line's continuation rows sit in past the line's own indentation. */
8
+ const WRAP_INDENT_STEP = 2;
9
+ const GEOMETRY_CACHE_SIZE = 400;
10
+ const PLAIN = /^[\x20-\x7e\t]*$/;
11
+ const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
12
+ function lowerBound(values, value) {
13
+ let low = 0;
14
+ let high = values.length;
15
+ while (low < high) {
16
+ const middle = (low + high) >>> 1;
17
+ if (values[middle] < value) {
18
+ low = middle + 1;
19
+ }
20
+ else {
21
+ high = middle;
22
+ }
23
+ }
24
+ return low;
25
+ }
26
+ /* Where each character starts, with the length last. A line that long is cut by code unit, since splitting it by grapheme would cost more than it shows. */
27
+ function characterOffsets(text) {
28
+ const offsets = [];
29
+ if (text.length > 20000 || PLAIN.test(text)) {
30
+ for (let i = 0; i < text.length; i++) {
31
+ offsets.push(i);
32
+ }
33
+ }
34
+ else {
35
+ for (const part of SEGMENTER.segment(text)) {
36
+ offsets.push(part.index);
37
+ }
38
+ }
39
+ offsets.push(text.length);
40
+ return offsets;
41
+ }
42
+ function isSpace(character) {
43
+ return character === ' ' || character === '\t';
44
+ }
45
+ function characterWidth(metrics, text, x) {
46
+ if (text === '\t') {
47
+ const tab = metrics.tabSize * metrics.charWidth;
48
+ return (Math.floor(x / tab) + 1) * tab - x;
49
+ }
50
+ return text.length === 1 && text >= ' ' && text <= '~' ? metrics.charWidth : metrics.measureText(text);
51
+ }
52
+ /* Where the continuation rows of a wrapped line start, kept to half the row so a deep indent never leaves no room for text. */
53
+ export function wrapIndent(text, metrics, limit) {
54
+ let x = 0;
55
+ for (const character of text) {
56
+ if (!isSpace(character)) {
57
+ break;
58
+ }
59
+ x += characterWidth(metrics, character, x);
60
+ }
61
+ return Math.min(x + WRAP_INDENT_STEP * metrics.charWidth, limit / 2);
62
+ }
63
+ /*
64
+ * The stops at which a wrapped line starts a new visual line, after the space a word ends in when
65
+ * there is one on the visual line and in the middle of a word when there is not.
66
+ */
67
+ export function wrapStops(text, offsets, metrics, limit) {
68
+ const indent = wrapIndent(text, metrics, limit);
69
+ const breaks = [];
70
+ const count = offsets.length - 1;
71
+ let rowStart = 0;
72
+ let opportunity = -1;
73
+ let x = 0;
74
+ let index = 0;
75
+ while (index < count) {
76
+ const character = text.slice(offsets[index], offsets[index + 1]);
77
+ const width = characterWidth(metrics, character, x);
78
+ if (index > rowStart && x + width > limit && !isSpace(character)) {
79
+ const target = opportunity > rowStart ? opportunity : index;
80
+ breaks.push(target);
81
+ rowStart = target;
82
+ opportunity = -1;
83
+ x = indent;
84
+ index = target;
85
+ continue;
86
+ }
87
+ x += width;
88
+ if (isSpace(character) && index + 1 < count && !isSpace(text[offsets[index + 1]])) {
89
+ opportunity = index + 1;
90
+ }
91
+ index++;
92
+ }
93
+ return breaks;
94
+ }
95
+ /* A line of text laid out against its metrics; wrapped at `limit` pixels unless that is null. */
96
+ export function scanLine(line, metrics, limit, lineInlays) {
97
+ const text = line.text;
98
+ const offsets = characterOffsets(text);
99
+ const breakStops = limit === null ? [] : wrapStops(text, offsets, metrics, limit);
100
+ const indent = limit === null ? 0 : wrapIndent(text, metrics, limit);
101
+ const geometry = {
102
+ start: line.start,
103
+ end: line.end,
104
+ text,
105
+ offsets,
106
+ before: [],
107
+ after: [],
108
+ rowOf: [],
109
+ rowStarts: [0],
110
+ rowEnds: [],
111
+ runs: [],
112
+ inlays: [],
113
+ subRows: breakStops.length + 1,
114
+ width: 0
115
+ };
116
+ let x = 0;
117
+ let row = 0;
118
+ let nextBreak = 0;
119
+ let nextInlay = 0;
120
+ let runStart = -1;
121
+ let runX = 0;
122
+ const flushRun = (end) => {
123
+ if (runStart >= 0 && end > runStart) {
124
+ geometry.runs.push({ from: line.start + runStart, to: line.start + end, x: runX, subRow: row, text: text.slice(runStart, end) });
125
+ }
126
+ runStart = -1;
127
+ };
128
+ for (let stop = 0; stop < offsets.length; stop++) {
129
+ const offset = offsets[stop];
130
+ if (nextBreak < breakStops.length && breakStops[nextBreak] === stop) {
131
+ flushRun(offset);
132
+ geometry.rowEnds.push(x);
133
+ geometry.rowStarts.push(stop);
134
+ nextBreak++;
135
+ row++;
136
+ x = indent;
137
+ }
138
+ geometry.before.push(x);
139
+ geometry.rowOf.push(row);
140
+ while (nextInlay < lineInlays.length && lineInlays[nextInlay].at - line.start <= offset) {
141
+ flushRun(offset);
142
+ const inlay = lineInlays[nextInlay++];
143
+ const width = inlay.composition || inlay.ghost ? metrics.measureText(inlay.text) : Math.max(8, metrics.measureInlay(inlay.text) + 12);
144
+ geometry.inlays.push({ inlay, x, width, subRow: row });
145
+ x += width;
146
+ }
147
+ geometry.after.push(x);
148
+ if (stop === offsets.length - 1) {
149
+ break;
150
+ }
151
+ const character = text.slice(offset, offsets[stop + 1]);
152
+ const width = characterWidth(metrics, character, x);
153
+ if (character === '\t') {
154
+ flushRun(offset);
155
+ }
156
+ else if (runStart < 0) {
157
+ runStart = offset;
158
+ runX = x;
159
+ }
160
+ x += width;
161
+ }
162
+ flushRun(text.length);
163
+ geometry.rowEnds.push(x);
164
+ geometry.width = Math.max(...geometry.rowEnds);
165
+ return geometry;
166
+ }
167
+ /*
168
+ * The rows of the document as they are drawn: one per line, taller for a wrapped line, one for a
169
+ * widget, none for a line a fold hides. Only the rows that are on screen are ever measured; the
170
+ * others stand on an estimate that the first draw of a row corrects.
171
+ */
172
+ export class EditorLayout {
173
+ rows = [];
174
+ height = 0;
175
+ /* The widest line measured since the text last changed, and the room past it; what a horizontal scroll bar needs. */
176
+ width = 0;
177
+ lineRows = [];
178
+ geometryCache = new Map();
179
+ measuredRows = new Map();
180
+ measuredHeights = new Map();
181
+ inlaysByLine = new Map();
182
+ inlays = [];
183
+ blocks = [];
184
+ folds = [];
185
+ wrapWidth = null;
186
+ document;
187
+ metrics;
188
+ constructor(document, metrics) {
189
+ this.document = document;
190
+ this.metrics = metrics;
191
+ this.rebuild();
192
+ }
193
+ get wrapping() {
194
+ return this.wrapWidth !== null;
195
+ }
196
+ configure(config) {
197
+ let measureAgain = false;
198
+ if (config.metrics) {
199
+ this.metrics = config.metrics;
200
+ this.measuredHeights.clear();
201
+ measureAgain = true;
202
+ }
203
+ if (config.wrapWidth !== undefined && config.wrapWidth !== this.wrapWidth) {
204
+ this.wrapWidth = config.wrapWidth;
205
+ measureAgain = true;
206
+ }
207
+ if (config.inlays) {
208
+ this.inlays = config.inlays;
209
+ measureAgain = true;
210
+ }
211
+ if (config.blocks) {
212
+ this.blocks = config.blocks;
213
+ }
214
+ if (config.folds) {
215
+ this.folds = config.folds;
216
+ }
217
+ if (measureAgain || config.textChanged) {
218
+ this.geometryCache.clear();
219
+ this.measuredRows.clear();
220
+ this.width = 0;
221
+ }
222
+ this.rebuild();
223
+ }
224
+ rowCountOf(line, length) {
225
+ const limit = this.wrapWidth;
226
+ if (limit === null) {
227
+ return 1;
228
+ }
229
+ const measured = this.measuredRows.get(line);
230
+ if (measured !== undefined) {
231
+ return measured;
232
+ }
233
+ return Math.max(1, Math.ceil((length * this.metrics.charWidth) / Math.max(limit, this.metrics.charWidth)));
234
+ }
235
+ rebuild() {
236
+ this.inlaysByLine.clear();
237
+ for (const inlay of this.inlays) {
238
+ const line = this.document.positionAt(inlay.at).line;
239
+ const list = this.inlaysByLine.get(line) ?? [];
240
+ list.push(inlay);
241
+ this.inlaysByLine.set(line, list);
242
+ }
243
+ for (const list of this.inlaysByLine.values()) {
244
+ list.sort((left, right) => left.at - right.at);
245
+ }
246
+ const above = new Map();
247
+ const below = new Map();
248
+ for (const block of this.blocks) {
249
+ const line = this.document.positionAt(block.at).line;
250
+ const target = block.placement === 'above' ? above : below;
251
+ target.set(line, [...(target.get(line) ?? []), block]);
252
+ }
253
+ const folds = new Map();
254
+ for (const fold of this.folds) {
255
+ if (fold.collapsed && fold.endLine > (folds.get(fold.startLine)?.endLine ?? 0)) {
256
+ folds.set(fold.startLine, fold);
257
+ }
258
+ }
259
+ const count = this.document.getLineCount();
260
+ this.rows = [];
261
+ this.lineRows = new Array(count);
262
+ const addBlock = (widget, line) => {
263
+ const key = `block:${widget.id}`;
264
+ this.rows.push({
265
+ key,
266
+ kind: 'block',
267
+ index: this.rows.length,
268
+ line,
269
+ widget,
270
+ top: 0,
271
+ height: widget.lens ? this.metrics.lineHeight : (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
272
+ });
273
+ };
274
+ for (let line = 0; line < count; line++) {
275
+ for (const block of above.get(line) ?? []) {
276
+ addBlock(block, line);
277
+ }
278
+ const fold = folds.get(line);
279
+ const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
280
+ const tail = fold?.closer !== undefined && last > line ? this.tailOf(fold.closer, last) : null;
281
+ const index = this.rows.length;
282
+ const bounds = this.wrapWidth === null ? null : this.document.getLine(line);
283
+ const subRows = this.rowCountOf(line, bounds === null ? 0 : bounds.end - bounds.start);
284
+ this.rows.push({
285
+ key: `line:${line}`,
286
+ kind: 'text',
287
+ index,
288
+ line,
289
+ lastLine: last,
290
+ tail,
291
+ subRows,
292
+ top: 0,
293
+ height: subRows * this.metrics.lineHeight
294
+ });
295
+ for (let covered = line; covered <= last; covered++) {
296
+ this.lineRows[covered] = index;
297
+ }
298
+ for (const block of below.get(line) ?? []) {
299
+ addBlock(block, line);
300
+ }
301
+ line = last;
302
+ }
303
+ this.reflow();
304
+ }
305
+ /* The closer a fold ends in, clamped to its last line since the fold may be a moment older than an edit. */
306
+ tailOf(closer, line) {
307
+ const bounds = this.document.getLine(line);
308
+ return Math.min(bounds.end, Math.max(bounds.start, closer));
309
+ }
310
+ reflow() {
311
+ let top = 0;
312
+ for (const row of this.rows) {
313
+ row.top = top;
314
+ top += row.height;
315
+ }
316
+ this.height = top + BOTTOM_PADDING;
317
+ }
318
+ /* A widget's measured height. Returns whether it moved anything. */
319
+ setMeasuredHeight(key, height) {
320
+ const row = this.rows.find((candidate) => candidate.key === key);
321
+ const next = Math.max(1, Math.round(height));
322
+ if (!row || !Number.isFinite(next) || row.height === next) {
323
+ return false;
324
+ }
325
+ this.measuredHeights.set(key, next);
326
+ row.height = next;
327
+ this.reflow();
328
+ return true;
329
+ }
330
+ /* The visual lines a drawn row really has, which its estimate may have missed. Returns whether any row changed. */
331
+ syncRows(rows) {
332
+ let changed = false;
333
+ for (const row of rows) {
334
+ if (row.kind !== 'text') {
335
+ continue;
336
+ }
337
+ const subRows = this.geometry(row).subRows;
338
+ if (subRows !== row.subRows) {
339
+ this.measuredRows.set(row.line, subRows);
340
+ row.subRows = subRows;
341
+ row.height = subRows * this.metrics.lineHeight;
342
+ changed = true;
343
+ }
344
+ }
345
+ if (changed) {
346
+ this.reflow();
347
+ }
348
+ return changed;
349
+ }
350
+ rowAt(y) {
351
+ let low = 0;
352
+ let high = this.rows.length - 1;
353
+ while (low < high) {
354
+ const middle = Math.ceil((low + high) / 2);
355
+ if (this.rows[middle].top <= y) {
356
+ low = middle;
357
+ }
358
+ else {
359
+ high = middle - 1;
360
+ }
361
+ }
362
+ return this.rows[low];
363
+ }
364
+ /* Where a line's text starts after its indentation, which is where a lens row begins. */
365
+ indentX(line) {
366
+ const row = this.rowForLine(line);
367
+ if (row.kind !== 'text') {
368
+ return TEXT_PADDING;
369
+ }
370
+ const geometry = this.geometry(row);
371
+ let stop = 0;
372
+ while (stop < geometry.text.length && isSpace(geometry.text[stop])) {
373
+ stop++;
374
+ }
375
+ return TEXT_PADDING + geometry.before[Math.min(stop, geometry.before.length - 1)];
376
+ }
377
+ rowForLine(line) {
378
+ return this.rows[this.lineRows[Math.max(0, Math.min(this.lineRows.length - 1, line))]];
379
+ }
380
+ /* Whether a fold hides the line. */
381
+ isHidden(line) {
382
+ const row = this.rowForLine(line);
383
+ return row.kind === 'text' && line > row.line && line <= row.lastLine;
384
+ }
385
+ visibleRows(top, height, overscan = 120) {
386
+ const result = [];
387
+ for (let index = this.rowAt(top - overscan).index; index < this.rows.length && this.rows[index].top < top + height + overscan; index++) {
388
+ result.push(this.rows[index]);
389
+ }
390
+ return result;
391
+ }
392
+ geometry(row) {
393
+ const cached = this.geometryCache.get(row.line);
394
+ if (cached) {
395
+ return cached;
396
+ }
397
+ const result = scanLine(this.document.getLine(row.line), this.metrics, this.wrapWidth, this.inlaysByLine.get(row.line) ?? []);
398
+ if (this.wrapWidth === null) {
399
+ this.width = Math.max(this.width, result.width + ADDITIONAL_COLUMNS * this.metrics.charWidth);
400
+ }
401
+ if (this.geometryCache.size >= GEOMETRY_CACHE_SIZE) {
402
+ this.geometryCache.delete(this.geometryCache.keys().next().value);
403
+ }
404
+ this.geometryCache.set(row.line, result);
405
+ return result;
406
+ }
407
+ stopOf(geometry, offset) {
408
+ return Math.min(geometry.offsets.length - 1, lowerBound(geometry.offsets, Math.max(0, offset - geometry.start)));
409
+ }
410
+ xAt(geometry, offset, affinity = 'after') {
411
+ return TEXT_PADDING + geometry[affinity][this.stopOf(geometry, offset)];
412
+ }
413
+ /*
414
+ * The offset nearest an x on one visual line of a laid out line. `rowEnd` is set when that is the
415
+ * offset a wrap starts the next visual line at, which the caret may be drawn at the end of this one for.
416
+ */
417
+ offsetAtX(geometry, subRow, x) {
418
+ const local = x - TEXT_PADDING;
419
+ for (const box of geometry.inlays) {
420
+ if (box.subRow === subRow && local >= box.x && local <= box.x + box.width) {
421
+ return { offset: box.inlay.at, rowEnd: false };
422
+ }
423
+ }
424
+ const first = geometry.rowStarts[subRow] ?? 0;
425
+ const last = subRow + 1 < geometry.rowStarts.length ? geometry.rowStarts[subRow + 1] : geometry.offsets.length - 1;
426
+ const wrapped = subRow + 1 < geometry.rowStarts.length;
427
+ let best = first;
428
+ let distance = Math.abs(local - geometry.before[first]);
429
+ // The stop that starts the next visual line stands in for the end of this one.
430
+ for (let stop = first + 1; stop <= last; stop++) {
431
+ const edge = stop === last && wrapped ? geometry.rowEnds[subRow] : geometry.before[stop];
432
+ const candidate = Math.abs(local - edge);
433
+ if (candidate < distance) {
434
+ distance = candidate;
435
+ best = stop;
436
+ }
437
+ }
438
+ return { offset: geometry.start + geometry.offsets[best], rowEnd: wrapped && best === last };
439
+ }
440
+ /* With `rowEnd`, an offset a wrap starts a visual line at is drawn at the end of the line before it. */
441
+ caret(offset, affinity = 'after', rowEnd = false) {
442
+ const position = this.document.positionAt(offset);
443
+ const row = this.rowForLine(position.line);
444
+ if (row.kind !== 'text') {
445
+ return { x: TEXT_PADDING, y: row.top, width: 1, height: Math.min(row.height, this.metrics.lineHeight) };
446
+ }
447
+ const geometry = this.geometry(row);
448
+ const stop = this.stopOf(geometry, offset);
449
+ const subRow = geometry.rowOf[stop];
450
+ if (rowEnd && subRow > 0 && geometry.rowStarts[subRow] === stop) {
451
+ return {
452
+ x: TEXT_PADDING + geometry.rowEnds[subRow - 1],
453
+ y: row.top + (subRow - 1) * this.metrics.lineHeight,
454
+ width: 1,
455
+ height: this.metrics.lineHeight
456
+ };
457
+ }
458
+ return { x: TEXT_PADDING + geometry[affinity][stop], y: row.top + subRow * this.metrics.lineHeight, width: 1, height: this.metrics.lineHeight };
459
+ }
460
+ /* A height snapped up to the top of the visual line it is in, so a page scrolls to whole lines. */
461
+ lineBase(y) {
462
+ const row = this.rowAt(y);
463
+ if (row.kind !== 'text') {
464
+ return y;
465
+ }
466
+ const base = row.top + Math.floor((y - row.top) / this.metrics.lineHeight) * this.metrics.lineHeight;
467
+ return y > base && y < base + this.metrics.lineHeight ? base : y;
468
+ }
469
+ /*
470
+ * The visual line an offset is on, as the platform counts it: a wrapped line is as many as it has
471
+ * rows and a collapsed fold is one. `end` is where its text stops and `next` where the line after it starts.
472
+ */
473
+ visualLine(offset) {
474
+ const position = this.document.positionAt(offset);
475
+ const row = this.rowForLine(position.line);
476
+ const line = this.document.getLine(row.kind === 'text' ? row.line : position.line);
477
+ if (row.kind !== 'text') {
478
+ return { start: line.start, end: line.end, next: line.next, y: row.top };
479
+ }
480
+ const geometry = this.geometry(row);
481
+ const lastLine = this.document.getLine(row.lastLine);
482
+ const hidden = position.line > row.line;
483
+ const subRow = hidden ? geometry.subRows - 1 : geometry.rowOf[this.stopOf(geometry, offset)];
484
+ const last = subRow + 1 >= geometry.subRows;
485
+ const start = line.start + geometry.offsets[geometry.rowStarts[subRow]];
486
+ const end = last ? lastLine.end : line.start + geometry.offsets[geometry.rowStarts[subRow + 1]];
487
+ return { start, end, next: last ? lastLine.next : end, y: row.top + subRow * this.metrics.lineHeight };
488
+ }
489
+ hitTest(x, y) {
490
+ return this.hitTestRow(x, y).offset;
491
+ }
492
+ hitTestRow(x, y) {
493
+ const row = this.rowAt(y);
494
+ if (row.kind === 'block') {
495
+ return { offset: row.widget.at, rowEnd: false };
496
+ }
497
+ const geometry = this.geometry(row);
498
+ const subRow = Math.max(0, Math.min(geometry.subRows - 1, Math.floor((y - row.top) / this.metrics.lineHeight)));
499
+ return this.offsetAtX(geometry, subRow, x);
500
+ }
501
+ /* The boxes a range is drawn in, one per visual line it touches, among the rows given. */
502
+ rectangles(from, to, rows) {
503
+ const result = [];
504
+ const lineHeight = this.metrics.lineHeight;
505
+ for (const row of rows) {
506
+ if (row.kind !== 'text') {
507
+ continue;
508
+ }
509
+ const geometry = this.geometry(row);
510
+ if (from === to || from > geometry.end || to <= geometry.start) {
511
+ continue;
512
+ }
513
+ const lastStop = geometry.offsets.length - 1;
514
+ const fromStop = this.stopOf(geometry, Math.max(from, geometry.start));
515
+ const toStop = this.stopOf(geometry, Math.min(to, geometry.end));
516
+ for (let subRow = 0; subRow < geometry.subRows; subRow++) {
517
+ const last = subRow + 1 === geometry.subRows;
518
+ const rowFirst = geometry.rowStarts[subRow];
519
+ const rowEnd = last ? lastStop : geometry.rowStarts[subRow + 1];
520
+ const first = Math.max(fromStop, rowFirst);
521
+ const stop = Math.min(toStop, rowEnd);
522
+ const newline = last && to > geometry.end;
523
+ if (first >= stop && !newline) {
524
+ continue;
525
+ }
526
+ const startX = first === fromStop ? geometry.after[first] : geometry.before[first];
527
+ const endX = stop < rowEnd || last ? geometry.before[stop] : geometry.rowEnds[subRow];
528
+ result.push({
529
+ x: TEXT_PADDING + startX,
530
+ y: row.top + subRow * lineHeight,
531
+ width: Math.max(1, endX - startX + (newline ? this.metrics.charWidth : 0)),
532
+ height: lineHeight
533
+ });
534
+ }
535
+ }
536
+ return result;
537
+ }
538
+ /*
539
+ * The offset a visual line above or below, `distance` pixels away, at an x. Widgets are walked
540
+ * over, and the document's ends stop the caret on the first or the last offset.
541
+ */
542
+ verticalOffset(offset, direction, x, distance = this.metrics.lineHeight) {
543
+ const caret = this.caret(offset);
544
+ let y = caret.y + direction * distance;
545
+ if (y < 0) {
546
+ return 0;
547
+ }
548
+ let row = this.rowAt(y);
549
+ while (row.kind === 'block') {
550
+ const next = this.rows[row.index + direction];
551
+ if (!next) {
552
+ return offset;
553
+ }
554
+ row = next;
555
+ y = direction < 0 ? row.top + row.height - 1 : row.top;
556
+ }
557
+ if (y >= this.height - BOTTOM_PADDING) {
558
+ return this.document.getLine(this.document.getLineCount() - 1).end;
559
+ }
560
+ return this.hitTest(x, y);
561
+ }
562
+ /* One character left or right, over the end of a line onto the next visible one. */
563
+ horizontalOffset(offset, direction) {
564
+ const row = this.rowForLine(this.document.positionAt(offset).line);
565
+ if (row.kind === 'text') {
566
+ const geometry = this.geometry(row);
567
+ const stop = this.stopOf(geometry, offset);
568
+ const next = stop + direction;
569
+ if (next >= 0 && next < geometry.offsets.length && offset >= geometry.start && offset <= geometry.end) {
570
+ return geometry.start + geometry.offsets[next];
571
+ }
572
+ }
573
+ for (let index = row.index + direction; index >= 0 && index < this.rows.length; index += direction) {
574
+ const next = this.rows[index];
575
+ if (next.kind !== 'text') {
576
+ continue;
577
+ }
578
+ const line = this.document.getLine(next.line);
579
+ return direction < 0 ? line.end : line.start;
580
+ }
581
+ return offset;
582
+ }
583
+ }
package/dist/lens.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { EditorCodeVisionEntry } from './types.ts';
2
+ export declare function lensSignature(entries: readonly EditorCodeVisionEntry[]): string;
3
+ export declare function fillLens(container: HTMLElement, entries: readonly EditorCodeVisionEntry[], lookup: (id: string) => EditorCodeVisionEntry | undefined): void;
package/dist/lens.js ADDED
@@ -0,0 +1,29 @@
1
+ /* What decides whether a row has to be drawn again: the words and the icons, never the handlers, which a press looks up at the time. */
2
+ export function lensSignature(entries) {
3
+ return entries.map((entry) => `${entry.id}\0${entry.text}\0${entry.icon ?? ''}`).join('\n');
4
+ }
5
+ /*
6
+ * The entries of a row as buttons. A press looks its entry up when it happens, so an entry the host set
7
+ * after the row was drawn is the one that answers.
8
+ */
9
+ export function fillLens(container, entries, lookup) {
10
+ const document = container.ownerDocument;
11
+ container.classList.add('se-lens');
12
+ for (const entry of entries) {
13
+ const button = document.createElement('button');
14
+ button.type = 'button';
15
+ button.className = 'se-lens-entry';
16
+ button.tabIndex = -1;
17
+ if (entry.icon !== undefined) {
18
+ const icon = document.createElement('span');
19
+ icon.className = `se-lens-icon se-lens-icon-${entry.icon}`;
20
+ icon.setAttribute('aria-hidden', 'true');
21
+ button.append(icon);
22
+ }
23
+ button.append(entry.text);
24
+ // The editor keeps the focus, so the caret stays where it was and a popover can hand the keyboard back.
25
+ button.addEventListener('pointerdown', (event) => event.preventDefault());
26
+ button.addEventListener('click', () => lookup(entry.id)?.activate(button.getBoundingClientRect()));
27
+ container.append(button);
28
+ }
29
+ }
@@ -0,0 +1,3 @@
1
+ export type Listener = () => void;
2
+ export declare function subscribe(listeners: Set<Listener>, listener: Listener): () => void;
3
+ export declare function emit(listeners: Set<Listener>): void;
@@ -0,0 +1,12 @@
1
+ export function subscribe(listeners, listener) {
2
+ listeners.add(listener);
3
+ return () => {
4
+ listeners.delete(listener);
5
+ };
6
+ }
7
+ /* Over a copy, so a listener that unsubscribes itself does not skip the next one. */
8
+ export function emit(listeners) {
9
+ for (const listener of [...listeners]) {
10
+ listener();
11
+ }
12
+ }
@@ -0,0 +1,8 @@
1
+ import type { LayoutMetrics } from './layout.ts';
2
+ export interface EditorFont {
3
+ family: string;
4
+ size: number;
5
+ lineHeight: number;
6
+ }
7
+ export declare function readEditorFont(element: HTMLElement): EditorFont;
8
+ export declare function createMetrics(font: EditorFont, tabSize: number, document: Document): LayoutMetrics;