@motion-script/code 0.1.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.
- package/README.md +85 -0
- package/dist/code-fragment.d.ts +42 -0
- package/dist/code-fragment.d.ts.map +1 -0
- package/dist/code-fragment.js +72 -0
- package/dist/code-fragment.js.map +1 -0
- package/dist/code-metrics.d.ts +11 -0
- package/dist/code-metrics.d.ts.map +1 -0
- package/dist/code-metrics.js +29 -0
- package/dist/code-metrics.js.map +1 -0
- package/dist/code-range.d.ts +44 -0
- package/dist/code-range.d.ts.map +1 -0
- package/dist/code-range.js +70 -0
- package/dist/code-range.js.map +1 -0
- package/dist/diff.d.ts +31 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +236 -0
- package/dist/diff.js.map +1 -0
- package/dist/highlighter.d.ts +69 -0
- package/dist/highlighter.d.ts.map +1 -0
- package/dist/highlighter.js +2 -0
- package/dist/highlighter.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +96 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +882 -0
- package/dist/node.js.map +1 -0
- package/dist/props.d.ts +14 -0
- package/dist/props.d.ts.map +1 -0
- package/dist/props.js +2 -0
- package/dist/props.js.map +1 -0
- package/dist/tokenizer.d.ts +8 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +50 -0
- package/dist/tokenizer.js.map +1 -0
- package/package.json +49 -0
package/dist/node.js
ADDED
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { createHighlighter } from "shiki";
|
|
8
|
+
import { rangeToCharOffsets } from "./code-range";
|
|
9
|
+
import { parseColor, Node, tween, property, resolvePadding, lerpEdgeInset } from "@motion-script/core";
|
|
10
|
+
let globalHighlighter = null;
|
|
11
|
+
let highlighterPromise = null;
|
|
12
|
+
function ensureHighlighter(themes = ['github-dark'], langs = ['typescript', 'javascript', 'json', 'python']) {
|
|
13
|
+
if (globalHighlighter)
|
|
14
|
+
return Promise.resolve(globalHighlighter);
|
|
15
|
+
if (!highlighterPromise) {
|
|
16
|
+
highlighterPromise = createHighlighter({ themes, langs }).then(h => {
|
|
17
|
+
globalHighlighter = h;
|
|
18
|
+
return h;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return highlighterPromise;
|
|
22
|
+
}
|
|
23
|
+
export async function initSyntaxHighlighter(themes = ['github-dark'], langs = ['typescript', 'javascript', 'json', 'python']) {
|
|
24
|
+
globalHighlighter = await ensureHighlighter(themes, langs);
|
|
25
|
+
}
|
|
26
|
+
let nextTokenId = 1;
|
|
27
|
+
let nextLineId = 1;
|
|
28
|
+
function makeIdToken(content, color) {
|
|
29
|
+
return { id: nextTokenId++, content, color };
|
|
30
|
+
}
|
|
31
|
+
function makeIdLine(tokens) {
|
|
32
|
+
return { id: nextLineId++, tokens };
|
|
33
|
+
}
|
|
34
|
+
function tokenizeCodeToIdLines(code, language, theme) {
|
|
35
|
+
if (!globalHighlighter) {
|
|
36
|
+
return code.split('\n').map(line => makeIdLine([makeIdToken(line, '#d1d5db')]));
|
|
37
|
+
}
|
|
38
|
+
const result = globalHighlighter.codeToTokens(code, { lang: language, theme: theme });
|
|
39
|
+
return result.tokens.map(line => makeIdLine(line.map(tok => makeIdToken(tok.content, tok.color))));
|
|
40
|
+
}
|
|
41
|
+
function sampleCurve(keys, values, p) {
|
|
42
|
+
if (p <= keys[0])
|
|
43
|
+
return values[0];
|
|
44
|
+
if (p >= keys[keys.length - 1])
|
|
45
|
+
return values[values.length - 1];
|
|
46
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
47
|
+
if (p <= keys[i + 1]) {
|
|
48
|
+
const span = keys[i + 1] - keys[i];
|
|
49
|
+
const local = span === 0 ? 0 : (p - keys[i]) / span;
|
|
50
|
+
return values[i] + (values[i + 1] - values[i]) * local;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return values[values.length - 1];
|
|
54
|
+
}
|
|
55
|
+
export class Code extends Node {
|
|
56
|
+
tokenLines = [];
|
|
57
|
+
tokenized = false;
|
|
58
|
+
transitions = [];
|
|
59
|
+
// Persistent dim state set by highlight() — applied during render to all
|
|
60
|
+
// tokens whose id is NOT in the highlight set. null means "not highlighting".
|
|
61
|
+
highlightDimOpacity = null;
|
|
62
|
+
highlightedIds = new Set();
|
|
63
|
+
constructor(props) {
|
|
64
|
+
super(props);
|
|
65
|
+
this.applyProp("width", props.width ?? "hug");
|
|
66
|
+
this.applyProp("height", props.height ?? "hug");
|
|
67
|
+
ensureHighlighter();
|
|
68
|
+
this.tokenize();
|
|
69
|
+
}
|
|
70
|
+
set(props) {
|
|
71
|
+
super.set(props);
|
|
72
|
+
if (props.code !== undefined || props.language !== undefined || props.theme !== undefined) {
|
|
73
|
+
this.tokenized = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
tokenize() {
|
|
77
|
+
this.tokenLines = tokenizeCodeToIdLines(this.code, this.language, this.theme);
|
|
78
|
+
this.tokenized = !!globalHighlighter;
|
|
79
|
+
}
|
|
80
|
+
prepare(storage) {
|
|
81
|
+
storage.requestLoader(async () => ensureHighlighter([this.theme], [this.language]));
|
|
82
|
+
}
|
|
83
|
+
// A token's advance width, honoring letterSpacing. The renderer applies the
|
|
84
|
+
// same letterSpacing when drawing (see drawSelf), so measure and draw stay
|
|
85
|
+
// in lockstep. fontWeight is left default; letterSpacing is the 5th arg.
|
|
86
|
+
tokenAdvance(scope, content) {
|
|
87
|
+
return scope.measureText(content, this.fontSize, this.fontFamily, undefined, this.letterSpacing);
|
|
88
|
+
}
|
|
89
|
+
// Horizontal gap between the line-number column and the code text. Kept in
|
|
90
|
+
// one place so gutterWidth() and the line-number x in drawSelf() agree.
|
|
91
|
+
// Sized in space-widths via the lineNumberGap prop so it scales with font.
|
|
92
|
+
gutterGap(scope) {
|
|
93
|
+
return scope.measureText(' ', this.fontSize, this.fontFamily) * this.lineNumberGap;
|
|
94
|
+
}
|
|
95
|
+
gutterWidth(scope) {
|
|
96
|
+
if (!this.showLineNumbers)
|
|
97
|
+
return 0;
|
|
98
|
+
const maxLine = Math.max(1, this.tokenLines.length);
|
|
99
|
+
const sample = String(maxLine);
|
|
100
|
+
const digitW = scope.measureText(sample, this.fontSize, this.fontFamily);
|
|
101
|
+
// digit column + the gap that separates the number from the code text.
|
|
102
|
+
return digitW + this.gutterGap(scope);
|
|
103
|
+
}
|
|
104
|
+
measure(constraints, scope) {
|
|
105
|
+
const pad = this.padding;
|
|
106
|
+
const wm = this.width;
|
|
107
|
+
const hm = this.height;
|
|
108
|
+
const lineH = this.fontSize * this.lineHeight;
|
|
109
|
+
const stateById = this.resolveTokenStates();
|
|
110
|
+
const lineHeightScales = this.resolveLineHeightScales();
|
|
111
|
+
let measuredW = 0;
|
|
112
|
+
let measuredH = 0;
|
|
113
|
+
const gutter = this.gutterWidth(scope);
|
|
114
|
+
for (const line of this.tokenLines) {
|
|
115
|
+
let lineW = 0;
|
|
116
|
+
for (const tok of line.tokens) {
|
|
117
|
+
const ws = stateById.get(tok.id)?.widthScale ?? 1;
|
|
118
|
+
lineW += this.tokenAdvance(scope, tok.content) * ws;
|
|
119
|
+
}
|
|
120
|
+
if (lineW > measuredW)
|
|
121
|
+
measuredW = lineW;
|
|
122
|
+
measuredH += lineH * (lineHeightScales.get(line.id) ?? 1);
|
|
123
|
+
}
|
|
124
|
+
measuredW += gutter;
|
|
125
|
+
const resolvedW = typeof wm === "number"
|
|
126
|
+
? wm
|
|
127
|
+
: wm === "hug"
|
|
128
|
+
? measuredW + pad.left + pad.right
|
|
129
|
+
: constraints.maxWidth ?? 0;
|
|
130
|
+
const resolvedH = typeof hm === "number"
|
|
131
|
+
? hm
|
|
132
|
+
: hm === "hug"
|
|
133
|
+
? measuredH + pad.top + pad.bottom
|
|
134
|
+
: constraints.maxHeight ?? 0;
|
|
135
|
+
return { width: resolvedW, height: resolvedH };
|
|
136
|
+
}
|
|
137
|
+
onRender(ctx) {
|
|
138
|
+
super.onRender(ctx);
|
|
139
|
+
if (!this.tokenized && globalHighlighter) {
|
|
140
|
+
this.tokenize();
|
|
141
|
+
}
|
|
142
|
+
this.drawSelf(ctx);
|
|
143
|
+
}
|
|
144
|
+
*append(code, duration, easing) {
|
|
145
|
+
const newLines = tokenizeCodeToIdLines(code, this.language, this.theme);
|
|
146
|
+
this.tokenLines = [...this.tokenLines, ...newLines];
|
|
147
|
+
const animTokens = [];
|
|
148
|
+
const lineHeightScales = new Map();
|
|
149
|
+
const introducedIds = new Set();
|
|
150
|
+
for (const line of newLines) {
|
|
151
|
+
lineHeightScales.set(line.id, { keys: [0, 1], values: [0, 1] });
|
|
152
|
+
for (const tok of line.tokens) {
|
|
153
|
+
introducedIds.add(tok.id);
|
|
154
|
+
animTokens.push(makeAnim(tok.id, {
|
|
155
|
+
opacity: { keys: [0, 1], values: [0, 1] },
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
yield* this.runTransition({ tokens: animTokens, lineHeightScales, introducedIds }, duration, easing);
|
|
160
|
+
}
|
|
161
|
+
*prepend(code, duration, easing) {
|
|
162
|
+
const newLines = tokenizeCodeToIdLines(code, this.language, this.theme);
|
|
163
|
+
this.tokenLines = [...newLines, ...this.tokenLines];
|
|
164
|
+
const animTokens = [];
|
|
165
|
+
const lineHeightScales = new Map();
|
|
166
|
+
const introducedIds = new Set();
|
|
167
|
+
for (const line of newLines) {
|
|
168
|
+
lineHeightScales.set(line.id, { keys: [0, 1], values: [0, 1] });
|
|
169
|
+
for (const tok of line.tokens) {
|
|
170
|
+
introducedIds.add(tok.id);
|
|
171
|
+
animTokens.push(makeAnim(tok.id, {
|
|
172
|
+
opacity: { keys: [0, 1], values: [0, 1] },
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
yield* this.runTransition({ tokens: animTokens, lineHeightScales, introducedIds }, duration, easing);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Highlight a range of code: tokens within the range stay at opacity 1,
|
|
180
|
+
* tokens outside dim to `opacity`. Persistent — call resetHighlight() to
|
|
181
|
+
* undo, or call highlight() again with a different range to cross-fade.
|
|
182
|
+
*/
|
|
183
|
+
*highlight(codeRange, duration = 0.4, easing, opacity = 0.4) {
|
|
184
|
+
const matchIds = this.tokenIdsInRange(codeRange);
|
|
185
|
+
if (matchIds.size === 0) {
|
|
186
|
+
yield* tween(duration, () => { });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const fromDim = this.highlightDimOpacity ?? 1;
|
|
190
|
+
const toDim = opacity;
|
|
191
|
+
const hadPrevious = this.highlightedIds.size > 0;
|
|
192
|
+
const previousIds = this.highlightedIds;
|
|
193
|
+
const animTokens = [];
|
|
194
|
+
for (const line of this.tokenLines) {
|
|
195
|
+
for (const tok of line.tokens) {
|
|
196
|
+
const wasHighlighted = !hadPrevious || previousIds.has(tok.id);
|
|
197
|
+
const isHighlighted = matchIds.has(tok.id);
|
|
198
|
+
const fromOp = wasHighlighted ? 1 : fromDim;
|
|
199
|
+
const toOp = isHighlighted ? 1 : toDim;
|
|
200
|
+
if (fromOp === toOp)
|
|
201
|
+
continue;
|
|
202
|
+
animTokens.push(makeAnim(tok.id, {
|
|
203
|
+
opacity: { keys: [0, 1], values: [fromOp, toOp] },
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
yield* this.runTransition({ tokens: animTokens }, duration, easing);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
this.highlightDimOpacity = toDim;
|
|
212
|
+
this.highlightedIds = matchIds;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Fade all dimmed tokens back to opacity 1 and clear the persistent
|
|
217
|
+
* highlight state.
|
|
218
|
+
*/
|
|
219
|
+
*resetHighlight(duration = 0.4, easing) {
|
|
220
|
+
if (this.highlightDimOpacity === null) {
|
|
221
|
+
yield* tween(duration, () => { });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const fromDim = this.highlightDimOpacity;
|
|
225
|
+
const previousIds = this.highlightedIds;
|
|
226
|
+
const animTokens = [];
|
|
227
|
+
for (const line of this.tokenLines) {
|
|
228
|
+
for (const tok of line.tokens) {
|
|
229
|
+
const wasHighlighted = previousIds.has(tok.id);
|
|
230
|
+
const fromOp = wasHighlighted ? 1 : fromDim;
|
|
231
|
+
if (fromOp === 1)
|
|
232
|
+
continue;
|
|
233
|
+
animTokens.push(makeAnim(tok.id, {
|
|
234
|
+
opacity: { keys: [0, 1], values: [fromOp, 1] },
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
yield* this.runTransition({ tokens: animTokens }, duration, easing);
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
this.highlightDimOpacity = null;
|
|
243
|
+
this.highlightedIds = new Set();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Replace the tokens in `codeRange` with `next`, cross-fading widths and
|
|
248
|
+
* opacities.
|
|
249
|
+
*/
|
|
250
|
+
*replace(codeRange, next, duration, easing) {
|
|
251
|
+
const span = this.rangeToTokenSpan(codeRange);
|
|
252
|
+
if (!span) {
|
|
253
|
+
yield* tween(duration, () => { });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const replacementLineGroups = tokenizeCodeToIdLines(next, this.language, this.theme);
|
|
257
|
+
const replacementTokens = [];
|
|
258
|
+
for (let i = 0; i < replacementLineGroups.length; i++) {
|
|
259
|
+
for (const tok of replacementLineGroups[i].tokens)
|
|
260
|
+
replacementTokens.push(tok);
|
|
261
|
+
if (i < replacementLineGroups.length - 1) {
|
|
262
|
+
replacementTokens.push(makeIdToken('\n'));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const oldTokens = [];
|
|
266
|
+
for (let li = span.fromLine; li <= span.toLine; li++) {
|
|
267
|
+
const line = this.tokenLines[li];
|
|
268
|
+
const start = li === span.fromLine ? span.fromIdx : 0;
|
|
269
|
+
const end = li === span.toLine ? span.toIdx : line.tokens.length;
|
|
270
|
+
for (let ti = start; ti < end; ti++)
|
|
271
|
+
oldTokens.push(line.tokens[ti]);
|
|
272
|
+
}
|
|
273
|
+
const fromLine = this.tokenLines[span.fromLine];
|
|
274
|
+
const toLine = this.tokenLines[span.toLine];
|
|
275
|
+
const prefix = fromLine.tokens.slice(0, span.fromIdx);
|
|
276
|
+
const suffix = toLine.tokens.slice(span.toIdx);
|
|
277
|
+
const mergedLine = makeIdLine([
|
|
278
|
+
...prefix,
|
|
279
|
+
...oldTokens,
|
|
280
|
+
...replacementTokens,
|
|
281
|
+
...suffix,
|
|
282
|
+
]);
|
|
283
|
+
mergedLine.id = fromLine.id;
|
|
284
|
+
const before = this.tokenLines.slice(0, span.fromLine);
|
|
285
|
+
const after = this.tokenLines.slice(span.toLine + 1);
|
|
286
|
+
this.tokenLines = [...before, mergedLine, ...after];
|
|
287
|
+
const oldIds = new Set(oldTokens.map(t => t.id));
|
|
288
|
+
const animTokens = [];
|
|
289
|
+
const introducedIds = new Set();
|
|
290
|
+
for (const tok of oldTokens) {
|
|
291
|
+
animTokens.push(makeAnim(tok.id, {
|
|
292
|
+
opacity: { keys: [0, 0.5, 1], values: [1, 0, 0] },
|
|
293
|
+
widthScale: { keys: [0, 0.5, 1], values: [1, 0, 0] },
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
for (const tok of replacementTokens) {
|
|
297
|
+
introducedIds.add(tok.id);
|
|
298
|
+
animTokens.push(makeAnim(tok.id, {
|
|
299
|
+
opacity: { keys: [0, 0.5, 1], values: [0, 0, 1] },
|
|
300
|
+
widthScale: { keys: [0, 0.5, 1], values: [0, 1, 1] },
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
yield* this.runTransition({ tokens: animTokens, introducedIds }, duration, easing);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
for (const line of this.tokenLines) {
|
|
308
|
+
line.tokens = line.tokens.filter(tok => !oldIds.has(tok.id));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Insert `code` at the given (line, col). Both are 1-indexed; col is the
|
|
314
|
+
* column BEFORE which the new content is inserted (col=1 means start of
|
|
315
|
+
* line). If `code` contains newlines, new lines are created in the middle
|
|
316
|
+
* of the existing line.
|
|
317
|
+
*/
|
|
318
|
+
*insert(position, code, duration, easing) {
|
|
319
|
+
if (this.tokenLines.length === 0) {
|
|
320
|
+
this.tokenLines = [makeIdLine([])];
|
|
321
|
+
}
|
|
322
|
+
const [rawLine, rawCol] = position;
|
|
323
|
+
const lineIdx = Math.max(0, Math.min(this.tokenLines.length - 1, rawLine - 1));
|
|
324
|
+
const targetLine = this.tokenLines[lineIdx];
|
|
325
|
+
const lineText = targetLine.tokens.map(t => t.content).join('');
|
|
326
|
+
const col = Math.max(0, Math.min(lineText.length, rawCol - 1));
|
|
327
|
+
// Split the target line's tokens at the character offset `col`.
|
|
328
|
+
const { before: tokBefore, after: tokAfter } = splitTokensAt(targetLine.tokens, col);
|
|
329
|
+
const insertedLineGroups = tokenizeCodeToIdLines(code, this.language, this.theme);
|
|
330
|
+
const animTokens = [];
|
|
331
|
+
const lineHeightScales = new Map();
|
|
332
|
+
const introducedIds = new Set();
|
|
333
|
+
// Two curves:
|
|
334
|
+
// - `newLineIntro`: for tokens on a line that's growing in height
|
|
335
|
+
// (multi-line insert). Token opacity ramps linearly 0→1 to match
|
|
336
|
+
// the line's heightScale ramp, so text, line number, and row
|
|
337
|
+
// height all reveal together. No widthScale anim needed — there's
|
|
338
|
+
// no existing content at the same x.
|
|
339
|
+
// - `inlineIntro`: for tokens being spliced into an existing line
|
|
340
|
+
// (single-line insert). The line's height isn't animating, so the
|
|
341
|
+
// suffix has to make room horizontally — widthScale 0→1 over the
|
|
342
|
+
// first half, then opacity fades in over the second half.
|
|
343
|
+
const collectIntro = (toks, mode) => {
|
|
344
|
+
for (const tok of toks) {
|
|
345
|
+
introducedIds.add(tok.id);
|
|
346
|
+
if (mode === 'newLine') {
|
|
347
|
+
animTokens.push(makeAnim(tok.id, {
|
|
348
|
+
opacity: { keys: [0, 1], values: [0, 1] },
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
animTokens.push(makeAnim(tok.id, {
|
|
353
|
+
opacity: { keys: [0, 0.5, 1], values: [0, 0, 1] },
|
|
354
|
+
widthScale: { keys: [0, 0.5, 1], values: [0, 1, 1] },
|
|
355
|
+
}));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
let newLines;
|
|
360
|
+
if (insertedLineGroups.length === 1) {
|
|
361
|
+
// Single-line insertion: splice tokens into the existing line.
|
|
362
|
+
const insertedTokens = insertedLineGroups[0].tokens;
|
|
363
|
+
collectIntro(insertedTokens, 'inline');
|
|
364
|
+
const merged = makeIdLine([...tokBefore, ...insertedTokens, ...tokAfter]);
|
|
365
|
+
merged.id = targetLine.id;
|
|
366
|
+
newLines = [merged];
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// Multi-line insertion. Split the inserted content into:
|
|
370
|
+
// first = prefix + insertedLineGroups[0] (sits on the original line)
|
|
371
|
+
// middle = insertedLineGroups[1..-2] (entirely new lines)
|
|
372
|
+
// last = insertedLineGroups[-1] + suffix (the original line's tail)
|
|
373
|
+
//
|
|
374
|
+
// Pre-existing tokens (tokBefore, tokAfter) keep their identity and
|
|
375
|
+
// shouldn't animate. Newly tokenized groups fade in.
|
|
376
|
+
//
|
|
377
|
+
// Critical for height animation: the line that contains the host
|
|
378
|
+
// line's pre-existing content must reuse the host id and NOT get a
|
|
379
|
+
// heightScale animation. The other produced lines are genuinely new
|
|
380
|
+
// and get the 0→1 reveal. We pick which side inherits the host id
|
|
381
|
+
// by where the cut lands — if tokAfter is empty (cut at end of
|
|
382
|
+
// line), the first produced line owns the original content. If
|
|
383
|
+
// tokBefore is empty (cut at start), the last produced line owns
|
|
384
|
+
// it. Otherwise both sides hold original content and the first
|
|
385
|
+
// keeps the host id by default.
|
|
386
|
+
const firstInserted = insertedLineGroups[0].tokens;
|
|
387
|
+
const middleInserted = insertedLineGroups.slice(1, -1).map(g => g.tokens);
|
|
388
|
+
const lastInserted = insertedLineGroups[insertedLineGroups.length - 1].tokens;
|
|
389
|
+
// The inheritor side (whichever produced line keeps the host id)
|
|
390
|
+
// doesn't get a height animation, so its inserted tokens need the
|
|
391
|
+
// inline (width-collapse) intro. The genuinely-new lines get the
|
|
392
|
+
// newLine intro (token fade matches the line's height ramp).
|
|
393
|
+
const cutAtStart = tokBefore.length === 0;
|
|
394
|
+
const firstLine = makeIdLine([...tokBefore, ...firstInserted]);
|
|
395
|
+
const middleLines = middleInserted.map(toks => makeIdLine(toks));
|
|
396
|
+
const lastLine = makeIdLine([...lastInserted, ...tokAfter]);
|
|
397
|
+
const inheritor = cutAtStart ? lastLine : firstLine;
|
|
398
|
+
inheritor.id = targetLine.id;
|
|
399
|
+
collectIntro(firstInserted, firstLine === inheritor ? 'inline' : 'newLine');
|
|
400
|
+
for (const m of middleInserted)
|
|
401
|
+
collectIntro(m, 'newLine');
|
|
402
|
+
collectIntro(lastInserted, lastLine === inheritor ? 'inline' : 'newLine');
|
|
403
|
+
// Every produced line OTHER than the inheritor is genuinely new and
|
|
404
|
+
// grows in height from 0→1.
|
|
405
|
+
const allProduced = [firstLine, ...middleLines, lastLine];
|
|
406
|
+
for (const ln of allProduced) {
|
|
407
|
+
if (ln.id !== targetLine.id) {
|
|
408
|
+
lineHeightScales.set(ln.id, { keys: [0, 1], values: [0, 1] });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
newLines = allProduced;
|
|
412
|
+
}
|
|
413
|
+
this.tokenLines = [
|
|
414
|
+
...this.tokenLines.slice(0, lineIdx),
|
|
415
|
+
...newLines,
|
|
416
|
+
...this.tokenLines.slice(lineIdx + 1),
|
|
417
|
+
];
|
|
418
|
+
yield* this.runTransition({ tokens: animTokens, lineHeightScales, introducedIds }, duration, easing);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Remove the tokens in `codeRange`. If the range spans whole lines, those
|
|
422
|
+
* lines collapse their height; partial line ranges only remove tokens
|
|
423
|
+
* (the surrounding text reflows).
|
|
424
|
+
*/
|
|
425
|
+
*remove(codeRange, duration, easing) {
|
|
426
|
+
const span = this.rangeToTokenSpan(codeRange);
|
|
427
|
+
if (!span) {
|
|
428
|
+
yield* tween(duration, () => { });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const removedTokens = [];
|
|
432
|
+
const fullyRemovedLineIds = [];
|
|
433
|
+
// A line is "fully removed" when the range covers every one of its
|
|
434
|
+
// tokens. Fully-removed lines get a height collapse + linear fade.
|
|
435
|
+
// Partially-removed lines keep their height; their removed tokens
|
|
436
|
+
// shrink horizontally so the surrounding text reflows.
|
|
437
|
+
for (let li = span.fromLine; li <= span.toLine; li++) {
|
|
438
|
+
const line = this.tokenLines[li];
|
|
439
|
+
const start = li === span.fromLine ? span.fromIdx : 0;
|
|
440
|
+
const end = li === span.toLine ? span.toIdx : line.tokens.length;
|
|
441
|
+
for (let ti = start; ti < end; ti++)
|
|
442
|
+
removedTokens.push(line.tokens[ti]);
|
|
443
|
+
if (start === 0 && end === line.tokens.length) {
|
|
444
|
+
fullyRemovedLineIds.push(line.id);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const animTokens = [];
|
|
448
|
+
const lineHeightScales = new Map();
|
|
449
|
+
const fullyRemovedLineIdSet = new Set(fullyRemovedLineIds);
|
|
450
|
+
const fullyRemovedTokenIds = new Set();
|
|
451
|
+
for (const line of this.tokenLines) {
|
|
452
|
+
if (fullyRemovedLineIdSet.has(line.id)) {
|
|
453
|
+
for (const tok of line.tokens)
|
|
454
|
+
fullyRemovedTokenIds.add(tok.id);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Whole-line removal: linear fade matching the linear height collapse,
|
|
458
|
+
// so text, line number, and row height all close together.
|
|
459
|
+
for (const lineId of fullyRemovedLineIds) {
|
|
460
|
+
lineHeightScales.set(lineId, { keys: [0, 1], values: [1, 0] });
|
|
461
|
+
}
|
|
462
|
+
for (const tok of removedTokens) {
|
|
463
|
+
if (fullyRemovedTokenIds.has(tok.id)) {
|
|
464
|
+
animTokens.push(makeAnim(tok.id, {
|
|
465
|
+
opacity: { keys: [0, 1], values: [1, 0] },
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
// Partial-line removal: still need width to collapse so the
|
|
470
|
+
// surrounding text on the line doesn't leave a gap.
|
|
471
|
+
animTokens.push(makeAnim(tok.id, {
|
|
472
|
+
opacity: { keys: [0, 0.5, 1], values: [1, 0, 0] },
|
|
473
|
+
widthScale: { keys: [0, 0.5, 1], values: [1, 0, 0] },
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const removedTokenIds = new Set(removedTokens.map(t => t.id));
|
|
478
|
+
try {
|
|
479
|
+
yield* this.runTransition({ tokens: animTokens, lineHeightScales }, duration, easing);
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
// Drop the tokens that animated out, then drop any lines that are
|
|
483
|
+
// either marked-fully-removed or have ended up empty.
|
|
484
|
+
const remaining = [];
|
|
485
|
+
for (const line of this.tokenLines) {
|
|
486
|
+
if (fullyRemovedLineIdSet.has(line.id))
|
|
487
|
+
continue;
|
|
488
|
+
line.tokens = line.tokens.filter(tok => !removedTokenIds.has(tok.id));
|
|
489
|
+
remaining.push(line);
|
|
490
|
+
}
|
|
491
|
+
this.tokenLines = remaining;
|
|
492
|
+
// Clean up highlight state pointing at removed tokens.
|
|
493
|
+
for (const id of removedTokenIds)
|
|
494
|
+
this.highlightedIds.delete(id);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Find every range matching the literal string `text` in the current
|
|
499
|
+
* source. Multi-line matches are supported.
|
|
500
|
+
*/
|
|
501
|
+
findAllRanges(text) {
|
|
502
|
+
const ranges = [];
|
|
503
|
+
if (!text)
|
|
504
|
+
return ranges;
|
|
505
|
+
const source = this.joinedSource();
|
|
506
|
+
const lineLens = this.lineLengths();
|
|
507
|
+
let from = 0;
|
|
508
|
+
while (true) {
|
|
509
|
+
const idx = source.indexOf(text, from);
|
|
510
|
+
if (idx === -1)
|
|
511
|
+
break;
|
|
512
|
+
ranges.push(charOffsetsToRange(idx, idx + text.length, lineLens));
|
|
513
|
+
from = idx + Math.max(1, text.length);
|
|
514
|
+
}
|
|
515
|
+
return ranges;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Find the `index`th range matching `text`. Returns null if not found.
|
|
519
|
+
*/
|
|
520
|
+
findRangeAt(text, index) {
|
|
521
|
+
const all = this.findAllRanges(text);
|
|
522
|
+
return all[index] ?? null;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Find the first range matching `text`. Returns null if not found.
|
|
526
|
+
*/
|
|
527
|
+
findFirstRange(text) {
|
|
528
|
+
return this.findRangeAt(text, 0);
|
|
529
|
+
}
|
|
530
|
+
*runTransition(partial, duration, easing) {
|
|
531
|
+
const transition = {
|
|
532
|
+
tokens: partial.tokens,
|
|
533
|
+
lineHeightScales: partial.lineHeightScales ?? new Map(),
|
|
534
|
+
progress: 0,
|
|
535
|
+
introducedIds: partial.introducedIds ?? new Set(),
|
|
536
|
+
};
|
|
537
|
+
this.transitions.push(transition);
|
|
538
|
+
try {
|
|
539
|
+
yield* tween(duration, (rawT) => {
|
|
540
|
+
transition.progress = easing ? easing(rawT) : rawT;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
finally {
|
|
544
|
+
this.transitions = this.transitions.filter(t => t !== transition);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
joinedSource() {
|
|
548
|
+
return this.tokenLines
|
|
549
|
+
.map(line => line.tokens.map(t => t.content).join(''))
|
|
550
|
+
.join('\n');
|
|
551
|
+
}
|
|
552
|
+
lineLengths() {
|
|
553
|
+
return this.tokenLines.map(line => line.tokens.reduce((acc, t) => acc + t.content.length, 0));
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Resolve a CodeRange to the set of token ids whose content overlaps the
|
|
557
|
+
* range. Tokens that partially overlap are included.
|
|
558
|
+
*/
|
|
559
|
+
tokenIdsInRange(codeRange) {
|
|
560
|
+
const result = new Set();
|
|
561
|
+
const lineLens = this.lineLengths();
|
|
562
|
+
if (lineLens.length === 0)
|
|
563
|
+
return result;
|
|
564
|
+
const { start: rStart, end: rEnd } = rangeToCharOffsets(codeRange, lineLens);
|
|
565
|
+
if (rEnd <= rStart)
|
|
566
|
+
return result;
|
|
567
|
+
// Walk tokens with running offsets in the joined string.
|
|
568
|
+
let off = 0;
|
|
569
|
+
for (let li = 0; li < this.tokenLines.length; li++) {
|
|
570
|
+
const line = this.tokenLines[li];
|
|
571
|
+
for (const tok of line.tokens) {
|
|
572
|
+
const tStart = off;
|
|
573
|
+
const tEnd = off + tok.content.length;
|
|
574
|
+
if (tEnd > rStart && tStart < rEnd) {
|
|
575
|
+
result.add(tok.id);
|
|
576
|
+
}
|
|
577
|
+
off = tEnd;
|
|
578
|
+
}
|
|
579
|
+
if (li < this.tokenLines.length - 1)
|
|
580
|
+
off += 1; // newline
|
|
581
|
+
}
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Resolve a CodeRange to a structural (fromLine, fromIdx)..(toLine, toIdx)
|
|
586
|
+
* token span. Snaps to whole tokens (any token that overlaps the range is
|
|
587
|
+
* included). Returns null if no tokens overlap.
|
|
588
|
+
*/
|
|
589
|
+
rangeToTokenSpan(codeRange) {
|
|
590
|
+
const lineLens = this.lineLengths();
|
|
591
|
+
if (lineLens.length === 0)
|
|
592
|
+
return null;
|
|
593
|
+
const { start: rStart, end: rEnd } = rangeToCharOffsets(codeRange, lineLens);
|
|
594
|
+
if (rEnd <= rStart)
|
|
595
|
+
return null;
|
|
596
|
+
let off = 0;
|
|
597
|
+
let fromLine = -1, fromIdx = -1, toLine = -1, toIdx = -1;
|
|
598
|
+
for (let li = 0; li < this.tokenLines.length; li++) {
|
|
599
|
+
const line = this.tokenLines[li];
|
|
600
|
+
for (let ti = 0; ti < line.tokens.length; ti++) {
|
|
601
|
+
const tok = line.tokens[ti];
|
|
602
|
+
const tStart = off;
|
|
603
|
+
const tEnd = off + tok.content.length;
|
|
604
|
+
if (tEnd > rStart && tStart < rEnd) {
|
|
605
|
+
if (fromLine === -1) {
|
|
606
|
+
fromLine = li;
|
|
607
|
+
fromIdx = ti;
|
|
608
|
+
}
|
|
609
|
+
toLine = li;
|
|
610
|
+
toIdx = ti + 1;
|
|
611
|
+
}
|
|
612
|
+
off = tEnd;
|
|
613
|
+
}
|
|
614
|
+
if (li < this.tokenLines.length - 1)
|
|
615
|
+
off += 1;
|
|
616
|
+
}
|
|
617
|
+
if (fromLine === -1)
|
|
618
|
+
return null;
|
|
619
|
+
return { fromLine, fromIdx, toLine, toIdx };
|
|
620
|
+
}
|
|
621
|
+
drawSelf(draw) {
|
|
622
|
+
const pad = this.padding;
|
|
623
|
+
const lineH = this.fontSize * this.lineHeight;
|
|
624
|
+
const stateById = this.resolveTokenStates();
|
|
625
|
+
const lineHeightScales = this.resolveLineHeightScales();
|
|
626
|
+
const lineWidths = this.tokenLines.map(line => {
|
|
627
|
+
let w = 0;
|
|
628
|
+
for (const tok of line.tokens) {
|
|
629
|
+
const ws = stateById.get(tok.id)?.widthScale ?? 1;
|
|
630
|
+
w += this.tokenAdvance(draw, tok.content) * ws;
|
|
631
|
+
}
|
|
632
|
+
return w;
|
|
633
|
+
});
|
|
634
|
+
const maxLineWidth = Math.max(...lineWidths, 0);
|
|
635
|
+
const gutter = this.gutterWidth(draw);
|
|
636
|
+
const blockWidth = maxLineWidth + gutter + pad.left + pad.right;
|
|
637
|
+
const lineHeights = this.tokenLines.map(line => lineH * (lineHeightScales.get(line.id) ?? 1));
|
|
638
|
+
const totalContentHeight = lineHeights.reduce((a, b) => a + b, 0);
|
|
639
|
+
const blockHeight = totalContentHeight + pad.top + pad.bottom;
|
|
640
|
+
const startX = -blockWidth / 2 + pad.left + gutter;
|
|
641
|
+
const startY = -blockHeight / 2 + pad.top;
|
|
642
|
+
// Gutter line-number color: a muted version of the standard text color.
|
|
643
|
+
const lineNumColor = [0.45, 0.5, 0.55, 1];
|
|
644
|
+
let yCursor = startY;
|
|
645
|
+
// Visible line counter — only fully-non-collapsed lines get a number.
|
|
646
|
+
// We number every modeled line (1-indexed) regardless of hScale so that
|
|
647
|
+
// animations that collapse a line still show its label fading out, which
|
|
648
|
+
// matches normal editor behaviour.
|
|
649
|
+
for (let lineIdx = 0; lineIdx < this.tokenLines.length; lineIdx++) {
|
|
650
|
+
const line = this.tokenLines[lineIdx];
|
|
651
|
+
const hScale = lineHeightScales.get(line.id) ?? 1;
|
|
652
|
+
// The renderer centers each single-token block on the (x, y) we pass
|
|
653
|
+
// (it shifts by -blockWidth/2, -blockHeight/2). So we anchor every
|
|
654
|
+
// token at the *center* of its cell, not its top-left/baseline:
|
|
655
|
+
// - y: the vertical middle of this line's slot. The slot is
|
|
656
|
+
// lineHeights[lineIdx] tall (= lineH * hScale); the full-size
|
|
657
|
+
// glyph block stays centered in it as the slot collapses.
|
|
658
|
+
// - x: the horizontal middle of each token (set per-token below).
|
|
659
|
+
// Passing lineHeight makes the token block's height deterministic
|
|
660
|
+
// (fontSize * lineHeight) rather than the font's natural metrics, so
|
|
661
|
+
// the vertical center lands exactly on the slot center.
|
|
662
|
+
const centerY = yCursor + lineHeights[lineIdx] / 2;
|
|
663
|
+
if (this.showLineNumbers) {
|
|
664
|
+
const label = String(lineIdx + 1);
|
|
665
|
+
const labelW = draw.measureText(label, this.fontSize, this.fontFamily);
|
|
666
|
+
// Right-align the number so its right edge sits one gutterGap to
|
|
667
|
+
// the left of where the code text begins.
|
|
668
|
+
const gx = -blockWidth / 2 + pad.left + (gutter - labelW) - this.gutterGap(draw);
|
|
669
|
+
// When a highlight is active, the number dims along with the code
|
|
670
|
+
// unless the WHOLE line is highlighted. We take the min opacity of
|
|
671
|
+
// the line's tokens: a fully-highlighted line is all 1s (bright),
|
|
672
|
+
// any dimmed token drags the number down. This also tweens for
|
|
673
|
+
// free during highlight()/resetHighlight() transitions.
|
|
674
|
+
const lineHighlightOpacity = this.lineHighlightOpacity(line, stateById);
|
|
675
|
+
draw.text({
|
|
676
|
+
text: label,
|
|
677
|
+
fontSize: this.fontSize,
|
|
678
|
+
fontFamily: this.fontFamily,
|
|
679
|
+
lineHeight: this.lineHeight,
|
|
680
|
+
x: gx + labelW / 2,
|
|
681
|
+
y: centerY,
|
|
682
|
+
align: 'left',
|
|
683
|
+
})
|
|
684
|
+
.fill([{ type: "color", color: lineNumColor, opacity: hScale * lineHighlightOpacity }]);
|
|
685
|
+
}
|
|
686
|
+
let x = startX;
|
|
687
|
+
for (const token of line.tokens) {
|
|
688
|
+
if (token.content.length === 0)
|
|
689
|
+
continue;
|
|
690
|
+
const color = token.color ? parseColor(token.color) : [0.82, 0.84, 0.86, 1];
|
|
691
|
+
const state = stateById.get(token.id);
|
|
692
|
+
const opacity = (state?.opacity ?? 1) * hScale;
|
|
693
|
+
const offsetY = state?.offsetY ?? 0;
|
|
694
|
+
const widthScale = state?.widthScale ?? 1;
|
|
695
|
+
// Advance width includes letterSpacing, matching what the
|
|
696
|
+
// renderer lays down when drawing with the same letterSpacing.
|
|
697
|
+
const tokWidth = this.tokenAdvance(draw, token.content);
|
|
698
|
+
if (opacity > 0 && widthScale > 0) {
|
|
699
|
+
draw.text({
|
|
700
|
+
text: token.content,
|
|
701
|
+
fontSize: this.fontSize,
|
|
702
|
+
fontFamily: this.fontFamily,
|
|
703
|
+
lineHeight: this.lineHeight,
|
|
704
|
+
letterSpacing: this.letterSpacing,
|
|
705
|
+
// Token is drawn at its natural width regardless of
|
|
706
|
+
// widthScale (widthScale only shrinks the advance), so
|
|
707
|
+
// its visual center is always x + tokWidth/2.
|
|
708
|
+
x: x + tokWidth / 2,
|
|
709
|
+
y: centerY + offsetY,
|
|
710
|
+
align: 'left',
|
|
711
|
+
})
|
|
712
|
+
.fill([{ type: "color", color, opacity }]);
|
|
713
|
+
}
|
|
714
|
+
x += tokWidth * widthScale;
|
|
715
|
+
}
|
|
716
|
+
yCursor += lineHeights[lineIdx];
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Is a highlight currently engaged — either a persistent dim is set, or a
|
|
720
|
+
// highlight()/resetHighlight() cross-fade is mid-flight? Intro/exit
|
|
721
|
+
// animations (append/insert/remove) are NOT highlights, so the line-number
|
|
722
|
+
// dimming below stays inert for them and only `hScale` affects the number.
|
|
723
|
+
isHighlightActive() {
|
|
724
|
+
if (this.highlightDimOpacity !== null)
|
|
725
|
+
return true;
|
|
726
|
+
return this.transitions.some(tr => tr.introducedIds.size === 0
|
|
727
|
+
&& tr.lineHeightScales.size === 0);
|
|
728
|
+
}
|
|
729
|
+
// Opacity multiplier for a line's number under the active highlight. The
|
|
730
|
+
// number stays bright only when the WHOLE line is highlighted, so we take
|
|
731
|
+
// the min token opacity on the line. Returns 1 (no dimming) when no
|
|
732
|
+
// highlight is active, so insert/remove intros don't drag the number down.
|
|
733
|
+
lineHighlightOpacity(line, stateById) {
|
|
734
|
+
if (!this.isHighlightActive())
|
|
735
|
+
return 1;
|
|
736
|
+
let min = 1;
|
|
737
|
+
for (const tok of line.tokens) {
|
|
738
|
+
if (tok.content.length === 0)
|
|
739
|
+
continue;
|
|
740
|
+
const op = stateById.get(tok.id)?.opacity ?? 1;
|
|
741
|
+
if (op < min)
|
|
742
|
+
min = op;
|
|
743
|
+
}
|
|
744
|
+
return min;
|
|
745
|
+
}
|
|
746
|
+
resolveTokenStates() {
|
|
747
|
+
const out = new Map();
|
|
748
|
+
if (this.highlightDimOpacity !== null) {
|
|
749
|
+
const dim = this.highlightDimOpacity;
|
|
750
|
+
for (const line of this.tokenLines) {
|
|
751
|
+
for (const tok of line.tokens) {
|
|
752
|
+
const isHighlighted = this.highlightedIds.has(tok.id);
|
|
753
|
+
out.set(tok.id, { opacity: isHighlighted ? 1 : dim, offsetY: 0, widthScale: 1 });
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
for (const tr of this.transitions) {
|
|
758
|
+
const t = tr.progress;
|
|
759
|
+
for (const at of tr.tokens) {
|
|
760
|
+
const op = sampleCurve(at.opacityKeys, at.opacity, t);
|
|
761
|
+
const oy = sampleCurve(at.offsetYKeys, at.offsetY, t);
|
|
762
|
+
const ws = sampleCurve(at.widthScaleKeys, at.widthScale, t);
|
|
763
|
+
const prev = out.get(at.id);
|
|
764
|
+
out.set(at.id, {
|
|
765
|
+
opacity: op,
|
|
766
|
+
offsetY: oy,
|
|
767
|
+
widthScale: prev ? prev.widthScale * ws : ws,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
773
|
+
resolveLineHeightScales() {
|
|
774
|
+
const out = new Map();
|
|
775
|
+
for (const tr of this.transitions) {
|
|
776
|
+
const t = tr.progress;
|
|
777
|
+
for (const [lineId, curve] of tr.lineHeightScales) {
|
|
778
|
+
const v = sampleCurve(curve.keys, curve.values, t);
|
|
779
|
+
out.set(lineId, (out.get(lineId) ?? 1) * v);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return out;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
__decorate([
|
|
786
|
+
property({ default: "" })
|
|
787
|
+
], Code.prototype, "code", void 0);
|
|
788
|
+
__decorate([
|
|
789
|
+
property({ default: "typescript" })
|
|
790
|
+
], Code.prototype, "language", void 0);
|
|
791
|
+
__decorate([
|
|
792
|
+
property({ default: "JetBrains Mono" })
|
|
793
|
+
], Code.prototype, "fontFamily", void 0);
|
|
794
|
+
__decorate([
|
|
795
|
+
property({ default: "github-dark" })
|
|
796
|
+
], Code.prototype, "theme", void 0);
|
|
797
|
+
__decorate([
|
|
798
|
+
property({ default: 16 })
|
|
799
|
+
], Code.prototype, "fontSize", void 0);
|
|
800
|
+
__decorate([
|
|
801
|
+
property({ default: 1.6 })
|
|
802
|
+
], Code.prototype, "lineHeight", void 0);
|
|
803
|
+
__decorate([
|
|
804
|
+
property({ default: 1.1 })
|
|
805
|
+
], Code.prototype, "letterSpacing", void 0);
|
|
806
|
+
__decorate([
|
|
807
|
+
property({ default: false })
|
|
808
|
+
], Code.prototype, "showLineNumbers", void 0);
|
|
809
|
+
__decorate([
|
|
810
|
+
property({ default: 4 })
|
|
811
|
+
], Code.prototype, "lineNumberGap", void 0);
|
|
812
|
+
__decorate([
|
|
813
|
+
property({ default: 0, mapper: resolvePadding, tween: lerpEdgeInset })
|
|
814
|
+
], Code.prototype, "padding", void 0);
|
|
815
|
+
function makeAnim(id, curves) {
|
|
816
|
+
const op = curves.opacity ?? { keys: [0, 1], values: [1, 1] };
|
|
817
|
+
const oy = curves.offsetY ?? { keys: [0, 1], values: [0, 0] };
|
|
818
|
+
const ws = curves.widthScale ?? { keys: [0, 1], values: [1, 1] };
|
|
819
|
+
return {
|
|
820
|
+
id,
|
|
821
|
+
opacity: op.values,
|
|
822
|
+
opacityKeys: op.keys,
|
|
823
|
+
offsetY: oy.values,
|
|
824
|
+
offsetYKeys: oy.keys,
|
|
825
|
+
widthScale: ws.values,
|
|
826
|
+
widthScaleKeys: ws.keys,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Split `tokens` at character offset `col` (relative to the joined content of
|
|
831
|
+
* the tokens). If `col` falls inside a token, that token is split into two new
|
|
832
|
+
* tokens with fresh ids (the original id is discarded — it didn't exist as a
|
|
833
|
+
* single unit on either side of the cut).
|
|
834
|
+
*/
|
|
835
|
+
function splitTokensAt(tokens, col) {
|
|
836
|
+
const before = [];
|
|
837
|
+
const after = [];
|
|
838
|
+
let off = 0;
|
|
839
|
+
for (const tok of tokens) {
|
|
840
|
+
const tStart = off;
|
|
841
|
+
const tEnd = off + tok.content.length;
|
|
842
|
+
if (tEnd <= col) {
|
|
843
|
+
before.push(tok);
|
|
844
|
+
}
|
|
845
|
+
else if (tStart >= col) {
|
|
846
|
+
after.push(tok);
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
const cut = col - tStart;
|
|
850
|
+
before.push(makeIdToken(tok.content.slice(0, cut), tok.color));
|
|
851
|
+
after.push(makeIdToken(tok.content.slice(cut), tok.color));
|
|
852
|
+
}
|
|
853
|
+
off = tEnd;
|
|
854
|
+
}
|
|
855
|
+
return { before, after };
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Convert [start, end) character offsets in joined source to a CodeRange.
|
|
859
|
+
* Lines/cols in the returned range are 1-indexed.
|
|
860
|
+
*/
|
|
861
|
+
function charOffsetsToRange(start, end, lineLengths) {
|
|
862
|
+
let off = 0;
|
|
863
|
+
let startLine = 1, startCol = 1, endLine = 1, endCol = 1;
|
|
864
|
+
let foundStart = false;
|
|
865
|
+
for (let li = 0; li < lineLengths.length; li++) {
|
|
866
|
+
const lineStart = off;
|
|
867
|
+
const lineEnd = off + lineLengths[li];
|
|
868
|
+
if (!foundStart && start >= lineStart && start <= lineEnd) {
|
|
869
|
+
startLine = li + 1;
|
|
870
|
+
startCol = (start - lineStart) + 1;
|
|
871
|
+
foundStart = true;
|
|
872
|
+
}
|
|
873
|
+
if (end >= lineStart && end <= lineEnd) {
|
|
874
|
+
endLine = li + 1;
|
|
875
|
+
endCol = (end - lineStart) + 1;
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
off = lineEnd + 1;
|
|
879
|
+
}
|
|
880
|
+
return { startLine, startCol, endLine, endCol };
|
|
881
|
+
}
|
|
882
|
+
//# sourceMappingURL=node.js.map
|