@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
package/src/layout.ts ADDED
@@ -0,0 +1,754 @@
1
+ import type { Position } from '@adecore/editor-core';
2
+
3
+ export type Affinity = 'before' | 'after';
4
+
5
+ export interface LayoutDocument {
6
+ getLineCount(): number;
7
+ getLine(line: number): { start: number; end: number; next: number; text: string };
8
+ positionAt(offset: number): Position;
9
+ }
10
+
11
+ export interface LayoutMetrics {
12
+ lineHeight: number;
13
+ charWidth: number;
14
+ tabSize: number;
15
+ measureText(text: string): number;
16
+ measureInlay(text: string): number;
17
+ }
18
+
19
+ /* Text drawn between two characters of a line, without being part of the document. */
20
+ export interface Inlay {
21
+ id: string;
22
+ at: number;
23
+ text: string;
24
+ tooltip?: string;
25
+ /* What an input method has composed: drawn as text in the code face, with no padding, so what stands around it does not move. */
26
+ composition?: boolean;
27
+ /* A suggestion the host drew after the caret: text in the code face with no padding, which the caret stays in front of. */
28
+ ghost?: boolean;
29
+ }
30
+
31
+ /* A row of the host's own DOM above or below a line, as tall as the host makes it. */
32
+ export interface BlockWidget {
33
+ id: string;
34
+ at: number;
35
+ placement: 'above' | 'below';
36
+ text?: string;
37
+ render?: (container: HTMLElement) => void;
38
+ height?: number;
39
+ /* Drawn again in the element it already has when this changes, which `render` alone never is. */
40
+ revision?: number;
41
+ /* A quiet row of one code line's height, placed at the indentation of its line and never measured. */
42
+ lens?: boolean;
43
+ }
44
+
45
+ /* Collapsed, it hides the lines after `startLine` through `endLine`. */
46
+ export interface FoldState {
47
+ startLine: number;
48
+ endLine: number;
49
+ collapsed: boolean;
50
+ /* Where the closing delimiter starts on `endLine`; a collapsed fold keeps it drawn after its placeholder. */
51
+ closer?: number;
52
+ }
53
+
54
+ interface RowBase {
55
+ key: string;
56
+ index: number;
57
+ /* The first document line the row stands for. */
58
+ line: number;
59
+ top: number;
60
+ height: number;
61
+ }
62
+
63
+ export interface TextRow extends RowBase {
64
+ kind: 'text';
65
+ /* The last document line the row covers: more than `line` when a fold hides the lines after it. */
66
+ lastLine: number;
67
+ /* The offset on `lastLine` from which a collapsed fold draws what stays visible after its placeholder. */
68
+ tail: number | null;
69
+ /* Visual lines, which is more than one for a wrapped line. */
70
+ subRows: number;
71
+ }
72
+
73
+ export interface BlockRow extends RowBase {
74
+ kind: 'block';
75
+ widget: BlockWidget;
76
+ }
77
+
78
+ export type LayoutRow = TextRow | BlockRow;
79
+
80
+ export interface TextRun {
81
+ from: number;
82
+ to: number;
83
+ x: number;
84
+ /* The visual line of the document line the run is on. */
85
+ subRow: number;
86
+ text: string;
87
+ }
88
+
89
+ export interface InlineBox {
90
+ inlay: Inlay;
91
+ x: number;
92
+ width: number;
93
+ subRow: number;
94
+ }
95
+
96
+ /* Where every character of one document line is, relative to the line's own origin. */
97
+ export interface LineGeometry {
98
+ start: number;
99
+ end: number;
100
+ text: string;
101
+ /* UTF-16 offsets into the line at which a character begins, then the line's length. */
102
+ offsets: number[];
103
+ /* The x of each stop before and after the inlays that sit on it. */
104
+ before: number[];
105
+ after: number[];
106
+ /* The visual line each stop is on. */
107
+ rowOf: number[];
108
+ /* The stop each visual line starts at. */
109
+ rowStarts: number[];
110
+ /* The x each visual line ends at. */
111
+ rowEnds: number[];
112
+ runs: TextRun[];
113
+ inlays: InlineBox[];
114
+ subRows: number;
115
+ width: number;
116
+ }
117
+
118
+ export interface LayoutRect {
119
+ x: number;
120
+ y: number;
121
+ width: number;
122
+ height: number;
123
+ }
124
+
125
+ export const TEXT_PADDING = 0;
126
+ export const BOTTOM_PADDING = 8;
127
+ export const RIGHT_PADDING = 16;
128
+ /* The room the platform keeps past the widest line, in columns of a space. */
129
+ const ADDITIONAL_COLUMNS = 3;
130
+ const DEFAULT_BLOCK_HEIGHT = 40;
131
+ /* How many characters a wrapped line's continuation rows sit in past the line's own indentation. */
132
+ const WRAP_INDENT_STEP = 2;
133
+ const GEOMETRY_CACHE_SIZE = 400;
134
+ const PLAIN = /^[\x20-\x7e\t]*$/;
135
+ const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
136
+
137
+ function lowerBound(values: readonly number[], value: number): number {
138
+ let low = 0;
139
+ let high = values.length;
140
+ while (low < high) {
141
+ const middle = (low + high) >>> 1;
142
+ if (values[middle]! < value) {
143
+ low = middle + 1;
144
+ } else {
145
+ high = middle;
146
+ }
147
+ }
148
+ return low;
149
+ }
150
+
151
+ /* 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. */
152
+ function characterOffsets(text: string): number[] {
153
+ const offsets: number[] = [];
154
+ if (text.length > 20000 || PLAIN.test(text)) {
155
+ for (let i = 0; i < text.length; i++) {
156
+ offsets.push(i);
157
+ }
158
+ } else {
159
+ for (const part of SEGMENTER.segment(text)) {
160
+ offsets.push(part.index);
161
+ }
162
+ }
163
+ offsets.push(text.length);
164
+ return offsets;
165
+ }
166
+
167
+ function isSpace(character: string | undefined): boolean {
168
+ return character === ' ' || character === '\t';
169
+ }
170
+
171
+ function characterWidth(metrics: LayoutMetrics, text: string, x: number): number {
172
+ if (text === '\t') {
173
+ const tab = metrics.tabSize * metrics.charWidth;
174
+ return (Math.floor(x / tab) + 1) * tab - x;
175
+ }
176
+ return text.length === 1 && text >= ' ' && text <= '~' ? metrics.charWidth : metrics.measureText(text);
177
+ }
178
+
179
+ /* Where the continuation rows of a wrapped line start, kept to half the row so a deep indent never leaves no room for text. */
180
+ export function wrapIndent(text: string, metrics: LayoutMetrics, limit: number): number {
181
+ let x = 0;
182
+ for (const character of text) {
183
+ if (!isSpace(character)) {
184
+ break;
185
+ }
186
+ x += characterWidth(metrics, character, x);
187
+ }
188
+ return Math.min(x + WRAP_INDENT_STEP * metrics.charWidth, limit / 2);
189
+ }
190
+
191
+ /*
192
+ * The stops at which a wrapped line starts a new visual line, after the space a word ends in when
193
+ * there is one on the visual line and in the middle of a word when there is not.
194
+ */
195
+ export function wrapStops(text: string, offsets: readonly number[], metrics: LayoutMetrics, limit: number): number[] {
196
+ const indent = wrapIndent(text, metrics, limit);
197
+ const breaks: number[] = [];
198
+ const count = offsets.length - 1;
199
+ let rowStart = 0;
200
+ let opportunity = -1;
201
+ let x = 0;
202
+ let index = 0;
203
+ while (index < count) {
204
+ const character = text.slice(offsets[index]!, offsets[index + 1]!);
205
+ const width = characterWidth(metrics, character, x);
206
+ if (index > rowStart && x + width > limit && !isSpace(character)) {
207
+ const target = opportunity > rowStart ? opportunity : index;
208
+ breaks.push(target);
209
+ rowStart = target;
210
+ opportunity = -1;
211
+ x = indent;
212
+ index = target;
213
+ continue;
214
+ }
215
+ x += width;
216
+ if (isSpace(character) && index + 1 < count && !isSpace(text[offsets[index + 1]!])) {
217
+ opportunity = index + 1;
218
+ }
219
+ index++;
220
+ }
221
+ return breaks;
222
+ }
223
+
224
+ /* A line of text laid out against its metrics; wrapped at `limit` pixels unless that is null. */
225
+ export function scanLine(
226
+ line: { start: number; end: number; text: string },
227
+ metrics: LayoutMetrics,
228
+ limit: number | null,
229
+ lineInlays: readonly Inlay[]
230
+ ): LineGeometry {
231
+ const text = line.text;
232
+ const offsets = characterOffsets(text);
233
+ const breakStops = limit === null ? [] : wrapStops(text, offsets, metrics, limit);
234
+ const indent = limit === null ? 0 : wrapIndent(text, metrics, limit);
235
+ const geometry: LineGeometry = {
236
+ start: line.start,
237
+ end: line.end,
238
+ text,
239
+ offsets,
240
+ before: [],
241
+ after: [],
242
+ rowOf: [],
243
+ rowStarts: [0],
244
+ rowEnds: [],
245
+ runs: [],
246
+ inlays: [],
247
+ subRows: breakStops.length + 1,
248
+ width: 0
249
+ };
250
+ let x = 0;
251
+ let row = 0;
252
+ let nextBreak = 0;
253
+ let nextInlay = 0;
254
+ let runStart = -1;
255
+ let runX = 0;
256
+ const flushRun = (end: number): void => {
257
+ if (runStart >= 0 && end > runStart) {
258
+ geometry.runs.push({ from: line.start + runStart, to: line.start + end, x: runX, subRow: row, text: text.slice(runStart, end) });
259
+ }
260
+ runStart = -1;
261
+ };
262
+ for (let stop = 0; stop < offsets.length; stop++) {
263
+ const offset = offsets[stop]!;
264
+ if (nextBreak < breakStops.length && breakStops[nextBreak] === stop) {
265
+ flushRun(offset);
266
+ geometry.rowEnds.push(x);
267
+ geometry.rowStarts.push(stop);
268
+ nextBreak++;
269
+ row++;
270
+ x = indent;
271
+ }
272
+ geometry.before.push(x);
273
+ geometry.rowOf.push(row);
274
+ while (nextInlay < lineInlays.length && lineInlays[nextInlay]!.at - line.start <= offset) {
275
+ flushRun(offset);
276
+ const inlay = lineInlays[nextInlay++]!;
277
+ const width = inlay.composition || inlay.ghost ? metrics.measureText(inlay.text) : Math.max(8, metrics.measureInlay(inlay.text) + 12);
278
+ geometry.inlays.push({ inlay, x, width, subRow: row });
279
+ x += width;
280
+ }
281
+ geometry.after.push(x);
282
+ if (stop === offsets.length - 1) {
283
+ break;
284
+ }
285
+ const character = text.slice(offset, offsets[stop + 1]!);
286
+ const width = characterWidth(metrics, character, x);
287
+ if (character === '\t') {
288
+ flushRun(offset);
289
+ } else if (runStart < 0) {
290
+ runStart = offset;
291
+ runX = x;
292
+ }
293
+ x += width;
294
+ }
295
+ flushRun(text.length);
296
+ geometry.rowEnds.push(x);
297
+ geometry.width = Math.max(...geometry.rowEnds);
298
+ return geometry;
299
+ }
300
+
301
+ export interface LayoutConfig {
302
+ /* The text changed, so nothing remembered about a line number holds any more. */
303
+ textChanged?: boolean;
304
+ metrics?: LayoutMetrics;
305
+ /* The pixels a visual line may take before it wraps; null for no wrapping. */
306
+ wrapWidth?: number | null;
307
+ inlays?: readonly Inlay[];
308
+ blocks?: readonly BlockWidget[];
309
+ folds?: readonly FoldState[];
310
+ }
311
+
312
+ /*
313
+ * The rows of the document as they are drawn: one per line, taller for a wrapped line, one for a
314
+ * widget, none for a line a fold hides. Only the rows that are on screen are ever measured; the
315
+ * others stand on an estimate that the first draw of a row corrects.
316
+ */
317
+ export class EditorLayout {
318
+ rows: LayoutRow[] = [];
319
+ height = 0;
320
+ /* The widest line measured since the text last changed, and the room past it; what a horizontal scroll bar needs. */
321
+ width = 0;
322
+ private lineRows: number[] = [];
323
+ private geometryCache = new Map<number, LineGeometry>();
324
+ private measuredRows = new Map<number, number>();
325
+ private measuredHeights = new Map<string, number>();
326
+ private inlaysByLine = new Map<number, Inlay[]>();
327
+ private inlays: readonly Inlay[] = [];
328
+ private blocks: readonly BlockWidget[] = [];
329
+ private folds: readonly FoldState[] = [];
330
+ private wrapWidth: number | null = null;
331
+
332
+ readonly document: LayoutDocument;
333
+ metrics: LayoutMetrics;
334
+
335
+ constructor(document: LayoutDocument, metrics: LayoutMetrics) {
336
+ this.document = document;
337
+ this.metrics = metrics;
338
+ this.rebuild();
339
+ }
340
+
341
+ get wrapping(): boolean {
342
+ return this.wrapWidth !== null;
343
+ }
344
+
345
+ configure(config: LayoutConfig): void {
346
+ let measureAgain = false;
347
+ if (config.metrics) {
348
+ this.metrics = config.metrics;
349
+ this.measuredHeights.clear();
350
+ measureAgain = true;
351
+ }
352
+ if (config.wrapWidth !== undefined && config.wrapWidth !== this.wrapWidth) {
353
+ this.wrapWidth = config.wrapWidth;
354
+ measureAgain = true;
355
+ }
356
+ if (config.inlays) {
357
+ this.inlays = config.inlays;
358
+ measureAgain = true;
359
+ }
360
+ if (config.blocks) {
361
+ this.blocks = config.blocks;
362
+ }
363
+ if (config.folds) {
364
+ this.folds = config.folds;
365
+ }
366
+ if (measureAgain || config.textChanged) {
367
+ this.geometryCache.clear();
368
+ this.measuredRows.clear();
369
+ this.width = 0;
370
+ }
371
+ this.rebuild();
372
+ }
373
+
374
+ private rowCountOf(line: number, length: number): number {
375
+ const limit = this.wrapWidth;
376
+ if (limit === null) {
377
+ return 1;
378
+ }
379
+ const measured = this.measuredRows.get(line);
380
+ if (measured !== undefined) {
381
+ return measured;
382
+ }
383
+ return Math.max(1, Math.ceil((length * this.metrics.charWidth) / Math.max(limit, this.metrics.charWidth)));
384
+ }
385
+
386
+ private rebuild(): void {
387
+ this.inlaysByLine.clear();
388
+ for (const inlay of this.inlays) {
389
+ const line = this.document.positionAt(inlay.at).line;
390
+ const list = this.inlaysByLine.get(line) ?? [];
391
+ list.push(inlay);
392
+ this.inlaysByLine.set(line, list);
393
+ }
394
+ for (const list of this.inlaysByLine.values()) {
395
+ list.sort((left, right) => left.at - right.at);
396
+ }
397
+ const above = new Map<number, BlockWidget[]>();
398
+ const below = new Map<number, BlockWidget[]>();
399
+ for (const block of this.blocks) {
400
+ const line = this.document.positionAt(block.at).line;
401
+ const target = block.placement === 'above' ? above : below;
402
+ target.set(line, [...(target.get(line) ?? []), block]);
403
+ }
404
+ const folds = new Map<number, FoldState>();
405
+ for (const fold of this.folds) {
406
+ if (fold.collapsed && fold.endLine > (folds.get(fold.startLine)?.endLine ?? 0)) {
407
+ folds.set(fold.startLine, fold);
408
+ }
409
+ }
410
+ const count = this.document.getLineCount();
411
+ this.rows = [];
412
+ this.lineRows = new Array<number>(count);
413
+ const addBlock = (widget: BlockWidget, line: number): void => {
414
+ const key = `block:${widget.id}`;
415
+ this.rows.push({
416
+ key,
417
+ kind: 'block',
418
+ index: this.rows.length,
419
+ line,
420
+ widget,
421
+ top: 0,
422
+ height: widget.lens ? this.metrics.lineHeight : (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
423
+ });
424
+ };
425
+ for (let line = 0; line < count; line++) {
426
+ for (const block of above.get(line) ?? []) {
427
+ addBlock(block, line);
428
+ }
429
+ const fold = folds.get(line);
430
+ const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
431
+ const tail = fold?.closer !== undefined && last > line ? this.tailOf(fold.closer, last) : null;
432
+ const index = this.rows.length;
433
+ const bounds = this.wrapWidth === null ? null : this.document.getLine(line);
434
+ const subRows = this.rowCountOf(line, bounds === null ? 0 : bounds.end - bounds.start);
435
+ this.rows.push({
436
+ key: `line:${line}`,
437
+ kind: 'text',
438
+ index,
439
+ line,
440
+ lastLine: last,
441
+ tail,
442
+ subRows,
443
+ top: 0,
444
+ height: subRows * this.metrics.lineHeight
445
+ });
446
+ for (let covered = line; covered <= last; covered++) {
447
+ this.lineRows[covered] = index;
448
+ }
449
+ for (const block of below.get(line) ?? []) {
450
+ addBlock(block, line);
451
+ }
452
+ line = last;
453
+ }
454
+ this.reflow();
455
+ }
456
+
457
+ /* The closer a fold ends in, clamped to its last line since the fold may be a moment older than an edit. */
458
+ private tailOf(closer: number, line: number): number {
459
+ const bounds = this.document.getLine(line);
460
+ return Math.min(bounds.end, Math.max(bounds.start, closer));
461
+ }
462
+
463
+ private reflow(): void {
464
+ let top = 0;
465
+ for (const row of this.rows) {
466
+ row.top = top;
467
+ top += row.height;
468
+ }
469
+ this.height = top + BOTTOM_PADDING;
470
+ }
471
+
472
+ /* A widget's measured height. Returns whether it moved anything. */
473
+ setMeasuredHeight(key: string, height: number): boolean {
474
+ const row = this.rows.find((candidate) => candidate.key === key);
475
+ const next = Math.max(1, Math.round(height));
476
+ if (!row || !Number.isFinite(next) || row.height === next) {
477
+ return false;
478
+ }
479
+ this.measuredHeights.set(key, next);
480
+ row.height = next;
481
+ this.reflow();
482
+ return true;
483
+ }
484
+
485
+ /* The visual lines a drawn row really has, which its estimate may have missed. Returns whether any row changed. */
486
+ syncRows(rows: readonly LayoutRow[]): boolean {
487
+ let changed = false;
488
+ for (const row of rows) {
489
+ if (row.kind !== 'text') {
490
+ continue;
491
+ }
492
+ const subRows = this.geometry(row).subRows;
493
+ if (subRows !== row.subRows) {
494
+ this.measuredRows.set(row.line, subRows);
495
+ row.subRows = subRows;
496
+ row.height = subRows * this.metrics.lineHeight;
497
+ changed = true;
498
+ }
499
+ }
500
+ if (changed) {
501
+ this.reflow();
502
+ }
503
+ return changed;
504
+ }
505
+
506
+ rowAt(y: number): LayoutRow {
507
+ let low = 0;
508
+ let high = this.rows.length - 1;
509
+ while (low < high) {
510
+ const middle = Math.ceil((low + high) / 2);
511
+ if (this.rows[middle]!.top <= y) {
512
+ low = middle;
513
+ } else {
514
+ high = middle - 1;
515
+ }
516
+ }
517
+ return this.rows[low]!;
518
+ }
519
+
520
+ /* Where a line's text starts after its indentation, which is where a lens row begins. */
521
+ indentX(line: number): number {
522
+ const row = this.rowForLine(line);
523
+ if (row.kind !== 'text') {
524
+ return TEXT_PADDING;
525
+ }
526
+ const geometry = this.geometry(row);
527
+ let stop = 0;
528
+ while (stop < geometry.text.length && isSpace(geometry.text[stop])) {
529
+ stop++;
530
+ }
531
+ return TEXT_PADDING + geometry.before[Math.min(stop, geometry.before.length - 1)]!;
532
+ }
533
+
534
+ rowForLine(line: number): LayoutRow {
535
+ return this.rows[this.lineRows[Math.max(0, Math.min(this.lineRows.length - 1, line))]!]!;
536
+ }
537
+
538
+ /* Whether a fold hides the line. */
539
+ isHidden(line: number): boolean {
540
+ const row = this.rowForLine(line);
541
+ return row.kind === 'text' && line > row.line && line <= row.lastLine;
542
+ }
543
+
544
+ visibleRows(top: number, height: number, overscan = 120): LayoutRow[] {
545
+ const result: LayoutRow[] = [];
546
+ for (let index = this.rowAt(top - overscan).index; index < this.rows.length && this.rows[index]!.top < top + height + overscan; index++) {
547
+ result.push(this.rows[index]!);
548
+ }
549
+ return result;
550
+ }
551
+
552
+ geometry(row: TextRow): LineGeometry {
553
+ const cached = this.geometryCache.get(row.line);
554
+ if (cached) {
555
+ return cached;
556
+ }
557
+ const result = scanLine(this.document.getLine(row.line), this.metrics, this.wrapWidth, this.inlaysByLine.get(row.line) ?? []);
558
+ if (this.wrapWidth === null) {
559
+ this.width = Math.max(this.width, result.width + ADDITIONAL_COLUMNS * this.metrics.charWidth);
560
+ }
561
+ if (this.geometryCache.size >= GEOMETRY_CACHE_SIZE) {
562
+ this.geometryCache.delete(this.geometryCache.keys().next().value!);
563
+ }
564
+ this.geometryCache.set(row.line, result);
565
+ return result;
566
+ }
567
+
568
+ private stopOf(geometry: LineGeometry, offset: number): number {
569
+ return Math.min(geometry.offsets.length - 1, lowerBound(geometry.offsets, Math.max(0, offset - geometry.start)));
570
+ }
571
+
572
+ xAt(geometry: LineGeometry, offset: number, affinity: Affinity = 'after'): number {
573
+ return TEXT_PADDING + geometry[affinity][this.stopOf(geometry, offset)]!;
574
+ }
575
+
576
+ /*
577
+ * The offset nearest an x on one visual line of a laid out line. `rowEnd` is set when that is the
578
+ * offset a wrap starts the next visual line at, which the caret may be drawn at the end of this one for.
579
+ */
580
+ offsetAtX(geometry: LineGeometry, subRow: number, x: number): { offset: number; rowEnd: boolean } {
581
+ const local = x - TEXT_PADDING;
582
+ for (const box of geometry.inlays) {
583
+ if (box.subRow === subRow && local >= box.x && local <= box.x + box.width) {
584
+ return { offset: box.inlay.at, rowEnd: false };
585
+ }
586
+ }
587
+ const first = geometry.rowStarts[subRow] ?? 0;
588
+ const last = subRow + 1 < geometry.rowStarts.length ? geometry.rowStarts[subRow + 1]! : geometry.offsets.length - 1;
589
+ const wrapped = subRow + 1 < geometry.rowStarts.length;
590
+ let best = first;
591
+ let distance = Math.abs(local - geometry.before[first]!);
592
+ // The stop that starts the next visual line stands in for the end of this one.
593
+ for (let stop = first + 1; stop <= last; stop++) {
594
+ const edge = stop === last && wrapped ? geometry.rowEnds[subRow]! : geometry.before[stop]!;
595
+ const candidate = Math.abs(local - edge);
596
+ if (candidate < distance) {
597
+ distance = candidate;
598
+ best = stop;
599
+ }
600
+ }
601
+ return { offset: geometry.start + geometry.offsets[best]!, rowEnd: wrapped && best === last };
602
+ }
603
+
604
+ /* With `rowEnd`, an offset a wrap starts a visual line at is drawn at the end of the line before it. */
605
+ caret(offset: number, affinity: Affinity = 'after', rowEnd = false): LayoutRect {
606
+ const position = this.document.positionAt(offset);
607
+ const row = this.rowForLine(position.line);
608
+ if (row.kind !== 'text') {
609
+ return { x: TEXT_PADDING, y: row.top, width: 1, height: Math.min(row.height, this.metrics.lineHeight) };
610
+ }
611
+ const geometry = this.geometry(row);
612
+ const stop = this.stopOf(geometry, offset);
613
+ const subRow = geometry.rowOf[stop]!;
614
+ if (rowEnd && subRow > 0 && geometry.rowStarts[subRow] === stop) {
615
+ return {
616
+ x: TEXT_PADDING + geometry.rowEnds[subRow - 1]!,
617
+ y: row.top + (subRow - 1) * this.metrics.lineHeight,
618
+ width: 1,
619
+ height: this.metrics.lineHeight
620
+ };
621
+ }
622
+ return { x: TEXT_PADDING + geometry[affinity][stop]!, y: row.top + subRow * this.metrics.lineHeight, width: 1, height: this.metrics.lineHeight };
623
+ }
624
+
625
+ /* A height snapped up to the top of the visual line it is in, so a page scrolls to whole lines. */
626
+ lineBase(y: number): number {
627
+ const row = this.rowAt(y);
628
+ if (row.kind !== 'text') {
629
+ return y;
630
+ }
631
+ const base = row.top + Math.floor((y - row.top) / this.metrics.lineHeight) * this.metrics.lineHeight;
632
+ return y > base && y < base + this.metrics.lineHeight ? base : y;
633
+ }
634
+
635
+ /*
636
+ * The visual line an offset is on, as the platform counts it: a wrapped line is as many as it has
637
+ * rows and a collapsed fold is one. `end` is where its text stops and `next` where the line after it starts.
638
+ */
639
+ visualLine(offset: number): { start: number; end: number; next: number; y: number } {
640
+ const position = this.document.positionAt(offset);
641
+ const row = this.rowForLine(position.line);
642
+ const line = this.document.getLine(row.kind === 'text' ? row.line : position.line);
643
+ if (row.kind !== 'text') {
644
+ return { start: line.start, end: line.end, next: line.next, y: row.top };
645
+ }
646
+ const geometry = this.geometry(row);
647
+ const lastLine = this.document.getLine(row.lastLine);
648
+ const hidden = position.line > row.line;
649
+ const subRow = hidden ? geometry.subRows - 1 : geometry.rowOf[this.stopOf(geometry, offset)]!;
650
+ const last = subRow + 1 >= geometry.subRows;
651
+ const start = line.start + geometry.offsets[geometry.rowStarts[subRow]!]!;
652
+ const end = last ? lastLine.end : line.start + geometry.offsets[geometry.rowStarts[subRow + 1]!]!;
653
+ return { start, end, next: last ? lastLine.next : end, y: row.top + subRow * this.metrics.lineHeight };
654
+ }
655
+
656
+ hitTest(x: number, y: number): number {
657
+ return this.hitTestRow(x, y).offset;
658
+ }
659
+
660
+ hitTestRow(x: number, y: number): { offset: number; rowEnd: boolean } {
661
+ const row = this.rowAt(y);
662
+ if (row.kind === 'block') {
663
+ return { offset: row.widget.at, rowEnd: false };
664
+ }
665
+ const geometry = this.geometry(row);
666
+ const subRow = Math.max(0, Math.min(geometry.subRows - 1, Math.floor((y - row.top) / this.metrics.lineHeight)));
667
+ return this.offsetAtX(geometry, subRow, x);
668
+ }
669
+
670
+ /* The boxes a range is drawn in, one per visual line it touches, among the rows given. */
671
+ rectangles(from: number, to: number, rows: readonly LayoutRow[]): LayoutRect[] {
672
+ const result: LayoutRect[] = [];
673
+ const lineHeight = this.metrics.lineHeight;
674
+ for (const row of rows) {
675
+ if (row.kind !== 'text') {
676
+ continue;
677
+ }
678
+ const geometry = this.geometry(row);
679
+ if (from === to || from > geometry.end || to <= geometry.start) {
680
+ continue;
681
+ }
682
+ const lastStop = geometry.offsets.length - 1;
683
+ const fromStop = this.stopOf(geometry, Math.max(from, geometry.start));
684
+ const toStop = this.stopOf(geometry, Math.min(to, geometry.end));
685
+ for (let subRow = 0; subRow < geometry.subRows; subRow++) {
686
+ const last = subRow + 1 === geometry.subRows;
687
+ const rowFirst = geometry.rowStarts[subRow]!;
688
+ const rowEnd = last ? lastStop : geometry.rowStarts[subRow + 1]!;
689
+ const first = Math.max(fromStop, rowFirst);
690
+ const stop = Math.min(toStop, rowEnd);
691
+ const newline = last && to > geometry.end;
692
+ if (first >= stop && !newline) {
693
+ continue;
694
+ }
695
+ const startX = first === fromStop ? geometry.after[first]! : geometry.before[first]!;
696
+ const endX = stop < rowEnd || last ? geometry.before[stop]! : geometry.rowEnds[subRow]!;
697
+ result.push({
698
+ x: TEXT_PADDING + startX,
699
+ y: row.top + subRow * lineHeight,
700
+ width: Math.max(1, endX - startX + (newline ? this.metrics.charWidth : 0)),
701
+ height: lineHeight
702
+ });
703
+ }
704
+ }
705
+ return result;
706
+ }
707
+
708
+ /*
709
+ * The offset a visual line above or below, `distance` pixels away, at an x. Widgets are walked
710
+ * over, and the document's ends stop the caret on the first or the last offset.
711
+ */
712
+ verticalOffset(offset: number, direction: -1 | 1, x: number, distance = this.metrics.lineHeight): number {
713
+ const caret = this.caret(offset);
714
+ let y = caret.y + direction * distance;
715
+ if (y < 0) {
716
+ return 0;
717
+ }
718
+ let row = this.rowAt(y);
719
+ while (row.kind === 'block') {
720
+ const next = this.rows[row.index + direction];
721
+ if (!next) {
722
+ return offset;
723
+ }
724
+ row = next;
725
+ y = direction < 0 ? row.top + row.height - 1 : row.top;
726
+ }
727
+ if (y >= this.height - BOTTOM_PADDING) {
728
+ return this.document.getLine(this.document.getLineCount() - 1).end;
729
+ }
730
+ return this.hitTest(x, y);
731
+ }
732
+
733
+ /* One character left or right, over the end of a line onto the next visible one. */
734
+ horizontalOffset(offset: number, direction: -1 | 1): number {
735
+ const row = this.rowForLine(this.document.positionAt(offset).line);
736
+ if (row.kind === 'text') {
737
+ const geometry = this.geometry(row);
738
+ const stop = this.stopOf(geometry, offset);
739
+ const next = stop + direction;
740
+ if (next >= 0 && next < geometry.offsets.length && offset >= geometry.start && offset <= geometry.end) {
741
+ return geometry.start + geometry.offsets[next]!;
742
+ }
743
+ }
744
+ for (let index = row.index + direction; index >= 0 && index < this.rows.length; index += direction) {
745
+ const next = this.rows[index]!;
746
+ if (next.kind !== 'text') {
747
+ continue;
748
+ }
749
+ const line = this.document.getLine(next.line);
750
+ return direction < 0 ? line.end : line.start;
751
+ }
752
+ return offset;
753
+ }
754
+ }