@motionscript/code 0.1.0-alpha.0 → 0.1.0-alpha.5

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 (65) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +201 -201
  3. package/NOTICE +48 -0
  4. package/README.md +161 -77
  5. package/dist/browser/THIRD_PARTY_LICENSES.txt +431 -0
  6. package/dist/browser/index.js +7 -7
  7. package/dist/browser/index.js.map +3 -3
  8. package/dist/browser/manifest.json +85 -3
  9. package/dist/code-range.d.ts +7 -1
  10. package/dist/code-range.d.ts.map +1 -1
  11. package/dist/code-range.js +9 -1
  12. package/dist/code-range.js.map +1 -1
  13. package/dist/diff.d.ts +14 -0
  14. package/dist/diff.d.ts.map +1 -1
  15. package/dist/diff.js +134 -2
  16. package/dist/diff.js.map +1 -1
  17. package/dist/engine.d.ts +1 -1
  18. package/dist/engine.d.ts.map +1 -1
  19. package/dist/engine.js +1 -1
  20. package/dist/engine.js.map +1 -1
  21. package/dist/highlight.d.ts +10 -2
  22. package/dist/highlight.d.ts.map +1 -1
  23. package/dist/highlight.js +40 -6
  24. package/dist/highlight.js.map +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/layout.d.ts +1 -1
  27. package/dist/layout.d.ts.map +1 -1
  28. package/dist/layout.js.map +1 -1
  29. package/dist/measure-cache.d.ts +1 -1
  30. package/dist/measure-cache.d.ts.map +1 -1
  31. package/dist/measure-cache.js.map +1 -1
  32. package/dist/node.d.ts +62 -7
  33. package/dist/node.d.ts.map +1 -1
  34. package/dist/node.js +224 -42
  35. package/dist/node.js.map +1 -1
  36. package/dist/nodes.js.map +1 -1
  37. package/dist/render.d.ts +7 -2
  38. package/dist/render.d.ts.map +1 -1
  39. package/dist/render.js +60 -1
  40. package/dist/render.js.map +1 -1
  41. package/dist/style.d.ts +3 -3
  42. package/dist/style.d.ts.map +1 -1
  43. package/dist/style.js +10 -6
  44. package/dist/style.js.map +1 -1
  45. package/dist/tokens.d.ts.map +1 -1
  46. package/dist/tokens.js +5 -1
  47. package/dist/tokens.js.map +1 -1
  48. package/dist/transitions.d.ts +19 -1
  49. package/dist/transitions.d.ts.map +1 -1
  50. package/dist/transitions.js.map +1 -1
  51. package/package.json +7 -4
  52. package/registry.json +6 -6
  53. package/src/code-range.ts +165 -156
  54. package/src/diff.ts +351 -216
  55. package/src/engine.ts +21 -21
  56. package/src/highlight.ts +340 -291
  57. package/src/index.ts +3 -3
  58. package/src/layout.ts +157 -157
  59. package/src/measure-cache.ts +0 -0
  60. package/src/node.ts +1038 -834
  61. package/src/nodes.ts +18 -18
  62. package/src/render.ts +312 -247
  63. package/src/style.ts +255 -251
  64. package/src/tokens.ts +79 -75
  65. package/src/transitions.ts +204 -184
