@adecore/editor 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/attribution.d.ts +9 -8
  2. package/dist/attribution.js +44 -9
  3. package/dist/bidi.d.ts +44 -0
  4. package/dist/bidi.js +26 -0
  5. package/dist/column-selection.js +2 -1
  6. package/dist/controller.d.ts +6 -1
  7. package/dist/controller.js +93 -3
  8. package/dist/editor.css +81 -1
  9. package/dist/engine.js +184 -41
  10. package/dist/fake.d.ts +43 -5
  11. package/dist/fake.js +117 -13
  12. package/dist/find-search.d.ts +26 -0
  13. package/dist/find-search.js +130 -0
  14. package/dist/find.d.ts +50 -2
  15. package/dist/find.js +575 -25
  16. package/dist/geometry-stops.d.ts +6 -0
  17. package/dist/geometry-stops.js +47 -0
  18. package/dist/grammar-preparation.d.ts +29 -0
  19. package/dist/grammar-preparation.js +147 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.js +2 -0
  22. package/dist/keymap-table.d.ts +10 -1
  23. package/dist/keymap-table.js +37 -1
  24. package/dist/keymap.d.ts +1 -0
  25. package/dist/keymap.js +4 -1
  26. package/dist/layout-changes.d.ts +6 -0
  27. package/dist/layout-changes.js +27 -0
  28. package/dist/layout.d.ts +45 -2
  29. package/dist/layout.js +202 -58
  30. package/dist/offsets.d.ts +1 -0
  31. package/dist/offsets.js +23 -0
  32. package/dist/outline.js +5 -4
  33. package/dist/paint.d.ts +11 -3
  34. package/dist/paint.js +180 -34
  35. package/dist/search-client.d.ts +27 -0
  36. package/dist/search-client.js +210 -0
  37. package/dist/search-protocol.d.ts +103 -0
  38. package/dist/search-protocol.js +3 -0
  39. package/dist/search-worker.d.ts +11 -0
  40. package/dist/search-worker.js +137 -0
  41. package/dist/semantic.d.ts +2 -1
  42. package/dist/semantic.js +6 -1
  43. package/dist/shiki-document.d.ts +8 -0
  44. package/dist/shiki-document.js +93 -0
  45. package/dist/shiki-worker.d.ts +65 -0
  46. package/dist/shiki-worker.js +265 -0
  47. package/dist/shiki.d.ts +1 -0
  48. package/dist/shiki.js +96 -33
  49. package/dist/testing.d.ts +1 -0
  50. package/dist/tokens.d.ts +2 -0
  51. package/dist/tokens.js +37 -7
  52. package/dist/touch.d.ts +27 -0
  53. package/dist/touch.js +100 -0
  54. package/dist/types.d.ts +84 -2
  55. package/dist/uniform-geometry.d.ts +7 -0
  56. package/dist/uniform-geometry.js +49 -0
  57. package/dist/view.d.ts +68 -17
  58. package/dist/view.js +424 -97
  59. package/dist/worker-client.d.ts +37 -0
  60. package/dist/worker-client.js +195 -0
  61. package/package.json +17 -2
  62. package/src/attribution.ts +50 -14
  63. package/src/bidi.ts +32 -0
  64. package/src/column-selection.ts +2 -1
  65. package/src/controller.ts +97 -3
  66. package/src/editor.css +81 -1
  67. package/src/engine.ts +216 -42
  68. package/src/fake.ts +152 -14
  69. package/src/find-search.ts +165 -0
  70. package/src/find.ts +622 -21
  71. package/src/geometry-stops.ts +51 -0
  72. package/src/grammar-preparation.ts +158 -0
  73. package/src/index.ts +10 -0
  74. package/src/keymap-table.ts +44 -2
  75. package/src/keymap.ts +5 -1
  76. package/src/layout-changes.ts +33 -0
  77. package/src/layout.ts +262 -63
  78. package/src/offsets.ts +24 -0
  79. package/src/outline.ts +5 -4
  80. package/src/paint.ts +202 -36
  81. package/src/search-client.ts +306 -0
  82. package/src/search-protocol.ts +100 -0
  83. package/src/search-worker.ts +171 -0
  84. package/src/semantic.ts +8 -2
  85. package/src/shiki-document.ts +143 -0
  86. package/src/shiki-worker.ts +344 -0
  87. package/src/shiki.ts +103 -33
  88. package/src/testing.ts +2 -0
  89. package/src/tokens.ts +36 -7
  90. package/src/touch.ts +134 -0
  91. package/src/types.ts +174 -8
  92. package/src/uniform-geometry.ts +57 -0
  93. package/src/view.ts +500 -102
  94. package/src/worker-client.ts +212 -0
