@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/CHANGELOG.md +21 -0
- package/README.md +81 -50
- package/dist/browser/index.js +6 -7
- package/dist/browser/index.js.map +3 -3
- package/dist/browser/manifest.json +2 -2
- package/dist/code-range.d.ts +1 -1
- package/dist/code-range.js +1 -1
- package/dist/code-range.js.map +1 -1
- package/dist/layout.d.ts +1 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js.map +1 -1
- package/dist/measure-cache.d.ts +1 -1
- package/dist/measure-cache.d.ts.map +1 -1
- package/dist/measure-cache.js.map +1 -1
- package/dist/node.d.ts +21 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +64 -9
- package/dist/node.js.map +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +1 -1
- package/dist/render.js.map +1 -1
- package/package.json +4 -3
- package/src/code-range.ts +1 -1
- package/src/layout.ts +1 -1
- package/src/measure-cache.ts +0 -0
- package/src/node.ts +80 -10
- package/src/render.ts +1 -1
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,
|
|
5
|
+
property, insetsProperty, lerpNumber,
|
|
6
6
|
command, driveCommand, type AssetScope, type Command, type CommandArgs, type Node, type TweenStepper, type Vector2,
|
|
7
|
-
} from "@motionscript/
|
|
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
|
-
@
|
|
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(
|
|
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.
|
|
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/
|
|
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";
|