@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/dist/view.js CHANGED
@@ -1,11 +1,14 @@
1
+ import { offsetOfStop, xOfStop } from './geometry-stops.js';
2
+ import { visibleBidi } from './bidi.js';
1
3
  import { indentationColumn, replacementText, scanBrackets } from '@adecore/editor-core';
4
+ import { createSearchClient } from './search-client.js';
2
5
  import { FindController } from './find.js';
3
- import { EditorLayout, RIGHT_PADDING } from './layout.js';
6
+ import { EditorLayout, RIGHT_PADDING, subRowBand } from './layout.js';
4
7
  import { createMetrics, readEditorFont } from './metrics.js';
5
8
  import { AttributionRuns, colorValue, tintValue } from './attribution.js';
6
9
  import { renderCodeBlock } from './code-block.js';
7
10
  import { emit, subscribe } from './listeners.js';
8
- import { mapOffset } from './offsets.js';
11
+ import { mapOffset, offsetMapper } from './offsets.js';
9
12
  import { mapTrackedRange } from './tracked-range.js';
10
13
  import { scrollPosition } from './scroll.js';
11
14
  import { easeOut, scrollDuration } from './scroll-animation.js';
@@ -17,16 +20,16 @@ import { TokenCache } from './tokens.js';
17
20
  function sameRect(left, right) {
18
21
  return left.left === right.left && left.top === right.top && left.right === right.right && left.bottom === right.bottom;
19
22
  }