package/dist/node.js CHANGED
@@ -4,12 +4,21 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { node, Clip, Node2D, property, insetsOps, lerpNumber, command, driveCommand, } from "@motionscript/core";
8
- import { rangeToCharOffsets, charOffsetsToRange, TokenAdvanceCache, tokenizeCodeToIdLines, defaultCodeDiff, layoutCode, metricsSignature, makeAnim, defaultCodePhases, resolveTokenStates, moveProgress, canHighlight, ensureHighlighter, DefaultHighlightStyle, drawCode, } from "./engine.js";
7
+ import { node, Clip, Node2D, property, insetsProperty, lerpNumber, colorOps, command, driveCommand, } from "@motionscript/sdk";
8
+ import { rangeToCharOffsets, charOffsetsToRange, TokenAdvanceCache, tokenizeCodeToIdLines, defaultCodeDiff, layoutCode, metricsSignature, makeAnim, defaultCodePhases, resolveTokenStates, moveProgress, canHighlight, ensureHighlighter, normalizeSource, DefaultHighlightStyle, drawCode, } from "./engine.js";
9
+ /** A row band's paint and corner radius when `highlightRows` is given neither. */
10
+ const ROW_BAND_COLOR = "rgba(255, 255, 255, 0.12)";
11
+ const ROW_BAND_RADIUS = 6;
9
12
  let Code = class Code extends Node2D {
10
13
  /** The settled structure. During an edit, the structure the edit lands on. */
11
14
  tokenLines = [];
12
- tokenized = false;
15
+ /** The source `tokenLines` spells, so a frame compares it with `code` without joining every token. */
16
+ _linesSource = "";
17
+ /** What `tokenLines` were coloured and split with, so a frame notices a `language` or `theme` that has moved since. */
18
+ _linesBasis = { language: "", theme: "", highlighted: false };
19
+ /** The last `code` seen and its normalised form, so a frame that finds it unchanged does not scan it. */
20
+ _rawCode = null;
21
+ _normalCode = "";
13
22
  transitions = [];
14
23
  // Caches expensive scope.measureText() calls; cleared when the font
15
24
  // signature (fontSize|fontFamily) changes. See TokenAdvanceCache.
@@ -25,6 +34,8 @@ let Code = class Code extends Node2D {
25
34
  // tokens whose id is NOT in the highlight set. null means "not highlighting".
26
35
  highlightDimOpacity = null;
27
36
  highlightedIds = new Set();
37
+ // The bands highlightRows() left behind whole rows; empty means none.
38
+ rowBands = [];
28
39
  /**
29
40
  * Which token became which across an edit, and when each part of that
30
41
  * edit happens. Construction-time-only (see {@link CodeStrategies}) —
@@ -34,10 +45,11 @@ let Code = class Code extends Node2D {
34
45
  phaseStrategy;
35
46
  constructor(props) {
36
47
  super(props);
37
- this.diffStrategy = props.diffStrategy ?? defaultCodeDiff;
38
- this.phaseStrategy = props.phaseStrategy ?? defaultCodePhases;
39
- this.applyProp("width", props.width ?? "hug");
40
- this.applyProp("height", props.height ?? "hug");
48
+ this.diffStrategy = props?.diffStrategy ?? defaultCodeDiff;
49
+ this.phaseStrategy = props?.phaseStrategy ?? defaultCodePhases;
50
+ this.applyProp("width", props?.width ?? "hug");
51
+ this.applyProp("height", props?.height ?? "hug");
52
+ this.applyProp("_driven", false);
41
53
  // Best-effort, for a highlighter constructed outside the asset pipeline
42
54
  // entirely (a bare `new Code(...)` in a test or a headless measure). On
43
55
  // the timeline the authoritative load is the one `declareAssets` declares,
@@ -45,19 +57,7 @@ let Code = class Code extends Node2D {
45
57
  // there the tokens are right the first time rather than upgrading later.
46
58
  // (Theme is a synchronous color style; only the language parser loads.)
47
59
  ensureHighlighter(undefined, [this.language]).catch(() => { });
48
- this.tokenize();
49
- }
50
- set(props) {
51
- super.set(props);
52
- if (props.code !== undefined || props.language !== undefined || props.theme !== undefined) {
53
- this.tokenized = false;
54
- }
55
- }
56
- tokenize() {
57
- this.setLines(tokenizeCodeToIdLines(this.code, this.language, this.theme));
58
- // Only consider ourselves tokenized once we actually highlighted; while
59
- // the language is still loading we keep retrying on each render.
60
- this.tokenized = canHighlight(this.language, this.theme);
60
+ this._sync();
61
61
  }
62
62
  /**
63
63
  * Swap in a new settled structure, invalidating the layout cache.
@@ -66,10 +66,12 @@ let Code = class Code extends Node2D {
66
66
  * counter rather than a hash of the content — a write that skipped the bump
67
67
  * would keep serving the previous frame's geometry for the new listing.
68
68
  */
69
- setLines(lines) {
69
+ setLines(lines, source, basis = this._basis()) {
70
70
  if (this.tokenLines === lines)
71
71
  return;
72
72
  this.tokenLines = lines;
73
+ this._linesSource = source;
74
+ this._linesBasis = basis;
73
75
  this.structureVersion++;
74
76
  this.layoutCache = null;
75
77
  this.layoutCacheKey = null;
@@ -112,6 +114,15 @@ let Code = class Code extends Node2D {
112
114
  }
113
115
  return null;
114
116
  }
117
+ /** The row-band change in flight, if any; the latest wins. */
118
+ activeRows() {
119
+ for (let i = this.transitions.length - 1; i >= 0; i--) {
120
+ const tr = this.transitions[i];
121
+ if (tr.kind === "rows")
122
+ return tr;
123
+ }
124
+ return null;
125
+ }
115
126
  settledLayout(m, scope) {
116
127
  const key = `${this.structureVersion}|${metricsSignature(m)}|${this.advanceCache.signature(m.fontSize, m.fontFamily)}`;
117
128
  if (this.layoutCache && this.layoutCacheKey === key)
@@ -149,6 +160,7 @@ let Code = class Code extends Node2D {
149
160
  return { from: edit.fromLayout, to: edit.toLayout, edit };
150
161
  }
151
162
  measure(constraints, scope) {
163
+ this._sync();
152
164
  this.advanceCache.sync(this.advanceCache.signature(this.fontSize, this.fontFamily));
153
165
  const wm = this.width;
154
166
  const hm = this.height;
@@ -183,13 +195,9 @@ let Code = class Code extends Node2D {
183
195
  // column-by-column rather than merely look different.
184
196
  ctx.pushTextStyle(null);
185
197
  try {
186
- // Keep retrying until the language+theme have actually loaded, so a frame
187
- // that rendered as plain text upgrades to full highlighting the moment the
188
- // asset loader resolves. Never mid-edit: re-tokenizing mints fresh ids,
189
- // and an in-flight transition is keyed by the ones it captured.
190
- if (!this.tokenized && !this.activeEdit() && canHighlight(this.language, this.theme)) {
191
- this.tokenize();
192
- }
198
+ // Also what upgrades a frame drawn as plain text to full highlighting
199
+ // the moment the asset loader resolves the grammar.
200
+ this._sync();
193
201
  super.renderContent(ctx);
194
202
  }
195
203
  finally {
@@ -237,6 +245,7 @@ let Code = class Code extends Node2D {
237
245
  * silhouette measured against stale metrics.
238
246
  */
239
247
  hitTestSelf(local, tolerance) {
248
+ this._sync();
240
249
  const layout = this.hitLayout();
241
250
  if (!layout)
242
251
  return super.hitTestSelf(local, tolerance);
@@ -286,11 +295,13 @@ let Code = class Code extends Node2D {
286
295
  * above it, instead of colouring the new text as its own program.
287
296
  */
288
297
  append(args) {
298
+ this._sync();
289
299
  const { code } = args.data;
290
300
  return this.editTo(this.joinedSource() + code, args.duration, args.easing);
291
301
  }
292
302
  /** Insert `code` above the listing. */
293
303
  prepend(args) {
304
+ this._sync();
294
305
  const { code } = args.data;
295
306
  return this.editTo(code + this.joinedSource(), args.duration, args.easing);
296
307
  }
@@ -301,6 +312,7 @@ let Code = class Code extends Node2D {
301
312
  * of the existing line.
302
313
  */
303
314
  insert(args) {
315
+ this._sync();
304
316
  const { position, code } = args.data;
305
317
  const source = this.joinedSource();
306
318
  const offset = this.offsetAt(position);
@@ -319,6 +331,7 @@ let Code = class Code extends Node2D {
319
331
  * the rest of the line reflows around the hole.
320
332
  */
321
333
  erase(args) {
334
+ this._sync();
322
335
  const { target: codeRange } = args.data;
323
336
  const { duration, easing } = args;
324
337
  const source = this.joinedSource();
@@ -345,6 +358,7 @@ let Code = class Code extends Node2D {
345
358
  }
346
359
  /** Replace the code in `codeRange` with `replacement`. */
347
360
  replace(args) {
361
+ this._sync();
348
362
  const { target: codeRange, replacement } = args.data;
349
363
  const source = this.joinedSource();
350
364
  const { start, end } = rangeToCharOffsets(codeRange, this.lineLengths());
@@ -358,6 +372,7 @@ let Code = class Code extends Node2D {
358
372
  * again with a different range to cross-fade.
359
373
  */
360
374
  highlight(args) {
375
+ this._sync();
361
376
  const { target: codeRange, dim } = args.data;
362
377
  const { duration, easing } = args;
363
378
  const toDim = dim ?? 0.4;
@@ -392,6 +407,7 @@ let Code = class Code extends Node2D {
392
407
  * highlight state.
393
408
  */
394
409
  resetHighlight(args) {
410
+ this._sync();
395
411
  const { duration, easing } = args;
396
412
  if (this.highlightDimOpacity === null)
397
413
  return driveCommand(duration, () => { });
@@ -411,6 +427,25 @@ let Code = class Code extends Node2D {
411
427
  this.highlightedIds = done ? new Set() : previousIds;
412
428
  });
413
429
  }
430
+ /**
431
+ * Lay a band of `color` behind whole rows: every line a range touches, across
432
+ * the block. Persistent, and independent of {@link highlight}'s dimming. A
433
+ * second call slides and resizes the bands onto their new rows, pairing them
434
+ * in the order given; `resetRowHighlight()` fades them out. A band keeps to
435
+ * its lines when an edit moves them.
436
+ */
437
+ highlightRows(args) {
438
+ this._sync();
439
+ const { target, color, radius } = args.data;
440
+ const paint = colorOps.resolve(color ?? ROW_BAND_COLOR);
441
+ const bands = this.rowBandsIn(normalizeCodeRanges(target), paint, Math.max(0, radius ?? ROW_BAND_RADIUS));
442
+ return this.runRows(this.rowBands, bands, args.duration, args.easing);
443
+ }
444
+ /** Fade every row band out and clear them. */
445
+ resetRowHighlight(args) {
446
+ this._sync();
447
+ return this.runRows(this.rowBands, [], args.duration, args.easing);
448
+ }
414
449
  /**
415
450
  * Animate the listing to a new source.
416
451
  *
@@ -426,10 +461,15 @@ let Code = class Code extends Node2D {
426
461
  * of two unrelated listings rather than as an edit being made.
427
462
  */
428
463
  editTo(next, duration, easing) {
464
+ if (!this.mounted)
465
+ return driveCommand(duration, () => { });
466
+ next = normalizeSource(next);
429
467
  const from = this.tokenLines;
430
468
  const fromCode = this.joinedSource();
431
469
  if (next === fromCode)
432
470
  return driveCommand(duration, () => { });
471
+ const fromBasis = this._linesBasis;
472
+ const basis = this._basis();
433
473
  const edit = this.diffStrategy(from, tokenizeCodeToIdLines(next, this.language, this.theme));
434
474
  const transition = {
435
475
  kind: "structural",
@@ -445,13 +485,12 @@ let Code = class Code extends Node2D {
445
485
  fromLayout: null,
446
486
  toLayout: null,
447
487
  };
448
- // A tokenize triggered by the grammar landing mid-edit would mint fresh
488
+ // A re-tokenize triggered by the grammar landing mid-edit would mint fresh
449
489
  // ids underneath this transition, and the transition is keyed by the ones
450
- // it captured — so the retry in `onRender` is gated on there being no edit
451
- // in flight, which means this flag has to be honest now rather than once
452
- // the edit settles.
453
- this.tokenized = canHighlight(this.language, this.theme);
490
+ // it captured — so `_sync` leaves the structure alone while an edit is in
491
+ // flight, and compares the basis recorded here once it settles.
454
492
  return driveCommand(duration, (t) => {
493
+ this._drive();
455
494
  const running = t > 0 && t < 1;
456
495
  const index = this.transitions.indexOf(transition);
457
496
  if (running && index < 0)
@@ -463,11 +502,11 @@ let Code = class Code extends Node2D {
463
502
  // starts, the listing is what preceded it; at or past the end, the
464
503
  // result. A `finally` could only ever have run forwards.
465
504
  if (t <= 0) {
466
- this.setLines(from);
505
+ this.setLines(from, fromCode, fromBasis);
467
506
  this._writeProp("code", fromCode);
468
507
  }
469
508
  else {
470
- this.setLines(edit.lines);
509
+ this.setLines(edit.lines, next, basis);
471
510
  this._writeProp("code", next);
472
511
  }
473
512
  });
@@ -482,8 +521,11 @@ let Code = class Code extends Node2D {
482
521
  * way*, and `progress` is assigned from `t` rather than accumulated.
483
522
  */
484
523
  runDim(tokens, duration, easing, settle) {
524
+ if (!this.mounted)
525
+ return driveCommand(duration, () => { });
485
526
  const transition = { kind: "dim", tokens, progress: 0 };
486
527
  return driveCommand(duration, (t) => {
528
+ this._drive();
487
529
  const running = t > 0 && t < 1;
488
530
  const index = this.transitions.indexOf(transition);
489
531
  if (running && index < 0)
@@ -494,6 +536,23 @@ let Code = class Code extends Node2D {
494
536
  settle?.(t >= 1);
495
537
  });
496
538
  }
539
+ /** The same membership rule as {@link runDim}: on the stack strictly inside the run, settled at either end. */
540
+ runRows(from, to, duration, easing) {
541
+ if (!this.mounted || (from.length === 0 && to.length === 0))
542
+ return driveCommand(duration, () => { });
543
+ const transition = { kind: "rows", from, to, progress: 0 };
544
+ return driveCommand(duration, (t) => {
545
+ this._drive();
546
+ const running = t > 0 && t < 1;
547
+ const index = this.transitions.indexOf(transition);
548
+ if (running && index < 0)
549
+ this.transitions.push(transition);
550
+ else if (!running && index >= 0)
551
+ this.transitions.splice(index, 1);
552
+ transition.progress = easing ? easing(t) : t;
553
+ this.rowBands = t >= 1 ? to : from;
554
+ });
555
+ }
497
556
  /**
498
557
  * Route a tweened `code` through the diff engine.
499
558
  *
@@ -504,8 +563,11 @@ let Code = class Code extends Node2D {
504
563
  * the rest of the step's props tween alongside it untouched.
505
564
  */
506
565
  _prepareStep(to, duration, easing) {
507
- const next = to.code;
508
- if (typeof next !== "string" || next === this.joinedSource()) {
566
+ if (!this.mounted)
567
+ return super._prepareStep(to, duration, easing);
568
+ this._sync();
569
+ const next = typeof to.code === "string" ? normalizeSource(to.code) : undefined;
570
+ if (next === undefined || next === this.joinedSource()) {
509
571
  return super._prepareStep(to, duration, easing);
510
572
  }
511
573
  const rest = { ...to };
@@ -536,6 +598,7 @@ let Code = class Code extends Node2D {
536
598
  const ranges = [];
537
599
  if (!text)
538
600
  return ranges;
601
+ this._sync();
539
602
  const source = this.joinedSource();
540
603
  const lineLens = this.lineLengths();
541
604
  let from = 0;
@@ -558,9 +621,7 @@ let Code = class Code extends Node2D {
558
621
  return this.findRangeAt(text, 0);
559
622
  }
560
623
  joinedSource() {
561
- return this.tokenLines
562
- .map(line => line.tokens.map(t => t.content).join(''))
563
- .join('\n');
624
+ return this._linesSource;
564
625
  }
565
626
  lineLengths() {
566
627
  return this.tokenLines.map(line => line.tokens.reduce((acc, t) => acc + t.content.length, 0));
@@ -609,6 +670,19 @@ let Code = class Code extends Node2D {
609
670
  return result;
610
671
  }
611
672
  // ── Drawing ─────────────────────────────────────────────────────────────
673
+ /** One band per range, over every line it touches; a range past the end of the listing gives none. */
674
+ rowBandsIn(codeRanges, color, radius) {
675
+ const count = this.tokenLines.length;
676
+ const bands = [];
677
+ for (const range of codeRanges) {
678
+ const first = Math.max(1, Math.round(range.startLine ?? 1));
679
+ const last = Math.min(count, range.endLine == null || range.endLine === Infinity ? count : Math.round(range.endLine));
680
+ if (first > last)
681
+ continue;
682
+ bands.push({ startId: this.tokenLines[first - 1].id, endId: this.tokenLines[last - 1].id, color, radius });
683
+ }
684
+ return bands;
685
+ }
612
686
  /**
613
687
  * Thin delegator: everything about *how* a frame is painted (settled or
614
688
  * mid structural-edit, entry/exit motion, colour cross-fade) lives in
@@ -625,8 +699,79 @@ let Code = class Code extends Node2D {
625
699
  ...this.frameLayout(draw),
626
700
  tokenStates: resolveTokenStates(this.tokenLines, this.transitions, this.highlightDimOpacity, this.highlightedIds),
627
701
  highlightActive: this.highlightDimOpacity !== null || this.transitions.some(tr => tr.kind === "dim"),
702
+ rowBands: this.rowBands,
703
+ rowsEdit: this.activeRows(),
704
+ boxW: this.layoutBounds.width,
628
705
  });
629
706
  }
707
+ /** Each source the props have named, tokenized once, so a listing derived again keeps the ids a highlight selected. */
708
+ _derived = new Map();
709
+ /** The first command to apply after a restore starts from the props' listing. */
710
+ _drive() {
711
+ this._sync();
712
+ this._writeProp("_driven", true);
713
+ }
714
+ /**
715
+ * Bring the settled structure up to the props it is derived from.
716
+ *
717
+ * Pulled from every reader rather than pushed from a prop write, because
718
+ * `code`, `language` and `theme` can each be tweened or bound, and neither
719
+ * has a write to hang a subscription on. Never mid-edit: re-tokenizing mints
720
+ * fresh ids, and an in-flight transition is keyed by the ones it captured.
721
+ */
722
+ _sync() {
723
+ const code = this._normalised();
724
+ if (!this._driven) {
725
+ this.transitions.length = 0;
726
+ this.highlightDimOpacity = null;
727
+ if (this.highlightedIds.size > 0)
728
+ this.highlightedIds = new Set();
729
+ if (this.rowBands.length > 0)
730
+ this.rowBands = [];
731
+ this.setLines(this._structureFor(code), code);
732
+ }
733
+ else if (!this.activeEdit()) {
734
+ const basis = this._basis();
735
+ const was = this._linesBasis;
736
+ if (this._linesSource === code && was.language === basis.language && was.theme === basis.theme && was.highlighted === basis.highlighted)
737
+ return;
738
+ const lines = this._structureFor(code);
739
+ // The same text split the same way, only coloured differently: the
740
+ // tokens are the ones a highlight or a transition already names.
741
+ if (this._linesSource === code && was.language === basis.language && was.highlighted === basis.highlighted) {
742
+ adoptIds(this.tokenLines, lines);
743
+ }
744
+ this.setLines(lines, code, basis);
745
+ }
746
+ }
747
+ _normalised() {
748
+ const code = this.code;
749
+ if (code !== this._rawCode) {
750
+ this._rawCode = code;
751
+ this._normalCode = normalizeSource(code);
752
+ }
753
+ return this._normalCode;
754
+ }
755
+ _basis() {
756
+ return { language: this.language, theme: this.theme, highlighted: canHighlight(this.language, this.theme) };
757
+ }
758
+ _structureFor(code) {
759
+ const { language, theme, highlighted } = this._basis();
760
+ let entry = this._derived.get(code);
761
+ if (!entry || entry.language !== language || entry.theme !== theme || entry.highlighted !== highlighted) {
762
+ entry = {
763
+ language,
764
+ theme,
765
+ highlighted,
766
+ lines: tokenizeCodeToIdLines(code, language, theme),
767
+ };
768
+ this._derived.delete(code);
769
+ this._derived.set(code, entry);
770
+ if (this._derived.size > DERIVED_LISTINGS)
771
+ this._derived.delete(this._derived.keys().next().value);
772
+ }
773
+ return entry.lines;
774
+ }
630
775
  };
631
776
  __decorate([
632
777
  property({ default: "" })
@@ -656,7 +801,7 @@ __decorate([
656
801
  property({ default: 2 })
657
802
  ], Code.prototype, "lineNumberGap", void 0);
658
803
  __decorate([
659
- property({ default: 0, mapper: insetsOps.resolve, tween: insetsOps.lerp })
804
+ insetsProperty()
660
805
  ], Code.prototype, "padding", void 0);
661
806
  __decorate([
662
807
  command({ args: [{ key: "code", kind: "text", multiline: true }] })
@@ -692,6 +837,18 @@ __decorate([
692
837
  __decorate([
693
838
  command()
694
839
  ], Code.prototype, "resetHighlight", null);
840
+ __decorate([
841
+ command({
842
+ args: [
843
+ { key: "target", kind: "selector" },
844
+ { key: "color", kind: "color", default: ROW_BAND_COLOR },
845
+ { key: "radius", kind: "number", default: ROW_BAND_RADIUS, min: 0, step: 1 },
846
+ ],
847
+ })
848
+ ], Code.prototype, "highlightRows", null);
849
+ __decorate([
850
+ command()
851
+ ], Code.prototype, "resetRowHighlight", null);
695
852
  Code = __decorate([
696
853
  node({
697
854
  key: "code",
@@ -705,18 +862,43 @@ Code = __decorate([
705
862
  })
706
863
  ], Code);
707
864
  export { Code };
865
+ /** Carry `from`'s line and token ids onto `to` when the two spell the same tokens. */
866
+ function adoptIds(from, to) {
867
+ if (from.length !== to.length)
868
+ return;
869
+ for (let i = 0; i < from.length; i++) {
870
+ const a = from[i].tokens;
871
+ const b = to[i].tokens;
872
+ if (a.length !== b.length)
873
+ return;
874
+ for (let j = 0; j < a.length; j++)
875
+ if (a[j].content !== b[j].content)
876
+ return;
877
+ }
878
+ for (let i = 0; i < from.length; i++) {
879
+ to[i].id = from[i].id;
880
+ for (let j = 0; j < from[i].tokens.length; j++)
881
+ to[i].tokens[j].id = from[i].tokens[j].id;
882
+ }
883
+ }
884
+ /** Sources a node keeps tokenized, oldest dropped first; a bound `code` can name any number of them. */
885
+ const DERIVED_LISTINGS = 16;
708
886
  /**
709
887
  * Normalize `highlight()`'s `codeRange` argument to a plain array: a single
710
888
  * `CodeRange` (a plain object, not iterable) becomes a one-element array; a
711
889
  * `CodeRangeChain`/plain array of ranges spreads as-is. `CodeRange` and
712
890
  * `CodeRangeChain` are distinguished by iterability, not `instanceof` — a
713
- * bare array of `CodeRange`s (no chain) works the same way.
891
+ * bare array of `CodeRange`s (no chain) works the same way. The `{ list }` shape
892
+ * a chain used to serialise as is read as the chain it came from.
714
893
  */
715
894
  function normalizeCodeRanges(codeRange) {
716
895
  const maybeIterable = codeRange;
717
896
  if (typeof maybeIterable[Symbol.iterator] === "function") {
718
897
  return [...codeRange];
719
898
  }
899
+ const stored = codeRange.list;
900
+ if (Array.isArray(stored))
901
+ return [...stored];
720
902
  return [codeRange];
721
903
  }
722
904
  const grammarLoaders = new Map();