package/src/layout.ts CHANGED
@@ -1,7 +1,31 @@
1
- import type { Position } from '@adecore/editor-core';
1
+ import type { DocumentChange, Position } from '@adecore/editor-core';
2
+
3
+ import { offsetOfStop, rowOfStop, stopAtOffset, stopCount, xOfStop } from './geometry-stops.ts';
4
+ import { BIDI_COLUMNS, bidiControl, visibleBidi } from './bidi.ts';
5
+ import { uniformGeometry } from './uniform-geometry.ts';
6
+ import { lineChangeMapper } from './layout-changes.ts';
2
7
 
3
8
  export type Affinity = 'before' | 'after';
4
9
 
10
+ export interface VerticalWindow {
11
+ top: number;
12
+ bottom: number;
13
+ }
14
+
15
+ export interface SubRowBand {
16
+ first: number;
17
+ last: number;
18
+ }
19
+
20
+ export function subRowBand(top: number, subRows: number, lineHeight: number, window: VerticalWindow, block = 1): SubRowBand {
21
+ const first = Math.max(0, Math.min(subRows - 1, Math.floor((window.top - top) / lineHeight)));
22
+ const last = Math.max(first, Math.min(subRows - 1, Math.ceil((window.bottom - top) / lineHeight) - 1));
23
+ return {
24
+ first: Math.floor(first / block) * block,
25
+ last: Math.min(subRows - 1, Math.ceil((last + 1) / block) * block - 1)
26
+ };
27
+ }
28
+
5
29
  export interface LayoutDocument {
6
30
  getLineCount(): number;
7
31
  getLine(line: number): { start: number; end: number; next: number; text: string };
@@ -22,6 +46,11 @@ export interface Inlay {
22
46
  at: number;
23
47
  text: string;
24
48
  tooltip?: string;
49
+ /* The text cut into pieces drawn as elements of their own; they add up to `text`. */
50
+ parts?: readonly string[];
51
+ /* Pixels of space kept clear before and after the pill. */
52
+ gapLeft?: number;
53
+ gapRight?: number;
25
54
  /* 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
55
  composition?: boolean;
27
56
  /* A suggestion the host drew after the caret: text in the code face with no padding, which the caret stays in front of. */
@@ -40,6 +69,26 @@ export interface BlockWidget {
40
69
  revision?: number;
41
70
  /* A quiet row of one code line's height, placed at the indentation of its line and never measured. */
42
71
  lens?: boolean;
72
+ fixed?: boolean;
73
+ }
74
+
75
+ /* The lines from `from` through `to` drawn as a row of the host's own DOM, as tall as the host makes it, while the text stays in the document. */
76
+ export interface BlockReplacement {
77
+ id: string;
78
+ /* Offsets inside the first and the last line it stands for. */
79
+ from: number;
80
+ to: number;
81
+ render: (container: HTMLElement) => void;
82
+ /* The height until the row has been measured. */
83
+ height?: number;
84
+ /* Drawn again in the element it already has when this changes, which `render` alone never is. */
85
+ revision?: number;
86
+ /* What assistive technology calls the row. */
87
+ label?: string;
88
+ /* Whether vertical caret movement may stop in the row's lines instead of stepping over the row. */
89
+ enterable?: boolean;
90
+ /* Called before the view scrolls to an offset inside the row's lines. */
91
+ onReveal?: () => void;
43
92
  }
44
93
 
45
94
  /* Collapsed, it hides the lines after `startLine` through `endLine`. */
@@ -72,6 +121,8 @@ export interface TextRow extends RowBase {
72
121
  head: number | null;
73
122
  /* Visual lines, which is more than one for a wrapped line. */
74
123
  subRows: number;
124
+ /* Set when the host draws the lines `line` through `lastLine` itself; the row is then as tall as its content. */
125
+ replacement?: BlockReplacement;
75
126
  }
76
127
 
77
128
  export interface BlockRow extends RowBase {
@@ -82,6 +133,7 @@ export interface BlockRow extends RowBase {
82
133
  export type LayoutRow = TextRow | BlockRow;
83
134
 
84
135
  export interface TextRun {
136
+ /* Line-relative offsets let preceding edits move a line without rewriting its runs. */
85
137
  from: number;
86
138
  to: number;
87
139
  x: number;
@@ -101,8 +153,9 @@ export interface InlineBox {
101
153
  export interface LineGeometry {
102
154
  start: number;
103
155
  end: number;
156
+ next: number;
104
157
  text: string;
105
- /* UTF-16 offsets into the line at which a character begins, then the line's length. */
158
+ /* Uniform ASCII rows share x prefixes instead of allocating these four arrays per character. */
106
159
  offsets: number[];
107
160
  /* The x of each stop before and after the inlays that sit on it. */
108
161
  before: number[];
@@ -113,6 +166,9 @@ export interface LineGeometry {
113
166
  rowStarts: number[];
114
167
  /* The x each visual line ends at. */
115
168
  rowEnds: number[];
169
+ /* The furthest stop a greedy wrap decision read, including its lookahead. */
170
+ rowTriggers: number[];
171
+ uniform?: { first: readonly number[]; continuation: readonly number[] };
116
172
  runs: TextRun[];
117
173
  inlays: InlineBox[];
118
174
  subRows: number;
@@ -139,20 +195,6 @@ const PLAIN = /^[\x20-\x7e\t]*$/;
139
195
  const GRAPHEME_LIMIT = 20000;
140
196
  const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
141
197
 
142
- function lowerBound(values: readonly number[], value: number): number {
143
- let low = 0;
144
- let high = values.length;
145
- while (low < high) {
146
- const middle = (low + high) >>> 1;
147
- if (values[middle]! < value) {
148
- low = middle + 1;
149
- } else {
150
- high = middle;
151
- }
152
- }
153
- return low;
154
- }
155
-
156
198
  /* Where each character starts, with the length last. A very long line is cut by code unit, since splitting it by grapheme would cost more than it shows. */
157
199
  function characterOffsets(text: string): number[] {
158
200
  const offsets: number[] = [];
@@ -178,7 +220,10 @@ function characterWidth(metrics: LayoutMetrics, text: string, x: number): number
178
220
  const tab = metrics.tabSize * metrics.charWidth;
179
221
  return (Math.floor(x / tab) + 1) * tab - x;
180
222
  }
181
- return text.length === 1 && text >= ' ' && text <= '~' ? metrics.charWidth : metrics.measureText(text);
223
+ if (text.length === 1 && text >= ' ' && text <= '~') {
224
+ return metrics.charWidth;
225
+ }
226
+ return bidiControl(text) !== null ? BIDI_COLUMNS * metrics.charWidth : metrics.measureText(text);
182
227
  }
183
228
 
184
229
  /* Where the continuation rows of a wrapped line start, kept to half the row so a deep indent never leaves no room for text. */
@@ -197,20 +242,22 @@ export function wrapIndent(text: string, metrics: LayoutMetrics, limit: number):
197
242
  * The stops at which a wrapped line starts a new visual line, after the space a word ends in when
198
243
  * there is one on the visual line and in the middle of a word when there is not.
199
244
  */
200
- export function wrapStops(text: string, offsets: readonly number[], metrics: LayoutMetrics, limit: number): number[] {
245
+ function wrapRows(text: string, offsets: readonly number[] | null, metrics: LayoutMetrics, limit: number): { breaks: number[]; triggers: number[] } {
201
246
  const indent = wrapIndent(text, metrics, limit);
202
247
  const breaks: number[] = [];
203
- const count = offsets.length - 1;
248
+ const triggers: number[] = [];
249
+ const count = offsets === null ? text.length : offsets.length - 1;
204
250
  let rowStart = 0;
205
251
  let opportunity = -1;
206
252
  let x = 0;
207
253
  let index = 0;
208
254
  while (index < count) {
209
- const character = text.slice(offsets[index]!, offsets[index + 1]!);
255
+ const character = offsets === null ? text[index]! : text.slice(offsets[index]!, offsets[index + 1]!);
210
256
  const width = characterWidth(metrics, character, x);
211
257
  if (index > rowStart && x + width > limit && !isSpace(character)) {
212
258
  const target = opportunity > rowStart ? opportunity : index;
213
259
  breaks.push(target);
260
+ triggers.push(index);
214
261
  rowStart = target;
215
262
  opportunity = -1;
216
263
  x = indent;
@@ -218,28 +265,35 @@ export function wrapStops(text: string, offsets: readonly number[], metrics: Lay
218
265
  continue;
219
266
  }
220
267
  x += width;
221
- if (isSpace(character) && index + 1 < count && !isSpace(text[offsets[index + 1]!])) {
268
+ if (isSpace(character) && index + 1 < count && !isSpace(text[offsets === null ? index + 1 : offsets[index + 1]!])) {
222
269
  opportunity = index + 1;
223
270
  }
224
271
  index++;
225
272
  }
226
- return breaks;
273
+ triggers.push(count);
274
+ return { breaks, triggers };
275
+ }
276
+
277
+ export function wrapStops(text: string, offsets: readonly number[], metrics: LayoutMetrics, limit: number): number[] {
278
+ return wrapRows(text, offsets, metrics, limit).breaks;
227
279
  }
228
280
 
229
281
  /* A line of text laid out against its metrics; wrapped at `limit` pixels unless that is null. */
230
- export function scanLine(
231
- line: { start: number; end: number; text: string },
282
+ export function scanDenseLine(
283
+ line: { start: number; end: number; text: string; next?: number },
232
284
  metrics: LayoutMetrics,
233
285
  limit: number | null,
234
286
  lineInlays: readonly Inlay[]
235
287
  ): LineGeometry {
236
288
  const text = line.text;
237
289
  const offsets = characterOffsets(text);
238
- const breakStops = limit === null ? [] : wrapStops(text, offsets, metrics, limit);
290
+ const wrapped = limit === null ? { breaks: [], triggers: [offsets.length - 1] } : wrapRows(text, offsets, metrics, limit);
291
+ const breakStops = wrapped.breaks;
239
292
  const indent = limit === null ? 0 : wrapIndent(text, metrics, limit);
240
293
  const geometry: LineGeometry = {
241
294
  start: line.start,
242
295
  end: line.end,
296
+ next: line.next ?? line.end,
243
297
  text,
244
298
  offsets,
245
299
  before: [],
@@ -247,6 +301,7 @@ export function scanLine(
247
301
  rowOf: [],
248
302
  rowStarts: [0],
249
303
  rowEnds: [],
304
+ rowTriggers: wrapped.triggers,
250
305
  runs: [],
251
306
  inlays: [],
252
307
  subRows: breakStops.length + 1,
@@ -260,7 +315,7 @@ export function scanLine(
260
315
  let runX = 0;
261
316
  const flushRun = (end: number): void => {
262
317
  if (runStart >= 0 && end > runStart) {
263
- geometry.runs.push({ from: line.start + runStart, to: line.start + end, x: runX, subRow: row, text: text.slice(runStart, end) });
318
+ geometry.runs.push({ from: runStart, to: end, x: runX, subRow: row, text: text.slice(runStart, end) });
264
319
  }
265
320
  runStart = -1;
266
321
  };
@@ -279,7 +334,11 @@ export function scanLine(
279
334
  while (nextInlay < lineInlays.length && lineInlays[nextInlay]!.at - line.start <= offset) {
280
335
  flushRun(offset);
281
336
  const inlay = lineInlays[nextInlay++]!;
282
- const width = inlay.composition || inlay.ghost ? metrics.measureText(inlay.text) : Math.max(8, metrics.measureInlay(inlay.text) + 12);
337
+ const shown = visibleBidi(inlay.text);
338
+ const width =
339
+ inlay.composition || inlay.ghost
340
+ ? metrics.measureText(shown)
341
+ : Math.max(8, metrics.measureInlay(shown) + 12) + (inlay.gapLeft ?? 0) + (inlay.gapRight ?? 0);
283
342
  geometry.inlays.push({ inlay, x, width, subRow: row });
284
343
  x += width;
285
344
  }
@@ -289,7 +348,7 @@ export function scanLine(
289
348
  }
290
349
  const character = text.slice(offset, offsets[stop + 1]!);
291
350
  const width = characterWidth(metrics, character, x);
292
- if (character === '\t') {
351
+ if (character === '\t' || bidiControl(character) !== null) {
293
352
  flushRun(offset);
294
353
  } else if (runStart < 0) {
295
354
  runStart = offset;
@@ -299,19 +358,39 @@ export function scanLine(
299
358
  }
300
359
  flushRun(text.length);
301
360
  geometry.rowEnds.push(x);
302
- geometry.width = Math.max(...geometry.rowEnds);
361
+ for (const end of geometry.rowEnds) {
362
+ geometry.width = Math.max(geometry.width, end);
363
+ }
303
364
  return geometry;
304
365
  }
305
366
 
367
+ export function scanLine(
368
+ line: { start: number; end: number; text: string; next?: number },
369
+ metrics: LayoutMetrics,
370
+ limit: number | null,
371
+ lineInlays: readonly Inlay[]
372
+ ): LineGeometry {
373
+ if (limit !== null && line.text.length > GRAPHEME_LIMIT && lineInlays.length === 0 && /^[\x20-\x7e]*$/.test(line.text)) {
374
+ const wrapped = wrapRows(line.text, null, metrics, limit);
375
+ const geometry = uniformGeometry(line, metrics, wrapped.breaks, wrapped.triggers, wrapIndent(line.text, metrics, limit));
376
+ if (geometry !== null) {
377
+ return geometry;
378
+ }
379
+ }
380
+ return scanDenseLine(line, metrics, limit, lineInlays);
381
+ }
382
+
306
383
  export interface LayoutConfig {
307
- /* The text changed, so nothing remembered about a line number holds any more. */
384
+ /* Clear remembered geometry when transaction offsets are unavailable. */
308
385
  textChanged?: boolean;
386
+ changes?: readonly (readonly DocumentChange[])[];
309
387
  metrics?: LayoutMetrics;
310
388
  /* The pixels a visual line may take before it wraps; null for no wrapping. */
311
389
  wrapWidth?: number | null;
312
390
  inlays?: readonly Inlay[];
313
391
  blocks?: readonly BlockWidget[];
314
392
  folds?: readonly FoldState[];
393
+ replacements?: readonly BlockReplacement[];
315
394
  }
316
395
 
317
396
  /*
@@ -322,16 +401,18 @@ export interface LayoutConfig {
322
401
  export class EditorLayout {
323
402
  rows: LayoutRow[] = [];
324
403
  height = 0;
404
+ lineHeightsVersion = 0;
325
405
  /* The widest line measured since the text last changed, and the room past it; what a horizontal scroll bar needs. */
326
406
  width = 0;
327
407
  private lineRows: number[] = [];
328
408
  private geometryCache = new Map<number, LineGeometry>();
329
- private measuredRows = new Map<number, number>();
409
+ private measuredRows = new Map<number, { start: number; end: number; next: number; subRows: number }>();
330
410
  private measuredHeights = new Map<string, number>();
331
411
  private inlaysByLine = new Map<number, Inlay[]>();
332
412
  private inlays: readonly Inlay[] = [];
333
413
  private blocks: readonly BlockWidget[] = [];
334
414
  private folds: readonly FoldState[] = [];
415
+ private replacements: readonly BlockReplacement[] = [];
335
416
  private wrapWidth: number | null = null;
336
417
 
337
418
  readonly document: LayoutDocument;
@@ -360,22 +441,80 @@ export class EditorLayout {
360
441
  }
361
442
  if (config.inlays) {
362
443
  this.inlays = config.inlays;
363
- measureAgain = true;
364
444
  }
365
445
  if (config.blocks) {
366
446
  this.blocks = config.blocks;
367
447
  }
368
448
  if (config.folds) {
369
449
  this.folds = config.folds;
450
+ this.lineHeightsVersion++;
370
451
  }
371
- if (measureAgain || config.textChanged) {
452
+ if (config.replacements) {
453
+ this.replacements = config.replacements;
454
+ this.lineHeightsVersion++;
455
+ }
456
+ if (measureAgain || (config.textChanged && !config.changes?.length)) {
457
+ this.lineHeightsVersion++;
372
458
  this.geometryCache.clear();
373
459
  this.measuredRows.clear();
374
460
  this.width = 0;
461
+ } else {
462
+ if (config.changes?.length) {
463
+ this.retainGeometry(config.changes);
464
+ this.lineHeightsVersion++;
465
+ }
466
+ if (config.inlays !== undefined) {
467
+ if (!config.changes?.length) {
468
+ for (const line of this.inlaysByLine.keys()) {
469
+ this.geometryCache.delete(line);
470
+ this.measuredRows.delete(line);
471
+ }
472
+ }
473
+ for (const inlay of config.inlays) {
474
+ const line = this.document.positionAt(inlay.at).line;
475
+ this.geometryCache.delete(line);
476
+ this.measuredRows.delete(line);
477
+ }
478
+ this.lineHeightsVersion++;
479
+ }
480
+ this.width = 0;
481
+ if (this.wrapWidth === null) {
482
+ for (const geometry of this.geometryCache.values()) {
483
+ this.width = Math.max(this.width, geometry.width + ADDITIONAL_COLUMNS * this.metrics.charWidth);
484
+ }
485
+ }
375
486
  }
376
487
  this.rebuild();
377
488
  }
378
489
 
490
+ private retainGeometry(batches: readonly (readonly DocumentChange[])[]): void {
491
+ const map = lineChangeMapper(batches);
492
+ const remap = (bounds: { start: number; end: number; next: number }): { line: number; start: number; end: number; next: number } | null => {
493
+ const next = map({ start: bounds.start, end: Math.max(bounds.end, bounds.next - 1) });
494
+ if (next === null) {
495
+ return null;
496
+ }
497
+ const delta = next.start - bounds.start;
498
+ return { start: next.start, end: bounds.end + delta, next: bounds.next + delta, line: this.document.positionAt(next.start).line };
499
+ };
500
+ const geometries = new Map<number, LineGeometry>();
501
+ for (const geometry of this.geometryCache.values()) {
502
+ const next = remap(geometry);
503
+ if (next !== null && geometry.inlays.length === 0) {
504
+ geometries.set(next.line, next.start === geometry.start ? geometry : { ...geometry, start: next.start, end: next.end, next: next.next });
505
+ }
506
+ }
507
+ const measured = new Map<number, { start: number; end: number; next: number; subRows: number }>();
508
+ for (const [line, bounds] of this.measuredRows) {
509
+ const next = remap(bounds);
510
+ if (next !== null && !this.inlaysByLine.has(line)) {
511
+ measured.set(next.line, { start: next.start, end: next.end, next: next.next, subRows: bounds.subRows });
512
+ }
513
+ }
514
+ this.geometryCache = geometries;
515
+ this.measuredRows = measured;
516
+ }
517
+
379
518
  private rowCountOf(line: number, length: number): number {
380
519
  const limit = this.wrapWidth;
381
520
  if (limit === null) {
@@ -383,7 +522,7 @@ export class EditorLayout {
383
522
  }
384
523
  const measured = this.measuredRows.get(line);
385
524
  if (measured !== undefined) {
386
- return measured;
525
+ return measured.subRows;
387
526
  }
388
527
  return Math.max(1, Math.ceil((length * this.metrics.charWidth) / Math.max(limit, this.metrics.charWidth)));
389
528
  }
@@ -412,6 +551,15 @@ export class EditorLayout {
412
551
  folds.set(fold.startLine, fold);
413
552
  }
414
553
  }
554
+ const replaced = new Map<number, { replacement: BlockReplacement; endLine: number }>();
555
+ for (const replacement of this.replacements) {
556
+ const startLine = this.document.positionAt(replacement.from).line;
557
+ const endLine = Math.max(startLine, this.document.positionAt(replacement.to).line);
558
+ // A fold the person collapsed shows its own placeholder, so folding a replaced comment keeps working.
559
+ if (!folds.has(startLine) && !replaced.has(startLine)) {
560
+ replaced.set(startLine, { replacement, endLine });
561
+ }
562
+ }
415
563
  const count = this.document.getLineCount();
416
564
  this.rows = [];
417
565
  this.lineRows = new Array<number>(count);
@@ -424,13 +572,44 @@ export class EditorLayout {
424
572
  line,
425
573
  widget,
426
574
  top: 0,
427
- height: widget.lens ? this.metrics.lineHeight : (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
575
+ height: widget.lens
576
+ ? this.metrics.lineHeight
577
+ : widget.fixed
578
+ ? Math.max(1, widget.height ?? DEFAULT_BLOCK_HEIGHT)
579
+ : (this.measuredHeights.get(key) ?? widget.height ?? DEFAULT_BLOCK_HEIGHT)
428
580
  });
429
581
  };
430
582
  for (let line = 0; line < count; line++) {
431
583
  for (const block of above.get(line) ?? []) {
432
584
  addBlock(block, line);
433
585
  }
586
+ const standIn = replaced.get(line);
587
+ if (standIn !== undefined) {
588
+ const last = Math.min(count - 1, standIn.endLine);
589
+ const key = `replaced:${standIn.replacement.id}`;
590
+ const index = this.rows.length;
591
+ this.rows.push({
592
+ key,
593
+ kind: 'text',
594
+ index,
595
+ line,
596
+ lastLine: last,
597
+ tail: null,
598
+ head: null,
599
+ subRows: 1,
600
+ top: 0,
601
+ height: this.measuredHeights.get(key) ?? standIn.replacement.height ?? DEFAULT_BLOCK_HEIGHT,
602
+ replacement: standIn.replacement
603
+ });
604
+ for (let covered = line; covered <= last; covered++) {
605
+ this.lineRows[covered] = index;
606
+ }
607
+ for (const block of below.get(last) ?? []) {
608
+ addBlock(block, last);
609
+ }
610
+ line = last;
611
+ continue;
612
+ }
434
613
  const fold = folds.get(line);
435
614
  const last = Math.max(line, Math.min(count - 1, fold?.endLine ?? line));
436
615
  const tail = fold?.closer !== undefined && last > line ? this.clampToLine(fold.closer, last) : null;
@@ -480,7 +659,7 @@ export class EditorLayout {
480
659
  setMeasuredHeight(key: string, height: number): boolean {
481
660
  const row = this.rows.find((candidate) => candidate.key === key);
482
661
  const next = Math.max(1, Math.round(height));
483
- if (!row || !Number.isFinite(next) || row.height === next) {
662
+ if (!row || (row.kind === 'block' && row.widget.fixed) || !Number.isFinite(next) || row.height === next) {
484
663
  return false;
485
664
  }
486
665
  this.measuredHeights.set(key, next);
@@ -493,18 +672,27 @@ export class EditorLayout {
493
672
  syncRows(rows: readonly LayoutRow[]): boolean {
494
673
  let changed = false;
495
674
  for (const row of rows) {
496
- if (row.kind !== 'text') {
675
+ if (row.kind !== 'text' || row.replacement !== undefined) {
497
676
  continue;
498
677
  }
499
- const subRows = this.geometry(row).subRows;
678
+ const geometry = this.geometry(row);
679
+ const subRows = geometry.subRows;
680
+ if (row.head === null && this.wrapWidth !== null) {
681
+ if (!this.measuredRows.has(row.line) && this.measuredRows.size >= GEOMETRY_CACHE_SIZE) {
682
+ this.measuredRows.delete(this.measuredRows.keys().next().value!);
683
+ }
684
+ this.measuredRows.set(row.line, { start: geometry.start, end: geometry.end, next: geometry.next, subRows });
685
+ } else {
686
+ this.measuredRows.delete(row.line);
687
+ }
500
688
  if (subRows !== row.subRows) {
501
- this.measuredRows.set(row.line, subRows);
502
689
  row.subRows = subRows;
503
690
  row.height = subRows * this.metrics.lineHeight;
504
691
  changed = true;
505
692
  }
506
693
  }
507
694
  if (changed) {
695
+ this.lineHeightsVersion++;
508
696
  this.reflow();
509
697
  }
510
698
  return changed;
@@ -535,7 +723,7 @@ export class EditorLayout {
535
723
  while (stop < geometry.text.length && isSpace(geometry.text[stop])) {
536
724
  stop++;
537
725
  }
538
- return TEXT_PADDING + geometry.before[Math.min(stop, geometry.before.length - 1)]!;
726
+ return TEXT_PADDING + xOfStop(geometry, Math.min(stop, stopCount(geometry) - 1));
539
727
  }
540
728
 
541
729
  rowForLine(line: number): LayoutRow {
@@ -591,11 +779,11 @@ export class EditorLayout {
591
779
  }
592
780
 
593
781
  private stopOf(geometry: LineGeometry, offset: number): number {
594
- return Math.min(geometry.offsets.length - 1, lowerBound(geometry.offsets, Math.max(0, offset - geometry.start)));
782
+ return stopAtOffset(geometry, Math.max(0, offset - geometry.start));
595
783
  }
596
784
 
597
785
  xAt(geometry: LineGeometry, offset: number, affinity: Affinity = 'after'): number {
598
- return TEXT_PADDING + geometry[affinity][this.stopOf(geometry, offset)]!;
786
+ return TEXT_PADDING + xOfStop(geometry, this.stopOf(geometry, offset), affinity);
599
787
  }
600
788
 
601
789
  /*
@@ -610,20 +798,20 @@ export class EditorLayout {
610
798
  }
611
799
  }
612
800
  const first = geometry.rowStarts[subRow] ?? 0;
613
- const last = subRow + 1 < geometry.rowStarts.length ? geometry.rowStarts[subRow + 1]! : geometry.offsets.length - 1;
801
+ const last = subRow + 1 < geometry.rowStarts.length ? geometry.rowStarts[subRow + 1]! : stopCount(geometry) - 1;
614
802
  const wrapped = subRow + 1 < geometry.rowStarts.length;
615
803
  let best = first;
616
- let distance = Math.abs(local - geometry.before[first]!);
804
+ let distance = Math.abs(local - xOfStop(geometry, first));
617
805
  // The stop that starts the next visual line stands in for the end of this one.
618
806
  for (let stop = first + 1; stop <= last; stop++) {
619
- const edge = stop === last && wrapped ? geometry.rowEnds[subRow]! : geometry.before[stop]!;
807
+ const edge = stop === last && wrapped ? geometry.rowEnds[subRow]! : xOfStop(geometry, stop);
620
808
  const candidate = Math.abs(local - edge);
621
809
  if (candidate < distance) {
622
810
  distance = candidate;
623
811
  best = stop;
624
812
  }
625
813
  }
626
- return { offset: geometry.start + geometry.offsets[best]!, rowEnd: wrapped && best === last };
814
+ return { offset: geometry.start + offsetOfStop(geometry, best), rowEnd: wrapped && best === last };
627
815
  }
628
816
 
629
817
  /* With `rowEnd`, an offset a wrap starts a visual line at is drawn at the end of the line before it. */
@@ -635,7 +823,7 @@ export class EditorLayout {
635
823
  }
636
824
  const geometry = this.geometry(row);
637
825
  const stop = this.stopOf(geometry, offset);
638
- const subRow = geometry.rowOf[stop]!;
826
+ const subRow = rowOfStop(geometry, stop);
639
827
  if (rowEnd && subRow > 0 && geometry.rowStarts[subRow] === stop) {
640
828
  return {
641
829
  x: TEXT_PADDING + geometry.rowEnds[subRow - 1]!,
@@ -644,7 +832,12 @@ export class EditorLayout {
644
832
  height: this.metrics.lineHeight
645
833
  };
646
834
  }
647
- return { x: TEXT_PADDING + geometry[affinity][stop]!, y: row.top + subRow * this.metrics.lineHeight, width: 1, height: this.metrics.lineHeight };
835
+ return {
836
+ x: TEXT_PADDING + xOfStop(geometry, stop, affinity),
837
+ y: row.top + subRow * this.metrics.lineHeight,
838
+ width: 1,
839
+ height: this.metrics.lineHeight
840
+ };
648
841
  }
649
842
 
650
843
  /* A height snapped up to the top of the visual line it is in, so a page scrolls to whole lines. */
@@ -671,10 +864,10 @@ export class EditorLayout {
671
864
  const geometry = this.geometry(row);
672
865
  const lastLine = this.document.getLine(row.lastLine);
673
866
  const hidden = position.line > row.line;
674
- const subRow = hidden ? geometry.subRows - 1 : geometry.rowOf[this.stopOf(geometry, offset)]!;
867
+ const subRow = hidden ? geometry.subRows - 1 : rowOfStop(geometry, this.stopOf(geometry, offset));
675
868
  const last = subRow + 1 >= geometry.subRows;
676
- const start = line.start + geometry.offsets[geometry.rowStarts[subRow]!]!;
677
- const end = last ? lastLine.end : line.start + geometry.offsets[geometry.rowStarts[subRow + 1]!]!;
869
+ const start = line.start + offsetOfStop(geometry, geometry.rowStarts[subRow]!);
870
+ const end = last ? lastLine.end : line.start + offsetOfStop(geometry, geometry.rowStarts[subRow + 1]!);
678
871
  return { start, end, next: last ? lastLine.next : end, y: row.top + subRow * this.metrics.lineHeight };
679
872
  }
680
873
 
@@ -693,21 +886,26 @@ export class EditorLayout {
693
886
  }
694
887
 
695
888
  /* The boxes a range is drawn in, one per visual line it touches, among the rows given. */
696
- rectangles(from: number, to: number, rows: readonly LayoutRow[]): LayoutRect[] {
889
+ rectangles(from: number, to: number, rows: readonly LayoutRow[], window?: VerticalWindow): LayoutRect[] {
697
890
  const result: LayoutRect[] = [];
698
891
  const lineHeight = this.metrics.lineHeight;
699
892
  for (const row of rows) {
700
- if (row.kind !== 'text') {
893
+ if (
894
+ row.kind !== 'text' ||
895
+ row.replacement !== undefined ||
896
+ (window !== undefined && (row.top >= window.bottom || row.top + row.height <= window.top))
897
+ ) {
701
898
  continue;
702
899
  }
703
900
  const geometry = this.geometry(row);
704
901
  if (from === to || from > geometry.end || to <= geometry.start) {
705
902
  continue;
706
903
  }
707
- const lastStop = geometry.offsets.length - 1;
904
+ const lastStop = stopCount(geometry) - 1;
708
905
  const fromStop = this.stopOf(geometry, Math.max(from, geometry.start));
709
906
  const toStop = this.stopOf(geometry, Math.min(to, geometry.end));
710
- for (let subRow = 0; subRow < geometry.subRows; subRow++) {
907
+ const band = window === undefined ? { first: 0, last: geometry.subRows - 1 } : subRowBand(row.top, geometry.subRows, lineHeight, window);
908
+ for (let subRow = band.first; subRow <= band.last; subRow++) {
711
909
  const last = subRow + 1 === geometry.subRows;
712
910
  const rowFirst = geometry.rowStarts[subRow]!;
713
911
  const rowEnd = last ? lastStop : geometry.rowStarts[subRow + 1]!;
@@ -717,8 +915,8 @@ export class EditorLayout {
717
915
  if (first >= stop && !newline) {
718
916
  continue;
719
917
  }
720
- const startX = first === fromStop ? geometry.after[first]! : geometry.before[first]!;
721
- const endX = stop < rowEnd || last ? geometry.before[stop]! : geometry.rowEnds[subRow]!;
918
+ const startX = first === fromStop ? xOfStop(geometry, first, 'after') : xOfStop(geometry, first);
919
+ const endX = stop < rowEnd || last ? xOfStop(geometry, stop) : geometry.rowEnds[subRow]!;
722
920
  result.push({
723
921
  x: TEXT_PADDING + startX,
724
922
  y: row.top + subRow * lineHeight,
@@ -731,8 +929,9 @@ export class EditorLayout {
731
929
  }
732
930
 
733
931
  /*
734
- * The offset a visual line above or below, `distance` pixels away, at an x. Widgets are walked
735
- * over, and the document's ends stop the caret on the first or the last offset.
932
+ * The offset a visual line above or below, `distance` pixels away, at an x. Widgets and the rows of
933
+ * replaced lines, unless the host made them enterable, are walked over, and the document's ends stop
934
+ * the caret on the first or the last offset.
736
935
  */
737
936
  verticalOffset(offset: number, direction: -1 | 1, x: number, distance = this.metrics.lineHeight): number {
738
937
  const caret = this.caret(offset);
@@ -741,10 +940,10 @@ export class EditorLayout {
741
940
  return 0;
742
941
  }
743
942
  let row = this.rowAt(y);
744
- while (row.kind === 'block') {
943
+ while (row.kind === 'block' || (row.replacement !== undefined && row.replacement.enterable !== true)) {
745
944
  const next = this.rows[row.index + direction];
746
945
  if (!next) {
747
- return offset;
946
+ return direction < 0 ? 0 : this.document.getLine(this.document.getLineCount() - 1).end;
748
947
  }
749
948
  row = next;
750
949
  y = direction < 0 ? row.top + row.height - 1 : row.top;
@@ -762,8 +961,8 @@ export class EditorLayout {
762
961
  const geometry = this.geometry(row);
763
962
  const stop = this.stopOf(geometry, offset);
764
963
  const next = stop + direction;
765
- if (next >= 0 && next < geometry.offsets.length && offset >= geometry.start && offset <= geometry.end) {
766
- return geometry.start + geometry.offsets[next]!;
964
+ if (next >= 0 && next < stopCount(geometry) && offset >= geometry.start && offset <= geometry.end) {
965
+ return geometry.start + offsetOfStop(geometry, next);
767
966
  }
768
967
  }
769
968
  for (let index = row.index + direction; index >= 0 && index < this.rows.length; index += direction) {