@codecanvas-ide/engine 0.0.0-stage → 0.2.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/dist/index.js ADDED
@@ -0,0 +1,3928 @@
1
+ // src/parse.ts
2
+ import ts from "typescript";
3
+ var PREVIEW_LINES = 6;
4
+ var CONTAINER_PREVIEW_LINES = 3;
5
+ var PREVIEW_WIDTH = 64;
6
+ var internals = /* @__PURE__ */ new WeakMap();
7
+ function parseFile(path, text) {
8
+ const m = parseStructure(path, text);
9
+ m.usages = computeUsages(m);
10
+ return m;
11
+ }
12
+ function parseStructure(path, text, prev) {
13
+ const kind = scriptKindOf(path);
14
+ const old = prev && prev.path === path ? internals.get(prev) : void 0;
15
+ let sf;
16
+ if (old && !old.reused) {
17
+ old.reused = true;
18
+ sf = ts.updateSourceFile(old.sf, text, changeRange(prev.text, text));
19
+ } else {
20
+ sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kind);
21
+ }
22
+ const b = new Builder(sf, text);
23
+ b.collectStatements(sf.statements, null, 0);
24
+ const syntax = sf.parseDiagnostics ?? [];
25
+ const model = {
26
+ path,
27
+ text,
28
+ roots: b.roots,
29
+ blocks: b.byId,
30
+ usages: [],
31
+ syntaxErrors: syntax.length,
32
+ firstErrorLine: -1,
33
+ diagnostics: [],
34
+ indentUnit: detectIndentUnit(text)
35
+ };
36
+ model.diagnostics = syntax.map((d) => toCodeDiagnostic(model, sf, d, "syntax"));
37
+ model.firstErrorLine = model.diagnostics[0]?.line ?? -1;
38
+ internals.set(model, { sf, builder: b, reused: false });
39
+ return model;
40
+ }
41
+ function blockAt(model, offset) {
42
+ let found = null;
43
+ let candidates = model.roots;
44
+ for (; ; ) {
45
+ const containing = candidates.find((b) => offset >= b.start && offset <= b.end);
46
+ if (!containing) return found;
47
+ found = containing;
48
+ candidates = containing.children;
49
+ }
50
+ }
51
+ function toCodeDiagnostic(model, sf, d, source) {
52
+ const start = d.start ?? 0;
53
+ return {
54
+ start,
55
+ length: d.length ?? 0,
56
+ line: sf.getLineAndCharacterOfPosition(Math.min(start, sf.text.length)).line + 1,
57
+ message: ts.flattenDiagnosticMessageText(d.messageText, "\n"),
58
+ source,
59
+ severity: d.category === ts.DiagnosticCategory.Error ? "error" : "warning",
60
+ blockId: blockAt(model, start)?.id ?? null
61
+ };
62
+ }
63
+ function builderOf(model) {
64
+ return internals.get(model)?.builder;
65
+ }
66
+ function computeUsages(model) {
67
+ const st = internals.get(model);
68
+ if (!st) return [];
69
+ if (st.reused) {
70
+ return parseFile(model.path, model.text).usages;
71
+ }
72
+ return st.builder.collectUsages(st.sf, createProgram(st.sf).getTypeChecker());
73
+ }
74
+ function changeRange(a, b) {
75
+ let s = 0;
76
+ const n = Math.min(a.length, b.length);
77
+ while (s < n && a.charCodeAt(s) === b.charCodeAt(s)) s++;
78
+ let e = 0;
79
+ while (e < n - s && a.charCodeAt(a.length - 1 - e) === b.charCodeAt(b.length - 1 - e)) e++;
80
+ return ts.createTextChangeRange(ts.createTextSpan(s, a.length - e - s), b.length - e - s);
81
+ }
82
+ function commonJsExportedFunction(s) {
83
+ if (!ts.isExpressionStatement(s) || !ts.isBinaryExpression(s.expression) || s.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null;
84
+ const { left, right } = s.expression;
85
+ const fn = unwrap(right);
86
+ const isFunction = ts.isFunctionExpression(fn) || ts.isArrowFunction(fn);
87
+ if (!isFunction || !ts.isPropertyAccessExpression(left) || !ts.isIdentifier(left.name)) return null;
88
+ const target = left.expression.getText();
89
+ const isExportsObject = target === "exports" || target === "module.exports";
90
+ return isExportsObject ? { assignment: s.expression, exportedName: left.name, fn } : null;
91
+ }
92
+ function scriptKindOf(path) {
93
+ if (/\.[cm]?jsx?$/.test(path)) return path.endsWith("x") ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
94
+ return path.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
95
+ }
96
+ function createProgram(sf) {
97
+ const options = {
98
+ noLib: true,
99
+ noResolve: true,
100
+ allowJs: true,
101
+ target: ts.ScriptTarget.Latest,
102
+ jsx: ts.JsxEmit.Preserve,
103
+ types: []
104
+ };
105
+ const host = {
106
+ getSourceFile: (name) => name === sf.fileName ? sf : void 0,
107
+ getDefaultLibFileName: () => "lib.d.ts",
108
+ writeFile: () => {
109
+ },
110
+ getCurrentDirectory: () => "/",
111
+ getCanonicalFileName: (f) => f,
112
+ useCaseSensitiveFileNames: () => true,
113
+ getNewLine: () => "\n",
114
+ fileExists: (f) => f === sf.fileName,
115
+ readFile: () => void 0,
116
+ getDirectories: () => []
117
+ };
118
+ return ts.createProgram({ rootNames: [sf.fileName], options, host });
119
+ }
120
+ function detectIndentUnit(text) {
121
+ const m = /\n(\t+|[ ]{2,8})\S/.exec(text);
122
+ if (!m) return " ";
123
+ return m[1].startsWith(" ") ? " " : m[1].length >= 4 && m[1].length % 4 === 0 ? " " : " ";
124
+ }
125
+ var Builder = class {
126
+ constructor(sf, text) {
127
+ this.sf = sf;
128
+ this.text = text;
129
+ }
130
+ sf;
131
+ text;
132
+ roots = [];
133
+ byId = /* @__PURE__ */ new Map();
134
+ /**
135
+ * Declaration node → block id, keyed by node position (see `nodeKey`) so a syntax tree parsed
136
+ * elsewhere from the same text (the project's language service) maps to the same blocks.
137
+ */
138
+ declToBlock = /* @__PURE__ */ new Map();
139
+ /** Range node → block, to find which block contains a usage. Keyed like `declToBlock`. */
140
+ rangeToBlock = /* @__PURE__ */ new Map();
141
+ /** Every name a usage could resolve to. Other identifiers skip the (slow) checker. */
142
+ names = /* @__PURE__ */ new Set();
143
+ // ---------- structure ----------
144
+ collectStatements(stmts, parent, depth, declarationsOnly = false) {
145
+ let group = [];
146
+ const flushGroup = () => {
147
+ if (!group.length) return;
148
+ const nodes = group;
149
+ group = [];
150
+ const first = nodes[0];
151
+ const firstLine = this.text.slice(first.getStart(this.sf), first.end).split("\n")[0].trim();
152
+ this.add(
153
+ {
154
+ kind: "statement",
155
+ key: "stmt:" + firstLine.slice(0, 40),
156
+ name: truncate(firstLine, 48),
157
+ detail: nodes.length > 1 ? `+${nodes.length - 1} more` : "",
158
+ tags: [],
159
+ rangeNodes: nodes,
160
+ declNodes: [],
161
+ previewRange: [nodes[0].getStart(this.sf), nodes[nodes.length - 1].end],
162
+ insert: null,
163
+ insertAt: -1
164
+ },
165
+ parent,
166
+ depth
167
+ );
168
+ };
169
+ for (let i = 0; i < stmts.length; i++) {
170
+ const s = stmts[i];
171
+ if (ts.isFunctionDeclaration(s)) {
172
+ flushGroup();
173
+ const overloads = [s];
174
+ while (!overloads[overloads.length - 1].body) {
175
+ const next = stmts[i + 1];
176
+ if (next && ts.isFunctionDeclaration(next) && next.name?.text === s.name?.text) {
177
+ overloads.push(next);
178
+ i++;
179
+ } else break;
180
+ }
181
+ this.addFunctionLike(overloads, s.name?.text ?? "default", parent, depth);
182
+ } else if (ts.isClassDeclaration(s)) {
183
+ flushGroup();
184
+ this.addClass(s, s, s.name?.text ?? "default", parent, depth);
185
+ } else if (ts.isVariableStatement(s)) {
186
+ flushGroup();
187
+ this.addVariableStatement(s, parent, depth, declarationsOnly);
188
+ } else if (commonJsExportedFunction(s)) {
189
+ flushGroup();
190
+ const { assignment, exportedName, fn } = commonJsExportedFunction(s);
191
+ const declNodes = [s, assignment, assignment.left];
192
+ this.addFunctionLike([fn], exportedName.text, parent, depth, { rangeNodes: [s], declNodes, extraTags: ["export"], nameNode: exportedName });
193
+ } else if (declarationsOnly) {
194
+ continue;
195
+ } else if (ts.isImportDeclaration(s) || ts.isImportEqualsDeclaration(s)) {
196
+ flushGroup();
197
+ this.addImport(s, parent, depth);
198
+ } else if (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) || ts.isEnumDeclaration(s)) {
199
+ flushGroup();
200
+ const keyword = ts.isInterfaceDeclaration(s) ? "interface" : ts.isEnumDeclaration(s) ? "enum" : "type";
201
+ const typeParams = "typeParameters" in s && s.typeParameters ? `<${s.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
202
+ const bodyStart = ts.isTypeAliasDeclaration(s) ? s.type.getStart(this.sf) : this.text.indexOf("{", s.name.end);
203
+ this.add(
204
+ {
205
+ kind: "type",
206
+ key: s.name.text,
207
+ name: s.name.text,
208
+ detail: typeParams,
209
+ tags: [...modifierTags(s), keyword],
210
+ rangeNodes: [s],
211
+ declNodes: [s],
212
+ nameNode: s.name,
213
+ previewRange: bodyStart >= 0 ? [bodyStart, s.end] : null,
214
+ insert: null,
215
+ insertAt: -1
216
+ },
217
+ parent,
218
+ depth
219
+ );
220
+ } else if (ts.isModuleDeclaration(s) && s.body && ts.isModuleBlock(s.body)) {
221
+ flushGroup();
222
+ const body = s.body;
223
+ this.add(
224
+ {
225
+ kind: "namespace",
226
+ key: s.name.getText(),
227
+ name: s.name.getText(),
228
+ detail: "",
229
+ tags: [...modifierTags(s), "namespace"],
230
+ rangeNodes: [s],
231
+ declNodes: [s],
232
+ nameNode: s.name,
233
+ previewRange: [body.getStart(this.sf) + 1, body.end - 1],
234
+ insert: "body",
235
+ insertAt: body.end - 1,
236
+ children: (blk) => this.collectStatements(body.statements, blk, depth + 1)
237
+ },
238
+ parent,
239
+ depth
240
+ );
241
+ } else {
242
+ group.push(s);
243
+ }
244
+ }
245
+ flushGroup();
246
+ }
247
+ addImport(s, parent, depth) {
248
+ let spec;
249
+ let names = [];
250
+ if (ts.isImportDeclaration(s)) {
251
+ spec = s.moduleSpecifier.text;
252
+ const clause = s.importClause;
253
+ if (clause?.name) names.push(clause.name.text);
254
+ const nb = clause?.namedBindings;
255
+ if (nb && ts.isNamespaceImport(nb)) names.push("* as " + nb.name.text);
256
+ if (nb && ts.isNamedImports(nb)) names.push(...nb.elements.map((e) => e.name.text));
257
+ if (!clause) names = ["(side effect)"];
258
+ } else {
259
+ spec = s.name.text;
260
+ names = [s.moduleReference.getText()];
261
+ this.names.add(s.name.text);
262
+ }
263
+ for (const n of names) this.names.add(n.replace("* as ", ""));
264
+ this.add(
265
+ {
266
+ kind: "import",
267
+ key: "import:" + spec,
268
+ name: spec,
269
+ detail: "",
270
+ tags: ts.isImportDeclaration(s) && s.importClause?.isTypeOnly ? ["type"] : [],
271
+ rangeNodes: [s],
272
+ declNodes: [s],
273
+ previewRange: null,
274
+ insert: null,
275
+ insertAt: -1
276
+ },
277
+ parent,
278
+ depth,
279
+ wrapNames(names)
280
+ );
281
+ }
282
+ addFunctionLike(nodes, name, parent, depth, opts = {}) {
283
+ const impl = nodes[nodes.length - 1];
284
+ const sig = nodes.find((n) => !!n.body) ?? impl;
285
+ const body = impl.body;
286
+ let previewRange = null;
287
+ let insert = null;
288
+ let insertAt = -1;
289
+ if (body && ts.isBlock(body)) {
290
+ previewRange = [body.getStart(this.sf) + 1, body.end - 1];
291
+ insert = "body";
292
+ const last = body.statements[body.statements.length - 1];
293
+ insertAt = last && ts.isReturnStatement(last) ? this.lineStartOf(last.getStart(this.sf)) : body.end - 1;
294
+ } else if (body) {
295
+ previewRange = [body.getStart(this.sf), body.end];
296
+ }
297
+ const typeParams = sig.typeParameters?.length ? `<${sig.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
298
+ const ret = sig.type ? ": " + sig.type.getText() : "";
299
+ const detail = squash(`${typeParams}(${sig.parameters.map((p) => p.getText()).join(", ")})${ret}`);
300
+ const tags = [...opts.extraTags ?? [], ...modifierTags(nodes[0])];
301
+ if (nodes.length > 1) tags.push(`${nodes.length - 1} overload${nodes.length > 2 ? "s" : ""}`);
302
+ if (impl.asteriskToken) tags.push("generator");
303
+ this.add(
304
+ {
305
+ kind: "function",
306
+ key: opts.key ?? name,
307
+ name,
308
+ detail,
309
+ tags,
310
+ rangeNodes: opts.rangeNodes ?? nodes,
311
+ declNodes: opts.declNodes ?? nodes,
312
+ nameNode: opts.nameNode ?? impl.name,
313
+ previewRange,
314
+ insert,
315
+ insertAt,
316
+ children: body && ts.isBlock(body) ? (blk) => this.collectStatements(body.statements, blk, depth + 1, true) : void 0
317
+ },
318
+ parent,
319
+ depth
320
+ );
321
+ }
322
+ addClass(cls, rangeNode, name, parent, depth, declNodes = [cls], extraTags = []) {
323
+ const heritage = (cls.heritageClauses ?? []).map((h) => h.getText()).join(" ");
324
+ const openBrace = this.text.indexOf("{", (cls.heritageClauses?.at(-1) ?? cls.typeParameters?.at(-1) ?? cls.name ?? cls).end);
325
+ const typeParams = cls.typeParameters?.length ? `<${cls.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
326
+ this.add(
327
+ {
328
+ kind: "class",
329
+ key: name,
330
+ name,
331
+ detail: squash(typeParams + (heritage ? " " + heritage : "")),
332
+ tags: [...extraTags, ...modifierTags(rangeNode === cls ? cls : rangeNode)],
333
+ rangeNodes: [rangeNode],
334
+ declNodes,
335
+ nameNode: cls.name,
336
+ previewRange: openBrace >= 0 ? [openBrace + 1, cls.end - 1] : null,
337
+ insert: "class",
338
+ insertAt: cls.end - 1,
339
+ children: (blk) => this.collectMembers(cls.members, blk, depth + 1)
340
+ },
341
+ parent,
342
+ depth
343
+ );
344
+ }
345
+ collectMembers(members, parent, depth) {
346
+ for (let i = 0; i < members.length; i++) {
347
+ const m = members[i];
348
+ if (ts.isMethodDeclaration(m) || ts.isConstructorDeclaration(m)) {
349
+ const name = ts.isConstructorDeclaration(m) ? "constructor" : m.name.getText();
350
+ const overloads = [m];
351
+ while (!overloads[overloads.length - 1].body) {
352
+ const next = members[i + 1];
353
+ const nextName = next && (ts.isConstructorDeclaration(next) ? "constructor" : ts.isMethodDeclaration(next) ? next.name.getText() : null);
354
+ if (nextName === name) {
355
+ overloads.push(next);
356
+ i++;
357
+ } else break;
358
+ }
359
+ this.addFunctionLike(overloads, name, parent, depth);
360
+ } else if (ts.isGetAccessor(m) || ts.isSetAccessor(m)) {
361
+ const name = m.name.getText();
362
+ const word = ts.isGetAccessor(m) ? "get" : "set";
363
+ this.addFunctionLike([m], name, parent, depth, { key: `${word} ${name}`, extraTags: [word] });
364
+ } else if (ts.isPropertyDeclaration(m)) {
365
+ const name = m.name.getText();
366
+ const init = m.initializer && unwrap(m.initializer);
367
+ if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
368
+ this.addFunctionLike([init], name, parent, depth, { rangeNodes: [m], declNodes: [m], extraTags: modifierTags(m), nameNode: m.name });
369
+ } else {
370
+ this.addVariable(m, m, name, m.type ? ": " + m.type.getText() : "", modifierTags(m), parent, depth, m.initializer);
371
+ }
372
+ }
373
+ }
374
+ }
375
+ addVariableStatement(s, parent, depth, functionsOnly) {
376
+ const decls = s.declarationList.declarations;
377
+ const keyword = s.declarationList.flags & ts.NodeFlags.Const ? "const" : s.declarationList.flags & ts.NodeFlags.Let ? "let" : "var";
378
+ for (const d of decls) {
379
+ const rangeNode = decls.length === 1 ? s : d;
380
+ const name = d.name.getText();
381
+ const tags = [...modifierTags(s), keyword];
382
+ const init = d.initializer && unwrap(d.initializer);
383
+ if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
384
+ this.addFunctionLike([init], name, parent, depth, { rangeNodes: [rangeNode], declNodes: [d], extraTags: tags, nameNode: d.name });
385
+ } else if (init && ts.isClassExpression(init)) {
386
+ this.addClass(init, rangeNode, name, parent, depth, [d, init], tags);
387
+ } else if (!functionsOnly) {
388
+ this.addVariable(d, rangeNode, name, d.type ? ": " + d.type.getText() : "", tags, parent, depth, d.initializer);
389
+ }
390
+ }
391
+ }
392
+ addVariable(decl, rangeNode, name, type, tags, parent, depth, initializer) {
393
+ const init = initializer && unwrap(initializer);
394
+ const objectChildren = init && ts.isObjectLiteralExpression(init) ? init.properties.filter(
395
+ (p) => ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p) && (ts.isArrowFunction(unwrap(p.initializer)) || ts.isFunctionExpression(unwrap(p.initializer)))
396
+ ) : [];
397
+ this.add(
398
+ {
399
+ kind: "variable",
400
+ key: name,
401
+ name,
402
+ detail: squash(type),
403
+ tags,
404
+ rangeNodes: [rangeNode],
405
+ declNodes: [decl],
406
+ nameNode: decl.name,
407
+ previewRange: initializer ? [initializer.getStart(this.sf), initializer.end] : null,
408
+ insert: null,
409
+ insertAt: -1,
410
+ children: objectChildren.length ? (blk) => {
411
+ for (const p of objectChildren) {
412
+ const pname = p.name.getText();
413
+ if (ts.isMethodDeclaration(p)) this.addFunctionLike([p], pname, blk, depth + 1);
414
+ else {
415
+ const fn = unwrap(p.initializer);
416
+ this.addFunctionLike([fn], pname, blk, depth + 1, { rangeNodes: [p], declNodes: [p], nameNode: p.name });
417
+ }
418
+ }
419
+ } : void 0
420
+ },
421
+ parent,
422
+ depth
423
+ );
424
+ }
425
+ add(p, parent, depth, presetPreview) {
426
+ const first = p.rangeNodes[0];
427
+ const last = p.rangeNodes[p.rangeNodes.length - 1];
428
+ const start = this.startWithComments(first);
429
+ const siblings = parent ? parent.children : this.roots;
430
+ let key = p.key;
431
+ if (siblings.some((s) => s.key === key)) {
432
+ let n = 2;
433
+ while (siblings.some((s) => s.key === `${p.key}#${n}`)) n++;
434
+ key = `${p.key}#${n}`;
435
+ }
436
+ const id = parent ? `${parent.id}/${key}` : key;
437
+ const block = {
438
+ id,
439
+ key,
440
+ kind: p.kind,
441
+ name: p.name,
442
+ detail: p.detail,
443
+ tags: p.tags,
444
+ start,
445
+ end: last.end,
446
+ nameStart: p.nameNode ? p.nameNode.getStart(this.sf) : -1,
447
+ preview: [],
448
+ previewMore: 0,
449
+ children: [],
450
+ parentId: parent?.id ?? null,
451
+ depth,
452
+ insert: p.insert,
453
+ insertAt: p.insertAt,
454
+ indent: this.indentAt(first.getStart(this.sf))
455
+ };
456
+ siblings.push(block);
457
+ this.byId.set(id, block);
458
+ this.names.add(p.name);
459
+ for (const n of p.rangeNodes) this.rangeToBlock.set(nodeKey(n), block);
460
+ for (const n of p.declNodes) this.declToBlock.set(nodeKey(n), id);
461
+ p.children?.(block);
462
+ if (presetPreview) {
463
+ block.preview = presetPreview;
464
+ } else if (p.previewRange) {
465
+ const max = block.children.length ? CONTAINER_PREVIEW_LINES : PREVIEW_LINES;
466
+ const { lines, more } = makePreview(this.text, p.previewRange, block.children, max);
467
+ block.preview = lines;
468
+ block.previewMore = more;
469
+ }
470
+ return block;
471
+ }
472
+ /** Include doc comments directly above the node (no blank line in between). */
473
+ startWithComments(node) {
474
+ let start = node.getStart(this.sf);
475
+ const comments = ts.getLeadingCommentRanges(this.text, node.pos) ?? [];
476
+ for (let i = comments.length - 1; i >= 0; i--) {
477
+ const gap = this.text.slice(comments[i].end, start);
478
+ if (/\S/.test(gap) || (gap.match(/\n/g)?.length ?? 0) > 1) break;
479
+ start = comments[i].pos;
480
+ }
481
+ return start;
482
+ }
483
+ lineStartOf(pos) {
484
+ return this.text.lastIndexOf("\n", pos - 1) + 1;
485
+ }
486
+ indentAt(pos) {
487
+ const ls = this.lineStartOf(pos);
488
+ return /^[ \t]*/.exec(this.text.slice(ls, pos))[0];
489
+ }
490
+ // ---------- usages ----------
491
+ blockIdForDeclaration(declaration) {
492
+ return this.declToBlock.get(nodeKey(declaration));
493
+ }
494
+ /**
495
+ * Link every name used inside a block to the block that declares it. `sf` is a tree of the same
496
+ * text (this builder's own, or the language service's when resolving across files).
497
+ */
498
+ collectUsages(sf, checker, otherFiles) {
499
+ const byPair = /* @__PURE__ */ new Map();
500
+ const visit = (node, current) => {
501
+ const blk = this.rangeToBlock.get(nodeKey(node));
502
+ if (blk) current = blk;
503
+ if (ts.isIdentifier(node) && current) this.recordUsage(node, current, checker, byPair, otherFiles);
504
+ ts.forEachChild(node, (c) => visit(c, current));
505
+ };
506
+ visit(sf, null);
507
+ return [...byPair.values()];
508
+ }
509
+ recordUsage(id, from, checker, out, otherFiles) {
510
+ const isKnownName = this.names.has(id.text) || !!otherFiles?.projectNames.has(id.text);
511
+ if (!isKnownName) return;
512
+ const parent = id.parent;
513
+ const isDeclarationName = !!parent && parent.name === id && !ts.isPropertyAccessExpression(parent) && !ts.isShorthandPropertyAssignment(parent);
514
+ if (isDeclarationName) return;
515
+ let sym;
516
+ try {
517
+ sym = parent && ts.isShorthandPropertyAssignment(parent) ? checker.getShorthandAssignmentValueSymbol(parent) : checker.getSymbolAtLocation(id);
518
+ } catch {
519
+ return;
520
+ }
521
+ if (!sym?.declarations) return;
522
+ const kind = usageKind(id);
523
+ let to;
524
+ let importBlockId;
525
+ for (const d of sym.declarations) {
526
+ to = this.declToBlock.get(nodeKey(d));
527
+ if (!to) {
528
+ let n = d;
529
+ while (n && !ts.isImportDeclaration(n) && !ts.isImportEqualsDeclaration(n) && !ts.isSourceFile(n)) n = n.parent;
530
+ if (n && !ts.isSourceFile(n)) importBlockId = to = this.declToBlock.get(nodeKey(n));
531
+ }
532
+ if (to) break;
533
+ }
534
+ if (otherFiles) {
535
+ const isImportedName = !!importBlockId && (sym.flags & ts.SymbolFlags.Alias) !== 0;
536
+ const target = isImportedName ? safeAliasedSymbol(checker, sym) : to ? void 0 : sym;
537
+ const declaredElsewhere = target?.declarations?.find((d) => d.getSourceFile() !== id.getSourceFile());
538
+ const external = declaredElsewhere && otherFiles.blockInOtherFile(declaredElsewhere);
539
+ if (external) addUsage(out, { from: from.id, to: external.id, toPath: external.path, via: importBlockId, kind, count: 1 });
540
+ }
541
+ if (!to || to === from.id) return;
542
+ if (from.id.startsWith(to + "/") || to.startsWith(from.id + "/")) return;
543
+ addUsage(out, { from: from.id, to, kind, count: 1 });
544
+ }
545
+ };
546
+ function addUsage(out, usage) {
547
+ const pairKey = `${usage.from}>${usage.toPath ?? ""}:${usage.to}`;
548
+ const existing = out.get(pairKey);
549
+ if (!existing) return void out.set(pairKey, usage);
550
+ existing.count++;
551
+ if (usage.kind === "call") existing.kind = "call";
552
+ }
553
+ function safeAliasedSymbol(checker, symbol) {
554
+ try {
555
+ return checker.getAliasedSymbol(symbol);
556
+ } catch {
557
+ return void 0;
558
+ }
559
+ }
560
+ function nodeKey(node) {
561
+ return `${node.kind}:${node.pos}:${node.end}`;
562
+ }
563
+ function usageKind(id) {
564
+ let n = id;
565
+ if (ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) n = n.parent;
566
+ const p = n.parent;
567
+ if ((ts.isCallExpression(p) || ts.isNewExpression(p)) && p.expression === n) return "call";
568
+ if (ts.isTaggedTemplateExpression(p) && p.tag === n) return "call";
569
+ if (ts.isJsxOpeningLikeElement(p) && p.tagName === n) return "call";
570
+ if (ts.isBinaryExpression(p) && p.left === n && p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts.SyntaxKind.LastAssignment) return "write";
571
+ if ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) return "write";
572
+ if (ts.isTypeReferenceNode(p) || ts.isExpressionWithTypeArguments(p) || ts.isTypeQueryNode(p) || ts.isQualifiedName(p)) return "type";
573
+ return "read";
574
+ }
575
+ function unwrap(e) {
576
+ while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e) || ts.isTypeAssertionExpression(e)) e = e.expression;
577
+ return e;
578
+ }
579
+ function modifierTags(n) {
580
+ const mods = ts.canHaveModifiers(n) ? ts.getModifiers(n) ?? [] : [];
581
+ return mods.map((m) => m.getText());
582
+ }
583
+ function squash(s) {
584
+ return s.replace(/\s+/g, " ").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/,\s*\)/g, ")").trim();
585
+ }
586
+ function truncate(s, n) {
587
+ return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
588
+ }
589
+ function wrapNames(names) {
590
+ const lines = [];
591
+ let cur = "";
592
+ for (const n of names) {
593
+ const piece = cur ? cur + ", " + n : n;
594
+ if (piece.length > 40 && cur) {
595
+ lines.push(cur + ",");
596
+ cur = n;
597
+ } else cur = piece;
598
+ }
599
+ if (cur) lines.push(cur);
600
+ return lines.slice(0, PREVIEW_LINES);
601
+ }
602
+ function codeWithoutChildren(text, range, children) {
603
+ let [a, b] = range;
604
+ let src = "";
605
+ const kids = children.filter((c) => c.start >= a && c.end <= b).sort((x, y) => x.start - y.start);
606
+ for (const k of kids) {
607
+ src += text.slice(a, k.start);
608
+ a = k.end;
609
+ }
610
+ return src + text.slice(a, b);
611
+ }
612
+ function makePreview(text, range, children, max) {
613
+ const src = codeWithoutChildren(text, range, children);
614
+ const kids = children.filter((c) => c.start >= range[0] && c.end <= range[1]);
615
+ let lines = src.split("\n").map((l) => l.replace(/\s+$/, ""));
616
+ if (kids.length) lines = lines.filter((l) => !/^[\s{}[\](),;]*$/.test(l) || !l.trim());
617
+ lines = lines.filter((l, i) => l.trim() || i > 0 && lines[i - 1].trim());
618
+ while (lines.length && !lines[0].trim()) lines.shift();
619
+ while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
620
+ const indents = lines.slice(lines.length > 1 && !/^\s/.test(lines[0]) ? 1 : 0).filter((l) => l.trim()).map((l) => /^[ \t]*/.exec(l)[0].length);
621
+ const minIndent = indents.length ? Math.min(...indents) : 0;
622
+ lines = lines.map((l) => (/^\s/.test(l) ? l.slice(minIndent) : l).replace(/\t/g, " "));
623
+ const more = Math.max(0, lines.length - max);
624
+ return { lines: lines.slice(0, max).map((l) => truncate(l, PREVIEW_WIDTH)), more };
625
+ }
626
+
627
+ // src/project.ts
628
+ import ts3 from "typescript";
629
+
630
+ // src/workspace.ts
631
+ import ts2 from "typescript";
632
+ function parseConfig(config, projectPaths) {
633
+ const configFileName = "/" + config.path;
634
+ const json = ts2.parseConfigFileTextToJson(configFileName, config.text);
635
+ if (json.error || !json.config) return null;
636
+ const readConfig = (name) => name === configFileName ? config.text : config.extendedFiles?.[name.slice(1)];
637
+ const host = {
638
+ useCaseSensitiveFileNames: true,
639
+ readDirectory: (rootDir, extensions, excludes, includes) => matchingPaths(projectPaths, rootDir, extensions, excludes ?? [], includes ?? ["**/*"]),
640
+ fileExists: (name) => readConfig(name) !== void 0,
641
+ readFile: readConfig
642
+ };
643
+ const folder = config.path.slice(0, config.path.lastIndexOf("/") + 1);
644
+ const parsed = ts2.parseJsonConfigFileContent(json.config, host, "/" + folder, void 0, configFileName);
645
+ const included = new Set(parsed.fileNames.map((name) => name.slice(1)));
646
+ return { path: config.path, folder, options: parsed.options, includes: (projectPath) => included.has(projectPath) };
647
+ }
648
+ function configForFile(projectPath, configs) {
649
+ const above = configs.filter((config) => projectPath.startsWith(config.folder));
650
+ const nearestFirst = [...above].sort((a, b) => b.folder.length - a.folder.length);
651
+ return nearestFirst.find((config) => config.includes(projectPath)) ?? nearestFirst[0] ?? null;
652
+ }
653
+ function workspacePackagePaths(packageFiles, hasFile) {
654
+ const paths = {};
655
+ for (const [packagePath, text] of Object.entries(packageFiles)) {
656
+ const manifest = parsePackageJson(text);
657
+ if (!manifest?.name) continue;
658
+ const folder = packagePath.slice(0, packagePath.lastIndexOf("/") + 1);
659
+ const entry = packageEntries(manifest).flatMap((named) => [named, ...sourceOfBuildOutput(named)]).concat(["src/index.ts", "src/index.tsx", "index.ts", "index.tsx"]).map((relative) => folder + relative.replace(/^\.\//, "")).find(hasFile);
660
+ if (entry) paths[manifest.name] = ["/" + entry];
661
+ paths[manifest.name + "/*"] = ["/" + folder + "src/*", "/" + folder + "*"];
662
+ }
663
+ return paths;
664
+ }
665
+ function parsePackageJson(text) {
666
+ try {
667
+ const value = JSON.parse(text);
668
+ return value && typeof value === "object" ? value : null;
669
+ } catch {
670
+ return null;
671
+ }
672
+ }
673
+ function packageEntries(manifest) {
674
+ const rootExport = manifest.exports && typeof manifest.exports === "object" && "." in manifest.exports ? manifest.exports["."] : manifest.exports;
675
+ return [manifest.types, manifest.typings, ...exportTargets(rootExport), manifest.module, manifest.main].filter((entry) => typeof entry === "string");
676
+ }
677
+ function exportTargets(target) {
678
+ if (typeof target === "string") return [target];
679
+ if (Array.isArray(target)) return target.flatMap(exportTargets);
680
+ if (target && typeof target === "object") return Object.values(target).flatMap(exportTargets);
681
+ return [];
682
+ }
683
+ function sourceOfBuildOutput(entry) {
684
+ const isBuildOutput = /\.(d\.ts|d\.mts|js|mjs|cjs|jsx)$/.test(entry);
685
+ if (!isBuildOutput) return [];
686
+ const withoutExtension = entry.replace(/\.(d\.ts|d\.mts|js|mjs|cjs|jsx)$/, "");
687
+ const inSourceFolder = withoutExtension.replace(/(^|\/)(dist|lib|build|out|esm|cjs)\//, "$1src/");
688
+ return [inSourceFolder, withoutExtension].flatMap((base) => [base + ".ts", base + ".tsx"]);
689
+ }
690
+ function matchingPaths(projectPaths, rootDir, extensions, excludes, includes) {
691
+ const base = rootDir.replace(/^\//, "").replace(/\/?$/, "/").replace(/^\/$/, "");
692
+ const includePatterns = includes.map((spec) => configPattern(base, spec, "files"));
693
+ const excludePatterns = excludes.map((spec) => configPattern(base, spec, "filesAndFolders"));
694
+ return projectPaths.filter((path) => path.startsWith(base) && extensions.some((extension) => path.endsWith(extension))).filter((path) => includePatterns.some((pattern) => pattern.test(path)) && !excludePatterns.some((pattern) => pattern.test(path))).map((path) => "/" + path);
695
+ }
696
+ function configPattern(base, spec, matching) {
697
+ const absolute = spec.startsWith("/") ? spec.slice(1) : normalizePath(base + spec.replace(/^\.\//, ""));
698
+ const lastPart = absolute.slice(absolute.lastIndexOf("/") + 1);
699
+ const isFolder = !/[*?]/.test(lastPart) && !/\.[^.]+$/.test(lastPart);
700
+ const pattern = isFolder ? absolute.replace(/\/?$/, "/**/*") : absolute;
701
+ let source = "";
702
+ for (let index = 0; index < pattern.length; index++) {
703
+ const char = pattern[index];
704
+ if (pattern.startsWith("**/", index)) {
705
+ source += "(?:[^/]+/)*";
706
+ index += 2;
707
+ } else if (char === "*") source += "[^/]*";
708
+ else if (char === "?") source += "[^/]";
709
+ else source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
710
+ }
711
+ return new RegExp(`^${source}${matching === "filesAndFolders" ? "(?:/.*)?" : ""}$`);
712
+ }
713
+ function normalizePath(path) {
714
+ const parts = [];
715
+ for (const part of path.split("/")) {
716
+ if (part === "..") parts.pop();
717
+ else if (part !== "." && part !== "") parts.push(part);
718
+ }
719
+ return parts.join("/");
720
+ }
721
+
722
+ // src/project.ts
723
+ var LIB_DIR = "/__lib__/";
724
+ var DEFAULT_LIB = ["lib.es2022.d.ts", "lib.dom.d.ts"];
725
+ var NO_CONFIG = "";
726
+ var REQUIRE_CALL = /\brequire\s*\(\s*(['"])([^'"]+)\1\s*\)/;
727
+ var RE_EXPORT = /^export\s+(?:type\s+)?(?:\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s*from\s*(['"])([^'"]+)\1/;
728
+ var Project = class {
729
+ constructor(files = {}, projectOptions = {}) {
730
+ this.projectOptions = projectOptions;
731
+ this.configure(projectOptions);
732
+ for (const [path, text] of Object.entries(files)) this.setFile(path, text);
733
+ }
734
+ projectOptions;
735
+ files = /* @__PURE__ */ new Map();
736
+ projectVersion = 0;
737
+ libFiles = {};
738
+ hasLib = false;
739
+ libOptions = {};
740
+ projectNamesCache = null;
741
+ workspace = null;
742
+ configPrograms = null;
743
+ /** Language services by config path, created when a file of that config is first resolved. */
744
+ services = /* @__PURE__ */ new Map();
745
+ /** Shared by the services, so a file several programs include is parsed once. */
746
+ documentRegistry = ts3.createDocumentRegistry(true, "/");
747
+ /** Changes when files are added or removed, or the settings change: where imports point may change then. */
748
+ fileSetVersion = 0;
749
+ /** Load the standard library later, e.g. once it has been downloaded. Type errors show from then on. */
750
+ setLibFiles(libFiles) {
751
+ this.projectOptions = { ...this.projectOptions, libFiles };
752
+ this.reconfigure();
753
+ }
754
+ /** Use the folder's tsconfig.json (or none, with null). */
755
+ setConfigFile(config) {
756
+ this.setWorkspace(config ? { configFiles: [config] } : null);
757
+ }
758
+ /** Use the folder's config files and workspace packages (or none, with null). */
759
+ setWorkspace(workspace) {
760
+ this.workspace = workspace;
761
+ this.reconfigure();
762
+ }
763
+ reconfigure() {
764
+ for (const service of this.services.values()) service.dispose();
765
+ this.services.clear();
766
+ this.configure(this.projectOptions);
767
+ this.projectVersion++;
768
+ this.fileSetVersion++;
769
+ }
770
+ configure(options) {
771
+ this.libFiles = options.libFiles ?? {};
772
+ const libEntries = (options.lib ?? DEFAULT_LIB).filter((name) => name in this.libFiles);
773
+ this.hasLib = libEntries.length > 0;
774
+ this.libOptions = this.hasLib ? { lib: libEntries, noLib: false } : { noLib: true };
775
+ }
776
+ /** Options for a program: the defaults, then the config's, then `ProjectOptions.compilerOptions`. */
777
+ compilerOptions(configOptions, packagePaths) {
778
+ const paths = { ...packagePaths, ...configOptions.paths };
779
+ return {
780
+ target: ts3.ScriptTarget.ES2022,
781
+ module: ts3.ModuleKind.ESNext,
782
+ moduleResolution: ts3.ModuleResolutionKind.Bundler,
783
+ strict: true,
784
+ jsx: ts3.JsxEmit.Preserve,
785
+ skipLibCheck: true,
786
+ allowImportingTsExtensions: true,
787
+ ...configOptions,
788
+ ...Object.keys(paths).length ? { paths } : {},
789
+ ...this.projectOptions.compilerOptions,
790
+ // JavaScript files are part of the project even where a tsconfig.json leaves `allowJs` off.
791
+ allowJs: true,
792
+ // The project loads no @types packages, its standard library is the one given here, and it never emits.
793
+ types: [],
794
+ noEmit: true,
795
+ ...this.libOptions
796
+ };
797
+ }
798
+ /** Which config each file uses, and the options of each. Worked out again when files are added or removed. */
799
+ programs() {
800
+ if (this.configPrograms?.fileSetVersion === this.fileSetVersion) return this.configPrograms;
801
+ const projectPaths = [...this.files.keys()];
802
+ const packagePaths = workspacePackagePaths(this.workspace?.packageFiles ?? {}, (path) => this.files.has(path));
803
+ const configs = (this.workspace?.configFiles ?? []).map((config) => parseConfig(config, projectPaths)).filter((config) => !!config);
804
+ const optionsByConfig = /* @__PURE__ */ new Map([[NO_CONFIG, this.compilerOptions({}, packagePaths)]]);
805
+ for (const config of configs) optionsByConfig.set(config.path, this.compilerOptions(config.options, packagePaths));
806
+ const pathsByConfig = /* @__PURE__ */ new Map();
807
+ const configOfPath = /* @__PURE__ */ new Map();
808
+ for (const path of projectPaths) {
809
+ const configPath = configForFile(path, configs)?.path ?? NO_CONFIG;
810
+ configOfPath.set(path, configPath);
811
+ pathsByConfig.set(configPath, [...pathsByConfig.get(configPath) ?? [], path]);
812
+ }
813
+ this.configPrograms = { fileSetVersion: this.fileSetVersion, optionsByConfig, pathsByConfig, configOfPath };
814
+ return this.configPrograms;
815
+ }
816
+ configOf(path) {
817
+ return this.programs().configOfPath.get(path) ?? NO_CONFIG;
818
+ }
819
+ /** The options a file's imports resolve with: its own config's, whichever program includes it. */
820
+ optionsOfFile(path) {
821
+ const { optionsByConfig } = this.programs();
822
+ return optionsByConfig.get(path ? this.configOf(path) : NO_CONFIG) ?? optionsByConfig.get(NO_CONFIG);
823
+ }
824
+ serviceOf(path) {
825
+ return this.serviceOfConfig(this.configOf(path));
826
+ }
827
+ serviceOfConfig(configPath) {
828
+ let service = this.services.get(configPath);
829
+ if (!service) {
830
+ service = ts3.createLanguageService(this.createHost(configPath), this.documentRegistry);
831
+ this.services.set(configPath, service);
832
+ }
833
+ return service;
834
+ }
835
+ // ---------------------------------------------------------------- files (fast path)
836
+ /** Add or update a file. Parses only this file, incrementally from its previous version. */
837
+ setFile(path, text) {
838
+ const existing = this.files.get(path);
839
+ if (existing && existing.text === text) return existing.model;
840
+ const model = parseStructure(path, text, existing?.model);
841
+ this.files.set(path, { text, version: (existing?.version ?? 0) + 1, model });
842
+ this.projectVersion++;
843
+ if (!existing) this.fileSetVersion++;
844
+ return model;
845
+ }
846
+ removeFile(path) {
847
+ if (!this.files.delete(path)) return;
848
+ this.projectVersion++;
849
+ this.fileSetVersion++;
850
+ }
851
+ hasFile(path) {
852
+ return this.files.has(path);
853
+ }
854
+ paths() {
855
+ return [...this.files.keys()];
856
+ }
857
+ getModel(path) {
858
+ return this.files.get(path)?.model;
859
+ }
860
+ getText(path) {
861
+ return this.files.get(path)?.text;
862
+ }
863
+ // ---------------------------------------------------------------- resolving (when typing pauses)
864
+ resolve(path, { diagnostics = true } = {}) {
865
+ return Promise.resolve({ usages: this.usages(path), incoming: this.usagesInto(path), diagnostics: diagnostics ? this.diagnostics(path) : [] });
866
+ }
867
+ /** Usage links from this file: within the file, and to blocks in other files (`toPath`). */
868
+ usages(path) {
869
+ const file = this.files.get(path);
870
+ if (!file) return [];
871
+ if (file.usages && file.usagesProjectVersion === this.projectVersion) return file.usages;
872
+ const program = this.serviceOf(path).getProgram();
873
+ const sf = program?.getSourceFile(toFileName(path));
874
+ const builder = builderOf(file.model);
875
+ if (!program || !sf || !builder || sf.text !== file.text) return [];
876
+ file.usages = builder.collectUsages(sf, program.getTypeChecker(), this.otherFileResolver());
877
+ file.usagesProjectVersion = this.projectVersion;
878
+ return file.usages;
879
+ }
880
+ /** Usages in other files that point at blocks in this file. */
881
+ usagesInto(path) {
882
+ const incoming = [];
883
+ for (const otherPath of this.importersThroughReExports(path)) {
884
+ for (const usage of this.usages(otherPath)) {
885
+ if (usage.toPath === path) incoming.push({ ...usage, fromPath: otherPath });
886
+ }
887
+ }
888
+ return incoming;
889
+ }
890
+ /** Syntax errors, plus type errors when the standard library is loaded and the syntax is valid. */
891
+ diagnostics(path) {
892
+ const file = this.files.get(path);
893
+ if (!file) return [];
894
+ const hasSyntaxErrors = file.model.diagnostics.length > 0;
895
+ if (hasSyntaxErrors || !this.hasLib) return file.model.diagnostics;
896
+ const service = this.serviceOf(path);
897
+ const sf = service.getProgram()?.getSourceFile(toFileName(path));
898
+ if (!sf || sf.text !== file.text) return [];
899
+ return service.getSemanticDiagnostics(toFileName(path)).map((d) => toCodeDiagnostic(file.model, sf, d, "type"));
900
+ }
901
+ /**
902
+ * Text edits that rename a block and every usage of it, in every file of the project.
903
+ * Usages in other files are renamed too, including imports (no `as` aliases are added).
904
+ */
905
+ renameBlock(path, blockId, newName) {
906
+ const file = this.files.get(path);
907
+ const block = file?.model.blocks.get(blockId);
908
+ if (!file || !block) return { ok: false, error: "Block not found" };
909
+ const isRenamable2 = block.nameStart >= 0 && block.kind !== "import" && block.kind !== "statement" && block.name !== "constructor";
910
+ if (!isRenamable2) return { ok: false, error: `${block.name} can't be renamed` };
911
+ if (newName === block.name) return { ok: true, edits: {} };
912
+ if (!isValidIdentifier(newName)) return { ok: false, error: `"${newName}" is not a valid name` };
913
+ if (namesVisibleFrom(file.model, block).has(newName)) return { ok: false, error: `${newName} already exists here` };
914
+ if (file.model.diagnostics.length) return { ok: false, error: `Fix the syntax error on line ${file.model.firstErrorLine} first` };
915
+ const fileName2 = toFileName(path);
916
+ const info = this.serviceOf(path).getRenameInfo(fileName2, block.nameStart, { allowRenameOfImportPath: false });
917
+ if (!info.canRename) return { ok: false, error: info.localizedErrorMessage };
918
+ const edits = {};
919
+ const renamedLocations = /* @__PURE__ */ new Set();
920
+ for (const configPath of this.configsUsing(path)) {
921
+ const service = this.serviceOfConfig(configPath);
922
+ if (!service.getProgram()?.getSourceFile(fileName2)) continue;
923
+ const renameStarts = [{ fileName: fileName2, position: block.nameStart }];
924
+ const visitedStarts = /* @__PURE__ */ new Set();
925
+ for (let start = renameStarts.pop(); start; start = renameStarts.pop()) {
926
+ const startKey = `${start.fileName}:${start.position}`;
927
+ if (visitedStarts.has(startKey)) continue;
928
+ visitedStarts.add(startKey);
929
+ const locations = service.findRenameLocations(start.fileName, start.position, false, false, { providePrefixAndSuffixTextForRename: true }) ?? [];
930
+ for (const location of locations) {
931
+ const locationPath = fromFileName(location.fileName);
932
+ if (!locationPath) continue;
933
+ if (isAliasInSpecifier(location)) renameStarts.push({ fileName: location.fileName, position: location.textSpan.start });
934
+ const locationKey = `${location.fileName}:${location.textSpan.start}`;
935
+ if (renamedLocations.has(locationKey)) continue;
936
+ renamedLocations.add(locationKey);
937
+ const text = isAliasInSpecifier(location) ? newName : (location.prefixText ?? "") + newName + (location.suffixText ?? "");
938
+ (edits[locationPath] ??= []).push({ start: location.textSpan.start, end: location.textSpan.start + location.textSpan.length, text });
939
+ }
940
+ }
941
+ }
942
+ for (const fileEdits of Object.values(edits)) fileEdits.sort((a, b) => a.start - b.start);
943
+ return { ok: true, edits };
944
+ }
945
+ // ---------------------------------------------------------------- internals
946
+ /** The configs of a file and of the files that can use its blocks: its importers, also through re-exports. */
947
+ configsUsing(path) {
948
+ const owner = this.configOf(path);
949
+ const importerConfigs = this.importersThroughReExports(path).map((importer) => this.configOf(importer));
950
+ return [owner, ...new Set(importerConfigs.filter((configPath) => configPath !== owner))];
951
+ }
952
+ /** Files that import `path`, or import a file that re-exports from it (an `index.ts` of a folder or package). */
953
+ importersThroughReExports(path) {
954
+ const importers = /* @__PURE__ */ new Set();
955
+ const reExporters = [path];
956
+ for (let reExported = reExporters.pop(); reExported !== void 0; reExported = reExporters.pop()) {
957
+ for (const importer of this.importersOf(reExported)) {
958
+ if (importers.has(importer) || importer === path) continue;
959
+ importers.add(importer);
960
+ if (this.importsOf(importer).reExportedPaths.has(reExported)) reExporters.push(importer);
961
+ }
962
+ }
963
+ return [...importers];
964
+ }
965
+ otherFileResolver() {
966
+ return {
967
+ projectNames: this.projectNames(),
968
+ blockInOtherFile: (declaration) => {
969
+ const sf = declaration.getSourceFile();
970
+ const path = fromFileName(sf.fileName);
971
+ const file = path ? this.files.get(path) : void 0;
972
+ if (!path || !file || file.text !== sf.text) return null;
973
+ const id = builderOf(file.model)?.blockIdForDeclaration(declaration);
974
+ return id ? { path, id } : null;
975
+ }
976
+ };
977
+ }
978
+ projectNames() {
979
+ if (this.projectNamesCache?.version === this.projectVersion) return this.projectNamesCache.names;
980
+ const names = /* @__PURE__ */ new Set();
981
+ for (const file of this.files.values()) for (const name of builderOf(file.model)?.names ?? []) names.add(name);
982
+ this.projectNamesCache = { version: this.projectVersion, names };
983
+ return names;
984
+ }
985
+ /** Files whose imports resolve to `path`. */
986
+ importersOf(path) {
987
+ const importers = [];
988
+ for (const [otherPath, file] of this.files) {
989
+ if (otherPath !== path && this.importsOf(otherPath, file).importedPaths.has(path)) importers.push(otherPath);
990
+ }
991
+ return importers;
992
+ }
993
+ importsOf(path, file = this.files.get(path)) {
994
+ if (file.imports?.fileSetVersion === this.fileSetVersion && file.imports.fileVersion === file.version) return file.imports;
995
+ const importedPaths = /* @__PURE__ */ new Set();
996
+ const reExportedPaths = /* @__PURE__ */ new Set();
997
+ const host = this.moduleResolutionHost();
998
+ const resolve = (specifier) => {
999
+ const result = ts3.resolveModuleName(specifier, toFileName(path), this.optionsOfFile(path), host).resolvedModule;
1000
+ return result ? fromFileName(result.resolvedFileName) : null;
1001
+ };
1002
+ for (const block of file.model.roots) {
1003
+ const code = block.kind === "statement" || block.kind === "variable" ? file.text.slice(block.start, block.end) : "";
1004
+ const reExportSpecifier = block.kind === "statement" ? RE_EXPORT.exec(code)?.[2] : void 0;
1005
+ const requiredSpecifier = block.kind === "variable" ? REQUIRE_CALL.exec(code)?.[2] : void 0;
1006
+ const specifier = block.kind === "import" ? block.name : reExportSpecifier ?? requiredSpecifier;
1007
+ const target = specifier ? resolve(specifier) : null;
1008
+ if (!target) continue;
1009
+ importedPaths.add(target);
1010
+ if (reExportSpecifier) reExportedPaths.add(target);
1011
+ }
1012
+ file.imports = { importedPaths, reExportedPaths, fileSetVersion: this.fileSetVersion, fileVersion: file.version };
1013
+ return file.imports;
1014
+ }
1015
+ moduleResolutionHost() {
1016
+ return {
1017
+ fileExists: (name) => this.fileExists(name),
1018
+ readFile: (name) => this.readFile(name),
1019
+ directoryExists: (dir) => this.directoryExists(dir),
1020
+ getCurrentDirectory: () => "/"
1021
+ };
1022
+ }
1023
+ fileExists(name) {
1024
+ if (name.startsWith(LIB_DIR)) return name.slice(LIB_DIR.length) in this.libFiles;
1025
+ const path = fromFileName(name);
1026
+ return !!path && this.files.has(path);
1027
+ }
1028
+ readFile(name) {
1029
+ if (name.startsWith(LIB_DIR)) return this.libFiles[name.slice(LIB_DIR.length)];
1030
+ const path = fromFileName(name);
1031
+ return path ? this.files.get(path)?.text : void 0;
1032
+ }
1033
+ directoryExists(dir) {
1034
+ const prefix = dir.replace(/^\//, "").replace(/\/?$/, "/");
1035
+ if (prefix === "/") return true;
1036
+ for (const path of this.files.keys()) if (path.startsWith(prefix)) return true;
1037
+ return false;
1038
+ }
1039
+ /** The host of one config's program. Its files are those that use the config; their imports come in from the rest. */
1040
+ createHost(configPath) {
1041
+ return {
1042
+ getCompilationSettings: () => this.programs().optionsByConfig.get(configPath) ?? this.programs().optionsByConfig.get(NO_CONFIG),
1043
+ getProjectVersion: () => String(this.projectVersion),
1044
+ getScriptFileNames: () => (this.programs().pathsByConfig.get(configPath) ?? []).map(toFileName),
1045
+ getScriptVersion: (name) => {
1046
+ if (name.startsWith(LIB_DIR)) return "0";
1047
+ const path = fromFileName(name);
1048
+ return String((path && this.files.get(path)?.version) ?? 0);
1049
+ },
1050
+ getScriptSnapshot: (name) => {
1051
+ if (name.startsWith(LIB_DIR)) {
1052
+ const text = this.libFiles[name.slice(LIB_DIR.length)];
1053
+ return text === void 0 ? void 0 : ts3.ScriptSnapshot.fromString(text);
1054
+ }
1055
+ const path = fromFileName(name);
1056
+ const file = path ? this.files.get(path) : void 0;
1057
+ if (!file) return void 0;
1058
+ file.snapshot ??= new TextSnapshot(file.text);
1059
+ return file.snapshot;
1060
+ },
1061
+ getCurrentDirectory: () => "/",
1062
+ // A file another package's program pulls in keeps its own package's aliases.
1063
+ resolveModuleNameLiterals: (moduleLiterals, containingFile) => {
1064
+ const containingPath = fromFileName(containingFile);
1065
+ const options = this.optionsOfFile(containingPath);
1066
+ const host = this.moduleResolutionHost();
1067
+ return moduleLiterals.map((literal) => ts3.resolveModuleName(literal.text, containingFile, options, host));
1068
+ },
1069
+ getDefaultLibFileName: () => LIB_DIR + "lib.d.ts",
1070
+ fileExists: (name) => this.fileExists(name),
1071
+ readFile: (name) => this.readFile(name),
1072
+ directoryExists: (dir) => this.directoryExists(dir),
1073
+ getDirectories: () => [],
1074
+ useCaseSensitiveFileNames: () => true
1075
+ };
1076
+ }
1077
+ };
1078
+ function isAliasInSpecifier(location) {
1079
+ return /\bas\s*$/.test(location.prefixText ?? "") || /^\s*as\b/.test(location.suffixText ?? "");
1080
+ }
1081
+ var TextSnapshot = class _TextSnapshot {
1082
+ constructor(text) {
1083
+ this.text = text;
1084
+ }
1085
+ text;
1086
+ getText(start, end) {
1087
+ return this.text.slice(start, end);
1088
+ }
1089
+ getLength() {
1090
+ return this.text.length;
1091
+ }
1092
+ getChangeRange(old) {
1093
+ if (!(old instanceof _TextSnapshot)) return void 0;
1094
+ return changedRegion(old.text, this.text);
1095
+ }
1096
+ };
1097
+ function changedRegion(before, after) {
1098
+ let start = 0;
1099
+ const shorter = Math.min(before.length, after.length);
1100
+ while (start < shorter && before.charCodeAt(start) === after.charCodeAt(start)) start++;
1101
+ let fromEnd = 0;
1102
+ while (fromEnd < shorter - start && before.charCodeAt(before.length - 1 - fromEnd) === after.charCodeAt(after.length - 1 - fromEnd)) fromEnd++;
1103
+ return ts3.createTextChangeRange(ts3.createTextSpan(start, before.length - fromEnd - start), after.length - fromEnd - start);
1104
+ }
1105
+ function toFileName(path) {
1106
+ return "/" + path;
1107
+ }
1108
+ function fromFileName(name) {
1109
+ if (name.startsWith(LIB_DIR) || !name.startsWith("/")) return null;
1110
+ return name.slice(1);
1111
+ }
1112
+ function namesVisibleFrom(model, block) {
1113
+ const names = /* @__PURE__ */ new Set();
1114
+ const siblings = block.parentId ? model.blocks.get(block.parentId).children : model.roots;
1115
+ for (const sibling of siblings) if (sibling !== block) names.add(sibling.name);
1116
+ let parent = block.parentId ? model.blocks.get(block.parentId) : void 0;
1117
+ const isClassMember = parent?.insert === "class";
1118
+ if (isClassMember) return names;
1119
+ while (parent) {
1120
+ const parentSiblings = parent.parentId ? model.blocks.get(parent.parentId).children : model.roots;
1121
+ for (const scopeBlock of [...parent.children, ...parentSiblings]) if (scopeBlock !== block) names.add(scopeBlock.name);
1122
+ parent = parent.parentId ? model.blocks.get(parent.parentId) : void 0;
1123
+ }
1124
+ return names;
1125
+ }
1126
+ function isValidIdentifier(name) {
1127
+ const scanner = ts3.createScanner(ts3.ScriptTarget.Latest, true, ts3.LanguageVariant.Standard, name);
1128
+ const token = scanner.scan();
1129
+ const isWholeText = scanner.getTokenEnd() === name.length && name.length > 0;
1130
+ const isNameToken = token === ts3.SyntaxKind.Identifier || token > ts3.SyntaxKind.LastReservedWord && token <= ts3.SyntaxKind.LastKeyword;
1131
+ return isWholeText && isNameToken;
1132
+ }
1133
+
1134
+ // src/remoteProject.ts
1135
+ var RemoteProject = class {
1136
+ constructor(callServer) {
1137
+ this.callServer = callServer;
1138
+ }
1139
+ callServer;
1140
+ files = /* @__PURE__ */ new Map();
1141
+ /** Load many files at once. They're parsed in the worker; here only when a canvas needs them. */
1142
+ setFiles(files) {
1143
+ for (const [path, text] of Object.entries(files)) this.files.set(path, { text });
1144
+ void this.callServer("setFiles", [files]);
1145
+ }
1146
+ setFile(path, text) {
1147
+ this.setFileText(path, text);
1148
+ return this.getModel(path);
1149
+ }
1150
+ /** Update a file no canvas shows (e.g. typed in a text editor): no parse on this thread. */
1151
+ setFileText(path, text) {
1152
+ const existing = this.files.get(path);
1153
+ if (existing?.text === text) return;
1154
+ this.files.set(path, { text, model: existing?.model });
1155
+ void this.callServer("setFile", [path, text]);
1156
+ }
1157
+ removeFile(path) {
1158
+ if (!this.files.delete(path)) return;
1159
+ void this.callServer("removeFile", [path]);
1160
+ }
1161
+ setLibFiles(libFiles) {
1162
+ void this.callServer("setLibFiles", [libFiles]);
1163
+ }
1164
+ /** The folder's tsconfig.json (or none, with null). */
1165
+ setConfigFile(config) {
1166
+ void this.callServer("setConfigFile", [config]);
1167
+ }
1168
+ /** The folder's config files (one per package in a monorepo) and package.json files (or none, with null). */
1169
+ setWorkspace(workspace) {
1170
+ void this.callServer("setWorkspace", [workspace]);
1171
+ }
1172
+ getModel(path) {
1173
+ const file = this.files.get(path);
1174
+ if (!file) return void 0;
1175
+ const isModelOfCurrentText = file.model?.text === file.text;
1176
+ if (!isModelOfCurrentText) file.model = parseStructure(path, file.text, file.model);
1177
+ return file.model;
1178
+ }
1179
+ getText(path) {
1180
+ return this.files.get(path)?.text;
1181
+ }
1182
+ paths() {
1183
+ return [...this.files.keys()];
1184
+ }
1185
+ resolve(path, options) {
1186
+ return this.callServer("resolve", [path, options]);
1187
+ }
1188
+ renameBlock(path, blockId, newName) {
1189
+ return this.callServer("renameBlock", [path, blockId, newName]);
1190
+ }
1191
+ };
1192
+
1193
+ // src/projectServer.ts
1194
+ var SERVER_METHODS = ["setLibFiles", "setConfigFile", "setWorkspace", "setFiles", "setFile", "removeFile", "resolve", "renameBlock"];
1195
+ var ProjectServer = class {
1196
+ project = new Project();
1197
+ setLibFiles(libFiles) {
1198
+ this.project.setLibFiles(libFiles);
1199
+ }
1200
+ setConfigFile(config) {
1201
+ this.project.setConfigFile(config);
1202
+ }
1203
+ setWorkspace(workspace) {
1204
+ this.project.setWorkspace(workspace);
1205
+ }
1206
+ setFiles(files) {
1207
+ for (const [path, text] of Object.entries(files)) this.project.setFile(path, text);
1208
+ }
1209
+ setFile(path, text) {
1210
+ this.project.setFile(path, text);
1211
+ }
1212
+ removeFile(path) {
1213
+ this.project.removeFile(path);
1214
+ }
1215
+ resolve(path, options) {
1216
+ return this.project.resolve(path, options);
1217
+ }
1218
+ renameBlock(path, blockId, newName) {
1219
+ return this.project.renameBlock(path, blockId, newName);
1220
+ }
1221
+ };
1222
+ function callProjectServer(server, method, args) {
1223
+ const isServerMethod = SERVER_METHODS.includes(method);
1224
+ if (!isServerMethod) throw new Error(`Unknown project method: ${method}`);
1225
+ return server[method](...args);
1226
+ }
1227
+
1228
+ // src/projectWorker.ts
1229
+ function serveProject(port) {
1230
+ const server = new ProjectServer();
1231
+ port.addEventListener("message", async (event) => {
1232
+ const { id, method, args } = event.data;
1233
+ try {
1234
+ port.postMessage({ id, result: await callProjectServer(server, method, args) });
1235
+ } catch (error) {
1236
+ port.postMessage({ id, error: error instanceof Error ? error.message : String(error) });
1237
+ }
1238
+ });
1239
+ port.start?.();
1240
+ }
1241
+ function connectProjectWorker(port) {
1242
+ let nextId = 0;
1243
+ const waiting = /* @__PURE__ */ new Map();
1244
+ port.addEventListener("message", (event) => {
1245
+ const reply = event.data;
1246
+ const caller = waiting.get(reply.id);
1247
+ waiting.delete(reply.id);
1248
+ if ("error" in reply) caller?.reject(new Error(reply.error));
1249
+ else caller?.resolve(reply.result);
1250
+ });
1251
+ port.start?.();
1252
+ return new RemoteProject((method, args) => {
1253
+ const id = nextId++;
1254
+ port.postMessage({ id, method, args });
1255
+ return new Promise((resolve, reject) => waiting.set(id, { resolve, reject }));
1256
+ });
1257
+ }
1258
+
1259
+ // src/edits.ts
1260
+ import ts4 from "typescript";
1261
+ function applyEdit(text, e) {
1262
+ return text.slice(0, e.start) + e.text + text.slice(e.end);
1263
+ }
1264
+ function applyEdits(text, edits) {
1265
+ const lastFirst = [...edits].sort((a, b) => b.start - a.start);
1266
+ return lastFirst.reduce(applyEdit, text);
1267
+ }
1268
+ function replaceBlock(model, id, code) {
1269
+ const b = must(model, id);
1270
+ return { start: b.start, end: b.end, text: code };
1271
+ }
1272
+ function deleteBlock(model, id) {
1273
+ const b = must(model, id);
1274
+ const t = model.text;
1275
+ let start = t.lastIndexOf("\n", b.start - 1) + 1;
1276
+ if (/\S/.test(t.slice(start, b.start))) start = b.start;
1277
+ let end = b.end;
1278
+ const trail = /^[ \t]*;?[ \t]*(\r?\n)?/.exec(t.slice(end));
1279
+ end += trail[0].length;
1280
+ const before = /(\r?\n[ \t]*)*$/.exec(t.slice(0, start))[0];
1281
+ const after = /^([ \t]*\r?\n)*/.exec(t.slice(end))[0];
1282
+ const blankBefore = (before.match(/\n/g) ?? []).length;
1283
+ const blankAfter = (after.match(/\n/g) ?? []).length;
1284
+ if (blankBefore >= 2 && blankAfter >= 1) end += after.length;
1285
+ else if (start === 0 && blankAfter) end += after.length;
1286
+ return { start, end, text: "" };
1287
+ }
1288
+ function uniqueName(model, parentId, base) {
1289
+ const taken = /* @__PURE__ */ new Set();
1290
+ const sibs = parentId ? must(model, parentId).children : model.roots;
1291
+ for (const s of sibs) taken.add(s.name);
1292
+ for (const r of model.roots) taken.add(r.name);
1293
+ if (!taken.has(base)) return base;
1294
+ let n = 2;
1295
+ while (taken.has(base + n)) n++;
1296
+ return base + n;
1297
+ }
1298
+ function containerFor(model, id) {
1299
+ let b = id ? model.blocks.get(id) ?? null : null;
1300
+ while (b && !b.insert) b = b.parentId ? model.blocks.get(b.parentId) ?? null : null;
1301
+ return b;
1302
+ }
1303
+ function newFunction(model, parentId, base = "newFunction") {
1304
+ const parent = containerFor(model, parentId);
1305
+ const name = uniqueName(model, parent?.id ?? null, base);
1306
+ const u = model.indentUnit;
1307
+ const code = parent?.insert === "class" ? `${name}() {
1308
+ ${u}
1309
+ }` : `function ${name}() {
1310
+ ${u}
1311
+ }`;
1312
+ const nameAt = code.indexOf(name);
1313
+ return insertCode(model, parent, code, name, [nameAt, nameAt + name.length]);
1314
+ }
1315
+ function copyBlock(model, id) {
1316
+ const b = must(model, id);
1317
+ const parent = b.parentId ? model.blocks.get(b.parentId) : null;
1318
+ return {
1319
+ code: dedent(model.text.slice(b.start, b.end), b.indent),
1320
+ name: b.name,
1321
+ kind: b.kind,
1322
+ fromClass: parent?.insert === "class"
1323
+ };
1324
+ }
1325
+ function pasteBlock(model, parentId, clip) {
1326
+ const parent = containerFor(model, parentId);
1327
+ const toClass = parent?.insert === "class";
1328
+ let code = clip.code;
1329
+ if (toClass && !clip.fromClass) code = toMember(code);
1330
+ if (!toClass && clip.fromClass) code = toTopLevel(code);
1331
+ if (!clip.name) {
1332
+ const r = insertCode(model, parent, code, "", [0, 0]);
1333
+ return { ...r, id: "" };
1334
+ }
1335
+ const name = uniqueName(model, parent?.id ?? null, clip.name);
1336
+ if (name !== clip.name) code = renameDeclaration(code, toClass, name);
1337
+ return insertCode(model, parent, code, name, [0, 0]);
1338
+ }
1339
+ function insertCode(model, parent, code, name, select) {
1340
+ const t = model.text;
1341
+ const key = name;
1342
+ const id = parent ? `${parent.id}/${key}` : key;
1343
+ if (!parent) {
1344
+ const end = t.trimEnd().length;
1345
+ const sep = end === 0 ? "" : "\n\n";
1346
+ return { edit: { start: end, end: t.length, text: sep + code + "\n" }, id, select };
1347
+ }
1348
+ const indent = parent.indent + model.indentUnit;
1349
+ const body = reindent(code, indent);
1350
+ const at = parent.insertAt;
1351
+ const lineStart = t.lastIndexOf("\n", at - 1) + 1;
1352
+ if (!/\S/.test(t.slice(lineStart, at))) {
1353
+ const prevLineEnd = lineStart - 1;
1354
+ const prevLineStart = t.lastIndexOf("\n", prevLineEnd - 1) + 1;
1355
+ const prevLine = t.slice(prevLineStart, prevLineEnd);
1356
+ const opener = /\{\s*$/.test(prevLine);
1357
+ const lead = opener || !/\S/.test(prevLine) ? "" : "\n";
1358
+ const trail = t[at] === "}" ? "" : "\n";
1359
+ return { edit: { start: lineStart, end: lineStart, text: lead + indent + body + "\n" + trail }, id, select };
1360
+ }
1361
+ return { edit: { start: at, end: at, text: "\n" + indent + body + "\n" + parent.indent }, id, select };
1362
+ }
1363
+ function reindent(code, indent) {
1364
+ return code.split("\n").map((l, i) => i === 0 || l === "" ? l : indent + l).join("\n");
1365
+ }
1366
+ function dedent(code, indent) {
1367
+ if (!indent) return code;
1368
+ return code.split("\n").map((l, i) => i > 0 && l.startsWith(indent) ? l.slice(indent.length) : l).join("\n");
1369
+ }
1370
+ function toMember(code) {
1371
+ const fn = /^((?:\/\*[\s\S]*?\*\/|\/\/[^\n]*|\s)*)(?:export\s+)?(?:default\s+)?(async\s+)?function\s*(\*?)\s*/.exec(code);
1372
+ if (fn) return fn[1] + (fn[2] ?? "") + fn[3] + code.slice(fn[0].length);
1373
+ const v = /^((?:\/\*[\s\S]*?\*\/|\/\/[^\n]*|\s)*)(?:export\s+)?(?:const|let|var)\s+/.exec(code);
1374
+ if (v) return v[1] + code.slice(v[0].length);
1375
+ return code;
1376
+ }
1377
+ function toTopLevel(code) {
1378
+ const m = /^((?:\/\*[\s\S]*?\*\/|\/\/[^\n]*|\s)*)((?:(?:public|private|protected|static|readonly|override|abstract)\s+)*)(async\s+)?(\*?)\s*([A-Za-z_$][\w$]*)\s*(<|\()/.exec(code);
1379
+ if (m) return `${m[1]}${m[3] ?? ""}function${m[4] ? "*" : ""} ${m[5]}${m[6]}` + code.slice(m[0].length);
1380
+ const p = /^((?:\/\*[\s\S]*?\*\/|\/\/[^\n]*|\s)*)((?:(?:public|private|protected|static|readonly|override)\s+)*)([A-Za-z_$][\w$]*)(\s*[:=])/.exec(code);
1381
+ if (p) return `${p[1]}const ${p[3]}${p[4]}` + code.slice(p[0].length);
1382
+ return code;
1383
+ }
1384
+ function renameDeclaration(code, inClass, name) {
1385
+ const prefix = inClass ? "class __cc__ {\n" : "";
1386
+ const src = prefix + code + (inClass ? "\n}" : "");
1387
+ const sf = ts4.createSourceFile("snippet.ts", src, ts4.ScriptTarget.Latest, true);
1388
+ let target;
1389
+ const first = sf.statements[0];
1390
+ if (inClass && first && ts4.isClassDeclaration(first)) target = first.members[0]?.name;
1391
+ else if (first && (ts4.isFunctionDeclaration(first) || ts4.isClassDeclaration(first) || ts4.isInterfaceDeclaration(first) || ts4.isTypeAliasDeclaration(first) || ts4.isEnumDeclaration(first)))
1392
+ target = first.name;
1393
+ else if (first && ts4.isVariableStatement(first)) target = first.declarationList.declarations[0]?.name;
1394
+ if (!target) return code;
1395
+ const s = target.getStart(sf) - prefix.length;
1396
+ return code.slice(0, s) + name + code.slice(target.end - prefix.length);
1397
+ }
1398
+ function must(model, id) {
1399
+ const b = model.blocks.get(id);
1400
+ if (!b) throw new Error(`No block ${id}`);
1401
+ return b;
1402
+ }
1403
+ function clipFromText(code) {
1404
+ const text = code.replace(/\s+$/, "");
1405
+ const sf = ts4.createSourceFile("clip.ts", text, ts4.ScriptTarget.Latest, true);
1406
+ const first = sf.statements[0];
1407
+ const diags = sf.parseDiagnostics ?? [];
1408
+ if (first && !diags.length) {
1409
+ let name;
1410
+ let kind = "statement";
1411
+ if (ts4.isFunctionDeclaration(first)) name = first.name?.text, kind = "function";
1412
+ else if (ts4.isClassDeclaration(first)) name = first.name?.text, kind = "class";
1413
+ else if (ts4.isVariableStatement(first)) name = first.declarationList.declarations[0]?.name.getText(), kind = "variable";
1414
+ else if (ts4.isInterfaceDeclaration(first) || ts4.isTypeAliasDeclaration(first) || ts4.isEnumDeclaration(first)) name = first.name.text, kind = "type";
1415
+ if (name) return { code: text, name, kind, fromClass: false };
1416
+ }
1417
+ const wrapped = ts4.createSourceFile("clip.ts", `class __cc__ {
1418
+ ${text}
1419
+ }`, ts4.ScriptTarget.Latest, true);
1420
+ const cls = wrapped.statements[0];
1421
+ const wd = wrapped.parseDiagnostics ?? [];
1422
+ if (cls && ts4.isClassDeclaration(cls) && !wd.length && cls.members[0]?.name) {
1423
+ const m = cls.members[0];
1424
+ return { code: text, name: m.name.getText(wrapped), kind: ts4.isPropertyDeclaration(m) ? "variable" : "function", fromClass: true };
1425
+ }
1426
+ return { code: text, name: "", kind: "statement", fromClass: false };
1427
+ }
1428
+
1429
+ // src/diff.ts
1430
+ function diffModels(base, current) {
1431
+ const changes = /* @__PURE__ */ new Map();
1432
+ for (const [id, block] of current.blocks) {
1433
+ const baseBlock = base.blocks.get(id);
1434
+ if (!baseBlock) changes.set(id, "added");
1435
+ else if (sameCode(fullCode(base, baseBlock), fullCode(current, block))) changes.set(id, "unchanged");
1436
+ else if (sameCode(ownCode(base, baseBlock), ownCode(current, block))) changes.set(id, "containsChanges");
1437
+ else changes.set(id, "changed");
1438
+ }
1439
+ const removed = [...base.blocks.values()].filter((block) => {
1440
+ const isGone = !current.blocks.has(block.id);
1441
+ const isParentGone = !!block.parentId && !current.blocks.has(block.parentId);
1442
+ return isGone && !isParentGone;
1443
+ });
1444
+ return { base, changes, removed };
1445
+ }
1446
+ function diffOwnCode(base, baseBlock, current, block) {
1447
+ const before = baseBlock ? codeLines(ownCode(base, baseBlock)) : [];
1448
+ return diffLines(before, codeLines(ownCode(current, block)));
1449
+ }
1450
+ function diffLines(before, after) {
1451
+ let prefix = 0;
1452
+ while (prefix < before.length && prefix < after.length && before[prefix] === after[prefix]) prefix++;
1453
+ let suffix = 0;
1454
+ while (suffix < before.length - prefix && suffix < after.length - prefix && before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) suffix++;
1455
+ const oldMiddle = before.slice(prefix, before.length - suffix);
1456
+ const newMiddle = after.slice(prefix, after.length - suffix);
1457
+ const common = Array.from({ length: oldMiddle.length + 1 }, () => new Uint32Array(newMiddle.length + 1));
1458
+ for (let i2 = oldMiddle.length - 1; i2 >= 0; i2--) {
1459
+ for (let j2 = newMiddle.length - 1; j2 >= 0; j2--) {
1460
+ common[i2][j2] = oldMiddle[i2] === newMiddle[j2] ? common[i2 + 1][j2 + 1] + 1 : Math.max(common[i2 + 1][j2], common[i2][j2 + 1]);
1461
+ }
1462
+ }
1463
+ const middle = [];
1464
+ let i = 0;
1465
+ let j = 0;
1466
+ while (i < oldMiddle.length || j < newMiddle.length) {
1467
+ if (i < oldMiddle.length && j < newMiddle.length && oldMiddle[i] === newMiddle[j]) {
1468
+ middle.push({ kind: "same", text: newMiddle[j] });
1469
+ i++;
1470
+ j++;
1471
+ } else if (i < oldMiddle.length && (j === newMiddle.length || common[i + 1][j] >= common[i][j + 1])) {
1472
+ middle.push({ kind: "removed", text: oldMiddle[i++] });
1473
+ } else {
1474
+ middle.push({ kind: "added", text: newMiddle[j++] });
1475
+ }
1476
+ }
1477
+ const same = (text) => ({ kind: "same", text });
1478
+ return [...before.slice(0, prefix).map(same), ...middle, ...after.slice(after.length - suffix).map(same)];
1479
+ }
1480
+ function diffPreview(lines, max) {
1481
+ const isNearChange = (index) => [index - 1, index, index + 1].some((near) => lines[near] && lines[near].kind !== "same");
1482
+ const changesWithContext = lines.filter((_, index) => isNearChange(index));
1483
+ const shown = changesWithContext.length <= max ? changesWithContext : lines.filter((line) => line.kind !== "same");
1484
+ return { lines: shown.slice(0, max), more: Math.max(0, shown.length - max) };
1485
+ }
1486
+ function sameCode(before, after) {
1487
+ return codeLines(before).join("\n") === codeLines(after).join("\n");
1488
+ }
1489
+ function fullCode(model, block) {
1490
+ return model.text.slice(block.start, block.end);
1491
+ }
1492
+ function ownCode(model, block) {
1493
+ return codeWithoutChildren(model.text, [block.start, block.end], block.children);
1494
+ }
1495
+ function codeLines(code) {
1496
+ const lines = code.split("\n").map((line) => line.replace(/\s+$/, "").replace(/\t/g, " ")).filter((line) => line);
1497
+ const indents = lines.slice(1).filter((line) => line).map((line) => /^ */.exec(line)[0].length);
1498
+ const commonIndent = indents.length ? Math.min(...indents) : 0;
1499
+ return lines.map((line, index) => index === 0 ? line.trimStart() : line.slice(Math.min(commonIndent, /^ */.exec(line)[0].length)));
1500
+ }
1501
+
1502
+ // src/conflicts.ts
1503
+ var CURRENT_MARKER = /^<{7}(?: (.*))?\r?$/;
1504
+ var BASE_MARKER = /^\|{7}(?: .*)?\r?$/;
1505
+ var SEPARATOR = /^={7}\r?$/;
1506
+ var INCOMING_MARKER = /^>{7}(?: (.*))?\r?$/;
1507
+ function findConflicts(text) {
1508
+ const hunks = [];
1509
+ let open = null;
1510
+ let lineStart = 0;
1511
+ while (lineStart < text.length) {
1512
+ const newline = text.indexOf("\n", lineStart);
1513
+ const lineEnd = newline === -1 ? text.length : newline;
1514
+ const nextLineStart = newline === -1 ? text.length : newline + 1;
1515
+ const line = text.slice(lineStart, lineEnd);
1516
+ const currentMarker = CURRENT_MARKER.exec(line);
1517
+ if (currentMarker) {
1518
+ open = { start: lineStart, currentLabel: currentMarker[1] ?? "", currentStart: nextLineStart };
1519
+ } else if (open && BASE_MARKER.test(line) && open.currentEnd === void 0) {
1520
+ open.currentEnd = lineStart;
1521
+ open.baseStart = nextLineStart;
1522
+ } else if (open && SEPARATOR.test(line) && open.incomingStart === void 0) {
1523
+ if (open.baseStart !== void 0) open.baseEnd = lineStart;
1524
+ else open.currentEnd = lineStart;
1525
+ open.incomingStart = nextLineStart;
1526
+ } else if (open && open.incomingStart !== void 0) {
1527
+ const incomingMarker = INCOMING_MARKER.exec(line);
1528
+ if (incomingMarker) {
1529
+ hunks.push({
1530
+ start: open.start,
1531
+ end: nextLineStart,
1532
+ current: { start: open.currentStart, end: open.currentEnd },
1533
+ incoming: { start: open.incomingStart, end: lineStart },
1534
+ base: open.baseStart !== void 0 ? { start: open.baseStart, end: open.baseEnd } : void 0,
1535
+ currentLabel: open.currentLabel,
1536
+ incomingLabel: incomingMarker[1] ?? ""
1537
+ });
1538
+ open = null;
1539
+ }
1540
+ }
1541
+ lineStart = nextLineStart;
1542
+ }
1543
+ return hunks;
1544
+ }
1545
+ function conflictSideText(text, hunks, side) {
1546
+ const pieces = [];
1547
+ const hunkRanges = [];
1548
+ let sideText = "";
1549
+ let fileOffset = 0;
1550
+ const copy = (start, end) => {
1551
+ pieces.push([sideText.length, start, end - start]);
1552
+ sideText += text.slice(start, end);
1553
+ };
1554
+ for (const hunk of hunks) {
1555
+ copy(fileOffset, hunk.start);
1556
+ const code = hunk[side];
1557
+ const rangeStart = sideText.length;
1558
+ copy(code.start, code.end);
1559
+ hunkRanges.push({ start: rangeStart, end: sideText.length });
1560
+ fileOffset = hunk.end;
1561
+ }
1562
+ copy(fileOffset, text.length);
1563
+ return {
1564
+ text: sideText,
1565
+ hunkRanges,
1566
+ toFileOffset(offset) {
1567
+ const piece = pieces.findLast(([sideStart]) => sideStart <= offset) ?? pieces[0];
1568
+ return piece[1] + Math.min(offset - piece[0], piece[2]);
1569
+ }
1570
+ };
1571
+ }
1572
+ function resolveConflict(text, hunk, choice) {
1573
+ const code = (side) => text.slice(side.start, side.end);
1574
+ const resolved = choice === "both" ? code(hunk.current) + code(hunk.incoming) : code(hunk[choice]);
1575
+ return { start: hunk.start, end: hunk.end, text: resolved };
1576
+ }
1577
+
1578
+ // src/layoutRules.ts
1579
+ var BAND_ORDER = ["data", "types", "main", "bottom"];
1580
+ var ZONE_RULES = [
1581
+ { zone: "left", description: "Imports on the left", matches: (block) => block.kind === "import" },
1582
+ { zone: "data", description: "Constants and other data at the top", matches: (block) => block.kind === "variable" },
1583
+ { zone: "types", description: "Then interfaces, types and enums", matches: (block) => block.kind === "type" },
1584
+ { zone: "bottom", description: "Code that runs when the file loads, at the bottom", matches: (block) => block.kind === "statement" },
1585
+ { zone: "main", description: "Functions, classes and the rest in between", matches: () => true }
1586
+ ];
1587
+ var STACK_RULES = [
1588
+ {
1589
+ description: "A getter above its setter: getTotal / setTotal, and get total / set total",
1590
+ pairs(blocks) {
1591
+ const pairs = [];
1592
+ for (const getter of blocks) {
1593
+ const accessed = accessedName(getter, "get");
1594
+ const setter = accessed && blocks.find((block) => block !== getter && accessedName(block, "set") === accessed);
1595
+ if (setter) pairs.push([getter, setter]);
1596
+ }
1597
+ return pairs;
1598
+ }
1599
+ }
1600
+ ];
1601
+ var RELATEDNESS_RULES = [
1602
+ {
1603
+ description: "One calls or uses the other",
1604
+ score: (a, b, factsOf) => factsOf(a).uses.has(b.id) || factsOf(b).uses.has(a.id) ? 3 : 0
1605
+ },
1606
+ {
1607
+ description: "They use the same things",
1608
+ score: (a, b, factsOf) => [...factsOf(a).uses].filter((id) => factsOf(b).uses.has(id)).length
1609
+ },
1610
+ {
1611
+ description: "Their names start the same way (formatPrice, formatDate)",
1612
+ score: (a, b) => firstWord(a.name) && firstWord(a.name) === firstWord(b.name) ? 1 : 0
1613
+ }
1614
+ ];
1615
+ var PLACEMENT_RULES = [
1616
+ {
1617
+ description: "A getter right above its setter, a setter right below its getter",
1618
+ spot(block, placed) {
1619
+ const siblings = [block, ...placed.map((sibling) => sibling.block)];
1620
+ for (const rule of STACK_RULES) {
1621
+ for (const [top, bottom] of rule.pairs(siblings)) {
1622
+ const placedBottom = top === block && placed.find((sibling) => sibling.block === bottom);
1623
+ if (placedBottom) return { sibling: placedBottom, side: "above" };
1624
+ const placedTop = bottom === block && placed.find((sibling) => sibling.block === top);
1625
+ if (placedTop) return { sibling: placedTop, side: "below" };
1626
+ }
1627
+ }
1628
+ return void 0;
1629
+ }
1630
+ },
1631
+ {
1632
+ description: "Like a newspaper: a function goes in the column right of its caller, after the function called before it",
1633
+ spot(block, placed, factsOf) {
1634
+ if (!isFunctionLike(block)) return void 0;
1635
+ const caller = placed.find((sibling) => isFunctionLike(sibling.block) && factsOf(sibling.block).calls.includes(block.id));
1636
+ if (!caller) return void 0;
1637
+ const calls = factsOf(caller.block).calls;
1638
+ const isInCallerColumn = (sibling) => sibling !== caller && sibling.x >= caller.x + caller.w;
1639
+ const calledBefore = calls.slice(0, calls.indexOf(block.id)).reverse();
1640
+ const previousInColumn = calledBefore.map((id) => placed.find((sibling) => sibling.block.id === id && isInCallerColumn(sibling))).find(Boolean);
1641
+ return previousInColumn ? { sibling: previousInColumn, side: "below" } : { sibling: caller, side: "right" };
1642
+ }
1643
+ },
1644
+ {
1645
+ description: "An import, constant or type below the last one of its kind before it in the file",
1646
+ spot(block, placed) {
1647
+ const zone = zoneOf(block);
1648
+ if (zone === "main") return void 0;
1649
+ const sameZone = placed.filter((sibling) => zoneOf(sibling.block) === zone);
1650
+ const lastBefore = sameZone.filter((sibling) => sibling.block.start < block.start).at(-1) ?? sameZone[0];
1651
+ return lastBefore ? { sibling: lastBefore, side: "below" } : void 0;
1652
+ }
1653
+ },
1654
+ {
1655
+ description: "Below the block it is most related to",
1656
+ spot(block, placed, factsOf) {
1657
+ let best;
1658
+ let bestScore = 0;
1659
+ for (const sibling of placed) {
1660
+ const score = RELATEDNESS_RULES.reduce((total, rule) => total + rule.score(block, sibling.block, factsOf), 0);
1661
+ if (score > bestScore) {
1662
+ best = sibling;
1663
+ bestScore = score;
1664
+ }
1665
+ }
1666
+ return best ? { sibling: best, side: "below" } : void 0;
1667
+ }
1668
+ }
1669
+ ];
1670
+ var zoneOf = (block) => ZONE_RULES.find((rule) => rule.matches(block)).zone;
1671
+ var NEWSPAPER_DEPTH = 3;
1672
+ var isFunctionLike = (block) => block.kind === "function";
1673
+ function accessedName(block, accessor) {
1674
+ if (block.kind !== "function") return void 0;
1675
+ if (block.tags.includes(accessor)) return block.name;
1676
+ const match = new RegExp(`^${accessor}([A-Z_$][\\w$]*)$`).exec(block.name);
1677
+ return match ? match[1].charAt(0).toLowerCase() + match[1].slice(1) : void 0;
1678
+ }
1679
+ function firstWord(name) {
1680
+ const match = /^([a-z]+)[A-Z0-9_]/.exec(name);
1681
+ const isAccessorWord = match?.[1] === "get" || match?.[1] === "set";
1682
+ return match && !isAccessorWord ? match[1] : void 0;
1683
+ }
1684
+
1685
+ // src/arrange.ts
1686
+ function arrangeSiblings(blocks, sizeOf, text, gap, isFile) {
1687
+ const factsById = siblingFacts(blocks, text);
1688
+ const factsOf = (block) => factsById.get(block.id);
1689
+ const units = stackedUnits(blocks);
1690
+ const zoneUnits = (zone) => units.filter((unit) => unit.zone === zone);
1691
+ const bands = BAND_ORDER.map((zone) => {
1692
+ const inZone = zoneUnits(zone);
1693
+ return zone === "main" ? orderByRelatedness(newspaperTrees(inZone, factsOf), factsOf) : inZone;
1694
+ });
1695
+ const positions = /* @__PURE__ */ new Map();
1696
+ let left = 0;
1697
+ const leftColumn = zoneUnits("left");
1698
+ if (leftColumn.length) {
1699
+ let y2 = 0;
1700
+ for (const unit of leftColumn) {
1701
+ place(unit.arrangement, 0, y2, sizeOf, gap, positions);
1702
+ y2 += measure(unit.arrangement, sizeOf, gap).h + 12;
1703
+ }
1704
+ left = Math.max(...leftColumn.map((unit) => measure(unit.arrangement, sizeOf, gap).w)) + gap * 2;
1705
+ }
1706
+ const bandUnits = bands.flat();
1707
+ const area = bandUnits.reduce((sum, unit) => {
1708
+ const size = measure(unit.arrangement, sizeOf, gap);
1709
+ return sum + (size.w + gap) * (size.h + gap);
1710
+ }, 0);
1711
+ const widest = Math.max(0, ...bandUnits.map((unit) => measure(unit.arrangement, sizeOf, gap).w));
1712
+ const rowWidth = Math.max(widest, Math.sqrt(area * (isFile ? 1.7 : 2.4)));
1713
+ let y = 0;
1714
+ for (const band of bands.filter((band2) => band2.length)) {
1715
+ let x = 0;
1716
+ let rowHeight = 0;
1717
+ for (const unit of band) {
1718
+ const size = measure(unit.arrangement, sizeOf, gap);
1719
+ if (x > 0 && x + size.w > rowWidth) {
1720
+ x = 0;
1721
+ y += rowHeight + gap;
1722
+ rowHeight = 0;
1723
+ }
1724
+ place(unit.arrangement, left + x, y, sizeOf, gap, positions);
1725
+ x += size.w + gap;
1726
+ rowHeight = Math.max(rowHeight, size.h);
1727
+ }
1728
+ y += rowHeight + gap;
1729
+ }
1730
+ return positions;
1731
+ }
1732
+ function newBlockSpots(siblings, text) {
1733
+ let factsById;
1734
+ const factsOf = (sibling) => factsById.get(sibling.id);
1735
+ return (block, placed) => {
1736
+ factsById ??= siblingFacts(siblings, text);
1737
+ for (const rule of PLACEMENT_RULES) {
1738
+ const spot = rule.spot(block, placed, factsOf);
1739
+ if (spot) return spot;
1740
+ }
1741
+ return void 0;
1742
+ };
1743
+ }
1744
+ function stackedUnits(blocks) {
1745
+ const unitOf = /* @__PURE__ */ new Map();
1746
+ const units = blocks.map((block, order) => {
1747
+ const unit = { members: [block], arrangement: { block: block.id }, zone: zoneOf(block), order };
1748
+ unitOf.set(block, unit);
1749
+ return unit;
1750
+ });
1751
+ for (const rule of STACK_RULES) {
1752
+ for (const [top, bottom] of rule.pairs(blocks)) {
1753
+ const topUnit = unitOf.get(top);
1754
+ const bottomUnit = unitOf.get(bottom);
1755
+ const canStack = topUnit !== bottomUnit && topUnit.members.length === 1 && bottomUnit.members.length === 1 && topUnit.zone === bottomUnit.zone;
1756
+ if (!canStack) continue;
1757
+ topUnit.members.push(bottom);
1758
+ topUnit.arrangement = { column: [{ block: top.id }, { block: bottom.id }] };
1759
+ topUnit.order = Math.min(topUnit.order, bottomUnit.order);
1760
+ unitOf.set(bottom, topUnit);
1761
+ units.splice(units.indexOf(bottomUnit), 1);
1762
+ }
1763
+ }
1764
+ return units;
1765
+ }
1766
+ function newspaperTrees(units, factsOf) {
1767
+ const unitOfBlock = /* @__PURE__ */ new Map();
1768
+ for (const unit of units) for (const member of unit.members) unitOfBlock.set(member.id, unit);
1769
+ const isFunctionUnit = (unit) => unit.members.some(isFunctionLike);
1770
+ const calledUnits = (unit) => {
1771
+ if (!isFunctionUnit(unit)) return [];
1772
+ const called = [];
1773
+ for (const member of unit.members) {
1774
+ for (const id of factsOf(member).calls) {
1775
+ const calledUnit = unitOfBlock.get(id);
1776
+ if (calledUnit && calledUnit !== unit && isFunctionUnit(calledUnit) && !called.includes(calledUnit)) called.push(calledUnit);
1777
+ }
1778
+ }
1779
+ return called;
1780
+ };
1781
+ const isCalledBySibling = new Set(units.flatMap(calledUnits));
1782
+ const claimed = /* @__PURE__ */ new Set();
1783
+ const tree = (unit, depth) => {
1784
+ if (depth >= NEWSPAPER_DEPTH) return unit.arrangement;
1785
+ const callees = calledUnits(unit).filter((callee) => !claimed.has(callee));
1786
+ for (const callee of callees) claimed.add(callee);
1787
+ if (!callees.length) return unit.arrangement;
1788
+ return { row: [unit.arrangement, { column: callees.map((callee) => tree(callee, depth + 1)) }] };
1789
+ };
1790
+ const trees = [];
1791
+ const starts = [...units.filter((unit) => !isCalledBySibling.has(unit)), ...units];
1792
+ for (const unit of starts) {
1793
+ if (claimed.has(unit)) continue;
1794
+ claimed.add(unit);
1795
+ const arrangement = tree(unit, 0);
1796
+ const members = [...units].filter((other) => other === unit || claimed.has(other) && contains(arrangement, other));
1797
+ trees.push({ ...unit, arrangement, members: members.flatMap((member) => member.members) });
1798
+ }
1799
+ return trees;
1800
+ }
1801
+ function orderByRelatedness(units, factsOf) {
1802
+ const relatedness = (a, b) => a.members.reduce((sum, x) => sum + b.members.reduce((inner, y) => inner + RELATEDNESS_RULES.reduce((total, rule) => total + rule.score(x, y, factsOf), 0), 0), 0);
1803
+ const left = [...units].sort((a, b) => a.order - b.order);
1804
+ const ordered = [];
1805
+ while (left.length) {
1806
+ const last = ordered[ordered.length - 1];
1807
+ let next = left[0];
1808
+ if (last) {
1809
+ let best = 0;
1810
+ for (const candidate of left) {
1811
+ const score = relatedness(last, candidate);
1812
+ if (score > best) {
1813
+ best = score;
1814
+ next = candidate;
1815
+ }
1816
+ }
1817
+ }
1818
+ left.splice(left.indexOf(next), 1);
1819
+ ordered.push(next);
1820
+ }
1821
+ return ordered;
1822
+ }
1823
+ function siblingFacts(blocks, text) {
1824
+ const idsByName = /* @__PURE__ */ new Map();
1825
+ for (const block of blocks) idsByName.set(block.name, [...idsByName.get(block.name) ?? [], block.id]);
1826
+ const facts = /* @__PURE__ */ new Map();
1827
+ for (const block of blocks) {
1828
+ const code = codeWithoutChildren(text, [block.start, block.end], block.children);
1829
+ const calls = [];
1830
+ const uses = /* @__PURE__ */ new Set();
1831
+ for (const match of code.matchAll(/[A-Za-z_$][\w$]*/g)) {
1832
+ const ids = idsByName.get(match[0])?.filter((id) => id !== block.id);
1833
+ if (!ids?.length) continue;
1834
+ const isCall = /^\s*\(/.test(code.slice(match.index + match[0].length, match.index + match[0].length + 40));
1835
+ for (const id of ids) {
1836
+ uses.add(id);
1837
+ if (isCall && !calls.includes(id)) calls.push(id);
1838
+ }
1839
+ }
1840
+ facts.set(block.id, { calls, uses });
1841
+ }
1842
+ return facts;
1843
+ }
1844
+ var measured = /* @__PURE__ */ new WeakMap();
1845
+ function measure(arrangement, sizeOf, gap) {
1846
+ if ("block" in arrangement) return sizeOf(arrangement.block);
1847
+ const known = measured.get(arrangement);
1848
+ if (known) return known;
1849
+ const size = measureGroup(arrangement, sizeOf, gap);
1850
+ measured.set(arrangement, size);
1851
+ return size;
1852
+ }
1853
+ function measureGroup(arrangement, sizeOf, gap) {
1854
+ const sizes = ("row" in arrangement ? arrangement.row : arrangement.column).map((item) => measure(item, sizeOf, gap));
1855
+ const gaps = gap * (sizes.length - 1);
1856
+ return "row" in arrangement ? { w: sizes.reduce((sum, size) => sum + size.w, 0) + gaps, h: Math.max(...sizes.map((size) => size.h)) } : { w: Math.max(...sizes.map((size) => size.w)), h: sizes.reduce((sum, size) => sum + size.h, 0) + gaps };
1857
+ }
1858
+ function place(arrangement, x, y, sizeOf, gap, positions) {
1859
+ if ("block" in arrangement) {
1860
+ positions.set(arrangement.block, { x: Math.round(x), y: Math.round(y) });
1861
+ return;
1862
+ }
1863
+ const isRow = "row" in arrangement;
1864
+ let offset = 0;
1865
+ for (const item of isRow ? arrangement.row : arrangement.column) {
1866
+ place(item, isRow ? x + offset : x, isRow ? y : y + offset, sizeOf, gap, positions);
1867
+ const size = measure(item, sizeOf, gap);
1868
+ offset += (isRow ? size.w : size.h) + gap;
1869
+ }
1870
+ }
1871
+ function contains(arrangement, unit) {
1872
+ if ("block" in arrangement) return unit.members.some((member) => member.id === arrangement.block);
1873
+ return ("row" in arrangement ? arrangement.row : arrangement.column).some((item) => contains(item, unit));
1874
+ }
1875
+
1876
+ // src/layout.ts
1877
+ var M = {
1878
+ headerH: 32,
1879
+ headerCharW: 7.9,
1880
+ // 13px monospace
1881
+ previewLineH: 15,
1882
+ previewCharW: 6.7,
1883
+ // 11px monospace
1884
+ previewPad: 10,
1885
+ pad: 14,
1886
+ // inner padding of containers
1887
+ gap: 28,
1888
+ // spacing between sibling blocks
1889
+ minW: 180,
1890
+ maxW: 460,
1891
+ miniH: 46,
1892
+ // collapsed container: faint mini-map of children
1893
+ fileHeader: 44,
1894
+ filePad: 48
1895
+ };
1896
+ function emptyLayout() {
1897
+ return { version: 1, blocks: {} };
1898
+ }
1899
+ function computeGeometry(model, layout, hints = {}, opts = {}) {
1900
+ const resolve = opts.resolveOverlaps ?? true;
1901
+ const geo = /* @__PURE__ */ new Map();
1902
+ const assigned = {};
1903
+ const collapsed = new Set(layout.collapsed ?? []);
1904
+ const pos = (id) => layout.blocks[id] ?? assigned[id];
1905
+ const measure2 = (b, hidden) => {
1906
+ const isCollapsed = collapsed.has(b.id) && b.children.length > 0;
1907
+ const kids = b.children.map((c) => measure2(c, hidden || isCollapsed));
1908
+ placeSiblings(kids, false);
1909
+ const head = headerChars(b);
1910
+ const previewChars = Math.max(0, ...b.preview.map((l) => l.length));
1911
+ const leafW = clamp(Math.ceil(Math.max(head * M.headerCharW, previewChars * M.previewCharW) + 28), M.minW, M.maxW);
1912
+ const lines = b.preview.length + (b.previewMore ? 1 : 0);
1913
+ const previewH = lines ? lines * M.previewLineH + M.previewPad : 4;
1914
+ let w = leafW;
1915
+ let h = M.headerH + previewH;
1916
+ let contentTop = h;
1917
+ if (kids.length && isCollapsed) {
1918
+ h += M.miniH;
1919
+ } else if (kids.length) {
1920
+ contentTop = h + 6;
1921
+ const right = Math.max(...kids.map((k) => k.x + k.w));
1922
+ const bottom = Math.max(...kids.map((k) => k.y + k.h));
1923
+ w = Math.max(leafW, right + M.pad * 2);
1924
+ h = contentTop + bottom + M.pad;
1925
+ }
1926
+ const g = { id: b.id, block: b, x: 0, y: 0, w, h, ax: 0, ay: 0, contentTop, collapsed: isCollapsed, hidden };
1927
+ geo.set(b.id, g);
1928
+ return g;
1929
+ };
1930
+ const placeSiblings = (sibs, isFile) => {
1931
+ if (!sibs.length) return;
1932
+ const isNeverLaidOut = sibs.every((g) => !layout.blocks[g.id] && !hints[g.id]);
1933
+ if (isNeverLaidOut) {
1934
+ const sizeOf = (id) => geo.get(id);
1935
+ const arranged = arrangeSiblings(sibs.map((g) => g.block), sizeOf, model.text, M.gap, isFile);
1936
+ for (const g of sibs) assigned[g.id] = arranged.get(g.id);
1937
+ }
1938
+ const placed = [];
1939
+ const unplaced = [];
1940
+ for (const g of sibs) {
1941
+ const p = pos(g.id);
1942
+ if (p) {
1943
+ g.x = p.x;
1944
+ g.y = p.y;
1945
+ placed.push(g);
1946
+ } else unplaced.push(g);
1947
+ }
1948
+ const spotByRules = newBlockSpots(sibs.map((sibling) => sibling.block), model.text);
1949
+ for (const g of unplaced) {
1950
+ const hint = hints[g.id];
1951
+ const ruleSpot = hint || !placed.length ? void 0 : spotByRules(g.block, placed);
1952
+ let p;
1953
+ if (hint) p = freeSpotNear(hint, g, placed, !isFile);
1954
+ else if (ruleSpot?.side === "below") p = freeSpotDown(pointBeside(ruleSpot, g), g, placed, !isFile);
1955
+ else if (ruleSpot) p = freeSpotNear(pointBeside(ruleSpot, g), g, placed, !isFile);
1956
+ else {
1957
+ const previousPlacedInFile = sibs.slice(0, sibs.indexOf(g)).reverse().find((sibling) => placed.includes(sibling));
1958
+ p = findFreeSpot(g, placed, previousPlacedInFile, !isFile);
1959
+ }
1960
+ g.x = p.x;
1961
+ g.y = p.y;
1962
+ assigned[g.id] = p;
1963
+ placed.push(g);
1964
+ }
1965
+ if (resolve) pushApart(sibs, assigned);
1966
+ };
1967
+ const roots = model.roots.map((r) => measure2(r, false));
1968
+ placeSiblings(roots, true);
1969
+ const order = [];
1970
+ const place2 = (g, ox, oy) => {
1971
+ g.ax = ox + g.x;
1972
+ g.ay = oy + g.y;
1973
+ order.push(g);
1974
+ for (const c of g.block.children) place2(geo.get(c.id), g.ax + M.pad, g.ay + g.contentTop);
1975
+ };
1976
+ for (const r of roots) place2(r, 0, 0);
1977
+ let minX = 0, minY = 0, maxX = 640, maxY = 360;
1978
+ if (roots.length) {
1979
+ minX = Math.min(...roots.map((g) => g.x));
1980
+ minY = Math.min(...roots.map((g) => g.y));
1981
+ maxX = Math.max(...roots.map((g) => g.x + g.w));
1982
+ maxY = Math.max(...roots.map((g) => g.y + g.h));
1983
+ }
1984
+ const frame = {
1985
+ x: minX - M.filePad,
1986
+ y: minY - M.filePad - M.fileHeader,
1987
+ w: Math.max(maxX - minX, 480) + M.filePad * 2,
1988
+ h: Math.max(maxY - minY, 240) + M.filePad * 2 + M.fileHeader
1989
+ };
1990
+ return { geo, order, frame, assigned };
1991
+ }
1992
+ function placeGhostBlocks(ghosts, ghostGeometry, currentModel, current) {
1993
+ const positions = /* @__PURE__ */ new Map();
1994
+ const placedGhostsByParent = /* @__PURE__ */ new Map();
1995
+ for (const ghost of ghosts) {
1996
+ const ghostGeo = ghostGeometry.geo.get(ghost.id);
1997
+ if (!ghostGeo) continue;
1998
+ const parentId = ghost.parentId ?? null;
1999
+ const parentGeo = parentId ? current.geo.get(parentId) : void 0;
2000
+ if (parentId && !parentGeo) continue;
2001
+ if (parentGeo && (parentGeo.hidden || parentGeo.collapsed)) continue;
2002
+ const currentSiblings = parentId ? currentModel.blocks.get(parentId).children : currentModel.roots;
2003
+ const placedGhosts = placedGhostsByParent.get(parentId) ?? [];
2004
+ const taken = [...currentSiblings.map((sibling) => current.geo.get(sibling.id)), ...placedGhosts];
2005
+ const spot = freeSpotNear({ x: ghostGeo.x, y: ghostGeo.y }, ghostGeo, taken, !!parentGeo);
2006
+ placedGhosts.push({ ...spot, w: ghostGeo.w, h: ghostGeo.h });
2007
+ placedGhostsByParent.set(parentId, placedGhosts);
2008
+ positions.set(ghost.id, parentGeo ? { x: parentGeo.ax + M.pad + spot.x, y: parentGeo.ay + parentGeo.contentTop + spot.y } : spot);
2009
+ }
2010
+ return positions;
2011
+ }
2012
+ function pushApart(sibs, assigned) {
2013
+ for (let iter = 0; iter < 100; iter++) {
2014
+ let moved = false;
2015
+ const sorted = [...sibs].sort((a, b) => a.y - b.y || a.x - b.x);
2016
+ for (let i = 0; i < sorted.length; i++) {
2017
+ for (let j = i + 1; j < sorted.length; j++) {
2018
+ const a = sorted[i];
2019
+ const b = sorted[j];
2020
+ if (!overlaps(a, b, 4)) continue;
2021
+ const down = a.y + a.h + M.gap - b.y;
2022
+ const right = b.x > a.x ? a.x + a.w + M.gap - b.x : Infinity;
2023
+ if (right < down) b.x += right;
2024
+ else b.y += down;
2025
+ assigned[b.id] = { x: Math.round(b.x), y: Math.round(b.y) };
2026
+ moved = true;
2027
+ }
2028
+ }
2029
+ if (!moved) break;
2030
+ }
2031
+ }
2032
+ function pointBeside(spot, size) {
2033
+ const { sibling, side } = spot;
2034
+ if (side === "right") return { x: sibling.x + sibling.w + M.gap, y: sibling.y };
2035
+ if (side === "below") return { x: sibling.x, y: sibling.y + sibling.h + M.gap };
2036
+ return { x: sibling.x, y: sibling.y - size.h - M.gap };
2037
+ }
2038
+ function headerChars(b) {
2039
+ const tags = b.tags.filter((t) => t !== "export").join(" ");
2040
+ return 4 + b.name.length + Math.min(b.detail.length, 40) + (tags ? tags.length + 2 : 0) + 3;
2041
+ }
2042
+ function overlaps(a, b, gap = M.gap / 2) {
2043
+ return a.x < b.x + b.w + gap && a.x + a.w + gap > b.x && a.y < b.y + b.h + gap && a.y + a.h + gap > b.y;
2044
+ }
2045
+ function fits(r, placed, clampPositive) {
2046
+ if (clampPositive && (r.x < 0 || r.y < 0)) return false;
2047
+ return !placed.some((p) => overlaps(r, p));
2048
+ }
2049
+ function findFreeSpot(g, placed, prev, clampPositive) {
2050
+ if (!placed.length) return { x: 0, y: 0 };
2051
+ const cands = [];
2052
+ if (prev) cands.push({ x: prev.x, y: prev.y + prev.h + M.gap }, { x: prev.x + prev.w + M.gap, y: prev.y });
2053
+ const origin = prev ?? placed[0];
2054
+ const others = [...placed].sort((a, b) => dist(a, origin) - dist(b, origin));
2055
+ for (const p of others) cands.push({ x: p.x, y: p.y + p.h + M.gap }, { x: p.x + p.w + M.gap, y: p.y });
2056
+ for (const c of cands) if (fits({ ...c, w: g.w, h: g.h }, placed, clampPositive)) return c;
2057
+ const bottom = Math.max(...placed.map((p) => p.y + p.h));
2058
+ return { x: Math.min(...placed.map((p) => p.x)), y: bottom + M.gap };
2059
+ }
2060
+ function freeSpotNear(at, g, placed, clampPositive) {
2061
+ const start = clampPositive ? { x: Math.max(0, at.x), y: Math.max(0, at.y) } : at;
2062
+ const others = placed.filter((p) => p.id === void 0 || p.id !== g.id);
2063
+ if (fits({ ...start, w: g.w, h: g.h }, others, clampPositive)) return round(start);
2064
+ const cands = [];
2065
+ for (const p of others) {
2066
+ cands.push(
2067
+ { x: p.x + p.w + M.gap, y: start.y },
2068
+ { x: start.x, y: p.y + p.h + M.gap },
2069
+ { x: p.x - g.w - M.gap, y: start.y },
2070
+ { x: start.x, y: p.y - g.h - M.gap },
2071
+ { x: p.x + p.w + M.gap, y: p.y },
2072
+ { x: p.x, y: p.y + p.h + M.gap }
2073
+ );
2074
+ }
2075
+ cands.sort((a, b) => dist(a, start) - dist(b, start));
2076
+ for (const c of cands) if (fits({ ...c, w: g.w, h: g.h }, others, clampPositive)) return round(c);
2077
+ const bottom = Math.max(...others.map((p) => p.y + p.h));
2078
+ return round({ x: start.x, y: bottom + M.gap });
2079
+ }
2080
+ function freeSpotDown(at, g, placed, clampPositive) {
2081
+ const spot = clampPositive ? { x: Math.max(0, at.x), y: Math.max(0, at.y) } : { ...at };
2082
+ const blockCoveringSpot = () => placed.find((p) => overlaps({ x: spot.x, y: spot.y, w: g.w, h: g.h }, p));
2083
+ for (let covering = blockCoveringSpot(); covering; covering = blockCoveringSpot()) spot.y = covering.y + covering.h + M.gap;
2084
+ return round(spot);
2085
+ }
2086
+ function dist(a, b) {
2087
+ return Math.hypot(a.x - b.x, a.y - b.y);
2088
+ }
2089
+ function clamp(v, lo, hi) {
2090
+ return Math.min(hi, Math.max(lo, v));
2091
+ }
2092
+ function round(p) {
2093
+ return { x: Math.round(p.x), y: Math.round(p.y) };
2094
+ }
2095
+ function carryRenames(prev, next, layout) {
2096
+ if (!prev) return false;
2097
+ const removedByParent = /* @__PURE__ */ new Map();
2098
+ const addedByParent = /* @__PURE__ */ new Map();
2099
+ for (const [id, b] of prev.blocks) if (!next.blocks.has(id)) push(removedByParent, b.parentId, b);
2100
+ for (const [id, b] of next.blocks) if (!prev.blocks.has(id)) push(addedByParent, b.parentId, b);
2101
+ let changed = false;
2102
+ for (const [parent, added] of addedByParent) {
2103
+ const removed = removedByParent.get(parent);
2104
+ if (added.length !== 1 || removed?.length !== 1) continue;
2105
+ const from = removed[0].id;
2106
+ const to = added[0].id;
2107
+ for (const key of Object.keys(layout.blocks)) {
2108
+ if (key === from || key.startsWith(from + "/")) {
2109
+ const nk = to + key.slice(from.length);
2110
+ if (!layout.blocks[nk]) layout.blocks[nk] = layout.blocks[key];
2111
+ changed = true;
2112
+ }
2113
+ }
2114
+ if (layout.collapsed?.includes(from)) layout.collapsed.push(to);
2115
+ }
2116
+ return changed;
2117
+ }
2118
+ function push(m, k, v) {
2119
+ const a = m.get(k);
2120
+ if (a) a.push(v);
2121
+ else m.set(k, [v]);
2122
+ }
2123
+ function serializeLayout(layout, model) {
2124
+ const blocks = {};
2125
+ for (const k of Object.keys(layout.blocks).sort()) {
2126
+ if (model && !model.blocks.has(k)) continue;
2127
+ const p = layout.blocks[k];
2128
+ blocks[k] = { x: Math.round(p.x), y: Math.round(p.y) };
2129
+ }
2130
+ const out = { version: 1, blocks };
2131
+ const collapsed = (layout.collapsed ?? []).filter((c) => !model || model.blocks.has(c)).sort();
2132
+ if (collapsed.length) out.collapsed = [...new Set(collapsed)];
2133
+ return JSON.stringify(out, null, 2) + "\n";
2134
+ }
2135
+
2136
+ // src/html.ts
2137
+ var policy;
2138
+ function trustedHtmlPolicy() {
2139
+ if (policy !== void 0) return policy;
2140
+ const factory = globalThis.trustedTypes;
2141
+ try {
2142
+ policy = factory ? factory.createPolicy("codecanvas", { createHTML: (html) => html }) : null;
2143
+ } catch {
2144
+ policy = null;
2145
+ }
2146
+ return policy;
2147
+ }
2148
+ function setHTML(element, html) {
2149
+ const htmlPolicy = trustedHtmlPolicy();
2150
+ element.innerHTML = htmlPolicy ? htmlPolicy.createHTML(html) : html;
2151
+ }
2152
+
2153
+ // src/styles.ts
2154
+ var CSS = (
2155
+ /* css */
2156
+ `
2157
+ .cc-root {
2158
+ --cc-bg: #15171c;
2159
+ --cc-dot: rgba(255,255,255,.07);
2160
+ --cc-frame-bg: rgba(255,255,255,.025);
2161
+ --cc-frame-border: rgba(255,255,255,.10);
2162
+ --cc-text: #e8eaee;
2163
+ --cc-muted: #9aa1ad;
2164
+ --cc-faint: #c4cad4;
2165
+ --cc-panel: #1f2229;
2166
+ --cc-panel-border: rgba(255,255,255,.12);
2167
+ --cc-accent: #7aa2ff;
2168
+ --cc-danger: #ff6b6b;
2169
+ --cc-shadow: 0 10px 30px rgba(0,0,0,.45);
2170
+
2171
+ --k-import: #6b7280; --k-import-head: #3b3f46; --k-import-bg: #25272c;
2172
+ --k-class: #34b27a; --k-class-head: #1d6b4a; --k-class-bg: rgba(52,178,122,.08);
2173
+ --k-function: #ec5fa5; --k-function-head: #8f2f62; --k-function-bg: rgba(236,95,165,.09);
2174
+ --k-variable: #4b8ef7; --k-variable-head: #23508f; --k-variable-bg: rgba(75,142,247,.09);
2175
+ --k-type: #d4a92a; --k-type-head: #6e5714; --k-type-bg: rgba(212,169,42,.08);
2176
+ --k-namespace: #9b8cf8; --k-namespace-head: #4d4291; --k-namespace-bg: rgba(155,140,248,.08);
2177
+ --k-statement: #7c8799; --k-statement-head: #39414f; --k-statement-bg: rgba(124,135,153,.08);
2178
+
2179
+ position: relative; overflow: hidden; width: 100%; height: 100%;
2180
+ background-color: var(--cc-bg);
2181
+ background-image: radial-gradient(var(--cc-dot) 1.2px, transparent 1.3px);
2182
+ color: var(--cc-text);
2183
+ font: 13px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
2184
+ touch-action: none; user-select: none; -webkit-user-select: none; outline: none;
2185
+ -webkit-tap-highlight-color: transparent;
2186
+ }
2187
+ .cc-root[data-theme="light"] {
2188
+ --cc-bg: #f6f7f9; --cc-dot: rgba(0,0,0,.09);
2189
+ --cc-frame-bg: rgba(255,255,255,.7); --cc-frame-border: rgba(0,0,0,.12);
2190
+ --cc-text: #1d2129; --cc-muted: #5d6573; --cc-faint: #39404c;
2191
+ --cc-panel: #ffffff; --cc-panel-border: rgba(0,0,0,.12); --cc-accent: #3366e6;
2192
+ --cc-shadow: 0 10px 30px rgba(20,30,50,.18);
2193
+ --k-import: #6b7280; --k-import-head: #4b5058; --k-import-bg: #eceef1;
2194
+ --k-class: #1f9963; --k-class-head: #1f9963; --k-class-bg: rgba(31,153,99,.07);
2195
+ --k-function: #d63e8c; --k-function-head: #d63e8c; --k-function-bg: rgba(214,62,140,.07);
2196
+ --k-variable: #2f6fe0; --k-variable-head: #2f6fe0; --k-variable-bg: rgba(47,111,224,.07);
2197
+ --k-type: #b58a0c; --k-type-head: #a87f08; --k-type-bg: rgba(181,138,12,.08);
2198
+ --k-namespace: #6f5ce6; --k-namespace-head: #6f5ce6; --k-namespace-bg: rgba(111,92,230,.07);
2199
+ --k-statement: #64708a; --k-statement-head: #64708a; --k-statement-bg: rgba(100,112,138,.07);
2200
+ }
2201
+ .cc-world { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
2202
+ .cc-root.cc-moving .cc-world { will-change: transform; }
2203
+ .cc-frame { position: absolute; border: 1px solid var(--cc-frame-border); background: var(--cc-frame-bg); border-radius: 18px; }
2204
+ .cc-frame-title { position: absolute; left: 20px; top: 12px; font: 600 15px/20px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); color: var(--cc-muted); white-space: nowrap; }
2205
+ .cc-empty { position: absolute; font-size: 15px; color: var(--cc-muted); white-space: nowrap; }
2206
+
2207
+ .cc-block {
2208
+ position: absolute; left: 0; top: 0; box-sizing: border-box; border-radius: 10px;
2209
+ border: 1.5px solid var(--k); background: var(--kbg); overflow: hidden; contain: layout style;
2210
+ cursor: default;
2211
+ }
2212
+ .cc-block.k-import { --k: var(--k-import); --kh: var(--k-import-head); --kbg: var(--k-import-bg); }
2213
+ .cc-block.k-class { --k: var(--k-class); --kh: var(--k-class-head); --kbg: var(--k-class-bg); }
2214
+ .cc-block.k-function { --k: var(--k-function); --kh: var(--k-function-head); --kbg: var(--k-function-bg); }
2215
+ .cc-block.k-variable { --k: var(--k-variable); --kh: var(--k-variable-head); --kbg: var(--k-variable-bg); }
2216
+ .cc-block.k-type { --k: var(--k-type); --kh: var(--k-type-head); --kbg: var(--k-type-bg); }
2217
+ .cc-block.k-namespace { --k: var(--k-namespace); --kh: var(--k-namespace-head); --kbg: var(--k-namespace-bg); }
2218
+ .cc-block.k-statement { --k: var(--k-statement); --kh: var(--k-statement-head); --kbg: var(--k-statement-bg); }
2219
+ .cc-block.k-import, .cc-root:not([data-theme="light"]) .cc-block.k-import { border-color: #555a63; }
2220
+
2221
+ .cc-head {
2222
+ display: flex; align-items: center; gap: 7px; height: 32px; padding: 0 10px; box-sizing: border-box;
2223
+ background: var(--kh); color: #fff; white-space: nowrap; overflow: hidden;
2224
+ font: 13px/32px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2225
+ }
2226
+ .cc-kind { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; opacity: .75; flex: none; }
2227
+ .cc-name { font-weight: 700; flex: none; max-width: 70%; overflow: hidden; text-overflow: ellipsis; }
2228
+ .cc-detail { opacity: .8; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 1 1 auto; }
2229
+ .cc-tags { font-size: 10px; opacity: .7; flex: none; }
2230
+ .cc-exp { width: 6px; height: 6px; border-radius: 50%; background: #fff; opacity: .8; flex: none; }
2231
+ .cc-fold {
2232
+ flex: none; margin-left: auto; width: 22px; height: 22px; border: 0; border-radius: 6px; padding: 0;
2233
+ background: rgba(255,255,255,.14); color: #fff; font-size: 12px; line-height: 22px; cursor: pointer;
2234
+ }
2235
+ .cc-fold:hover { background: rgba(255,255,255,.28); }
2236
+ .cc-prev {
2237
+ margin: 0; padding: 5px 12px 0; white-space: pre; overflow: hidden; color: var(--cc-faint); opacity: .5;
2238
+ font: 11px/15px var(--cc-font, "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
2239
+ }
2240
+ .cc-more { opacity: .7; font-style: italic; }
2241
+ .cc-mini { position: relative; margin: 6px 12px 0; height: 34px; opacity: .45; }
2242
+ .cc-mini > i { position: absolute; border-radius: 2px; border: 1px solid var(--cc-faint); background: rgba(255,255,255,.06); }
2243
+ .cc-mini > i.k-class { border-color: var(--k-class); } .cc-mini > i.k-function { border-color: var(--k-function); }
2244
+ .cc-mini > i.k-variable { border-color: var(--k-variable); } .cc-mini > i.k-type { border-color: var(--k-type); }
2245
+
2246
+ .cc-err {
2247
+ flex: none; min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box; border-radius: 9px;
2248
+ background: var(--cc-danger); color: #fff; font: 700 11px/18px ui-sans-serif, system-ui, sans-serif; text-align: center;
2249
+ }
2250
+ .cc-block.cc-has-error { border-color: var(--cc-danger); }
2251
+ .cc-root.lod-2 .cc-err { min-width: 34px; height: 34px; border-radius: 17px; font-size: 22px; line-height: 34px; }
2252
+ .cc-rename {
2253
+ font: inherit; font-weight: 700; color: #fff; background: rgba(0,0,0,.35); border: 1px solid var(--cc-accent);
2254
+ border-radius: 4px; padding: 0 4px; height: 22px; outline: none; user-select: text; -webkit-user-select: text; max-width: 100%;
2255
+ }
2256
+ .cc-rename.invalid { border-color: var(--cc-danger); }
2257
+ .cc-name:has(.cc-rename) { max-width: none; overflow: visible; }
2258
+
2259
+ /* merge conflicts */
2260
+ .cc-block.cc-conflict { border-color: #f0883e; box-shadow: 0 0 0 2px rgba(240, 136, 62, .35); }
2261
+ .cc-conflict-chip {
2262
+ flex: none; min-width: 18px; height: 18px; border-radius: 9px; background: #f0883e; color: #1b1300;
2263
+ font: 900 13px/18px ui-sans-serif, system-ui, sans-serif; text-align: center;
2264
+ }
2265
+ .cc-root.lod-2 .cc-conflict-chip { min-width: 34px; height: 34px; border-radius: 17px; font-size: 24px; line-height: 34px; }
2266
+ .cc-block.cc-conflict-incoming {
2267
+ --k: #f0883e; --kh: #8a4a17; --kbg: rgba(240, 136, 62, .08);
2268
+ border-style: dashed; pointer-events: none; opacity: .9;
2269
+ }
2270
+ .cc-diff-chip.incoming { background: #f0883e; }
2271
+ .cc-banner.cc-banner-conflicts { border-color: #f0883e; }
2272
+
2273
+ /* changes against a base version (setDiffBase) */
2274
+ .cc-block.cc-diff-unchanged { filter: saturate(.3) brightness(.92); }
2275
+ .cc-diff-chip {
2276
+ flex: none; min-width: 18px; height: 18px; padding: 0 4px; box-sizing: border-box; border-radius: 5px;
2277
+ font: 800 13px/18px ui-sans-serif, system-ui, sans-serif; text-align: center; color: #fff;
2278
+ }
2279
+ .cc-diff-chip.added { background: #2ea043; }
2280
+ .cc-diff-chip.changed { background: #d29922; }
2281
+ .cc-diff-chip.removed { background: var(--cc-danger); }
2282
+ .cc-root.lod-2 .cc-diff-chip { min-width: 34px; height: 34px; border-radius: 8px; font-size: 24px; line-height: 34px; }
2283
+ .cc-block.cc-diff-removed {
2284
+ --k: var(--cc-danger); --kh: #9c2f2f; --kbg: rgba(255, 107, 107, .10);
2285
+ border-style: dashed; pointer-events: none; opacity: .9;
2286
+ }
2287
+ .cc-root[data-theme="light"] .cc-block.cc-diff-removed { --kh: #d64545; --kbg: rgba(214, 69, 69, .08); }
2288
+ .cc-prev.cc-diff-prev { opacity: .95; }
2289
+ .cc-diff-prev > span { display: block; margin: 0 -12px; padding: 0 12px; }
2290
+ .cc-diff-prev .cc-line-same { opacity: .5; }
2291
+ .cc-diff-prev .cc-line-added { background: rgba(46, 160, 67, .30); }
2292
+ .cc-diff-prev .cc-line-removed { background: rgba(248, 81, 73, .26); text-decoration: line-through; text-decoration-color: rgba(248, 81, 73, .7); }
2293
+ .cc-diff-summary { padding: 0 8px; font-size: 12px; color: var(--cc-muted); white-space: nowrap; }
2294
+
2295
+ .cc-block.cc-sel { box-shadow: 0 0 0 2px var(--cc-bg), 0 0 0 4px var(--k); }
2296
+ .cc-block.cc-editing { box-shadow: 0 0 0 2px var(--cc-bg), 0 0 0 4px var(--cc-accent); }
2297
+ .cc-block.cc-dragging { box-shadow: var(--cc-shadow); opacity: .92; z-index: 9999 !important; }
2298
+ .cc-root.has-hl .cc-block.cc-linked { box-shadow: 0 0 0 2px var(--k); }
2299
+
2300
+ .cc-edges { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
2301
+ .cc-edge { fill: none; stroke-width: 1.5; opacity: .22; }
2302
+ .cc-edge.u-type { stroke-dasharray: 4 4; }
2303
+ .cc-root.has-hl .cc-edge { opacity: .07; }
2304
+ .cc-root.has-hl .cc-edge.hl { opacity: .95; stroke-width: 2.5; }
2305
+
2306
+ /* level of detail when zoomed out */
2307
+ .cc-root.lod-1 .cc-tags { visibility: hidden; }
2308
+ .cc-root.lod-2 .cc-prev, .cc-root.lod-2 .cc-detail, .cc-root.lod-2 .cc-mini { visibility: hidden; }
2309
+ .cc-root.lod-2 .cc-head { font-size: 26px; }
2310
+ .cc-root.lod-2 .cc-kind, .cc-root.lod-2 .cc-fold { display: none; }
2311
+ .cc-root.lod-2 .cc-frame-title { font-size: 40px; line-height: 44px; top: -8px; }
2312
+
2313
+ /* overlays (screen space) */
2314
+ .cc-banner {
2315
+ position: absolute; left: 50%; top: 12px; transform: translateX(-50%); z-index: 20; display: flex; gap: 10px; align-items: center;
2316
+ padding: 8px 12px; border-radius: 10px; background: var(--cc-panel); border: 1px solid var(--cc-danger); box-shadow: var(--cc-shadow);
2317
+ font-size: 13px; max-width: calc(100% - 32px);
2318
+ }
2319
+ .cc-banner[hidden], .cc-bar[hidden], .cc-refs[hidden] { display: none; }
2320
+ .cc-toast {
2321
+ position: absolute; left: 50%; bottom: 76px; transform: translateX(-50%); z-index: 30; padding: 8px 14px; border-radius: 10px;
2322
+ background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow); font-size: 13px;
2323
+ transition: opacity .2s; pointer-events: none; max-width: calc(100% - 32px);
2324
+ }
2325
+ .cc-bar {
2326
+ position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 20; display: flex; align-items: center; gap: 4px;
2327
+ padding: 5px; border-radius: 12px; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow);
2328
+ width: max-content; max-width: calc(100% - 24px); box-sizing: border-box; flex-wrap: wrap; justify-content: center;
2329
+ }
2330
+ .cc-bar .cc-chip { padding: 0 10px 0 6px; font: 600 12px/30px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); white-space: nowrap; display: flex; align-items: center; gap: 6px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; }
2331
+ .cc-bar .cc-chip i { width: 10px; height: 10px; border-radius: 3px; background: var(--k); flex: none; }
2332
+ .cc-btn {
2333
+ border: 0; background: transparent; color: var(--cc-text); font: 500 13px/30px inherit; font-family: inherit; padding: 0 10px; border-radius: 8px;
2334
+ cursor: pointer; white-space: nowrap; min-height: 32px;
2335
+ }
2336
+ .cc-btn:hover { background: rgba(127,127,127,.16); }
2337
+ .cc-btn.danger { color: var(--cc-danger); }
2338
+ .cc-btn.primary { background: var(--cc-accent); color: #fff; }
2339
+ .cc-sep { width: 1px; height: 20px; background: var(--cc-panel-border); margin: 0 2px; flex: none; }
2340
+ .cc-refs {
2341
+ position: absolute; left: 50%; bottom: 64px; transform: translateX(-50%); z-index: 21; width: min(360px, calc(100% - 24px)); max-height: 45%;
2342
+ overflow: auto; border-radius: 12px; background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow); padding: 6px;
2343
+ }
2344
+ .cc-refs h4 { margin: 8px 8px 4px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--cc-muted); }
2345
+ .cc-refs button {
2346
+ display: flex; width: 100%; align-items: center; gap: 8px; border: 0; background: transparent; color: var(--cc-text); text-align: left;
2347
+ padding: 6px 8px; border-radius: 8px; cursor: pointer; font: 12px/18px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace);
2348
+ }
2349
+ .cc-refs button:hover { background: rgba(127,127,127,.16); }
2350
+ .cc-refs button i { width: 10px; height: 10px; border-radius: 3px; background: var(--k); flex: none; }
2351
+ .cc-refs button span:last-child { margin-left: auto; color: var(--cc-muted); }
2352
+ .cc-refs button:disabled { cursor: default; opacity: .7; }
2353
+ .cc-refs button:disabled:hover { background: transparent; }
2354
+ .cc-refs .cc-file { color: var(--cc-muted); }
2355
+ .cc-refs .cc-error-row { --k: var(--cc-danger); align-items: flex-start; }
2356
+ .cc-refs .cc-error-row i { border-radius: 50%; margin-top: 4px; }
2357
+ .cc-refs .cc-error-row span:nth-child(2) { white-space: pre-wrap; font-family: ui-sans-serif, system-ui, sans-serif; }
2358
+ .cc-refs .cc-error-row span:last-child { white-space: nowrap; }
2359
+ .cc-refs .none { padding: 4px 8px 8px; color: var(--cc-muted); font-size: 12px; }
2360
+
2361
+ .cc-editor {
2362
+ position: absolute; z-index: 25; display: flex; flex-direction: column; border-radius: 12px; overflow: hidden;
2363
+ background: var(--cc-panel); border: 1px solid var(--cc-panel-border); box-shadow: var(--cc-shadow);
2364
+ }
2365
+ .cc-editor-head { display: flex; align-items: center; gap: 8px; padding: 4px 6px 4px 12px; border-bottom: 1px solid var(--cc-panel-border); }
2366
+ .cc-editor-head .cc-chip { flex: 1; min-width: 0; font: 600 12px/30px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); display: flex; align-items: center; gap: 6px; overflow: hidden; white-space: nowrap; }
2367
+ .cc-editor-head .cc-chip i { width: 10px; height: 10px; border-radius: 3px; background: var(--k); flex: none; }
2368
+ .cc-editor-body { flex: 1; min-height: 0; overflow: auto; user-select: text; -webkit-user-select: text; touch-action: auto; }
2369
+ .cc-editor-hint { font-size: 11px; color: var(--cc-muted); white-space: nowrap; }
2370
+ @media (max-width: 640px) { .cc-editor-hint { display: none; } }
2371
+ `
2372
+ );
2373
+ var injected = false;
2374
+ function injectStyles(doc = document) {
2375
+ if (injected) return;
2376
+ injected = true;
2377
+ const s = doc.createElement("style");
2378
+ s.dataset.codecanvas = "";
2379
+ s.textContent = CSS;
2380
+ doc.head.appendChild(s);
2381
+ }
2382
+
2383
+ // src/canvas.ts
2384
+ var KIND_LABEL = {
2385
+ import: "import",
2386
+ class: "class",
2387
+ function: "fn",
2388
+ variable: "var",
2389
+ type: "type",
2390
+ namespace: "ns",
2391
+ statement: "code"
2392
+ };
2393
+ var DIFF_CHIPS = {
2394
+ added: '<span class="cc-diff-chip added" title="Added">+</span>',
2395
+ changed: '<span class="cc-diff-chip changed" title="Changed">~</span>',
2396
+ removed: '<span class="cc-diff-chip removed" title="Removed">\u2212</span>',
2397
+ incoming: '<span class="cc-diff-chip incoming" title="Incoming">+</span>',
2398
+ containsChanges: "",
2399
+ unchanged: ""
2400
+ };
2401
+ var LABEL_TAGS = /* @__PURE__ */ new Set(["const", "let", "var", "interface", "type", "enum", "namespace", "get", "set"]);
2402
+ var MIN_SCALE = 0.04;
2403
+ var MAX_SCALE = 4;
2404
+ var CodeCanvas = class {
2405
+ constructor(container, opts) {
2406
+ this.opts = opts;
2407
+ this.project = opts.project ?? new Project();
2408
+ injectStyles(container.ownerDocument);
2409
+ const root = this.root = el("div", "cc-root");
2410
+ root.tabIndex = 0;
2411
+ root.dataset.theme = opts.theme ?? "dark";
2412
+ this.world = el("div", "cc-world");
2413
+ this.frameEl = el("div", "cc-frame");
2414
+ this.blocksEl = el("div", "cc-blocks");
2415
+ this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2416
+ this.svg.classList.add("cc-edges");
2417
+ setHTML(this.svg, `<defs>${["import", "class", "function", "variable", "type", "namespace", "statement"].map(
2418
+ (k) => `<marker id="cc-arrow-${k}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="var(--k-${k})"/></marker>`
2419
+ ).join("")}</defs>`);
2420
+ this.world.append(this.frameEl, this.blocksEl, this.svg);
2421
+ this.banner = el("div", "cc-banner");
2422
+ this.banner.hidden = true;
2423
+ this.toastEl = el("div", "cc-toast");
2424
+ this.toastEl.style.opacity = "0";
2425
+ this.bar = el("div", "cc-bar");
2426
+ this.refsEl = el("div", "cc-refs");
2427
+ this.refsEl.hidden = true;
2428
+ root.append(this.world, this.banner, this.refsEl, this.bar, this.toastEl);
2429
+ container.appendChild(root);
2430
+ this.bindEvents();
2431
+ this.ro = new ResizeObserver(() => this.fittedToView ? this.fit(false) : this.queueCamera());
2432
+ this.ro.observe(root);
2433
+ this.barObserver = new ResizeObserver(([entry]) => {
2434
+ const barTop = entry.borderBoxSize?.[0]?.blockSize ?? this.bar.offsetHeight;
2435
+ this.refsEl.style.bottom = barTop + 28 + "px";
2436
+ this.toastEl.style.bottom = barTop + 40 + "px";
2437
+ });
2438
+ this.barObserver.observe(this.bar);
2439
+ this.renderBar();
2440
+ }
2441
+ opts;
2442
+ root;
2443
+ world;
2444
+ frameEl;
2445
+ blocksEl;
2446
+ svg;
2447
+ bar;
2448
+ refsEl;
2449
+ banner;
2450
+ toastEl;
2451
+ path = "";
2452
+ text = "";
2453
+ /** Latest parse. */
2454
+ model = null;
2455
+ /** What's on screen: the latest parse without syntax errors. */
2456
+ view = null;
2457
+ layout = emptyLayout();
2458
+ geom = null;
2459
+ hints = {};
2460
+ cam = { x: 0, y: 0, s: 1 };
2461
+ camQueued = false;
2462
+ movingTimer = 0;
2463
+ anim = 0;
2464
+ lod = -1;
2465
+ /** The camera still shows the whole file as `fit` left it: resizing the view fits it again. */
2466
+ fittedToView = false;
2467
+ els = /* @__PURE__ */ new Map();
2468
+ edges = [];
2469
+ selected = null;
2470
+ hovered = null;
2471
+ refsOpen = false;
2472
+ project;
2473
+ /** Errors on screen: type errors from the last resolve, or syntax errors mapped onto the last good view. */
2474
+ diagnostics = [];
2475
+ incomingUsages = [];
2476
+ /** The version changes are shown against (`setDiffBase`), and its geometry, computed once. */
2477
+ diffBase = null;
2478
+ diffCache = null;
2479
+ /** Merge conflicts in the text. The canvas then shows its current side, marking the blocks they touch. */
2480
+ conflicts = null;
2481
+ ghostBlockEls = { removed: [], incoming: [] };
2482
+ /** Errors from the host (`setDiagnostics`), shown instead of the project's type errors. */
2483
+ hostDiagnostics = null;
2484
+ renaming = null;
2485
+ /** The layout before the last Arrange, until the next layout change: Undo arrange puts it back. */
2486
+ layoutBeforeArrange = null;
2487
+ /** Where the camera was before the block editor moved it out of the way, and where it moved it to. */
2488
+ cameraBeforeEditor = null;
2489
+ editing = null;
2490
+ clip = null;
2491
+ pointers = /* @__PURE__ */ new Map();
2492
+ gesture = null;
2493
+ lastTap = { t: 0, x: 0, y: 0, id: "" };
2494
+ lastPointer = null;
2495
+ toastTimer = 0;
2496
+ ro;
2497
+ barObserver;
2498
+ cleanup = [];
2499
+ // ---------------------------------------------------------------- public API
2500
+ /** Show a file. Fits it to the view. */
2501
+ openFile(path, text, layout) {
2502
+ this.closeEditor();
2503
+ this.path = path;
2504
+ this.text = text;
2505
+ this.layout = layout ? structuredClone(layout) : emptyLayout();
2506
+ this.model = this.view = null;
2507
+ this.selected = this.hovered = null;
2508
+ this.refsOpen = false;
2509
+ this.renaming = null;
2510
+ this.diagnostics = [];
2511
+ this.hostDiagnostics = null;
2512
+ this.diffBase = null;
2513
+ this.diffCache = null;
2514
+ this.conflicts = null;
2515
+ this.layoutBeforeArrange = null;
2516
+ this.incomingUsages = [];
2517
+ this.hints = {};
2518
+ for (const { el: el2 } of this.els.values()) el2.remove();
2519
+ this.els.clear();
2520
+ this.parseNow();
2521
+ this.fit(false);
2522
+ this.renderBar();
2523
+ }
2524
+ /** The text changed outside the canvas (e.g. the text editor). */
2525
+ setText(text) {
2526
+ if (text === this.text) return;
2527
+ const isEditedByOpenEditor = !!this.editing?.handle.editsFile;
2528
+ if (isEditedByOpenEditor) this.followEditedRange(this.text, text);
2529
+ else if (this.editing) this.closeEditor();
2530
+ this.cancelRename();
2531
+ this.text = text;
2532
+ this.parseNow();
2533
+ }
2534
+ /**
2535
+ * Show the changes since a base version of the file (e.g. the last commit): unchanged blocks are
2536
+ * desaturated, added and changed ones get a chip, removed ones show in red where they were.
2537
+ * `layout` is the base version's saved layout, for placing removed blocks. Null hides the changes.
2538
+ */
2539
+ setDiffBase(base) {
2540
+ this.diffBase = base ? { model: parseStructure(this.path, base.text), layout: base.layout ?? null } : null;
2541
+ this.diffCache = null;
2542
+ this.render();
2543
+ this.renderBar();
2544
+ }
2545
+ get isShowingChanges() {
2546
+ return !!this.diffBase;
2547
+ }
2548
+ conflictsOf(hunks, current, currentModel) {
2549
+ const incoming = conflictSideText(this.text, hunks, "incoming");
2550
+ const incomingModel = parseStructure(this.path, incoming.text);
2551
+ const hunksByBlock = /* @__PURE__ */ new Map();
2552
+ const markTouched = (id, hunkIndex) => {
2553
+ const list = hunksByBlock.get(id) ?? [];
2554
+ if (!list.includes(hunkIndex)) list.push(hunkIndex);
2555
+ hunksByBlock.set(id, list);
2556
+ };
2557
+ const incomingOnly = [];
2558
+ hunks.forEach((_, hunkIndex) => {
2559
+ for (const block of innermostBlocksTouching(currentModel, current.hunkRanges[hunkIndex])) markTouched(block.id, hunkIndex);
2560
+ for (const block of innermostBlocksTouching(incomingModel, incoming.hunkRanges[hunkIndex])) {
2561
+ let outermostNew = currentModel.blocks.has(block.id) ? void 0 : block;
2562
+ while (outermostNew?.parentId && !currentModel.blocks.has(outermostNew.parentId)) outermostNew = incomingModel.blocks.get(outermostNew.parentId);
2563
+ if (!outermostNew) markTouched(block.id, hunkIndex);
2564
+ else if (!incomingOnly.includes(outermostNew)) incomingOnly.push(outermostNew);
2565
+ }
2566
+ });
2567
+ return { hunks, current, incomingModel, hunksByBlock, incomingOnly };
2568
+ }
2569
+ /** Resolve the conflicts that touch a block: their sections become the chosen side. */
2570
+ keepConflictSide(id, choice) {
2571
+ const conflicts = this.conflicts;
2572
+ const hunkIndexes = conflicts?.hunksByBlock.get(id);
2573
+ if (!conflicts || !hunkIndexes) return;
2574
+ const edits = hunkIndexes.map((index) => resolveConflict(this.text, conflicts.hunks[index], choice));
2575
+ this.commit(applyEdits(this.text, edits));
2576
+ const remaining = this.conflicts?.hunks.length ?? 0;
2577
+ this.toast(remaining ? `Resolved. ${plural(remaining, "conflict")} left` : "All conflicts in this file are resolved");
2578
+ }
2579
+ /** While changes are shown: the block's code in the base version, '' if it's new. */
2580
+ baseCodeOf(block) {
2581
+ const diff = this.currentDiff();
2582
+ if (!diff || !block) return void 0;
2583
+ const baseBlock = diff.base.blocks.get(block.id);
2584
+ return baseBlock ? diff.base.text.slice(baseBlock.start, baseBlock.end) : "";
2585
+ }
2586
+ /** The changes of what's on screen, while a diff base is set. */
2587
+ currentDiff() {
2588
+ if (!this.diffBase || !this.view) return null;
2589
+ if (this.diffCache?.view !== this.view) this.diffCache = { view: this.view, diff: diffModels(this.diffBase.model, this.view) };
2590
+ return this.diffCache.diff;
2591
+ }
2592
+ /**
2593
+ * Errors reported by the host, e.g. VS Code's Problems. While set, the canvas shows these instead of
2594
+ * resolving type errors itself. Pass null to go back to the project's own.
2595
+ */
2596
+ setDiagnostics(diagnostics) {
2597
+ this.hostDiagnostics = diagnostics;
2598
+ if (!this.view || this.stale || this.conflicts) return;
2599
+ this.diagnostics = this.hostDiagnosticsOnBlocks(this.view);
2600
+ this.updateErrorMarkers();
2601
+ this.renderBar();
2602
+ }
2603
+ setLayout(layout) {
2604
+ this.layout = structuredClone(layout);
2605
+ this.relayout();
2606
+ }
2607
+ getLayout() {
2608
+ return structuredClone(this.layout);
2609
+ }
2610
+ getModel() {
2611
+ return this.model;
2612
+ }
2613
+ /** Re-resolve usages and errors, e.g. after other files of the project changed. */
2614
+ refreshProject() {
2615
+ if (this.view && !this.stale) this.scheduleUsages(this.view, true);
2616
+ }
2617
+ getDiagnostics() {
2618
+ return [...this.diagnostics];
2619
+ }
2620
+ setTheme(theme) {
2621
+ this.root.dataset.theme = theme;
2622
+ }
2623
+ get scale() {
2624
+ return this.cam.s;
2625
+ }
2626
+ /** Lay the whole file out again by the layout rules (layoutRules.ts), forgetting saved positions. */
2627
+ arrange() {
2628
+ if (!this.view) return;
2629
+ const before = structuredClone(this.layout);
2630
+ this.layout = { ...emptyLayout(), collapsed: this.layout.collapsed };
2631
+ this.hints = {};
2632
+ this.relayout();
2633
+ this.emitLayout("user");
2634
+ this.layoutBeforeArrange = before;
2635
+ this.fit();
2636
+ this.renderBar();
2637
+ }
2638
+ undoArrange() {
2639
+ const before = this.layoutBeforeArrange;
2640
+ if (!before) return;
2641
+ this.layout = before;
2642
+ this.relayout();
2643
+ this.emitLayout("user");
2644
+ this.fit();
2645
+ this.renderBar();
2646
+ }
2647
+ /** Fit the whole file in the view. */
2648
+ fit(animate = true) {
2649
+ if (!this.geom) return;
2650
+ const f = this.geom.frame;
2651
+ const { width: vw, height: vh } = this.root.getBoundingClientRect();
2652
+ if (!vw || !vh) {
2653
+ requestAnimationFrame(() => this.root.isConnected && this.fit(animate));
2654
+ return;
2655
+ }
2656
+ const pad = 24;
2657
+ const bottom = 64;
2658
+ const s = clamp2(Math.min((vw - pad * 2) / f.w, (vh - pad - bottom) / f.h, 1.1), MIN_SCALE, MAX_SCALE);
2659
+ const to = { s, x: (vw - f.w * s) / 2 - f.x * s, y: pad + (vh - pad - bottom - f.h * s) / 2 - f.y * s };
2660
+ this.moveCamera(to, animate);
2661
+ this.fittedToView = true;
2662
+ }
2663
+ zoomBy(factor) {
2664
+ const r = this.root.getBoundingClientRect();
2665
+ this.zoomAt({ x: r.width / 2, y: r.height / 2 }, factor, true);
2666
+ }
2667
+ /** Center a block and select it. */
2668
+ reveal(id) {
2669
+ const g = this.geom?.geo.get(id);
2670
+ if (!g) return;
2671
+ this.select(id);
2672
+ const { width: vw, height: vh } = this.root.getBoundingClientRect();
2673
+ const s = clamp2(Math.min(Math.max(this.cam.s, 0.85), (vw - 80) / g.w, (vh - 160) / g.h), MIN_SCALE, 1.4);
2674
+ this.moveCamera({ s, x: vw / 2 - (g.ax + g.w / 2) * s, y: (vh - 60) / 2 - (g.ay + g.h / 2) * s }, true);
2675
+ }
2676
+ select(id) {
2677
+ if (id === this.selected) return;
2678
+ this.selected = id;
2679
+ if (!id) this.refsOpen = false;
2680
+ this.updateHighlight();
2681
+ this.renderBar();
2682
+ this.opts.onSelect?.(id && this.view?.blocks.get(id) || null);
2683
+ }
2684
+ destroy() {
2685
+ this.closeEditor();
2686
+ this.ro.disconnect();
2687
+ this.barObserver.disconnect();
2688
+ for (const c of this.cleanup) c();
2689
+ this.root.remove();
2690
+ }
2691
+ // ---------------------------------------------------------------- model
2692
+ usageTimer = 0;
2693
+ parseNow() {
2694
+ const hunks = this.text.includes("<<<<<<<") ? findConflicts(this.text) : [];
2695
+ const currentSide = hunks.length ? conflictSideText(this.text, hunks, "current") : null;
2696
+ const m = currentSide ? parseStructure(this.path, currentSide.text, this.model) : this.project.setFile(this.path, this.text);
2697
+ this.model = m;
2698
+ this.conflicts = currentSide ? this.conflictsOf(hunks, currentSide, m) : null;
2699
+ const stale = m.syntaxErrors > 0 && this.view !== null;
2700
+ if (this.conflicts) this.diagnostics = [];
2701
+ else if (stale) this.diagnostics = this.syntaxDiagnosticsOnView(m);
2702
+ else if (m.syntaxErrors) this.diagnostics = m.diagnostics;
2703
+ else if (this.hostDiagnostics) this.diagnostics = this.hostDiagnosticsOnBlocks(m);
2704
+ else this.diagnostics = this.diagnostics.filter((d) => !d.blockId || m.blocks.has(d.blockId));
2705
+ if (!stale) {
2706
+ if (this.view) m.usages = this.view.usages.filter((u) => m.blocks.has(u.from) && m.blocks.has(u.to));
2707
+ if (carryRenames(this.view, m, this.layout)) this.emitLayout("automatic");
2708
+ this.view = m;
2709
+ if (this.selected && !m.blocks.has(this.selected)) this.selected = null;
2710
+ if (this.editing) {
2711
+ const b = [...m.blocks.values()].find((x) => x.start === this.editing.start);
2712
+ this.editing.id = b?.id ?? null;
2713
+ if (b && this.selected !== b.id) this.selected = b.id;
2714
+ }
2715
+ this.relayout();
2716
+ if (!this.conflicts) this.scheduleUsages(m);
2717
+ }
2718
+ const isShowingConflicts = !!this.conflicts && !stale;
2719
+ this.banner.hidden = !stale && !isShowingConflicts;
2720
+ this.banner.classList.toggle("cc-banner-conflicts", isShowingConflicts);
2721
+ if (isShowingConflicts) {
2722
+ setHTML(this.banner, `<span>${plural(this.conflicts.hunks.length, "merge conflict")} in this file. Select a block marked ! to keep a side, or edit it.</span>`);
2723
+ } else if (stale) {
2724
+ this.updateErrorMarkers();
2725
+ setHTML(this.banner, `<span>Syntax error on line ${m.firstErrorLine}. The diagram is paused until it's fixed.</span>`);
2726
+ if (!this.editing) {
2727
+ const b = button("Edit file", () => this.openRangeEditor(0, this.text.length, null), "primary");
2728
+ this.banner.appendChild(b);
2729
+ }
2730
+ }
2731
+ this.renderBar();
2732
+ }
2733
+ /** Usage links and type errors go through the project's language service: only once typing pauses. */
2734
+ scheduleUsages(m, soon = false) {
2735
+ clearTimeout(this.usageTimer);
2736
+ const run = async () => {
2737
+ if (this.view !== m) return;
2738
+ const resolution = await this.project.resolve(this.path, { diagnostics: !this.hostDiagnostics });
2739
+ if (this.view !== m) return;
2740
+ m.usages = resolution.usages;
2741
+ this.incomingUsages = resolution.incoming;
2742
+ this.diagnostics = this.hostDiagnostics ? this.hostDiagnosticsOnBlocks(m) : resolution.diagnostics;
2743
+ if (!this.geom) return;
2744
+ this.renderEdges();
2745
+ this.updateHighlight();
2746
+ this.updateErrorMarkers();
2747
+ this.renderBar();
2748
+ };
2749
+ const pauseMs = soon ? 0 : m.text.length < 2e4 ? 30 : 180;
2750
+ this.usageTimer = window.setTimeout(() => void run(), pauseMs);
2751
+ }
2752
+ hostDiagnosticsOnBlocks(model) {
2753
+ return (this.hostDiagnostics ?? []).map((d) => ({ ...d, blockId: blockAt(model, d.start)?.id ?? null }));
2754
+ }
2755
+ /** The text changed while an editor on the file is open: keep tracking the part of the file it shows. */
2756
+ followEditedRange(before, after) {
2757
+ const editing = this.editing;
2758
+ const change = changedRegion2(before, after);
2759
+ const lengthChange = after.length - before.length;
2760
+ const isBeforeEditedRange = change.end <= editing.start && change.start < editing.start;
2761
+ const isInsideEditedRange = change.start >= editing.start && change.start <= editing.start + editing.len;
2762
+ if (isBeforeEditedRange) editing.start += lengthChange;
2763
+ else if (isInsideEditedRange) editing.len = Math.max(0, editing.len + lengthChange);
2764
+ }
2765
+ /** While the syntax is broken the last good view stays on screen: put its errors on the closest block it still has. */
2766
+ syntaxDiagnosticsOnView(model) {
2767
+ const closestShownBlock = (id) => {
2768
+ let current = id;
2769
+ while (current && !this.view?.blocks.has(current)) current = model.blocks.get(current)?.parentId ?? null;
2770
+ return current;
2771
+ };
2772
+ return model.diagnostics.map((d) => ({ ...d, blockId: closestShownBlock(d.blockId) }));
2773
+ }
2774
+ get stale() {
2775
+ return !!this.model && this.model !== this.view;
2776
+ }
2777
+ /** Apply a text edit made on the canvas. */
2778
+ commit(text, otherFiles) {
2779
+ this.text = text;
2780
+ this.opts.onTextChange(text, otherFiles);
2781
+ this.parseNow();
2782
+ }
2783
+ relayout() {
2784
+ if (!this.view) return;
2785
+ const g = computeGeometry(this.view, this.layout, this.hints);
2786
+ this.geom = g;
2787
+ const assigned = Object.keys(g.assigned);
2788
+ if (assigned.length) {
2789
+ const isPlacedByUser = assigned.some((id) => id in this.hints);
2790
+ for (const id of assigned) {
2791
+ this.layout.blocks[id] = g.assigned[id];
2792
+ delete this.hints[id];
2793
+ }
2794
+ this.emitLayout(isPlacedByUser ? "user" : "automatic");
2795
+ }
2796
+ this.render();
2797
+ }
2798
+ emitLayout(reason) {
2799
+ if (reason === "user") this.layoutBeforeArrange = null;
2800
+ this.opts.onLayoutChange(structuredClone(this.layout), reason);
2801
+ }
2802
+ // ---------------------------------------------------------------- rendering
2803
+ render() {
2804
+ const g = this.geom;
2805
+ const view = this.view;
2806
+ if (!g || !view) return;
2807
+ const f = g.frame;
2808
+ Object.assign(this.frameEl.style, { transform: `translate(${f.x}px,${f.y}px)`, width: f.w + "px", height: f.h + "px" });
2809
+ setHTML(this.frameEl, `<div class="cc-frame-title">${esc(this.path)}</div>${view.roots.length ? "" : `<div class="cc-empty" style="left:${M.filePad}px;top:${M.filePad + M.fileHeader}px">Empty file. Double-click (or double-tap) to create a function.</div>`}`);
2810
+ const seen = /* @__PURE__ */ new Set();
2811
+ const diff = this.currentDiff();
2812
+ for (const geo of g.order) {
2813
+ const id = geo.id;
2814
+ seen.add(id);
2815
+ let entry = this.els.get(id);
2816
+ if (geo.hidden) {
2817
+ if (entry) entry.el.style.display = "none";
2818
+ continue;
2819
+ }
2820
+ if (!entry) {
2821
+ const e = el("div", "cc-block");
2822
+ e.dataset.id = id;
2823
+ this.blocksEl.appendChild(e);
2824
+ entry = { el: e, sig: "", box: "" };
2825
+ this.els.set(id, entry);
2826
+ }
2827
+ const b = geo.block;
2828
+ const mini = geo.collapsed ? this.miniMap(geo) : "";
2829
+ const change = diff?.changes.get(id);
2830
+ const changedLines = change === "changed" ? this.changedLinesPreview(diff, b) : "";
2831
+ const isConflicted = !!this.conflicts?.hunksByBlock.has(id);
2832
+ const sig = `${b.kind}|${b.name}|${b.detail}|${b.tags.join()}|${b.preview.join("\n")}|${b.previewMore}|${geo.collapsed}|${b.children.length}|${mini}|${change ?? ""}|${changedLines}|${isConflicted}`;
2833
+ if (sig !== entry.sig) {
2834
+ entry.sig = sig;
2835
+ entry.el.className = `cc-block k-${b.kind}${change ? ` cc-diff-${change}` : ""}${isConflicted ? " cc-conflict" : ""}`;
2836
+ setHTML(entry.el, this.blockHTML(b, geo, mini, change, changedLines, isConflicted));
2837
+ }
2838
+ const box = `${geo.ax},${geo.ay},${geo.w},${geo.h},${b.depth}`;
2839
+ if (box !== entry.box) {
2840
+ entry.box = box;
2841
+ const s = entry.el.style;
2842
+ s.transform = `translate(${geo.ax}px,${geo.ay}px)`;
2843
+ s.width = geo.w + "px";
2844
+ s.height = geo.h + "px";
2845
+ s.zIndex = String(b.depth + 1);
2846
+ s.display = "";
2847
+ }
2848
+ }
2849
+ for (const [id, entry] of this.els) {
2850
+ if (!seen.has(id)) {
2851
+ entry.el.remove();
2852
+ this.els.delete(id);
2853
+ }
2854
+ }
2855
+ this.renderRemovedBlocks(diff);
2856
+ this.renderIncomingBlocks();
2857
+ this.renderEdges();
2858
+ this.updateHighlight();
2859
+ this.updateErrorMarkers();
2860
+ }
2861
+ /** Blocks only the incoming side of a conflict has, drawn dashed where they would go. */
2862
+ renderIncomingBlocks() {
2863
+ const conflicts = this.conflicts;
2864
+ if (!conflicts?.incomingOnly.length || !this.geom) return this.drawGhostBlocks("incoming", [], null);
2865
+ conflicts.geometry ??= computeGeometry(conflicts.incomingModel, this.layout, {}, { resolveOverlaps: false });
2866
+ this.drawGhostBlocks("incoming", conflicts.incomingOnly, conflicts.geometry);
2867
+ }
2868
+ /** A changed block's preview: its changed lines with a line of context, as many as fit. */
2869
+ changedLinesPreview(diff, block) {
2870
+ const lines = diffOwnCode(diff.base, diff.base.blocks.get(block.id), this.view, block);
2871
+ const fitting = Math.max(1, block.preview.length);
2872
+ const preview = diffPreview(lines, fitting);
2873
+ const marker = { same: " ", added: "+", removed: "\u2212" };
2874
+ const sharedIndent = Math.min(...preview.lines.map((line) => /^ */.exec(line.text)[0].length));
2875
+ const rows = preview.lines.map((line) => `<span class="cc-line-${line.kind}">${marker[line.kind]} ${esc(line.text.slice(sharedIndent))}</span>`);
2876
+ const more = preview.more ? `<span class="cc-more">\u2026 ${preview.more} more changed line${preview.more > 1 ? "s" : ""}</span>` : "";
2877
+ return rows.join("") + more;
2878
+ }
2879
+ /** Removed blocks stay visible, in red, where they were in the base version. They can't be selected. */
2880
+ renderRemovedBlocks(diff) {
2881
+ if (!diff?.removed.length || !this.diffBase || !this.geom) return this.drawGhostBlocks("removed", [], null);
2882
+ this.diffBase.geometry ??= computeGeometry(diff.base, this.diffBase.layout ?? this.layout, {}, { resolveOverlaps: false });
2883
+ this.drawGhostBlocks("removed", diff.removed, this.diffBase.geometry);
2884
+ }
2885
+ /** Blocks of another version (removed, or incoming in a conflict): drawn, but not part of this file. */
2886
+ drawGhostBlocks(kind, ghosts, ghostGeometry) {
2887
+ for (const ghostEl of this.ghostBlockEls[kind]) ghostEl.remove();
2888
+ this.ghostBlockEls[kind] = [];
2889
+ if (!ghostGeometry || !this.geom || !this.view) return;
2890
+ const positions = placeGhostBlocks(ghosts, ghostGeometry, this.view, this.geom);
2891
+ for (const block of ghosts) {
2892
+ const position = positions.get(block.id);
2893
+ const ghostGeo = ghostGeometry.geo.get(block.id);
2894
+ if (!position || !ghostGeo) continue;
2895
+ const ghostEl = el("div", `cc-block k-${block.kind} ${kind === "removed" ? "cc-diff-removed" : "cc-conflict-incoming"}`);
2896
+ setHTML(ghostEl, this.blockHTML(block, { ...ghostGeo, collapsed: true }, "", kind));
2897
+ Object.assign(ghostEl.style, { transform: `translate(${position.x}px,${position.y}px)`, width: ghostGeo.w + "px", height: ghostGeo.h + "px", zIndex: String(block.depth + 1) });
2898
+ ghostEl.title = kind === "removed" ? `${block.name}: removed` : `${block.name}: only in the incoming side of a merge conflict`;
2899
+ this.blocksEl.appendChild(ghostEl);
2900
+ this.ghostBlockEls[kind].push(ghostEl);
2901
+ }
2902
+ }
2903
+ /** Diagnostics grouped by the block that shows them: the innermost visible block containing each error. */
2904
+ diagnosticsByShownBlock() {
2905
+ const byBlock = /* @__PURE__ */ new Map();
2906
+ const geometry = this.geom;
2907
+ if (!geometry) return byBlock;
2908
+ for (const d of this.diagnostics) {
2909
+ let geo = d.blockId ? geometry.geo.get(d.blockId) : void 0;
2910
+ while (geo?.hidden && geo.block.parentId) geo = geometry.geo.get(geo.block.parentId);
2911
+ if (!geo) continue;
2912
+ const list = byBlock.get(geo.id);
2913
+ if (list) list.push(d);
2914
+ else byBlock.set(geo.id, [d]);
2915
+ }
2916
+ return byBlock;
2917
+ }
2918
+ updateErrorMarkers() {
2919
+ const byBlock = this.diagnosticsByShownBlock();
2920
+ for (const [id, { el: blockEl }] of this.els) {
2921
+ const list = byBlock.get(id);
2922
+ const head = blockEl.querySelector(".cc-head");
2923
+ let badge = head?.querySelector(".cc-err") ?? null;
2924
+ blockEl.classList.toggle("cc-has-error", !!list);
2925
+ if (!list) {
2926
+ badge?.remove();
2927
+ continue;
2928
+ }
2929
+ if (!badge && head) {
2930
+ badge = el("span", "cc-err");
2931
+ head.insertBefore(badge, head.querySelector(".cc-fold"));
2932
+ }
2933
+ if (!badge) continue;
2934
+ badge.textContent = String(list.length);
2935
+ badge.title = list.map((d) => `Line ${d.line}: ${d.message}`).join("\n");
2936
+ }
2937
+ }
2938
+ blockHTML(b, geo, mini, change, changedLines = "", isConflicted = false) {
2939
+ const label = blockLabel(b);
2940
+ const tags = b.tags.filter((t) => t !== label && t !== "export" && (b.kind === "import" || !LABEL_TAGS.has(t))).join(" ");
2941
+ const exp = b.tags.includes("export") ? '<span class="cc-exp" title="exported"></span>' : "";
2942
+ const isRemoved = change === "removed" || change === "incoming";
2943
+ const fold = b.children.length && !isRemoved ? `<button class="cc-fold" title="${geo.collapsed ? "Expand" : "Collapse"}">${geo.collapsed ? "\u25B8" : "\u25BE"}</button>` : "";
2944
+ const prev = changedLines ? `<pre class="cc-prev cc-diff-prev">${changedLines}</pre>` : b.preview.length ? `<pre class="cc-prev">${esc(b.preview.join("\n"))}${b.previewMore ? `
2945
+ <span class="cc-more">\u2026 ${b.previewMore} more line${b.previewMore > 1 ? "s" : ""}</span>` : ""}</pre>` : "";
2946
+ const diffChip = (change ? DIFF_CHIPS[change] : "") + (isConflicted ? '<span class="cc-conflict-chip" title="Merge conflict">!</span>' : "");
2947
+ return `<div class="cc-head"><span class="cc-kind">${esc(label)}</span>${exp}<span class="cc-name">${esc(b.name)}</span>${diffChip}<span class="cc-detail">${esc(
2948
+ b.detail
2949
+ )}</span>${tags ? `<span class="cc-tags">${esc(tags)}</span>` : ""}${fold}</div>${prev}${mini ? `<div class="cc-mini">${mini}</div>` : ""}`;
2950
+ }
2951
+ /** Faint miniature of a collapsed block's nested blocks. */
2952
+ miniMap(geo) {
2953
+ const kids = geo.block.children.map((c) => this.geom.geo.get(c.id));
2954
+ const right = Math.max(...kids.map((k) => k.x + k.w));
2955
+ const bottom = Math.max(...kids.map((k) => k.y + k.h));
2956
+ const bw = geo.w - 24;
2957
+ const bh = 34;
2958
+ const s = Math.min(bw / right, bh / bottom);
2959
+ return kids.map(
2960
+ (k) => `<i class="k-${k.block.kind}" style="left:${(k.x * s).toFixed(1)}px;top:${(k.y * s).toFixed(1)}px;width:${Math.max(2, k.w * s).toFixed(1)}px;height:${Math.max(
2961
+ 2,
2962
+ k.h * s
2963
+ ).toFixed(1)}px"></i>`
2964
+ ).join("");
2965
+ }
2966
+ renderEdges() {
2967
+ const g = this.geom;
2968
+ const view = this.view;
2969
+ const visible = (id) => {
2970
+ let geo = g.geo.get(id);
2971
+ while (geo?.hidden && geo.block.parentId) geo = g.geo.get(geo.block.parentId);
2972
+ return geo;
2973
+ };
2974
+ const want = /* @__PURE__ */ new Map();
2975
+ for (const u of view.usages) {
2976
+ if (u.toPath) continue;
2977
+ const a = visible(u.from);
2978
+ const b = visible(u.to);
2979
+ if (!a || !b || a === b) continue;
2980
+ if (a.id.startsWith(b.id + "/") || b.id.startsWith(a.id + "/")) continue;
2981
+ const key = `${a.id}>${b.id}`;
2982
+ if (!want.has(key)) want.set(key, { from: a, to: b, kind: b.block.kind, usage: u.kind, uf: u.from, ut: u.to });
2983
+ }
2984
+ const old = new Map(this.edges.map((e) => [e.key, e]));
2985
+ const next = [];
2986
+ for (const [key, w] of want) {
2987
+ let e = old.get(key);
2988
+ if (e) old.delete(key);
2989
+ else {
2990
+ const p = document.createElementNS("http://www.w3.org/2000/svg", "path");
2991
+ this.svg.appendChild(p);
2992
+ e = { key, from: w.from.id, to: w.to.id, el: p };
2993
+ }
2994
+ e.el.setAttribute("class", `cc-edge u-${w.usage}`);
2995
+ e.el.setAttribute("stroke", `var(--k-${w.kind})`);
2996
+ e.el.setAttribute("marker-end", `url(#cc-arrow-${w.kind})`);
2997
+ e.el.setAttribute("d", edgePath(w.from, w.to));
2998
+ next.push(e);
2999
+ }
3000
+ for (const e of old.values()) e.el.remove();
3001
+ this.edges = next;
3002
+ }
3003
+ updateHighlight() {
3004
+ const h = this.selected ?? this.hovered;
3005
+ const inH = (id) => !!h && (id === h || id.startsWith(h + "/"));
3006
+ const linked = /* @__PURE__ */ new Set();
3007
+ for (const e of this.edges) {
3008
+ const on = inH(e.from) || inH(e.to);
3009
+ e.el.classList.toggle("hl", on);
3010
+ if (on) {
3011
+ linked.add(e.from);
3012
+ linked.add(e.to);
3013
+ }
3014
+ }
3015
+ this.root.classList.toggle("has-hl", !!h);
3016
+ for (const [id, { el: e }] of this.els) {
3017
+ e.classList.toggle("cc-sel", id === this.selected);
3018
+ e.classList.toggle("cc-linked", linked.has(id) && !inH(id));
3019
+ e.classList.toggle("cc-editing", !!this.editing && id === this.editing.id);
3020
+ }
3021
+ if (this.refsOpen) this.renderRefs();
3022
+ }
3023
+ // ---------------------------------------------------------------- camera
3024
+ queueCamera() {
3025
+ if (this.camQueued) return;
3026
+ this.camQueued = true;
3027
+ requestAnimationFrame(() => {
3028
+ this.camQueued = false;
3029
+ this.applyCamera();
3030
+ });
3031
+ }
3032
+ applyCamera() {
3033
+ const { x, y, s } = this.cam;
3034
+ this.world.style.transform = `translate(${x}px,${y}px) scale(${s})`;
3035
+ const grid = 24 * s;
3036
+ this.root.style.backgroundSize = s < 0.25 ? "0 0" : `${grid}px ${grid}px`;
3037
+ this.root.style.backgroundPosition = `${x}px ${y}px`;
3038
+ const lod = s < 0.3 ? 2 : s < 0.55 ? 1 : 0;
3039
+ if (lod !== this.lod) {
3040
+ this.lod = lod;
3041
+ this.root.classList.toggle("lod-1", lod >= 1);
3042
+ this.root.classList.toggle("lod-2", lod >= 2);
3043
+ }
3044
+ this.root.classList.add("cc-moving");
3045
+ clearTimeout(this.movingTimer);
3046
+ this.movingTimer = window.setTimeout(() => this.root.classList.remove("cc-moving"), 160);
3047
+ this.opts.onCameraChange?.(s);
3048
+ }
3049
+ setCamera(c) {
3050
+ this.fittedToView = false;
3051
+ cancelAnimationFrame(this.anim);
3052
+ this.cam = { x: c.x, y: c.y, s: clamp2(c.s, MIN_SCALE, MAX_SCALE) };
3053
+ this.queueCamera();
3054
+ }
3055
+ moveCamera(to, animate) {
3056
+ this.fittedToView = false;
3057
+ cancelAnimationFrame(this.anim);
3058
+ if (!animate || matchMedia("(prefers-reduced-motion: reduce)").matches) {
3059
+ this.setCamera(to);
3060
+ return;
3061
+ }
3062
+ const from = { ...this.cam };
3063
+ const t0 = performance.now();
3064
+ const dur = 280;
3065
+ const step = (now) => {
3066
+ const k = Math.min(1, (now - t0) / dur);
3067
+ const e = 1 - Math.pow(1 - k, 3);
3068
+ const s = Math.exp(Math.log(from.s) + (Math.log(to.s) - Math.log(from.s)) * e);
3069
+ this.cam = { s, x: from.x + (to.x - from.x) * e, y: from.y + (to.y - from.y) * e };
3070
+ this.applyCamera();
3071
+ if (k < 1) this.anim = requestAnimationFrame(step);
3072
+ };
3073
+ this.anim = requestAnimationFrame(step);
3074
+ }
3075
+ zoomAt(p, factor, animate = false) {
3076
+ const s = clamp2(this.cam.s * factor, MIN_SCALE, MAX_SCALE);
3077
+ const wx = (p.x - this.cam.x) / this.cam.s;
3078
+ const wy = (p.y - this.cam.y) / this.cam.s;
3079
+ this.moveCamera({ s, x: p.x - wx * s, y: p.y - wy * s }, animate);
3080
+ }
3081
+ toWorld(clientX, clientY) {
3082
+ const r = this.root.getBoundingClientRect();
3083
+ return { x: (clientX - r.left - this.cam.x) / this.cam.s, y: (clientY - r.top - this.cam.y) / this.cam.s };
3084
+ }
3085
+ local(e) {
3086
+ const r = this.root.getBoundingClientRect();
3087
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
3088
+ }
3089
+ // ---------------------------------------------------------------- hit testing
3090
+ /** What's under a world point: a block (header or leaf), or empty space inside a container/the file. */
3091
+ hitTest(p) {
3092
+ const g = this.geom;
3093
+ if (!g) return { block: null, container: null };
3094
+ let hit = null;
3095
+ for (let i = g.order.length - 1; i >= 0; i--) {
3096
+ const geo = g.order[i];
3097
+ if (geo.hidden) continue;
3098
+ if (p.x >= geo.ax && p.x <= geo.ax + geo.w && p.y >= geo.ay && p.y <= geo.ay + geo.h) {
3099
+ if (!hit || geo.block.depth > hit.block.depth) hit = geo;
3100
+ }
3101
+ }
3102
+ if (!hit) return { block: null, container: null };
3103
+ const hasKids = hit.block.children.length > 0 && !hit.collapsed;
3104
+ if (hasKids && p.y > hit.ay + hit.contentTop) return { block: null, container: hit.id };
3105
+ return { block: hit.id, container: hit.block.parentId };
3106
+ }
3107
+ // ---------------------------------------------------------------- events
3108
+ bindEvents() {
3109
+ const r = this.root;
3110
+ const on = (t, type, fn, o) => {
3111
+ t.addEventListener(type, fn, o);
3112
+ this.cleanup.push(() => t.removeEventListener(type, fn, o));
3113
+ };
3114
+ on(r, "pointerdown", (e) => this.onDown(e));
3115
+ on(r, "pointermove", (e) => this.onMove(e));
3116
+ on(r, "pointerup", (e) => this.onUp(e));
3117
+ on(r, "pointercancel", (e) => this.onUp(e, true));
3118
+ on(r, "wheel", (e) => this.onWheel(e), { passive: false });
3119
+ on(r, "keydown", (e) => this.onKey(e));
3120
+ on(r, "pointerover", (e) => {
3121
+ if (e.pointerType !== "mouse" || this.gesture) return;
3122
+ const id = e.target.closest?.(".cc-block")?.getAttribute("data-id") ?? null;
3123
+ if (id !== this.hovered) {
3124
+ this.hovered = id;
3125
+ this.updateHighlight();
3126
+ }
3127
+ });
3128
+ on(r, "pointerleave", () => {
3129
+ if (this.hovered) {
3130
+ this.hovered = null;
3131
+ this.updateHighlight();
3132
+ }
3133
+ });
3134
+ on(r, "gesturestart", (e) => e.preventDefault());
3135
+ const doc = r.ownerDocument;
3136
+ on(doc, "copy", (e) => this.onClipboard(e, "copy"));
3137
+ on(doc, "cut", (e) => this.onClipboard(e, "cut"));
3138
+ on(doc, "paste", (e) => this.onClipboard(e, "paste"));
3139
+ }
3140
+ isOverlay(t) {
3141
+ return !!t?.closest?.(".cc-editor, .cc-bar, .cc-refs, .cc-banner, .cc-rename");
3142
+ }
3143
+ onDown(e) {
3144
+ if (this.isOverlay(e.target)) return;
3145
+ if (e.pointerType === "mouse" && e.button !== 0 && e.button !== 1) return;
3146
+ this.root.focus({ preventScroll: true });
3147
+ this.root.setPointerCapture(e.pointerId);
3148
+ this.pointers.set(e.pointerId, this.local(e));
3149
+ if (this.pointers.size === 2) {
3150
+ if (this.gesture?.t === "drag") this.endDrag(false);
3151
+ const [a, b] = [...this.pointers.values()];
3152
+ this.gesture = { t: "pinch", d0: Math.hypot(a.x - b.x, a.y - b.y), m0: mid(a, b), cam: { ...this.cam } };
3153
+ return;
3154
+ }
3155
+ if (this.pointers.size > 2) return;
3156
+ const p = this.local(e);
3157
+ if (e.button === 1) {
3158
+ this.gesture = { t: "pan", id: e.pointerId, sx: p.x, sy: p.y, cam: { ...this.cam } };
3159
+ return;
3160
+ }
3161
+ this.gesture = { t: "pending", id: e.pointerId, sx: p.x, sy: p.y, touch: e.pointerType !== "mouse", target: e.target };
3162
+ }
3163
+ onMove(e) {
3164
+ const p = this.local(e);
3165
+ if (e.pointerType === "mouse") this.lastPointer = this.toWorld(e.clientX, e.clientY);
3166
+ if (!this.pointers.has(e.pointerId)) return;
3167
+ this.pointers.set(e.pointerId, p);
3168
+ const g = this.gesture;
3169
+ if (!g) return;
3170
+ if (g.t === "pinch") {
3171
+ if (this.pointers.size < 2) return;
3172
+ const [a, b] = [...this.pointers.values()];
3173
+ const d = Math.hypot(a.x - b.x, a.y - b.y);
3174
+ const m = mid(a, b);
3175
+ const s = clamp2(g.cam.s * (d / g.d0), MIN_SCALE, MAX_SCALE);
3176
+ const wx = (g.m0.x - g.cam.x) / g.cam.s;
3177
+ const wy = (g.m0.y - g.cam.y) / g.cam.s;
3178
+ this.setCamera({ s, x: m.x - wx * s, y: m.y - wy * s });
3179
+ return;
3180
+ }
3181
+ if (g.id !== e.pointerId) return;
3182
+ const dx = p.x - g.sx;
3183
+ const dy = p.y - g.sy;
3184
+ if (g.t === "pending") {
3185
+ if (Math.hypot(dx, dy) < (g.touch ? 8 : 4)) return;
3186
+ const w = this.toWorld(e.clientX - dx, e.clientY - dy);
3187
+ const hit = g.target?.closest(".cc-fold") ? { block: null } : this.hitTest(w);
3188
+ const geo = hit.block ? this.geom.geo.get(hit.block) : null;
3189
+ if (geo && !this.stale) {
3190
+ this.gesture = { t: "drag", id: g.id, sx: g.sx, sy: g.sy, block: geo.id, orig: { x: geo.x, y: geo.y }, nested: !!geo.block.parentId };
3191
+ this.els.get(geo.id)?.el.classList.add("cc-dragging");
3192
+ this.hovered = geo.id;
3193
+ } else {
3194
+ this.gesture = { t: "pan", id: g.id, sx: g.sx, sy: g.sy, cam: { ...this.cam } };
3195
+ }
3196
+ return this.onMove(e);
3197
+ }
3198
+ if (g.t === "pan") {
3199
+ this.setCamera({ ...g.cam, x: g.cam.x + dx, y: g.cam.y + dy });
3200
+ } else if (g.t === "drag") {
3201
+ let x = g.orig.x + dx / this.cam.s;
3202
+ let y = g.orig.y + dy / this.cam.s;
3203
+ if (g.nested) x = Math.max(0, x), y = Math.max(0, y);
3204
+ this.layout.blocks[g.block] = { x: Math.round(x), y: Math.round(y) };
3205
+ if (!this.camQueued) this.relayoutSoon();
3206
+ }
3207
+ }
3208
+ relayoutQueued = false;
3209
+ relayoutSoon() {
3210
+ if (this.relayoutQueued) return;
3211
+ this.relayoutQueued = true;
3212
+ requestAnimationFrame(() => {
3213
+ this.relayoutQueued = false;
3214
+ if (!this.view) return;
3215
+ this.geom = computeGeometry(this.view, this.layout, this.hints, { resolveOverlaps: false });
3216
+ this.render();
3217
+ });
3218
+ }
3219
+ onUp(e, cancelled = false) {
3220
+ if (!this.pointers.has(e.pointerId)) return;
3221
+ this.pointers.delete(e.pointerId);
3222
+ const g = this.gesture;
3223
+ if (!g) return;
3224
+ if (g.t === "pinch") {
3225
+ if (this.pointers.size === 0) this.gesture = null;
3226
+ else {
3227
+ const [id, p] = [...this.pointers.entries()][0];
3228
+ this.gesture = { t: "pan", id, sx: p.x, sy: p.y, cam: { ...this.cam } };
3229
+ }
3230
+ return;
3231
+ }
3232
+ if (g.id !== e.pointerId) return;
3233
+ if (g.t === "drag") return this.endDrag(!cancelled);
3234
+ this.gesture = null;
3235
+ if (g.t !== "pending" || cancelled) return;
3236
+ this.onTap(e, g.target);
3237
+ }
3238
+ endDrag(commit) {
3239
+ const g = this.gesture?.t === "drag" ? this.gesture : null;
3240
+ this.gesture = null;
3241
+ if (!g) return;
3242
+ this.els.get(g.block)?.el.classList.remove("cc-dragging");
3243
+ if (!commit) {
3244
+ this.layout.blocks[g.block] = g.orig;
3245
+ this.relayout();
3246
+ return;
3247
+ }
3248
+ const geom = computeGeometry(this.view, this.layout, this.hints, { resolveOverlaps: false });
3249
+ const geo = geom.geo.get(g.block);
3250
+ if (geo) {
3251
+ const parent = geo.block.parentId;
3252
+ const sibs = [...geom.geo.values()].filter((s) => s.block.parentId === parent && s.id !== g.block);
3253
+ this.layout.blocks[g.block] = freeSpotNear({ x: geo.x, y: geo.y }, geo, sibs, !!parent);
3254
+ }
3255
+ this.relayout();
3256
+ this.emitLayout("user");
3257
+ }
3258
+ onTap(e, target) {
3259
+ const fold = target?.closest(".cc-fold");
3260
+ const w = this.toWorld(e.clientX, e.clientY);
3261
+ const hit = this.hitTest(w);
3262
+ const now = performance.now();
3263
+ const p = this.local(e);
3264
+ const dbl = now - this.lastTap.t < 350 && Math.hypot(p.x - this.lastTap.x, p.y - this.lastTap.y) < 24 && this.lastTap.id === (hit.block ?? "@" + hit.container);
3265
+ this.lastTap = { t: dbl ? 0 : now, x: p.x, y: p.y, id: hit.block ?? "@" + hit.container };
3266
+ if (fold && hit.block) return this.toggleCollapse(hit.block);
3267
+ if (hit.block) {
3268
+ if (this.selected === hit.block && !this.editing) return this.openEditor(hit.block);
3269
+ if (this.editing && this.editing.id !== hit.block) this.closeEditor();
3270
+ this.select(hit.block);
3271
+ return;
3272
+ }
3273
+ if (dbl) return this.createFunctionAt(w, hit.container);
3274
+ if (this.editing) this.closeEditor();
3275
+ this.select(hit.container);
3276
+ }
3277
+ onWheel(e) {
3278
+ if (this.isOverlay(e.target)) return;
3279
+ e.preventDefault();
3280
+ const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 400 : 1;
3281
+ const p = this.local(e);
3282
+ if (e.ctrlKey || e.metaKey) {
3283
+ this.zoomAt(p, Math.exp(-e.deltaY * unit * 25e-4));
3284
+ } else {
3285
+ const dx = (e.shiftKey && !e.deltaX ? e.deltaY : e.deltaX) * unit;
3286
+ const dy = (e.shiftKey && !e.deltaX ? 0 : e.deltaY) * unit;
3287
+ this.setCamera({ ...this.cam, x: this.cam.x - dx, y: this.cam.y - dy });
3288
+ }
3289
+ }
3290
+ onKey(e) {
3291
+ const isHandledByEditor = e.defaultPrevented;
3292
+ if (isHandledByEditor || this.isOverlay(e.target) && e.target !== this.root) return;
3293
+ const mod = e.metaKey || e.ctrlKey;
3294
+ const k = e.key;
3295
+ if (mod && k.toLowerCase() === "z") {
3296
+ e.preventDefault();
3297
+ return e.shiftKey ? this.opts.onRedo?.() : this.opts.onUndo?.();
3298
+ }
3299
+ if (mod && k.toLowerCase() === "y") {
3300
+ e.preventDefault();
3301
+ return this.opts.onRedo?.();
3302
+ }
3303
+ if (mod && k.toLowerCase() === "d" && this.selected) {
3304
+ e.preventDefault();
3305
+ return this.duplicate(this.selected);
3306
+ }
3307
+ if (mod) return;
3308
+ if (k === "Escape") return this.editing ? this.closeEditor() : this.select(null);
3309
+ if (k === "Enter" && this.selected) {
3310
+ e.preventDefault();
3311
+ return this.openEditor(this.selected);
3312
+ }
3313
+ if (k === "F2" && this.selected) {
3314
+ e.preventDefault();
3315
+ return this.startRename(this.selected);
3316
+ }
3317
+ if ((k === "Delete" || k === "Backspace") && this.selected) {
3318
+ e.preventDefault();
3319
+ return this.deleteBlock(this.selected);
3320
+ }
3321
+ if (k === "0" || k === "!") return this.fit();
3322
+ if (k === "=" || k === "+") return this.zoomBy(1.25);
3323
+ if (k === "-" || k === "_") return this.zoomBy(0.8);
3324
+ if (k === "n" || k === "N") return this.createFunctionAtCenter();
3325
+ if (k === "r" && this.selected) {
3326
+ this.refsOpen = !this.refsOpen;
3327
+ return this.renderBar();
3328
+ }
3329
+ }
3330
+ onClipboard(e, type) {
3331
+ const active = this.root.ownerDocument.activeElement;
3332
+ if (active !== this.root) return;
3333
+ if (type === "paste") {
3334
+ const custom = e.clipboardData?.getData("application/x-codecanvas");
3335
+ const textData = e.clipboardData?.getData("text/plain");
3336
+ let clip2 = null;
3337
+ if (custom) {
3338
+ try {
3339
+ clip2 = JSON.parse(custom);
3340
+ } catch {
3341
+ }
3342
+ }
3343
+ if (!clip2 && textData) clip2 = clipFromText(textData);
3344
+ if (clip2) {
3345
+ e.preventDefault();
3346
+ this.paste(clip2);
3347
+ }
3348
+ return;
3349
+ }
3350
+ if (!this.selected || !this.view || this.stale) return;
3351
+ const clip = copyBlock(this.view, this.selected);
3352
+ this.clip = clip;
3353
+ e.clipboardData?.setData("text/plain", clip.code);
3354
+ e.clipboardData?.setData("application/x-codecanvas", JSON.stringify(clip));
3355
+ e.preventDefault();
3356
+ if (type === "cut") this.deleteBlock(this.selected);
3357
+ else this.toast(`Copied ${clip.name || "block"}`);
3358
+ this.renderBar();
3359
+ }
3360
+ // ---------------------------------------------------------------- actions
3361
+ guard() {
3362
+ if (this.stale || !this.view) {
3363
+ this.toast(`Fix the syntax error on line ${this.model?.firstErrorLine} first`);
3364
+ return false;
3365
+ }
3366
+ return true;
3367
+ }
3368
+ /** Edits that add, remove or rename blocks work on the text's structure, which conflicts break. */
3369
+ guardStructureEdit() {
3370
+ if (!this.guard()) return false;
3371
+ if (this.conflicts) {
3372
+ this.toast("Resolve the merge conflicts in this file first");
3373
+ return false;
3374
+ }
3375
+ return true;
3376
+ }
3377
+ toggleCollapse(id) {
3378
+ const set = new Set(this.layout.collapsed ?? []);
3379
+ if (set.has(id)) set.delete(id);
3380
+ else set.add(id);
3381
+ this.layout.collapsed = [...set];
3382
+ this.relayout();
3383
+ this.emitLayout("user");
3384
+ }
3385
+ createFunctionAt(w, containerId) {
3386
+ if (!this.guardStructureEdit()) return;
3387
+ const view = this.view;
3388
+ const container = containerFor(view, containerId);
3389
+ const r = newFunction(view, container?.id ?? null);
3390
+ this.hints[r.id] = this.relativeTo(container?.id ?? null, { x: w.x - 24, y: w.y - 16 });
3391
+ this.applyInsert(r);
3392
+ }
3393
+ createFunctionAtCenter() {
3394
+ const rect = this.root.getBoundingClientRect();
3395
+ const w = this.toWorld(rect.left + rect.width / 2, rect.top + rect.height / 2);
3396
+ const target = this.selected ? containerFor(this.view, this.selected)?.id ?? null : this.hitTest(w).container;
3397
+ if (target) {
3398
+ const g = this.geom.geo.get(target);
3399
+ return this.createFunctionAt({ x: g.ax + M.pad + 24, y: g.ay + g.h }, target);
3400
+ }
3401
+ this.createFunctionAt(w, null);
3402
+ }
3403
+ /** Convert a world point into coordinates relative to a container's content origin. */
3404
+ relativeTo(containerId, w) {
3405
+ if (!containerId) return { x: Math.round(w.x), y: Math.round(w.y) };
3406
+ const g = this.geom.geo.get(containerId);
3407
+ return { x: Math.max(0, Math.round(w.x - g.ax - M.pad)), y: Math.max(0, Math.round(w.y - g.ay - g.contentTop)) };
3408
+ }
3409
+ applyInsert(r, edit = true) {
3410
+ this.commit(applyEdit(this.text, r.edit));
3411
+ if (r.id && this.view?.blocks.has(r.id)) {
3412
+ this.select(r.id);
3413
+ if (edit) this.openEditor(r.id, r.select);
3414
+ }
3415
+ }
3416
+ deleteBlock(id) {
3417
+ if (!this.guardStructureEdit()) return;
3418
+ const b = this.view.blocks.get(id);
3419
+ if (!b) return;
3420
+ if (this.editing) this.closeEditor();
3421
+ this.select(b.parentId);
3422
+ this.commit(applyEdit(this.text, deleteBlock(this.view, id)));
3423
+ this.toast(`Deleted ${b.name}. Undo with ${isMac() ? "\u2318" : "Ctrl"}+Z`);
3424
+ }
3425
+ duplicate(id) {
3426
+ if (!this.guardStructureEdit()) return;
3427
+ const view = this.view;
3428
+ const b = view.blocks.get(id);
3429
+ if (!b || b.kind === "import" || b.kind === "statement") return;
3430
+ const clip = copyBlock(view, id);
3431
+ const r = pasteBlock(view, b.parentId, clip);
3432
+ const geo = this.geom.geo.get(id);
3433
+ if (r.id) this.hints[r.id] = { x: geo.x + 24, y: geo.y + geo.h + M.gap };
3434
+ this.applyInsert(r, false);
3435
+ }
3436
+ copy(id) {
3437
+ if (!this.view || this.stale) return;
3438
+ this.clip = copyBlock(this.view, id);
3439
+ navigator.clipboard?.writeText(this.clip.code).catch(() => {
3440
+ });
3441
+ this.toast(`Copied ${this.clip.name}`);
3442
+ this.renderBar();
3443
+ }
3444
+ paste(clip = this.clip) {
3445
+ if (!clip || !this.guardStructureEdit()) return;
3446
+ const view = this.view;
3447
+ let target = null;
3448
+ let at = null;
3449
+ if (this.lastPointer) {
3450
+ const hit = this.hitTest(this.lastPointer);
3451
+ target = hit.block ? containerFor(view, hit.block)?.id ?? null : hit.container;
3452
+ at = this.lastPointer;
3453
+ } else if (this.selected) {
3454
+ target = containerFor(view, this.selected)?.id ?? null;
3455
+ }
3456
+ const r = pasteBlock(view, target, clip);
3457
+ if (r.id && at) this.hints[r.id] = this.relativeTo(target, { x: at.x - 24, y: at.y - 16 });
3458
+ this.applyInsert(r, false);
3459
+ }
3460
+ // ---------------------------------------------------------------- rename
3461
+ /** Show a name field in the block's header. Enter renames the block and its usages in every file. */
3462
+ startRename(id) {
3463
+ if (!this.guardStructureEdit()) return;
3464
+ const block = this.view.blocks.get(id);
3465
+ if (!block) return;
3466
+ if (!isRenamable(block)) return this.toast(`${block.name} can't be renamed`);
3467
+ if (this.editing) this.closeEditor();
3468
+ this.cancelRename();
3469
+ this.select(id);
3470
+ if (this.cam.s < 0.6) this.reveal(id);
3471
+ const nameEl = this.els.get(id)?.el.querySelector(".cc-name");
3472
+ if (!nameEl) return;
3473
+ const input = document.createElement("input");
3474
+ input.className = "cc-rename";
3475
+ input.value = block.name;
3476
+ input.spellcheck = false;
3477
+ input.setAttribute("aria-label", `New name for ${block.name}`);
3478
+ const fitWidth = () => input.style.width = `${Math.max(input.value.length, 4) + 2}ch`;
3479
+ fitWidth();
3480
+ input.addEventListener("input", () => {
3481
+ fitWidth();
3482
+ input.classList.remove("invalid");
3483
+ });
3484
+ input.addEventListener("keydown", (e) => {
3485
+ e.stopPropagation();
3486
+ if (e.key === "Enter") {
3487
+ e.preventDefault();
3488
+ this.commitRename();
3489
+ } else if (e.key === "Escape") {
3490
+ e.preventDefault();
3491
+ this.cancelRename();
3492
+ this.root.focus({ preventScroll: true });
3493
+ }
3494
+ });
3495
+ input.addEventListener("blur", () => this.cancelRename());
3496
+ nameEl.replaceChildren(input);
3497
+ this.renaming = { id, input };
3498
+ input.focus({ preventScroll: true });
3499
+ input.select();
3500
+ }
3501
+ cancelRename() {
3502
+ const renaming = this.renaming;
3503
+ if (!renaming) return;
3504
+ this.renaming = null;
3505
+ const name = this.view?.blocks.get(renaming.id)?.name ?? "";
3506
+ renaming.input.parentElement?.replaceChildren(document.createTextNode(name));
3507
+ }
3508
+ async commitRename() {
3509
+ const renaming = this.renaming;
3510
+ const block = renaming && this.view?.blocks.get(renaming.id);
3511
+ if (!renaming || !block) return this.cancelRename();
3512
+ if (renaming.isApplying) return;
3513
+ const newName = renaming.input.value.trim();
3514
+ if (newName === block.name) {
3515
+ this.cancelRename();
3516
+ return this.root.focus({ preventScroll: true });
3517
+ }
3518
+ renaming.isApplying = true;
3519
+ const textAtRequest = this.text;
3520
+ const result = await this.project.renameBlock(this.path, block.id, newName);
3521
+ renaming.isApplying = false;
3522
+ if (this.renaming !== renaming) return;
3523
+ if (this.text !== textAtRequest) {
3524
+ renaming.input.classList.add("invalid");
3525
+ return this.toast("The file changed. Press Enter to rename again");
3526
+ }
3527
+ if (!result.ok) {
3528
+ renaming.input.classList.add("invalid");
3529
+ return this.toast(result.error);
3530
+ }
3531
+ this.renaming = null;
3532
+ const ownEdits = result.edits[this.path] ?? [];
3533
+ const shiftBeforeBlock = ownEdits.filter((e) => e.end <= block.start).reduce((sum, e) => sum + e.text.length - (e.end - e.start), 0);
3534
+ const otherPaths = Object.keys(result.edits).filter((path) => path !== this.path);
3535
+ const otherFiles = otherPaths.map((path) => {
3536
+ const previousModel = this.project.getModel(path);
3537
+ const before = this.project.getText(path) ?? "";
3538
+ const after = applyEdits(before, result.edits[path]);
3539
+ this.project.setFile(path, after);
3540
+ return { path, before, after, previousModel };
3541
+ });
3542
+ this.commit(applyEdits(this.text, ownEdits), otherFiles.length ? otherFiles : void 0);
3543
+ const renamed = [...this.view?.blocks.values() ?? []].find((b) => b.start === block.start + shiftBeforeBlock && b.name === newName);
3544
+ if (renamed) this.select(renamed.id);
3545
+ this.root.focus({ preventScroll: true });
3546
+ this.toast(otherPaths.length ? `Renamed to ${newName} in ${otherPaths.length + 1} files` : `Renamed to ${newName}`);
3547
+ }
3548
+ // ---------------------------------------------------------------- inline editor
3549
+ openEditor(id, selection) {
3550
+ if (!this.guard()) return;
3551
+ const b = this.view.blocks.get(id);
3552
+ if (!b) return;
3553
+ if (this.conflicts) {
3554
+ const touching = (this.conflicts.hunksByBlock.get(id) ?? []).map((index) => this.conflicts.hunks[index]);
3555
+ const start = Math.min(this.conflicts.current.toFileOffset(b.start), ...touching.map((hunk) => hunk.start));
3556
+ const end = Math.max(this.conflicts.current.toFileOffset(b.end), ...touching.map((hunk) => hunk.end));
3557
+ return this.openRangeEditor(start, end, b);
3558
+ }
3559
+ this.openRangeEditor(b.start, b.end, b, selection);
3560
+ }
3561
+ openRangeEditor(start, end, b, selection) {
3562
+ this.closeEditor({ restoreCamera: false });
3563
+ this.refsOpen = false;
3564
+ const panel = el("div", "cc-editor");
3565
+ const head = el("div", "cc-editor-head");
3566
+ const chip = el("div", "cc-chip");
3567
+ chip.classList.add(`k-${b?.kind ?? "statement"}`);
3568
+ chip.style.setProperty("--k", `var(--k-${b?.kind ?? "statement"})`);
3569
+ setHTML(chip, `<i></i><span>${esc(b ? b.name : this.path)}</span>`);
3570
+ const hint = el("span", "cc-editor-hint");
3571
+ hint.textContent = "Esc to close";
3572
+ head.append(chip, hint, button("Done", () => this.closeEditor(), "primary"));
3573
+ const body = el("div", "cc-editor-body");
3574
+ panel.append(head, body);
3575
+ this.root.appendChild(panel);
3576
+ const code = this.text.slice(start, end);
3577
+ const lines = code.split("\n").length;
3578
+ const { width: vw, height: vh } = this.root.getBoundingClientRect();
3579
+ const mobile = vw < 700;
3580
+ if (mobile) {
3581
+ Object.assign(panel.style, { left: "0", right: "0", bottom: "0", height: Math.round(vh * 0.52) + "px", borderRadius: "14px 14px 0 0" });
3582
+ } else {
3583
+ const w = Math.min(640, vw - 32);
3584
+ const h = Math.min(vh - 32, Math.max(160, lines * 19 + 70));
3585
+ Object.assign(panel.style, { right: "16px", top: "16px", width: w + "px", height: h + "px" });
3586
+ }
3587
+ if (b) {
3588
+ const g = this.geom?.geo.get(b.id);
3589
+ if (g) {
3590
+ const s = this.cam.s;
3591
+ const sx = g.ax * s + this.cam.x;
3592
+ const sy = g.ay * s + this.cam.y;
3593
+ let dx = 0, dy = 0;
3594
+ if (mobile) {
3595
+ const avail = vh * 0.48;
3596
+ if (sy < 12 || sy > avail - 60) dy = 24 - sy;
3597
+ if (sx < 12 || sx + Math.min(g.w * s, vw) > vw) dx = 16 - sx;
3598
+ } else {
3599
+ const right = vw - Math.min(640, vw - 32) - 32;
3600
+ if (sx + g.w * s > right) dx = Math.max(right - (sx + g.w * s), 24 - sx);
3601
+ if (sx + dx < 12) dx = 24 - sx;
3602
+ if (sy < 12 || sy > vh - 80) dy = 40 - sy;
3603
+ }
3604
+ if (dx || dy) {
3605
+ const shifted = { ...this.cam, x: this.cam.x + dx, y: this.cam.y + dy };
3606
+ this.cameraBeforeEditor = { before: this.cameraBeforeEditor?.before ?? { ...this.cam }, shifted };
3607
+ this.moveCamera(shifted, true);
3608
+ }
3609
+ }
3610
+ }
3611
+ const handle = this.opts.createEditor({
3612
+ parent: body,
3613
+ text: code,
3614
+ start,
3615
+ end,
3616
+ selection,
3617
+ diffBase: this.conflicts ? void 0 : this.baseCodeOf(b),
3618
+ onChange: (t) => this.onEditorChange(t),
3619
+ onClose: () => {
3620
+ this.closeEditor();
3621
+ this.root.focus({ preventScroll: true });
3622
+ }
3623
+ });
3624
+ this.editing = { start, len: code.length, id: b?.id ?? null, handle, el: panel };
3625
+ if (b) this.selected = b.id;
3626
+ this.banner.querySelector("button")?.remove();
3627
+ this.updateHighlight();
3628
+ this.renderBar();
3629
+ handle.focus();
3630
+ }
3631
+ onEditorChange(code) {
3632
+ const ed = this.editing;
3633
+ if (!ed) return;
3634
+ const text = this.text.slice(0, ed.start) + code + this.text.slice(ed.start + ed.len);
3635
+ ed.len = code.length;
3636
+ this.commit(text);
3637
+ }
3638
+ closeEditor({ restoreCamera = true } = {}) {
3639
+ const ed = this.editing;
3640
+ if (!ed) return;
3641
+ this.editing = null;
3642
+ const cameraBeforeEditor = this.cameraBeforeEditor;
3643
+ if (restoreCamera) {
3644
+ this.cameraBeforeEditor = null;
3645
+ const isStillWhereEditorPutIt = !!cameraBeforeEditor && Math.abs(this.cam.x - cameraBeforeEditor.shifted.x) < 1 && Math.abs(this.cam.y - cameraBeforeEditor.shifted.y) < 1 && this.cam.s === cameraBeforeEditor.shifted.s;
3646
+ if (isStillWhereEditorPutIt) this.moveCamera(cameraBeforeEditor.before, true);
3647
+ }
3648
+ ed.handle.destroy();
3649
+ ed.el.remove();
3650
+ if (this.stale) this.parseNow();
3651
+ this.updateHighlight();
3652
+ this.renderBar();
3653
+ }
3654
+ // ---------------------------------------------------------------- toolbar & references
3655
+ renderBar() {
3656
+ const bar = this.bar;
3657
+ bar.replaceChildren();
3658
+ bar.hidden = !!this.editing;
3659
+ const b = this.selected ? this.view?.blocks.get(this.selected) : null;
3660
+ if (b) {
3661
+ const chip = el("div", "cc-chip");
3662
+ chip.style.setProperty("--k", `var(--k-${b.kind})`);
3663
+ setHTML(chip, `<i></i>${esc(b.name)}`);
3664
+ const refs = this.refsFor(b.id);
3665
+ const errors = this.diagnosticsInside(b.id);
3666
+ if (this.conflicts) {
3667
+ const isConflicted = this.conflicts.hunksByBlock.has(b.id);
3668
+ const firstHunk = isConflicted ? this.conflicts.hunks[this.conflicts.hunksByBlock.get(b.id)[0]] : void 0;
3669
+ const keep = (label, choice, side) => {
3670
+ const keepButton = button(label, () => this.keepConflictSide(b.id, choice), choice === "current" ? "primary" : void 0);
3671
+ if (side) keepButton.title = side;
3672
+ return keepButton;
3673
+ };
3674
+ bar.append(
3675
+ chip,
3676
+ el("div", "cc-sep"),
3677
+ ...firstHunk ? [keep("Keep current", "current", firstHunk.currentLabel), keep("Keep incoming", "incoming", firstHunk.incomingLabel), keep("Keep both", "both"), el("div", "cc-sep")] : [],
3678
+ button("Edit", () => this.openEditor(b.id)),
3679
+ button("Copy", () => this.copy(b.id))
3680
+ );
3681
+ return this.renderRefs();
3682
+ }
3683
+ bar.append(
3684
+ chip,
3685
+ el("div", "cc-sep"),
3686
+ ...errors.length ? [button(plural(errors.length, "error"), () => this.toggleRefs(), "danger")] : [],
3687
+ button("Edit", () => this.openEditor(b.id)),
3688
+ ...isRenamable(b) ? [button("Rename", () => this.startRename(b.id))] : [],
3689
+ button(`Refs ${refs.incoming.length + refs.outgoing.length}`, () => this.toggleRefs()),
3690
+ button("Copy", () => this.copy(b.id)),
3691
+ ...b.kind !== "import" && b.kind !== "statement" ? [button("Duplicate", () => this.duplicate(b.id))] : [],
3692
+ ...b.insert ? [button("+ Inside", () => this.createFunctionAtCenter())] : [],
3693
+ button("Delete", () => this.deleteBlock(b.id), "danger")
3694
+ );
3695
+ } else {
3696
+ const firstErrorBlock = this.diagnostics.find((d) => d.blockId)?.blockId;
3697
+ if (firstErrorBlock) bar.append(button(plural(this.diagnostics.length, "error"), () => this.showErrorsOf(firstErrorBlock), "danger"));
3698
+ const diff = this.currentDiff();
3699
+ if (diff) bar.append(changesSummary(diff), el("div", "cc-sep"));
3700
+ bar.append(button("+ Function", () => this.createFunctionAtCenter()));
3701
+ if (this.clip) bar.append(button("Paste", () => (this.lastPointer = null, this.paste())));
3702
+ bar.append(
3703
+ this.layoutBeforeArrange ? button("Undo arrange", () => this.undoArrange()) : button("Arrange", () => this.arrange()),
3704
+ el("div", "cc-sep"),
3705
+ button("Fit", () => this.fit()),
3706
+ button("\u2212", () => this.zoomBy(0.8)),
3707
+ button("+", () => this.zoomBy(1.25))
3708
+ );
3709
+ }
3710
+ this.renderRefs();
3711
+ }
3712
+ toggleRefs() {
3713
+ this.refsOpen = !this.refsOpen;
3714
+ this.renderBar();
3715
+ }
3716
+ showErrorsOf(id) {
3717
+ this.reveal(id);
3718
+ this.refsOpen = true;
3719
+ this.renderBar();
3720
+ }
3721
+ diagnosticsInside(id) {
3722
+ return this.diagnostics.filter((d) => d.blockId === id || !!d.blockId?.startsWith(id + "/"));
3723
+ }
3724
+ refsFor(id) {
3725
+ const usages = this.view?.usages ?? [];
3726
+ const inside = (x) => x === id || x.startsWith(id + "/");
3727
+ const importsReachedElsewhere = new Set(usages.filter((u) => u.toPath && u.via).map((u) => `${u.from}>${u.via}`));
3728
+ const isImportLinkWithTargets = (u) => !u.toPath && importsReachedElsewhere.has(`${u.from}>${u.to}`);
3729
+ return {
3730
+ incoming: [...usages.filter((u) => !u.toPath && inside(u.to) && !inside(u.from)), ...this.incomingUsages.filter((u) => inside(u.to))],
3731
+ outgoing: usages.filter((u) => inside(u.from) && (u.toPath || !inside(u.to)) && !isImportLinkWithTargets(u))
3732
+ };
3733
+ }
3734
+ renderRefs() {
3735
+ const panel = this.refsEl;
3736
+ const selected = this.selected ? this.view?.blocks.get(this.selected) : null;
3737
+ panel.hidden = !this.refsOpen || !selected || !!this.editing;
3738
+ if (panel.hidden || !selected) return;
3739
+ const { incoming, outgoing } = this.refsFor(selected.id);
3740
+ const errors = this.diagnosticsInside(selected.id);
3741
+ const heading = (text) => Object.assign(el("h4"), { textContent: text });
3742
+ const note = (text) => Object.assign(el("div", "none"), { textContent: text });
3743
+ const row = (path, blockId, usage) => {
3744
+ const isOtherFile = !!path && path !== this.path;
3745
+ const model = isOtherFile ? this.project.getModel(path) : this.view;
3746
+ const block = model?.blocks.get(blockId);
3747
+ const btn = keepsCanvasFocus(document.createElement("button"));
3748
+ btn.style.setProperty("--k", `var(--k-${block?.kind ?? "statement"})`);
3749
+ const name = block && model ? displayName(model, block) : blockId;
3750
+ const label = isOtherFile ? `<span class="cc-file">${esc(fileName(path))} \u203A</span> ${esc(name)}` : esc(name);
3751
+ setHTML(btn, `<i></i><span>${label}</span><span>${usage.kind}${usage.count > 1 ? " \xD7" + usage.count : ""}</span>`);
3752
+ btn.onclick = () => isOtherFile ? this.opts.onOpenBlock?.(path, blockId) : this.reveal(blockId);
3753
+ btn.disabled = isOtherFile && !this.opts.onOpenBlock;
3754
+ return btn;
3755
+ };
3756
+ panel.replaceChildren();
3757
+ if (errors.length) {
3758
+ panel.append(heading(`Errors (${errors.length})`));
3759
+ for (const d of errors) {
3760
+ const btn = keepsCanvasFocus(document.createElement("button"));
3761
+ btn.className = "cc-error-row";
3762
+ setHTML(btn, `<i></i><span>${esc(d.message)}</span><span>line ${d.line}</span>`);
3763
+ btn.onclick = () => this.openEditorAtDiagnostic(d);
3764
+ panel.append(btn);
3765
+ }
3766
+ }
3767
+ panel.append(heading(`Used by (${incoming.length})`));
3768
+ if (incoming.length) for (const u of incoming) panel.append(row("fromPath" in u ? u.fromPath : void 0, u.from, u));
3769
+ else panel.append(note("Not used anywhere"));
3770
+ panel.append(heading(`Uses (${outgoing.length})`));
3771
+ if (outgoing.length) for (const u of outgoing) panel.append(row(u.toPath, u.to, u));
3772
+ else panel.append(note("Uses nothing from this project"));
3773
+ }
3774
+ openEditorAtDiagnostic(d) {
3775
+ if (this.stale) return this.openRangeEditor(0, this.text.length, null, [d.start, d.start + d.length]);
3776
+ const block = d.blockId ? this.view?.blocks.get(d.blockId) : void 0;
3777
+ if (!block) return;
3778
+ const offset = d.start - block.start;
3779
+ this.openEditor(block.id, [offset, offset + d.length]);
3780
+ }
3781
+ toast(msg) {
3782
+ this.toastEl.textContent = msg;
3783
+ this.toastEl.style.opacity = "1";
3784
+ clearTimeout(this.toastTimer);
3785
+ this.toastTimer = window.setTimeout(() => this.toastEl.style.opacity = "0", 2200);
3786
+ }
3787
+ };
3788
+ function blockLabel(b) {
3789
+ if (b.kind === "function") return b.tags.find((t) => t === "get" || t === "set") ?? "fn";
3790
+ if (b.kind === "variable" || b.kind === "type") return b.tags.find((t) => LABEL_TAGS.has(t)) ?? KIND_LABEL[b.kind];
3791
+ return KIND_LABEL[b.kind];
3792
+ }
3793
+ function innermostBlocksTouching(model, range) {
3794
+ const touches = (block) => range.start === range.end ? block.start < range.start && range.start < block.end : block.start < range.end && range.start < block.end;
3795
+ const found = [];
3796
+ const visit = (blocks) => {
3797
+ for (const block of blocks.filter(touches)) {
3798
+ const touchingChildren = block.children.filter(touches);
3799
+ if (touchingChildren.length) visit(block.children);
3800
+ else found.push(block);
3801
+ }
3802
+ };
3803
+ visit(model.roots);
3804
+ return found;
3805
+ }
3806
+ function changedRegion2(before, after) {
3807
+ let start = 0;
3808
+ const shorter = Math.min(before.length, after.length);
3809
+ while (start < shorter && before.charCodeAt(start) === after.charCodeAt(start)) start++;
3810
+ let fromEnd = 0;
3811
+ while (fromEnd < shorter - start && before.charCodeAt(before.length - 1 - fromEnd) === after.charCodeAt(after.length - 1 - fromEnd)) fromEnd++;
3812
+ return { start, end: before.length - fromEnd };
3813
+ }
3814
+ function isRenamable(b) {
3815
+ return b.nameStart >= 0 && b.kind !== "import" && b.kind !== "statement" && b.name !== "constructor";
3816
+ }
3817
+ function displayName(model, b) {
3818
+ if (b.kind === "import") return "import " + b.name;
3819
+ const names = [b.name];
3820
+ let parent = b.parentId ? model.blocks.get(b.parentId) : null;
3821
+ while (parent) {
3822
+ names.unshift(parent.name);
3823
+ parent = parent.parentId ? model.blocks.get(parent.parentId) : null;
3824
+ }
3825
+ return names.join(".");
3826
+ }
3827
+ function fileName(path) {
3828
+ return path.slice(path.lastIndexOf("/") + 1);
3829
+ }
3830
+ function changesSummary(diff) {
3831
+ const changes = [...diff.changes.values()];
3832
+ const counts = [
3833
+ [changes.filter((change) => change === "changed").length, "changed"],
3834
+ [changes.filter((change) => change === "added").length, "added"],
3835
+ [diff.removed.length, "removed"]
3836
+ ];
3837
+ const parts = counts.filter(([count]) => count > 0).map(([count, word]) => `${count} ${word}`);
3838
+ return Object.assign(el("span", "cc-diff-summary"), { textContent: parts.length ? parts.join(" \xB7 ") : "No changes" });
3839
+ }
3840
+ function plural(count, word) {
3841
+ return `${count} ${word}${count === 1 ? "" : "s"}`;
3842
+ }
3843
+ function edgePath(a, b) {
3844
+ const ay = a.ay + Math.min(a.h / 2, 16);
3845
+ const by = b.ay + Math.min(b.h / 2, 16);
3846
+ if (b.ax >= a.ax + a.w + 8) return curveH(a.ax + a.w, ay, b.ax, by);
3847
+ if (b.ax + b.w <= a.ax - 8) return curveH(a.ax, ay, b.ax + b.w, by);
3848
+ const ax = a.ax + a.w / 2;
3849
+ const bx = b.ax + b.w / 2;
3850
+ if (b.ay > a.ay) return curveV(ax, a.ay + a.h, bx, b.ay);
3851
+ return curveV(ax, a.ay, bx, b.ay + b.h);
3852
+ }
3853
+ function curveH(x1, y1, x2, y2) {
3854
+ const c = Math.max(30, Math.abs(x2 - x1) * 0.5) * Math.sign(x2 - x1 || 1);
3855
+ return `M${x1} ${y1}C${x1 + c} ${y1} ${x2 - c} ${y2} ${x2} ${y2}`;
3856
+ }
3857
+ function curveV(x1, y1, x2, y2) {
3858
+ const c = Math.max(30, Math.abs(y2 - y1) * 0.5) * Math.sign(y2 - y1 || 1);
3859
+ return `M${x1} ${y1}C${x1} ${y1 + c} ${x2} ${y2 - c} ${x2} ${y2}`;
3860
+ }
3861
+ function el(tag, cls) {
3862
+ const e = document.createElement(tag);
3863
+ if (cls) e.className = cls;
3864
+ return e;
3865
+ }
3866
+ function button(label, onClick, variant) {
3867
+ const b = keepsCanvasFocus(document.createElement("button"));
3868
+ b.className = "cc-btn" + (variant ? " " + variant : "");
3869
+ b.textContent = label;
3870
+ b.addEventListener("click", (e) => {
3871
+ e.stopPropagation();
3872
+ onClick();
3873
+ });
3874
+ return b;
3875
+ }
3876
+ function keepsCanvasFocus(element) {
3877
+ element.addEventListener("pointerdown", (e) => e.preventDefault());
3878
+ return element;
3879
+ }
3880
+ function esc(s) {
3881
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
3882
+ }
3883
+ function clamp2(v, lo, hi) {
3884
+ return Math.min(hi, Math.max(lo, v));
3885
+ }
3886
+ function mid(a, b) {
3887
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
3888
+ }
3889
+ function isMac() {
3890
+ return /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
3891
+ }
3892
+ export {
3893
+ CodeCanvas,
3894
+ M,
3895
+ Project,
3896
+ ProjectServer,
3897
+ RemoteProject,
3898
+ applyEdit,
3899
+ applyEdits,
3900
+ blockAt,
3901
+ callProjectServer,
3902
+ carryRenames,
3903
+ clipFromText,
3904
+ computeGeometry,
3905
+ conflictSideText,
3906
+ connectProjectWorker,
3907
+ containerFor,
3908
+ copyBlock,
3909
+ deleteBlock,
3910
+ detectIndentUnit,
3911
+ diffLines,
3912
+ diffModels,
3913
+ diffOwnCode,
3914
+ diffPreview,
3915
+ emptyLayout,
3916
+ findConflicts,
3917
+ freeSpotNear,
3918
+ injectStyles,
3919
+ newFunction,
3920
+ parseFile,
3921
+ pasteBlock,
3922
+ replaceBlock,
3923
+ resolveConflict,
3924
+ serializeLayout,
3925
+ serveProject,
3926
+ uniqueName
3927
+ };
3928
+ //# sourceMappingURL=index.js.map