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

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.
package/src/node.ts CHANGED
@@ -2,9 +2,9 @@ import {
2
2
  node,
3
3
  RenderContext2D, RenderPass2D, Clip, EasingFunction, NodeConfig, Size2D, SizeConstraints,
4
4
  Node2D, Node2DProps, MeasureContext2D, InsetsResolved, InsetsProps,
5
- property, insetsOps, lerpNumber,
5
+ property, insetsProperty, lerpNumber,
6
6
  command, driveCommand, type AssetScope, type Command, type CommandArgs, type Node, type TweenStepper, type Vector2,
7
- } from "@motionscript/core";
7
+ } from "@motionscript/sdk";
8
8
  import {
9
9
  CodeRange,
10
10
  rangeToCharOffsets,
@@ -101,11 +101,13 @@ export class Code extends Node2D<CodeProps> {
101
101
  // in space-widths (so it scales with fontSize). Only applies when
102
102
  // showLineNumbers is on.
103
103
  @property({ default: 2 }) declare readonly lineNumberGap: number;
104
- @property({ default: 0, mapper: insetsOps.resolve, tween: insetsOps.lerp }) declare readonly padding: InsetsResolved;
104
+ @insetsProperty() declare readonly padding: InsetsResolved;
105
105
 
106
106
  /** The settled structure. During an edit, the structure the edit lands on. */
107
107
  private tokenLines: IdLine[] = [];
108
108
  private tokenized: boolean = false;
109
+ /** The source `tokenLines` spells, so a frame compares it with `code` without joining every token. */
110
+ private _linesSource: string = "";
109
111
 
110
112
  private transitions: CodeTransition[] = [];
111
113
 
@@ -140,6 +142,7 @@ export class Code extends Node2D<CodeProps> {
140
142
  this.phaseStrategy = props.phaseStrategy ?? defaultCodePhases;
141
143
  this.applyProp("width", props.width ?? "hug");
142
144
  this.applyProp("height", props.height ?? "hug");
145
+ this.applyProp("_driven", false);
143
146
 
144
147
  // Best-effort, for a highlighter constructed outside the asset pipeline
145
148
  // entirely (a bare `new Code(...)` in a test or a headless measure). On
@@ -159,7 +162,7 @@ export class Code extends Node2D<CodeProps> {
159
162
  }
160
163
 
161
164
  private tokenize(): void {
162
- this.setLines(tokenizeCodeToIdLines(this.code, this.language, this.theme));
165
+ this.setLines(this._structureFor(this.code), this.code);
163
166
  // Only consider ourselves tokenized once we actually highlighted; while
164
167
  // the language is still loading we keep retrying on each render.
165
168
  this.tokenized = canHighlight(this.language, this.theme);
@@ -172,9 +175,10 @@ export class Code extends Node2D<CodeProps> {
172
175
  * counter rather than a hash of the content — a write that skipped the bump
173
176
  * would keep serving the previous frame's geometry for the new listing.
174
177
  */
175
- private setLines(lines: IdLine[]): void {
178
+ private setLines(lines: IdLine[], source: string): void {
176
179
  if (this.tokenLines === lines) return;
177
180
  this.tokenLines = lines;
181
+ this._linesSource = source;
178
182
  this.structureVersion++;
179
183
  this.layoutCache = null;
180
184
  this.layoutCacheKey = null;
@@ -263,6 +267,7 @@ export class Code extends Node2D<CodeProps> {
263
267
  }
264
268
 
265
269
  override measure(constraints: SizeConstraints, scope: MeasureContext2D): Partial<Size2D> {
270
+ this._sync();
266
271
  this.advanceCache.sync(this.advanceCache.signature(this.fontSize, this.fontFamily));
267
272
  const wm = this.width;
268
273
  const hm = this.height;
@@ -302,6 +307,7 @@ export class Code extends Node2D<CodeProps> {
302
307
  // column-by-column rather than merely look different.
303
308
  ctx.pushTextStyle(null);
304
309
  try {
310
+ this._sync();
305
311
  // Keep retrying until the language+theme have actually loaded, so a frame
306
312
  // that rendered as plain text upgrades to full highlighting the moment the
307
313
  // asset loader resolves. Never mid-edit: re-tokenizing mints fresh ids,
@@ -359,6 +365,7 @@ export class Code extends Node2D<CodeProps> {
359
365
  * silhouette measured against stale metrics.
360
366
  */
361
367
  protected override hitTestSelf(local: Vector2, tolerance: number): boolean {
368
+ this._sync();
362
369
  const layout = this.hitLayout();
363
370
  if (!layout) return super.hitTestSelf(local, tolerance);
364
371
 
@@ -409,6 +416,7 @@ export class Code extends Node2D<CodeProps> {
409
416
  */
410
417
  @command({ args: [{ key: "code", kind: "text", multiline: true }] })
411
418
  append(args: CommandArgs<{ code: string }> & { duration: number }): Command<Record<string, never>> {
419
+ this._sync();
412
420
  const { code } = args.data as { code: string };
413
421
  return this.editTo(this.joinedSource() + code, args.duration, args.easing);
414
422
  }
@@ -416,6 +424,7 @@ export class Code extends Node2D<CodeProps> {
416
424
  /** Insert `code` above the listing. */
417
425
  @command({ args: [{ key: "code", kind: "text", multiline: true }] })
418
426
  prepend(args: CommandArgs<{ code: string }> & { duration: number }): Command<Record<string, never>> {
427
+ this._sync();
419
428
  const { code } = args.data as { code: string };
420
429
  return this.editTo(code + this.joinedSource(), args.duration, args.easing);
421
430
  }
@@ -443,6 +452,7 @@ export class Code extends Node2D<CodeProps> {
443
452
  insert(
444
453
  args: CommandArgs<{ position: [number, number]; code: string }> & { duration: number },
445
454
  ): Command<Record<string, never>> {
455
+ this._sync();
446
456
  const { position, code } = args.data as { position: [number, number]; code: string };
447
457
  const source = this.joinedSource();
448
458
  const offset = this.offsetAt(position);
@@ -465,6 +475,7 @@ export class Code extends Node2D<CodeProps> {
465
475
  erase(
466
476
  args: CommandArgs<{ target: CodeRange }> & { duration: number },
467
477
  ): Command<Record<string, never>> {
478
+ this._sync();
468
479
  const { target: codeRange } = args.data as { target: CodeRange };
469
480
  const { duration, easing } = args;
470
481
  const source = this.joinedSource();
@@ -491,6 +502,7 @@ export class Code extends Node2D<CodeProps> {
491
502
  replace(
492
503
  args: CommandArgs<{ target: CodeRange; replacement: string }> & { duration: number },
493
504
  ): Command<Record<string, never>> {
505
+ this._sync();
494
506
  const { target: codeRange, replacement } = args.data as { target: CodeRange; replacement: string };
495
507
  const source = this.joinedSource();
496
508
  const { start, end } = rangeToCharOffsets(codeRange, this.lineLengths());
@@ -508,6 +520,7 @@ export class Code extends Node2D<CodeProps> {
508
520
  highlight(
509
521
  args: CommandArgs<{ target: CodeRange | Iterable<CodeRange>; dim?: number }> & { duration: number },
510
522
  ): Command<Record<string, never>> {
523
+ this._sync();
511
524
  const { target: codeRange, dim } = args.data as { target: CodeRange | Iterable<CodeRange>; dim?: number };
512
525
  const { duration, easing } = args;
513
526
  const toDim = dim ?? 0.4;
@@ -545,6 +558,7 @@ export class Code extends Node2D<CodeProps> {
545
558
  */
546
559
  @command()
547
560
  resetHighlight(args: CommandArgs<Record<string, never>> & { duration: number }): Command<Record<string, never>> {
561
+ this._sync();
548
562
  const { duration, easing } = args;
549
563
  if (this.highlightDimOpacity === null) return driveCommand(duration, () => { });
550
564
 
@@ -610,6 +624,7 @@ export class Code extends Node2D<CodeProps> {
610
624
  this.tokenized = canHighlight(this.language, this.theme);
611
625
 
612
626
  return driveCommand(duration, (t) => {
627
+ this._drive();
613
628
  const running = t > 0 && t < 1;
614
629
  const index = this.transitions.indexOf(transition);
615
630
  if (running && index < 0) this.transitions.push(transition);
@@ -621,10 +636,10 @@ export class Code extends Node2D<CodeProps> {
621
636
  // starts, the listing is what preceded it; at or past the end, the
622
637
  // result. A `finally` could only ever have run forwards.
623
638
  if (t <= 0) {
624
- this.setLines(from);
639
+ this.setLines(from, fromCode);
625
640
  this._writeProp("code", fromCode);
626
641
  } else {
627
- this.setLines(edit.lines);
642
+ this.setLines(edit.lines, next);
628
643
  this._writeProp("code", next);
629
644
  }
630
645
  });
@@ -648,6 +663,7 @@ export class Code extends Node2D<CodeProps> {
648
663
  const transition = { kind: "dim" as const, tokens, progress: 0 };
649
664
 
650
665
  return driveCommand(duration, (t) => {
666
+ this._drive();
651
667
  const running = t > 0 && t < 1;
652
668
  const index = this.transitions.indexOf(transition);
653
669
  if (running && index < 0) this.transitions.push(transition);
@@ -672,6 +688,7 @@ export class Code extends Node2D<CodeProps> {
672
688
  duration: number,
673
689
  easing?: EasingFunction,
674
690
  ): TweenStepper {
691
+ this._sync();
675
692
  const next = to.code;
676
693
  if (typeof next !== "string" || next === this.joinedSource()) {
677
694
  return super._prepareStep(to, duration, easing);
@@ -707,6 +724,7 @@ export class Code extends Node2D<CodeProps> {
707
724
  findAllRanges(text: string): CodeRange[] {
708
725
  const ranges: CodeRange[] = [];
709
726
  if (!text) return ranges;
727
+ this._sync();
710
728
  const source = this.joinedSource();
711
729
  const lineLens = this.lineLengths();
712
730
  let from = 0;
@@ -731,9 +749,7 @@ export class Code extends Node2D<CodeProps> {
731
749
  }
732
750
 
733
751
  private joinedSource(): string {
734
- return this.tokenLines
735
- .map(line => line.tokens.map(t => t.content).join(''))
736
- .join('\n');
752
+ return this._linesSource;
737
753
  }
738
754
 
739
755
  private lineLengths(): number[] {
@@ -802,8 +818,62 @@ export class Code extends Node2D<CodeProps> {
802
818
  highlightActive: this.highlightDimOpacity !== null || this.transitions.some(tr => tr.kind === "dim"),
803
819
  });
804
820
  }
821
+
822
+ /**
823
+ * Whether a listing command has applied since the timeline last restored the
824
+ * props.
825
+ *
826
+ * A slot rather than a field because slots are all a restore puts back. The
827
+ * structure, highlight and transitions live outside the props, and only a
828
+ * command that has started writes them, so before the first one starts (or
829
+ * after a seek back past it) they still hold whatever the last evaluation
830
+ * left — the end of every command, after compile. This reads false again
831
+ * there, and `_sync` derives them from the props instead.
832
+ */
833
+ declare private readonly _driven: boolean;
834
+
835
+ /** Each source the props have named, tokenized once, so a listing derived again keeps the ids a highlight selected. */
836
+ private readonly _derived = new Map<string, { language: string; theme: CodeTheme; highlighted: boolean; lines: IdLine[] }>();
837
+
838
+ /** The first command to apply after a restore starts from the props' listing. */
839
+ private _drive(): void {
840
+ this._sync();
841
+ this._writeProp("_driven", true);
842
+ }
843
+
844
+ private _sync(): void {
845
+ if (!this._driven) {
846
+ this.transitions.length = 0;
847
+ this.highlightDimOpacity = null;
848
+ if (this.highlightedIds.size > 0) this.highlightedIds = new Set();
849
+ this.setLines(this._structureFor(this.code), this.code);
850
+ } else if (this._linesSource !== this.code && !this.activeEdit()) {
851
+ // A `set` or a binding moved `code` outside a listing command.
852
+ this.setLines(this._structureFor(this.code), this.code);
853
+ }
854
+ }
855
+
856
+ private _structureFor(code: string): IdLine[] {
857
+ const highlighted = canHighlight(this.language, this.theme);
858
+ let entry = this._derived.get(code);
859
+ if (!entry || entry.language !== this.language || entry.theme !== this.theme || entry.highlighted !== highlighted) {
860
+ entry = {
861
+ language: this.language,
862
+ theme: this.theme,
863
+ highlighted,
864
+ lines: tokenizeCodeToIdLines(code, this.language, this.theme),
865
+ };
866
+ this._derived.delete(code);
867
+ this._derived.set(code, entry);
868
+ if (this._derived.size > DERIVED_LISTINGS) this._derived.delete(this._derived.keys().next().value as string);
869
+ }
870
+ return entry.lines;
871
+ }
805
872
  }
806
873
 
874
+ /** Sources a node keeps tokenized, oldest dropped first; a bound `code` can name any number of them. */
875
+ const DERIVED_LISTINGS = 16;
876
+
807
877
  /**
808
878
  * Normalize `highlight()`'s `codeRange` argument to a plain array: a single
809
879
  * `CodeRange` (a plain object, not iterable) becomes a one-element array; a
package/src/render.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { RenderContext2D, Graphics2D, NormalizedColor, parseColor, lerpNumber } from "@motionscript/core";
1
+ import { RenderContext2D, Graphics2D, NormalizedColor, parseColor, lerpNumber } from "@motionscript/sdk";
2
2
  import type { CodeLayout, CodeMetrics } from "./layout";
3
3
  import type { IdLine } from "./tokens";
4
4
  import { windowProgress, moveProgress } from "./transitions";