@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.
- package/CHANGELOG.md +66 -0
- package/LICENSE +201 -201
- package/NOTICE +48 -0
- package/README.md +161 -77
- package/dist/browser/THIRD_PARTY_LICENSES.txt +431 -0
- package/dist/browser/index.js +7 -7
- package/dist/browser/index.js.map +3 -3
- package/dist/browser/manifest.json +85 -3
- package/dist/code-range.d.ts +7 -1
- package/dist/code-range.d.ts.map +1 -1
- package/dist/code-range.js +9 -1
- package/dist/code-range.js.map +1 -1
- package/dist/diff.d.ts +14 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +134 -2
- package/dist/diff.js.map +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/highlight.d.ts +10 -2
- package/dist/highlight.d.ts.map +1 -1
- package/dist/highlight.js +40 -6
- package/dist/highlight.js.map +1 -1
- package/dist/index.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 +62 -7
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +224 -42
- package/dist/node.js.map +1 -1
- package/dist/nodes.js.map +1 -1
- package/dist/render.d.ts +7 -2
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +60 -1
- package/dist/render.js.map +1 -1
- package/dist/style.d.ts +3 -3
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +10 -6
- package/dist/style.js.map +1 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +5 -1
- package/dist/tokens.js.map +1 -1
- package/dist/transitions.d.ts +19 -1
- package/dist/transitions.d.ts.map +1 -1
- package/dist/transitions.js.map +1 -1
- package/package.json +7 -4
- package/registry.json +6 -6
- package/src/code-range.ts +165 -156
- package/src/diff.ts +351 -216
- package/src/engine.ts +21 -21
- package/src/highlight.ts +340 -291
- package/src/index.ts +3 -3
- package/src/layout.ts +157 -157
- package/src/measure-cache.ts +0 -0
- package/src/node.ts +1038 -834
- package/src/nodes.ts +18 -18
- package/src/render.ts +312 -247
- package/src/style.ts +255 -251
- package/src/tokens.ts +79 -75
- 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,
|
|
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
|
-
|
|
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
|
|
38
|
-
this.phaseStrategy = props
|
|
39
|
-
this.applyProp("width", props
|
|
40
|
-
this.applyProp("height", props
|
|
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.
|
|
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
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
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
|
|
451
|
-
//
|
|
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
|
-
|
|
508
|
-
|
|
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.
|
|
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
|
-
|
|
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();
|