20
- function mapRange(range, changes) {
21
- return { ...range, from: mapOffset(range.from, changes), to: mapOffset(range.to, changes) };
23
+ function mapRange(range, map) {
24
+ return { ...range, from: map(range.from), to: map(range.to) };
22
25
  }
23
- function mapFoldHints(hints, changes) {
26
+ function mapFoldHints(hints, map) {
24
27
  return {
25
- symbols: hints.symbols?.map((hint) => mapRange(hint, changes)) ?? [],
28
+ symbols: hints.symbols?.map((hint) => mapRange(hint, map)) ?? [],
26
29
  ranges: hints.ranges?.map((hint) => ({
27
- ...mapRange(hint, changes),
28
- ...(hint.head === undefined ? {} : { head: mapOffset(hint.head, changes) }),
29
- ...(hint.tail === undefined ? {} : { tail: mapOffset(hint.tail, changes) })
30
+ ...mapRange(hint, map),
31
+ ...(hint.head === undefined ? {} : { head: map(hint.head) }),
32
+ ...(hint.tail === undefined ? {} : { tail: map(hint.tail) })
30
33
  })) ?? []
31
34
  };
32
35
  }
@@ -82,6 +85,7 @@ export class EditorView {
82
85
  preview;
83
86
  signs;
84
87
  notice;
88
+ live;
85
89
  sticky;
86
90
  overview;
87
91
  tickTip;
@@ -93,10 +97,17 @@ export class EditorView {
93
97
  findListeners = new Set();
94
98
  scopeListeners = new Set();
95
99
  viewListeners = new Set();
100
+ lineHeightListeners = new Set();
101
+ announcedLineHeights = -1;
96
102
  gutterActionListeners = new Set();
97
103
  gutterAction = null;
98
104
  gutterMarkerListeners = new Set();
99
105
  gutterMarkers = new Map();
106
+ lineTokenProvider = null;
107
+ lineNumberProvider = null;
108
+ lineNumberLabels = [];
109
+ lineNumberRevision = -1;
110
+ lineNumberWidth = 0;
100
111
  semantic = [];
101
112
  scopeColors = null;
102
113
  semanticVersion = 0;
@@ -110,8 +121,9 @@ export class EditorView {
110
121
  outline = new Outline();
111
122
  stickyEntries = [];
112
123
  scopeKey = '';
113
- changeMarks = [];
124
+ changeLayers = new Map();
114
125
  changeVersion = 0;
126
+ changeCache = null;
115
127
  attribution = new AttributionRuns();
116
128
  highlights = new AttributionRuns();
117
129
  remoteCarets = [];
@@ -137,9 +149,14 @@ export class EditorView {
137
149
  frameGeometry = null;
138
150
  disposed = false;
139
151
  inlays = [];
152
+ inlayHoverListeners = new Set();
153
+ inlayHover = null;
154
+ inlayLink = null;
155
+ inlayLinkElement = null;
140
156
  /* The host's widgets by owner, so one owner setting its rows never takes another's away. */
141
157
  blocks = new Map();
142
158
  lenses = [];
159
+ replacements = new Map();
143
160
  occurrences = [];
144
161
  replacePreview = null;
145
162
  dropCaret = null;
@@ -166,6 +183,8 @@ export class EditorView {
166
183
  foldTimer;
167
184
  colorTimer;
168
185
  noticeTimer;
186
+ liveTimer;
187
+ lineLabelProvider = null;
169
188
  bracketCache;
170
189
  wrapWidth = null;
171
190
  /* The offset a click past the end of a wrapped visual line chose, which its caret is drawn at the end of that line for. */
@@ -220,6 +239,8 @@ export class EditorView {
220
239
  for (const name of ['autocapitalize', 'autocomplete', 'autocorrect']) {
221
240
  this.input.setAttribute(name, 'off');
222
241
  }
242
+ this.input.setAttribute('enterkeyhint', 'enter');
243
+ this.input.setAttribute('inputmode', 'text');
223
244
  this.input.setAttribute('aria-multiline', 'true');
224
245
  this.sticky.setAttribute('aria-hidden', 'true');
225
246
  this.sticky.hidden = true;
@@ -230,13 +251,30 @@ export class EditorView {
230
251
  this.notice = make('div', 'se-notice');
231
252
  this.notice.setAttribute('role', 'status');
232
253
  this.notice.hidden = true;
233
- this.root.append(this.viewport, this.overview, this.tickTip, this.input, this.notice);
254
+ this.live = make('div', 'se-live');
255
+ this.live.setAttribute('role', 'status');
256
+ this.live.setAttribute('aria-live', 'polite');
257
+ this.live.setAttribute('aria-atomic', 'true');
258
+ this.root.append(this.viewport, this.overview, this.tickTip, this.input, this.notice, this.live);
234
259
  this.wireOverview();
235
260
  this.wireAttribution();
236
261
  container.append(this.root);
237
262
  this.font = readEditorFont(this.root);
238
263
  this.layout = new EditorLayout(model, createMetrics(this.font, settings.tabSize, document));
239
- this.find = new FindController(model);
264
+ this.find = new FindController(model, {
265
+ createClient: () => (settings.searchWorker === undefined ? null : createSearchClient({ workerFactory: settings.searchWorker })),
266
+ changed: (reveal) => {
267
+ if (this.disposed) {
268
+ return;
269
+ }
270
+ this.announceFind();
271
+ if (reveal) {
272
+ this.revealFind();
273
+ }
274
+ this.paintPreview();
275
+ this.requestRender();
276
+ }
277
+ });
240
278
  this.tokens = new TokenCache({ getLineCount: () => model.getLineCount(), getLineText: (line) => model.getLine(line).text });
241
279
  const ResizeObserverClass = document.defaultView?.ResizeObserver;
242
280
  this.blockObserver = ResizeObserverClass
@@ -272,6 +310,10 @@ export class EditorView {
272
310
  this.layout.configure({ metrics: createMetrics(this.font, this.settings.tabSize, this.document) });
273
311
  this.render();
274
312
  }
313
+ /* Words the gutter draws changed, not a metric: the lines keep their layout. */
314
+ repaint() {
315
+ this.render();
316
+ }
275
317
  refreshFont() {
276
318
  this.font = readEditorFont(this.root);
277
319
  this.applySettings();
@@ -281,8 +323,33 @@ export class EditorView {
281
323
  this.colorAhead();
282
324
  this.render();
283
325
  }
326
+ grammarChanged() {
327
+ if (!this.disposed && this.lineTokenProvider === null && this.tokens.revalidate()) {
328
+ this.colorAhead(true);
329
+ }
330
+ }
331
+ setLineTokens(tokens) {
332
+ this.lineTokenProvider = tokens;
333
+ clearTimeout(this.colorTimer);
334
+ this.semanticLines.clear();
335
+ this.layoutVersion++;
336
+ this.render();
337
+ }
338
+ setLineNumbers(labels) {
339
+ this.lineNumberProvider = labels;
340
+ this.lineNumberRevision = -1;
341
+ this.stickyEntries = [];
342
+ this.render();
343
+ }
344
+ lineNumberLabel(line) {
345
+ return this.lineNumberProvider === null ? String(line + 1) : (this.lineNumberLabels[line] ?? null);
346
+ }
284
347
  setInlays(inlays) {
285
- this.inlays = inlays.map((inlay) => ({ ...inlay, text: inlay.text.replace(/[\r\n]/g, ' ') }));
348
+ this.inlays = inlays.map((inlay) => ({
349
+ ...inlay,
350
+ text: inlay.text.replace(/[\r\n]/g, ' '),
351
+ ...(inlay.parts === undefined ? {} : { parts: inlay.parts.map((part) => part.replace(/[\r\n]/g, ' ')) })
352
+ }));
286
353
  this.configureText();
287
354
  }
288
355
  setBlockWidgets(owner, blocks) {
@@ -295,6 +362,23 @@ export class EditorView {
295
362
  this.anchorScroll(() => this.layout.configure({ blocks: this.allBlocks() }));
296
363
  this.render();
297
364
  }
365
+ /* Lines the host draws as a row of its own, by owner; where two owners claim the same line the owner that sorts first keeps it. */
366
+ setReplacements(owner, replacements) {
367
+ if (replacements.length === 0 && !this.replacements.has(owner)) {
368
+ return;
369
+ }
370
+ if (replacements.length === 0) {
371
+ this.replacements.delete(owner);
372
+ }
373
+ else {
374
+ this.replacements.set(owner, [...replacements]);
375
+ }
376
+ this.anchorScroll(() => this.layout.configure({ replacements: this.allReplacements() }));
377
+ this.render();
378
+ }
379
+ allReplacements() {
380
+ return [...this.replacements.keys()].sort().flatMap((owner) => this.replacements.get(owner));
381
+ }
298
382
  /* The quiet rows above declarations: kept apart from the host's widgets, so setting one kind never takes the other away. */
299
383
  setLenses(lenses) {
300
384
  this.lenses = [...lenses];
@@ -334,7 +418,7 @@ export class EditorView {
334
418
  }
335
419
  /* The grammar's colors of a line with the servers' laid over them, the same object for as long as neither changed. */
336
420
  styledTokensOf(line) {
337
- const base = this.tokens.tokensOf(line);
421
+ const base = this.lineTokenProvider === null ? this.tokens.tokensOf(line) : (this.lineTokenProvider(line) ?? null);
338
422
  if (base === null || this.semantic.length === 0 || this.scopeColors === null) {
339
423
  return base;
340
424
  }
@@ -342,7 +426,7 @@ export class EditorView {
342
426
  if (spans === undefined) {
343
427
  return base;
344
428
  }
345
- const signature = spans.map((span) => `${span.from}:${span.to}:${span.color}:${span.fontStyle}`).join('|');
429
+ const signature = spans.map((span) => `${span.from}:${span.to}:${span.color}:${span.fontStyle}:${span.within ?? ''}`).join('|');
346
430
  const cached = this.semanticLines.get(line);
347
431
  if (cached?.base === base && cached.signature === signature) {
348
432
  return cached.result;
@@ -373,7 +457,7 @@ export class EditorView {
373
457
  continue;
374
458
  }
375
459
  const list = lines.get(line) ?? [];
376
- list.push({ from: token.from - start, to: token.to - start, color: style.color, fontStyle: style.fontStyle });
460
+ list.push({ from: token.from - start, to: token.to - start, color: style.color, fontStyle: style.fontStyle, within: token.within });
377
461
  lines.set(line, list);
378
462
  }
379
463
  for (const list of lines.values()) {
@@ -400,6 +484,41 @@ export class EditorView {
400
484
  this.notice.hidden = true;
401
485
  }, NOTICE_MS);
402
486
  }
487
+ foldName(line, collapsed) {
488
+ const template = (collapsed ? this.settings.messages.unfold : this.settings.messages.fold) ??
489
+ (collapsed ? 'Unfold lines {start} to {end}' : 'Fold lines {start} to {end}');
490
+ const end = this.foldAt(line)?.endLine ?? line;
491
+ return template.replace('{start}', String(line + 1)).replace('{end}', String(end + 1));
492
+ }
493
+ /* A new child each time, since a region only speaks what is added to it and a repeated sentence would otherwise stay silent. */
494
+ announce(message) {
495
+ const sentence = this.document.createElement('span');
496
+ sentence.textContent = message;
497
+ this.live.replaceChildren(sentence);
498
+ clearTimeout(this.liveTimer);
499
+ this.liveTimer = setTimeout(() => this.live.replaceChildren(), NOTICE_MS);
500
+ }
501
+ setInputPopup(popup) {
502
+ if (popup === null) {
503
+ for (const name of ['aria-expanded', 'aria-controls', 'aria-autocomplete', 'aria-activedescendant']) {
504
+ this.input.removeAttribute(name);
505
+ }
506
+ return;
507
+ }
508
+ this.input.setAttribute('aria-expanded', 'true');
509
+ this.input.setAttribute('aria-autocomplete', 'list');
510
+ this.input.setAttribute('aria-controls', popup.controls);
511
+ if (popup.activeDescendant === undefined) {
512
+ this.input.removeAttribute('aria-activedescendant');
513
+ }
514
+ else {
515
+ this.input.setAttribute('aria-activedescendant', popup.activeDescendant);
516
+ }
517
+ }
518
+ setLineLabels(labels) {
519
+ this.lineLabelProvider = labels;
520
+ this.render();
521
+ }
403
522
  get gutterElement() {
404
523
  return this.gutter;
405
524
  }
@@ -412,6 +531,9 @@ export class EditorView {
412
531
  get focused() {
413
532
  return this.document.activeElement === this.input;
414
533
  }
534
+ get contentHeight() {
535
+ return this.layout.height + Math.max(0, (this.viewport.offsetHeight ?? 0) - (this.viewport.clientHeight ?? 0));
536
+ }
415
537
  get viewportHeight() {
416
538
  return this.clientHeight || DEFAULT_HEIGHT;
417
539
  }
@@ -461,7 +583,14 @@ export class EditorView {
461
583
  this.outline.edited(batches);
462
584
  this.changeVersion++;
463
585
  this.tokens.edited(batches, (offset) => this.model.positionAt(offset).line);
464
- this.layout.configure({ textChanged: true, inlays: this.displayedInlays(), blocks: this.allBlocks(), folds: this.foldStates() });
586
+ this.layout.configure({
587
+ textChanged: true,
588
+ changes: batches,
589
+ inlays: this.displayedInlays(),
590
+ blocks: this.allBlocks(),
591
+ folds: this.foldStates(),
592
+ replacements: this.allReplacements()
593
+ });
465
594
  if (anchor && this.viewport.scrollTop > 0) {
466
595
  let offset = anchor.offset;
467
596
  for (const changes of batches) {
@@ -481,28 +610,34 @@ export class EditorView {
481
610
  }
482
611
  /* Moves everything anchored to an offset along with one batch of changes. */
483
612
  followEdit(changes) {
484
- this.inlays = this.inlays.map((inlay) => ({ ...inlay, at: mapOffset(inlay.at, changes) }));
613
+ const map = offsetMapper(changes);
614
+ this.inlays = this.inlays.map((inlay) => ({ ...inlay, at: map(inlay.at) }));
485
615
  for (const [owner, blocks] of this.blocks) {
486
- this.blocks.set(owner, blocks.map((block) => ({ ...block, at: mapOffset(block.at, changes) })));
616
+ this.blocks.set(owner, blocks.map((block) => ({ ...block, at: map(block.at) })));
487
617
  }
488
- this.lenses = this.lenses.map((lens) => ({ ...lens, at: mapOffset(lens.at, changes) }));
489
- this.collapsed = new Set([...this.collapsed].map((anchor) => mapOffset(anchor, changes)));
490
- this.settled = new Set([...this.settled].map((anchor) => mapOffset(anchor, changes)));
618
+ this.lenses = this.lenses.map((lens) => ({ ...lens, at: map(lens.at) }));
619
+ for (const [owner, entries] of this.replacements) {
620
+ this.replacements.set(owner, entries.map((entry) => ({ ...entry, from: map(entry.from), to: map(entry.to) })));
621
+ }
622
+ this.collapsed = new Set([...this.collapsed].map((anchor) => map(anchor)));
623
+ this.settled = new Set([...this.settled].map((anchor) => map(anchor)));
491
624
  if (this.foldHints !== null) {
492
- this.foldHints = mapFoldHints(this.foldHints, changes);
625
+ this.foldHints = mapFoldHints(this.foldHints, map);
493
626
  }
494
- this.customFolds = this.customFolds.map((fold) => mapRange(fold, changes));
627
+ this.customFolds = this.customFolds.map((fold) => mapRange(fold, map));
495
628
  this.find.mapBounds(changes);
496
- this.changeMarks = this.changeMarks.map((mark) => mapRange(mark, changes));
497
- this.markers = this.markers.map((marker) => mapRange(marker, changes));
629
+ for (const [layer, marks] of this.changeLayers) {
630
+ this.changeLayers.set(layer, marks.map((mark) => mapRange(mark, map)));
631
+ }
632
+ this.markers = this.markers.map((marker) => mapRange(marker, map));
498
633
  this.mapTracked(changes);
499
- this.attribution.map(changes);
500
- this.highlights.map(changes);
501
- this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at: mapOffset(caret.at, changes) }));
634
+ this.attribution.map(changes, map);
635
+ this.highlights.map(changes, map);
636
+ this.remoteCarets = this.remoteCarets.map((caret) => ({ ...caret, at: map(caret.at) }));
502
637
  for (const [owner, entries] of this.lineActions) {
503
- this.lineActions.set(owner, entries.map((entry) => ({ ...entry, at: mapOffset(entry.at, changes) })));
638
+ this.lineActions.set(owner, entries.map((entry) => ({ ...entry, at: map(entry.at) })));
504
639
  }
505
- this.semantic = this.semantic.map((token) => mapRange(token, changes));
640
+ this.semantic = this.semantic.map((token) => mapRange(token, map));
506
641
  }
507
642
  mapTracked(changes) {
508
643
  for (const entry of this.tracked) {
@@ -531,13 +666,14 @@ export class EditorView {
531
666
  }
532
667
  /* The ranges follow the text until the next read of the document, so a fold above an edit stays closed meanwhile. */
533
668
  mappedFolds(batches) {
669
+ const mappers = batches.map(offsetMapper);
534
670
  const mapped = [];
535
671
  for (const range of this.foldRanges) {
536
672
  let from = range.from;
537
673
  let to = range.to;
538
- for (const changes of batches) {
539
- from = mapOffset(from, changes);
540
- to = mapOffset(to, changes);
674
+ for (const map of mappers) {
675
+ from = map(from);
676
+ to = map(to);
541
677
  }
542
678
  const startLine = this.model.positionAt(from).line;
543
679
  const endLine = this.model.positionAt(to).line;
@@ -574,7 +710,7 @@ export class EditorView {
574
710
  height: rest.length * this.layout.metrics.lineHeight,
575
711
  render: (container) => {
576
712
  container.classList.add('se-widget', 'se-ghost-rows');
577
- renderCodeBlock(container, rest.join('\n'), null, this.settings.tabSize, {});
713
+ renderCodeBlock(container, visibleBidi(rest.join('\n')), null, this.settings.tabSize, {});
578
714
  }
579
715
  }
580
716
  ]);
@@ -611,11 +747,14 @@ export class EditorView {
611
747
  /* The folds of the document, a moment after the last edit since they read all of it. */
612
748
  scheduleFolds() {
613
749
  clearTimeout(this.foldTimer);
750
+ if (this.settings.folding === false) {
751
+ return;
752
+ }
614
753
  this.foldTimer = setTimeout(() => this.refreshFolds(), FOLD_DELAY_MS);
615
754
  }
616
755
  /* Reads the folds off the document now, which otherwise waits for a pause in editing. */
617
756
  refreshFolds() {
618
- if (this.disposed) {
757
+ if (this.disposed || this.settings.folding === false) {
619
758
  return;
620
759
  }
621
760
  const ranges = this.model.getLength() > FOLD_LIMIT
@@ -768,6 +907,9 @@ export class EditorView {
768
907
  * and closes one of the document's own. A selection inside one line has no line to fold.
769
908
  */
770
909
  foldSelection() {
910
+ if (this.settings.folding === false) {
911
+ return;
912
+ }
771
913
  const { anchor, head } = this.model.getPrimary();
772
914
  let from = Math.min(anchor, head);
773
915
  let to = Math.max(anchor, head);
@@ -816,6 +958,9 @@ export class EditorView {
816
958
  * is what a file opens as the first time, and again as a language server names more of them.
817
959
  */
818
960
  restoreFolds(state, defaults = []) {
961
+ if (this.settings.folding === false) {
962
+ return;
963
+ }
819
964
  const last = this.model.getLineCount() - 1;
820
965
  if (state !== null) {
821
966
  this.customFolds = state.custom
@@ -909,6 +1054,10 @@ export class EditorView {
909
1054
  /* Opens the folds that hide an offset, so a caret or a search result is where it can be seen. */
910
1055
  ensureVisible(offset) {
911
1056
  const line = this.model.positionAt(offset).line;
1057
+ const row = this.layout.rowForLine(line);
1058
+ if (row.kind === 'text') {
1059
+ row.replacement?.onReveal?.();
1060
+ }
912
1061
  if (!this.layout.hides(offset)) {
913
1062
  return;
914
1063
  }
@@ -932,7 +1081,8 @@ export class EditorView {
932
1081
  action();
933
1082
  const next = this.layout.rows.find((row) => row.key === anchor.key);
934
1083
  if (next) {
935
- this.viewport.scrollTop = Math.max(0, next.top + offset);
1084
+ // Unwrapping can shrink the anchor below its old offset.
1085
+ this.viewport.scrollTop = Math.max(0, next.top + Math.min(offset, Math.max(0, next.height - 1)));
936
1086
  }
937
1087
  }
938
1088
  syncWrap() {
@@ -1094,6 +1244,22 @@ export class EditorView {
1094
1244
  this.selectionOccurrenceCache = { key, ranges };
1095
1245
  return ranges;
1096
1246
  }
1247
+ get paintWindow() {
1248
+ return { top: this.scrollTop - this.overscan, bottom: this.scrollTop + this.viewportHeight + this.overscan };
1249
+ }
1250
+ paintRectangles(from, to, rows) {
1251
+ return this.layout.rectangles(from, to, rows, this.paintWindow);
1252
+ }
1253
+ visibleTextBounds(row) {
1254
+ const geometry = this.layout.geometry(row);
1255
+ const band = subRowBand(row.top, geometry.subRows, this.layout.metrics.lineHeight, this.paintWindow);
1256
+ return {
1257
+ from: geometry.start + offsetOfStop(geometry, geometry.rowStarts[band.first]),
1258
+ to: band.last + 1 === geometry.subRows
1259
+ ? this.model.getLine(row.line).next
1260
+ : geometry.start + offsetOfStop(geometry, geometry.rowStarts[band.last + 1])
1261
+ };
1262
+ }
1097
1263
  /* The arrows of the soft wraps in the rows on screen. */
1098
1264
  wrapSigns(rows) {
1099
1265
  if (!this.layout.wrapping) {
@@ -1106,14 +1272,15 @@ export class EditorView {
1106
1272
  continue;
1107
1273
  }
1108
1274
  const geometry = this.layout.geometry(row);
1109
- for (let subRow = 0; subRow < geometry.subRows; subRow++) {
1275
+ const band = subRowBand(row.top, geometry.subRows, lineHeight, this.paintWindow);
1276
+ for (let subRow = band.first; subRow <= band.last; subRow++) {
1110
1277
  const y = row.top + subRow * lineHeight;
1111
1278
  if (subRow + 1 < geometry.subRows) {
1112
1279
  const x = geometry.rowEnds[subRow];
1113
1280
  signs.push({ x, y, width: this.viewportWidth - x });
1114
1281
  }
1115
1282
  if (subRow > 0) {
1116
- signs.push({ x: 0, y, width: geometry.before[geometry.rowStarts[subRow]] });
1283
+ signs.push({ x: 0, y, width: xOfStop(geometry, geometry.rowStarts[subRow]) });
1117
1284
  }
1118
1285
  }
1119
1286
  }
@@ -1276,6 +1443,53 @@ export class EditorView {
1276
1443
  hoverMoved(event) {
1277
1444
  this.content.classList.toggle('se-over-selection', !this.settings.readOnly && event.buttons === 0 && this.selectionAtPoint(event.clientX, event.clientY) !== null);
1278
1445
  this.setHover(event.buttons === 0 ? this.characterAtPoint(event.clientX, event.clientY, event.target) : null);
1446
+ this.setInlayHover(event.buttons === 0 ? event.target : null);
1447
+ }
1448
+ /* The hint, and the part of its label, an event happened on, or null anywhere else. */
1449
+ inlayAt(target) {
1450
+ const element = target?.closest?.('.se-inlay');
1451
+ if (!element?.dataset.inlayId) {
1452
+ return null;
1453
+ }
1454
+ const part = target.closest('[data-part]');
1455
+ return { id: element.dataset.inlayId, part: part === null ? null : Number(part.dataset.part), element: part ?? element };
1456
+ }
1457
+ /* Says which hint the pointer is on, or that it left them all. */
1458
+ setInlayHover(target) {
1459
+ const found = this.inlayAt(target);
1460
+ if (found?.id === this.inlayHover?.id && found?.part === this.inlayHover?.part) {
1461
+ return;
1462
+ }
1463
+ this.inlayHover = found === null ? null : { id: found.id, part: found.part };
1464
+ emit(this.inlayHoverListeners, found === null ? null : { id: found.id, part: found.part, rect: found.element.getBoundingClientRect() });
1465
+ }
1466
+ onInlayHover(listener) {
1467
+ return subscribe(this.inlayHoverListeners, listener);
1468
+ }
1469
+ /* Draws a hint or one of its parts as a link, which only the pill on screen can show. The target outlives a repaint of its line. */
1470
+ setInlayLink(target) {
1471
+ this.inlayLink = target === null ? null : { id: target.id, part: target.part };
1472
+ this.applyInlayLink();
1473
+ }
1474
+ applyInlayLink() {
1475
+ const target = this.inlayLink;
1476
+ if (target === null && this.inlayLinkElement === null) {
1477
+ return;
1478
+ }
1479
+ let element;
1480
+ if (target !== null) {
1481
+ const pill = Array.from(this.content.querySelectorAll('[data-inlay-id]')).find((candidate) => candidate.dataset.inlayId === target.id);
1482
+ element =
1483
+ target.part === null
1484
+ ? pill
1485
+ : (Array.from(pill?.querySelectorAll('[data-part]') ?? []).find((part) => part.dataset.part === String(target.part)) ?? pill);
1486
+ }
1487
+ if (element === this.inlayLinkElement) {
1488
+ return;
1489
+ }
1490
+ this.inlayLinkElement?.classList.remove('se-inlay-link');
1491
+ element?.classList.add('se-inlay-link');
1492
+ this.inlayLinkElement = element ?? null;
1279
1493
  }
1280
1494
  setHover(offset) {
1281
1495
  if (offset === this.hoverOffset) {
@@ -1284,6 +1498,27 @@ export class EditorView {
1284
1498
  this.hoverOffset = offset;
1285
1499
  emit(this.hoverListeners, offset);
1286
1500
  }
1501
+ getLineHeight(line) {
1502
+ if (!Number.isInteger(line) || line < 0 || line >= this.model.getLineCount() || this.layout.isHidden(line)) {
1503
+ return 0;
1504
+ }
1505
+ return this.layout.rowForLine(line).height;
1506
+ }
1507
+ onLineHeights(listener) {
1508
+ return subscribe(this.lineHeightListeners, listener);
1509
+ }
1510
+ setScrollTop(top) {
1511
+ if (!Number.isFinite(top)) {
1512
+ return;
1513
+ }
1514
+ this.cancelScroll();
1515
+ const next = Math.max(0, top);
1516
+ if (this.viewport.scrollTop === next) {
1517
+ return;
1518
+ }
1519
+ this.viewport.scrollTop = next;
1520
+ this.render();
1521
+ }
1287
1522
  onViewChange(listener) {
1288
1523
  return subscribe(this.viewListeners, listener);
1289
1524
  }
@@ -1386,25 +1621,20 @@ export class EditorView {
1386
1621
  /* What a regular expression replacement writes for the match the find is on, drawn under that match; null takes it away. */
1387
1622
  setReplacePreview(replacement, preserveCase = false) {
1388
1623
  this.replacePreview = replacement === null ? null : { text: replacement, preserveCase };
1624
+ this.find.setPreview(replacement !== null);
1389
1625
  this.paintPreview();
1390
1626
  }
1391
1627
  /* Shown only when it says something the replacement text does not, as a regular expression's does. */
1392
1628
  paintPreview() {
1393
- const mark = this.find.currentMark;
1394
- const query = this.find.activeQuery;
1629
+ const match = this.find.currentMatch;
1395
1630
  const wanted = this.replacePreview;
1396
1631
  let shown = null;
1397
- if (wanted !== null && mark !== null && query?.regex === true) {
1632
+ if (wanted !== null && match?.regex === true) {
1398
1633
  try {
1399
- const match = this.model.find(query.text, {
1400
- caseSensitive: query.caseSensitive,
1401
- wholeWord: query.wholeWord,
1402
- regex: true,
1403
- from: mark.from,
1404
- to: mark.to,
1405
- maxResults: 1
1406
- })[0];
1407
- const text = match === undefined ? wanted.text : replacementText(this.model.getText(), match, wanted.text, false, wanted.preserveCase);
1634
+ if (wanted.text.length > 10000) {
1635
+ throw new RangeError('The replacement preview is too large.');
1636
+ }
1637
+ const text = replacementText(this.model.getText(), match, wanted.text, false, wanted.preserveCase, 10000);
1408
1638
  shown = text === wanted.text ? null : text;
1409
1639
  }
1410
1640
  catch {
@@ -1412,11 +1642,11 @@ export class EditorView {
1412
1642
  }
1413
1643
  }
1414
1644
  this.preview.hidden = shown === null;
1415
- if (shown === null || mark === null) {
1645
+ if (shown === null || match === null) {
1416
1646
  return;
1417
1647
  }
1418
- const start = this.layout.caret(mark.from);
1419
- const end = this.layout.caret(mark.to);
1648
+ const start = this.layout.caret(match.from);
1649
+ const end = this.layout.caret(match.to);
1420
1650
  this.preview.textContent = shown;
1421
1651
  this.preview.style.left = `${(start.x + end.x) / 2}px`;
1422
1652
  this.preview.style.top = `${end.y + end.height}px`;
@@ -1449,7 +1679,7 @@ export class EditorView {
1449
1679
  }
1450
1680
  /* Colors the lines on and just past the screen, a slice at a time so typing never waits on a grammar. */
1451
1681
  colorAhead(immediate = false) {
1452
- if (this.disposed || !this.tokens.ready) {
1682
+ if (this.disposed || this.lineTokenProvider !== null || !this.tokens.ready) {
1453
1683
  return;
1454
1684
  }
1455
1685
  clearTimeout(this.colorTimer);
@@ -1465,9 +1695,16 @@ export class EditorView {
1465
1695
  this.colorAhead();
1466
1696
  }, immediate ? 0 : 1);
1467
1697
  }
1468
- /* The gutter is as wide as its widest number needs, which only changes when the line count gains a digit. */
1698
+ /* Source numbering can be much larger than the display document's line count. */
1469
1699
  syncGutter() {
1470
- const digits = String(this.model.getLineCount()).length;
1700
+ if (this.lineNumberProvider !== null && this.lineNumberRevision !== this.model.getRevision()) {
1701
+ const provider = this.lineNumberProvider;
1702
+ this.lineNumberLabels = Array.from({ length: this.model.getLineCount() }, (_, line) => provider(line));
1703
+ this.lineNumberWidth = this.lineNumberLabels.reduce((width, label) => Math.max(width, label?.length ?? 0), 0);
1704
+ this.lineNumberRevision = this.model.getRevision();
1705
+ this.stickyEntries = [];
1706
+ }
1707
+ const digits = this.lineNumberProvider === null ? String(this.model.getLineCount()).length : this.lineNumberWidth;
1471
1708
  const width = Math.max(MIN_GUTTER_WIDTH, Math.ceil(digits * this.layout.metrics.charWidth) + 40);
1472
1709
  if (width !== this.gutterWidth) {
1473
1710
  this.gutterWidth = width;
@@ -1520,7 +1757,7 @@ export class EditorView {
1520
1757
  const lastLine = rows.at(-1)?.line ?? 0;
1521
1758
  const firstRow = this.layout.rowAt(this.scrollTop);
1522
1759
  this.topAnchor = { offset: this.model.getLine(firstRow.line).start, delta: this.scrollTop - firstRow.top };
1523
- if (lastLine - this.tokens.colored <= COLOR_MARGIN * 4) {
1760
+ if (this.lineTokenProvider === null && lastLine - this.tokens.colored <= COLOR_MARGIN * 4) {
1524
1761
  this.tokens.advance(lastLine, FIRST_PAINT_MS);
1525
1762
  }
1526
1763
  const paint = {
@@ -1528,6 +1765,7 @@ export class EditorView {
1528
1765
  whitespace: this.settings.whitespace,
1529
1766
  tokensOf: (line) => this.styledTokensOf(line),
1530
1767
  viewportWidth: this.viewportWidth - RIGHT_PADDING,
1768
+ window: this.paintWindow,
1531
1769
  hostLeft: this.scrollLeft - this.gutterWidth,
1532
1770
  hostWidth: this.viewportWidth + this.gutterWidth,
1533
1771
  onUnfold: (line) => this.toggleFold(line, false)
@@ -1536,6 +1774,7 @@ export class EditorView {
1536
1774
  rows = this.layout.visibleRows(this.scrollTop, this.viewportHeight, this.overscan);
1537
1775
  this.painter.paint(rows, paint);
1538
1776
  }
1777
+ this.applyInlayLink();
1539
1778
  this.content.style.height = `${this.layout.height}px`;
1540
1779
  this.content.style.width = `${Math.max(this.layout.width, this.viewportWidth)}px`;
1541
1780
  this.renderedScroll = { top: this.scrollTop, left: this.scrollLeft };
@@ -1556,6 +1795,11 @@ export class EditorView {
1556
1795
  this.frameGeometry = null;
1557
1796
  this.rendering = false;
1558
1797
  }
1798
+ if (!this.disposed && this.announcedLineHeights !== this.layout.lineHeightsVersion) {
1799
+ this.announcedLineHeights = this.layout.lineHeightsVersion;
1800
+ // Hosts may adjust rows in response; rendering must already have finished.
1801
+ emit(this.lineHeightListeners);
1802
+ }
1559
1803
  }
1560
1804
  paintDecorations(rows) {
1561
1805
  const selections = this.model.getSelections();
@@ -1570,11 +1814,14 @@ export class EditorView {
1570
1814
  const lastLine = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
1571
1815
  const highlighted = this.highlightedLines(firstLine, lastLine);
1572
1816
  paintGutter(this.gutterLines, this.layout, rows, {
1817
+ lineNumber: (line) => this.lineNumberLabel(line),
1573
1818
  changes: this.changedLines(rows),
1574
1819
  attribution: this.attributedLines(firstLine, lastLine),
1575
1820
  highlights: highlighted,
1576
1821
  activeLines: new Set(selections.map((selection) => this.model.positionAt(selection.head).line)),
1577
1822
  foldable,
1823
+ lineLabel: (line) => this.lineLabelProvider?.(line) ?? null,
1824
+ foldName: (line, collapsed) => this.foldName(line, collapsed),
1578
1825
  action: this.gutterAction,
1579
1826
  markers: this.markersByLine()
1580
1827
  });
@@ -1588,7 +1835,7 @@ export class EditorView {
1588
1835
  if (group.length > 0) {
1589
1836
  marks.push({
1590
1837
  className: kind === 'write' ? 'se-occurrence se-occurrence-write' : 'se-occurrence',
1591
- rects: group.flatMap((mark) => this.layout.rectangles(mark.from, mark.to, rows))
1838
+ rects: group.flatMap((mark) => this.paintRectangles(mark.from, mark.to, rows))
1592
1839
  });
1593
1840
  }
1594
1841
  }
@@ -1604,7 +1851,7 @@ export class EditorView {
1604
1851
  paintSigns(this.signs, this.wrapSigns(rows), this.layout.metrics.lineHeight);
1605
1852
  const alike = this.selectionOccurrences();
1606
1853
  if (alike.length > 0) {
1607
- marks.push({ className: 'se-occurrence', rects: alike.flatMap((range) => this.layout.rectangles(range.from, range.to, rows)) });
1854
+ marks.push({ className: 'se-occurrence', rects: alike.flatMap((range) => this.paintRectangles(range.from, range.to, rows)) });
1608
1855
  }
1609
1856
  const faded = [];
1610
1857
  const struck = [];
@@ -1612,7 +1859,7 @@ export class EditorView {
1612
1859
  if (marker.to <= marker.from) {
1613
1860
  continue;
1614
1861
  }
1615
- const rects = this.layout.rectangles(marker.from, marker.to, rows);
1862
+ const rects = this.paintRectangles(marker.from, marker.to, rows);
1616
1863
  if (marker.severity !== 'hint') {
1617
1864
  marks.push({ className: `se-squiggle se-squiggle-${marker.severity}`, rects });
1618
1865
  }
@@ -1625,11 +1872,21 @@ export class EditorView {
1625
1872
  }
1626
1873
  const current = this.find.currentMark;
1627
1874
  const textRows = rows.filter((candidate) => candidate.kind === 'text');
1628
- const visibleFrom = textRows.length > 0 ? this.model.getLine(textRows[0].line).start : 0;
1629
- const visibleTo = textRows.length > 0 ? this.model.getLine(textRows.at(-1).line).next : 0;
1630
- for (const mark of this.find.matches) {
1631
- if (mark.to > mark.from && mark.to >= visibleFrom && mark.from <= visibleTo) {
1632
- marks.push({ className: mark === current ? 'se-find-current' : 'se-find-match', rects: this.layout.rectangles(mark.from, mark.to, rows) });
1875
+ const visibleFrom = textRows.length > 0 ? this.visibleTextBounds(textRows[0]).from : 0;
1876
+ const visibleTo = textRows.length > 0 ? this.visibleTextBounds(textRows.at(-1)).to : 0;
1877
+ const found = this.find.matchesBetween(visibleFrom, visibleTo);
1878
+ const spans = current !== null &&
1879
+ !found.some((mark) => mark.from === current.from && mark.to === current.to) &&
1880
+ current.to >= visibleFrom &&
1881
+ current.from <= visibleTo
1882
+ ? [...found, current]
1883
+ : found;
1884
+ for (const mark of spans) {
1885
+ if (mark.to > mark.from) {
1886
+ marks.push({
1887
+ className: mark.from === current?.from && mark.to === current.to ? 'se-find-current' : 'se-find-match',
1888
+ rects: this.paintRectangles(mark.from, mark.to, rows)
1889
+ });
1633
1890
  }
1634
1891
  }
1635
1892
  const seen = new Set();
@@ -1640,25 +1897,25 @@ export class EditorView {
1640
1897
  }
1641
1898
  if (bracket.mate === undefined) {
1642
1899
  if (this.unmatchedBracket(bracket.at)) {
1643
- marks.push({ className: 'se-bracket-unmatched', rects: this.layout.rectangles(bracket.at, bracket.at + 1, rows) });
1900
+ marks.push({ className: 'se-bracket-unmatched', rects: this.paintRectangles(bracket.at, bracket.at + 1, rows) });
1644
1901
  }
1645
1902
  continue;
1646
1903
  }
1647
1904
  for (const offset of [bracket.at, bracket.mate]) {
1648
1905
  if (!seen.has(offset)) {
1649
1906
  seen.add(offset);
1650
- marks.push({ className: 'se-bracket-match', rects: this.layout.rectangles(offset, offset + 1, rows) });
1907
+ marks.push({ className: 'se-bracket-match', rects: this.paintRectangles(offset, offset + 1, rows) });
1651
1908
  }
1652
1909
  }
1653
1910
  }
1654
1911
  const selected = selections.flatMap((selection) => selection.anchor === selection.head
1655
1912
  ? []
1656
- : this.layout.rectangles(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), rows));
1913
+ : this.paintRectangles(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head), rows));
1657
1914
  paintOverlays(this.overlays, { currentLine, selections: selected, focused, marks });
1658
1915
  paintOver(this.over, [
1659
1916
  { className: 'se-faded', rects: faded },
1660
1917
  { className: 'se-struck', rects: struck },
1661
- { className: 'se-link', rects: this.link === null ? [] : this.layout.rectangles(this.link.from, this.link.to, rows) }
1918
+ { className: 'se-link', rects: this.link === null ? [] : this.paintRectangles(this.link.from, this.link.to, rows) }
1662
1919
  ]);
1663
1920
  const top = this.scrollTop;
1664
1921
  const caretRects = focused
@@ -1680,36 +1937,81 @@ export class EditorView {
1680
1937
  this.input.style.height = `${this.layout.metrics.lineHeight}px`;
1681
1938
  this.root.dataset.carets = String(selections.length);
1682
1939
  }
1683
- /* The lines the host marked, which stay on their lines through edits until it marks them again. */
1684
- setChangeMarks(marks) {
1940
+ /*
1941
+ * The lines the host marked in a layer, which stay on their lines through edits until the layer is set again. Layers
1942
+ * draw together; where two mark the same line the layer that was set first after being empty sits below the later one.
1943
+ */
1944
+ setChangeMarks(marks, layer = '') {
1685
1945
  const last = this.model.getLineCount();
1686
1946
  const clamp = (line) => Math.min(last, Math.max(1, Math.trunc(line) || 1)) - 1;
1687
- this.changeMarks = marks.map((mark) => {
1688
- const from = this.model.getLine(clamp(mark.startLine)).start;
1689
- return { kind: mark.kind, from, to: mark.kind === 'deleted' ? from : this.model.getLine(Math.max(clamp(mark.startLine), clamp(mark.endLine))).end };
1690
- });
1947
+ if (marks.length === 0) {
1948
+ this.changeLayers.delete(layer);
1949
+ }
1950
+ else {
1951
+ this.changeLayers.set(layer, marks.map((mark) => {
1952
+ const from = this.model.getLine(clamp(mark.startLine)).start;
1953
+ return {
1954
+ kind: mark.kind,
1955
+ from,
1956
+ to: mark.kind === 'deleted' ? from : this.model.getLine(Math.max(clamp(mark.startLine), clamp(mark.endLine))).end
1957
+ };
1958
+ }));
1959
+ }
1691
1960
  this.changeVersion++;
1692
1961
  this.render();
1693
1962
  }
1694
- markedLines(mark) {
1695
- const start = this.model.positionAt(mark.from).line;
1696
- return { start, end: mark.kind === 'deleted' ? start : this.model.positionAt(Math.max(mark.from, mark.to)).line };
1963
+ /*
1964
+ * How every marked line differs. The layer that last became non-empty wins a shared line for every kind; inside one
1965
+ * layer a removal never covers a line that was added or changed, as its mark sits on the line after the removed text.
1966
+ */
1967
+ resolvedChanges() {
1968
+ if (this.changeCache?.version === this.changeVersion) {
1969
+ return this.changeCache;
1970
+ }
1971
+ const lines = new Map();
1972
+ for (const marks of this.changeLayers.values()) {
1973
+ const layer = new Map();
1974
+ for (const mark of marks) {
1975
+ const start = this.model.positionAt(mark.from).line;
1976
+ const end = mark.kind === 'deleted' ? start : this.model.positionAt(Math.max(mark.from, mark.to)).line;
1977
+ for (let line = start; line <= end; line++) {
1978
+ if (mark.kind !== 'deleted' || !layer.has(line)) {
1979
+ layer.set(line, mark.kind);
1980
+ }
1981
+ }
1982
+ }
1983
+ for (const [line, kind] of layer) {
1984
+ lines.set(line, kind);
1985
+ }
1986
+ }
1987
+ const runs = [];
1988
+ for (const line of [...lines.keys()].sort((left, right) => left - right)) {
1989
+ const kind = lines.get(line);
1990
+ const run = runs.at(-1);
1991
+ if (run !== undefined && kind !== 'deleted' && run.kind === kind && run.end === line - 1) {
1992
+ run.end = line;
1993
+ }
1994
+ else {
1995
+ runs.push({ kind, start: line, end: line });
1996
+ }
1997
+ }
1998
+ this.changeCache = { version: this.changeVersion, lines, runs };
1999
+ return this.changeCache;
1697
2000
  }
1698
- /* How each line on screen differs, a removal never covering a line that was added or changed. */
2001
+ /* How each line on screen differs. */
1699
2002
  changedLines(rows) {
1700
- const lines = new Map();
2003
+ const { lines } = this.resolvedChanges();
1701
2004
  const first = rows[0]?.line ?? 0;
1702
2005
  const lastRow = rows.at(-1);
1703
2006
  const last = lastRow?.kind === 'text' ? lastRow.lastLine : (lastRow?.line ?? 0);
1704
- for (const mark of this.changeMarks) {
1705
- const { start, end } = this.markedLines(mark);
1706
- for (let line = Math.max(start, first); line <= Math.min(end, last); line++) {
1707
- if (mark.kind !== 'deleted' || !lines.has(line)) {
1708
- lines.set(line, mark.kind);
1709
- }
2007
+ const visible = new Map();
2008
+ for (let line = first; line <= last; line++) {
2009
+ const kind = lines.get(line);
2010
+ if (kind !== undefined) {
2011
+ visible.set(line, kind);
1710
2012
  }
1711
2013
  }
1712
- return lines;
2014
+ return visible;
1713
2015
  }
1714
2016
  /* The carets of agents, each at an offset that follows its text through edits until the host sets them again. */
1715
2017
  setRemoteCarets(carets) {
@@ -1809,6 +2111,7 @@ export class EditorView {
1809
2111
  highlightMarks(rows, lines) {
1810
2112
  const byBackground = new Map();
1811
2113
  const width = Math.max(this.layout.width, this.viewportWidth);
2114
+ const emphasisByColor = new Map();
1812
2115
  for (const row of rows) {
1813
2116
  const highlight = row.kind === 'text' ? lines.get(row.line) : undefined;
1814
2117
  if (highlight !== undefined) {
@@ -1816,13 +2119,25 @@ export class EditorView {
1816
2119
  const rects = byBackground.get(background) ?? [];
1817
2120
  rects.push({ x: 0, y: row.top, width, height: row.height });
1818
2121
  byBackground.set(background, rects);
2122
+ if (row.kind === 'text' && highlight.emphasis !== undefined) {
2123
+ const bounds = this.model.getLine(row.line);
2124
+ const emphasisBackground = `color-mix(in srgb, ${colorValue(highlight.color)} 22%, transparent)`;
2125
+ const emphasized = emphasisByColor.get(emphasisBackground) ?? [];
2126
+ for (const range of highlight.emphasis) {
2127
+ emphasized.push(...this.paintRectangles(Math.max(bounds.start, range.from), Math.min(bounds.end, range.to), [row]));
2128
+ }
2129
+ emphasisByColor.set(emphasisBackground, emphasized);
2130
+ }
1819
2131
  }
1820
2132
  }
1821
- return [...byBackground].map(([background, rects]) => ({ className: 'se-line-highlight', rects, background }));
2133
+ return [
2134
+ ...[...byBackground].map(([background, rects]) => ({ className: 'se-line-highlight', rects, background })),
2135
+ ...[...emphasisByColor].map(([background, rects]) => ({ className: 'se-line-emphasis', rects, background }))
2136
+ ];
1822
2137
  }
1823
2138
  /* Code that is not in the document, drawn in the editor's own face and colors. */
1824
2139
  renderCode(container, text, options) {
1825
- renderCodeBlock(container, text, this.tokens.tokenizeText(text.split(/\r?\n/)), this.settings.tabSize, options);
2140
+ renderCodeBlock(container, text, options.tokens === undefined ? this.tokens.tokenizeText(text.split(/\r?\n/)) : options.tokens, this.settings.tabSize, options);
1826
2141
  }
1827
2142
  attributedLines(first, last) {
1828
2143
  return this.attribution.linesIn(first, last, this.model.getLine(first).start, this.model.getLine(last).end, (offset) => this.model.positionAt(offset).line);
@@ -1926,9 +2241,8 @@ export class EditorView {
1926
2241
  const row = this.layout.rowForLine(line);
1927
2242
  return row.top + row.height;
1928
2243
  };
1929
- for (const mark of this.changeMarks) {
1930
- const { start, end } = this.markedLines(mark);
1931
- spans.push({ kind: mark.kind, top: rowTop(start), bottom: mark.kind === 'deleted' ? rowTop(start) : rowBottom(end) });
2244
+ for (const { kind, start, end } of this.resolvedChanges().runs) {
2245
+ spans.push({ kind, top: rowTop(start), bottom: kind === 'deleted' ? rowTop(start) : rowBottom(end) });
1932
2246
  }
1933
2247
  for (const marker of this.markers) {
1934
2248
  if (marker.severity !== 'hint' && marker.to > marker.from) {
@@ -2003,12 +2317,17 @@ export class EditorView {
2003
2317
  const placements = this.scrollTop <= 0 ? [] : this.stickyAt(this.scrollTop);
2004
2318
  const entries = placements.map((placement) => {
2005
2319
  const row = this.layout.rowForLine(placement.block.startLine);
2006
- return { line: placement.block.startLine, geometry: this.layout.geometry(row), tokens: this.styledTokensOf(placement.block.startLine) };
2320
+ return {
2321
+ line: placement.block.startLine,
2322
+ label: this.lineNumberLabel(placement.block.startLine),
2323
+ geometry: this.layout.geometry(row),
2324
+ tokens: this.styledTokensOf(placement.block.startLine)
2325
+ };
2007
2326
  });
2008
2327
  const changed = entries.length !== this.stickyEntries.length ||
2009
2328
  entries.some((entry, index) => {
2010
2329
  const before = this.stickyEntries[index];
2011
- return entry.line !== before.line || entry.geometry !== before.geometry || entry.tokens !== before.tokens;
2330
+ return entry.line !== before.line || entry.label !== before.label || entry.geometry !== before.geometry || entry.tokens !== before.tokens;
2012
2331
  });
2013
2332
  this.sticky.hidden = entries.length === 0;
2014
2333
  this.sticky.style.setProperty('--se-scroll-left', `${this.scrollLeft}px`);
@@ -2047,7 +2366,11 @@ export class EditorView {
2047
2366
  setGutterMarkers(owner, markers) {
2048
2367
  const before = this.gutterMarkers.get(owner) ?? [];
2049
2368
  if (before.length === markers.length &&
2050
- before.every((marker, index) => marker.id === markers[index].id && marker.line === markers[index].line && marker.label === markers[index].label)) {
2369
+ before.every((marker, index) => marker.id === markers[index].id &&
2370
+ marker.line === markers[index].line &&
2371
+ marker.label === markers[index].label &&
2372
+ marker.icon === markers[index].icon &&
2373
+ marker.pressed === markers[index].pressed)) {
2051
2374
  return;
2052
2375
  }
2053
2376
  if (markers.length === 0) {
@@ -2075,7 +2398,7 @@ export class EditorView {
2075
2398
  for (const owner of [...this.gutterMarkers.keys()].sort()) {
2076
2399
  for (const marker of this.gutterMarkers.get(owner)) {
2077
2400
  if (!byLine.has(marker.line)) {
2078
- byLine.set(marker.line, { id: marker.id, label: marker.label });
2401
+ byLine.set(marker.line, marker);
2079
2402
  }
2080
2403
  }
2081
2404
  }
@@ -2100,12 +2423,14 @@ export class EditorView {
2100
2423
  }
2101
2424
  this.disposed = true;
2102
2425
  this.cancelScroll();
2426
+ this.find.dispose();
2103
2427
  this.subscription.dispose();
2104
2428
  this.resizeObserver?.disconnect();
2105
2429
  this.blockObserver?.disconnect();
2106
2430
  clearTimeout(this.foldTimer);
2107
2431
  clearTimeout(this.colorTimer);
2108
2432
  clearTimeout(this.noticeTimer);
2433
+ clearTimeout(this.liveTimer);
2109
2434
  clearTimeout(this.caretTimer);
2110
2435
  if (this.frame !== undefined) {
2111
2436
  this.document.defaultView?.cancelAnimationFrame?.(this.frame);
@@ -2113,7 +2438,9 @@ export class EditorView {
2113
2438
  this.findListeners.clear();
2114
2439
  this.scopeListeners.clear();
2115
2440
  this.viewListeners.clear();
2441
+ this.lineHeightListeners.clear();
2116
2442
  this.hoverListeners.clear();
2443
+ this.inlayHoverListeners.clear();
2117
2444
  this.gutterActionListeners.clear();
2118
2445
  this.gutterMarkerListeners.clear();
2119
2446
  this.gutterMarkers.clear();