@codecanvas-ide/engine 0.1.0 → 0.3.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 CHANGED
@@ -1,5 +1,101 @@
1
1
  // src/parse.ts
2
+ import ts3 from "typescript";
3
+
4
+ // src/react.ts
5
+ import ts2 from "typescript";
6
+
7
+ // src/syntax.ts
2
8
  import ts from "typescript";
9
+ function unwrap(e) {
10
+ while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e) || ts.isTypeAssertionExpression(e)) e = e.expression;
11
+ return e;
12
+ }
13
+
14
+ // src/react.ts
15
+ var REACT_MODULE = /^(react|react-dom|react-native)(\/|$)/;
16
+ function fileLanguageOf(path, sf) {
17
+ const script = /\.[cm]?jsx?$/.test(path) ? "js" : "ts";
18
+ const hasJsxExtension = /\.[jt]sx$/.test(path);
19
+ const mayHaveJsx = script === "js" && /<[A-Za-z>]/.test(sf.text);
20
+ const hasJsx = hasJsxExtension || mayHaveJsx && containsJsx(sf);
21
+ const isReact = hasJsx || importsReact(sf);
22
+ return { script, hasJsx, isReact, label: languageLabel(script, hasJsx, isReact) };
23
+ }
24
+ function languageLabel(script, hasJsx, isReact) {
25
+ if (hasJsx) return script === "js" ? "React JSX" : "React TSX";
26
+ if (isReact) return script === "js" ? "React JS" : "React TS";
27
+ return script === "js" ? "JS" : "TS";
28
+ }
29
+ function isJsx(node) {
30
+ return ts2.isJsxElement(node) || ts2.isJsxSelfClosingElement(node) || ts2.isJsxFragment(node);
31
+ }
32
+ function containsJsx(node) {
33
+ return isJsx(node) || !!ts2.forEachChild(node, (child) => containsJsx(child) || void 0);
34
+ }
35
+ function importsReact(sf) {
36
+ return sf.statements.some((statement) => {
37
+ const isImportOrReExport = ts2.isImportDeclaration(statement) || ts2.isExportDeclaration(statement);
38
+ if (isImportOrReExport) return !!statement.moduleSpecifier && ts2.isStringLiteral(statement.moduleSpecifier) && REACT_MODULE.test(statement.moduleSpecifier.text);
39
+ if (ts2.isImportEqualsDeclaration(statement)) return ts2.isExternalModuleReference(statement.moduleReference) && isReactModuleName(statement.moduleReference.expression);
40
+ if (ts2.isVariableStatement(statement)) return statement.declarationList.declarations.some((declaration) => !!declaration.initializer && isRequireOfReact(declaration.initializer));
41
+ return false;
42
+ });
43
+ }
44
+ function isRequireOfReact(expression) {
45
+ if (ts2.isPropertyAccessExpression(expression)) return isRequireOfReact(expression.expression);
46
+ const isRequireCall = ts2.isCallExpression(expression) && ts2.isIdentifier(expression.expression) && expression.expression.text === "require";
47
+ return isRequireCall && expression.arguments.length === 1 && isReactModuleName(expression.arguments[0]);
48
+ }
49
+ function isReactModuleName(expression) {
50
+ return ts2.isStringLiteral(expression) && REACT_MODULE.test(expression.text);
51
+ }
52
+ var COMPONENT_WRAPPERS = /* @__PURE__ */ new Set(["memo", "forwardRef", "React.memo", "React.forwardRef"]);
53
+ var COMPONENT_BASE_CLASSES = /* @__PURE__ */ new Set(["Component", "PureComponent", "React.Component", "React.PureComponent"]);
54
+ var isComponentName = (name) => /^[A-Z]/.test(name);
55
+ function returnsJsx(fn) {
56
+ if (!fn.body) return false;
57
+ if (!ts2.isBlock(fn.body)) return isJsxValue(fn.body);
58
+ const hasJsxReturn = (node) => {
59
+ if (ts2.isReturnStatement(node)) return !!node.expression && isJsxValue(node.expression);
60
+ if (ts2.isFunctionLike(node) || ts2.isClassLike(node)) return false;
61
+ return !!ts2.forEachChild(node, (child) => hasJsxReturn(child) || void 0);
62
+ };
63
+ return hasJsxReturn(fn.body);
64
+ }
65
+ function isJsxValue(expression) {
66
+ const value = unwrap(expression);
67
+ if (isJsx(value)) return true;
68
+ if (ts2.isConditionalExpression(value)) return isJsxValue(value.whenTrue) || isJsxValue(value.whenFalse);
69
+ if (ts2.isBinaryExpression(value)) return isJsxValue(value.left) || isJsxValue(value.right);
70
+ return false;
71
+ }
72
+ function componentFunctionOf(initializer) {
73
+ const value = unwrap(initializer);
74
+ if (ts2.isArrowFunction(value) || ts2.isFunctionExpression(value)) return returnsJsx(value) ? value : void 0;
75
+ const isWrapperCall = ts2.isCallExpression(value) && value.arguments.length > 0 && COMPONENT_WRAPPERS.has(value.expression.getText());
76
+ if (!isWrapperCall) return void 0;
77
+ const wrapped = unwrap(value.arguments[0]);
78
+ if (ts2.isArrowFunction(wrapped) || ts2.isFunctionExpression(wrapped)) return wrapped;
79
+ return componentFunctionOf(wrapped);
80
+ }
81
+ function isComponentClass(cls) {
82
+ const extendsClause = cls.heritageClauses?.find((clause) => clause.token === ts2.SyntaxKind.ExtendsKeyword);
83
+ const baseClass = extendsClause?.types[0]?.expression.getText();
84
+ return !!baseClass && COMPONENT_BASE_CLASSES.has(baseClass);
85
+ }
86
+ var STATE_HOOKS = /* @__PURE__ */ new Set(["useState", "useReducer", "React.useState", "React.useReducer"]);
87
+ function stateHookOf(declaration) {
88
+ const call = declaration.initializer && unwrap(declaration.initializer);
89
+ const isStateHookCall = !!call && ts2.isCallExpression(call) && STATE_HOOKS.has(call.expression.getText());
90
+ if (!isStateHookCall) return void 0;
91
+ if (ts2.isIdentifier(declaration.name)) return { call, value: declaration };
92
+ if (!ts2.isArrayBindingPattern(declaration.name)) return void 0;
93
+ const [value, setter] = declaration.name.elements.map((element) => ts2.isBindingElement(element) && ts2.isIdentifier(element.name) ? element : void 0);
94
+ return value || setter ? { call, value, setter } : void 0;
95
+ }
96
+ var isHookName = (name) => /^use[A-Z0-9]/.test(name);
97
+
98
+ // src/parse.ts
3
99
  var PREVIEW_LINES = 6;
4
100
  var CONTAINER_PREVIEW_LINES = 3;
5
101
  var PREVIEW_WIDTH = 64;
@@ -10,16 +106,17 @@ function parseFile(path, text) {
10
106
  return m;
11
107
  }
12
108
  function parseStructure(path, text, prev) {
13
- const kind = /\.[jt]sx$/.test(path) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
109
+ const kind = scriptKindOf(path);
14
110
  const old = prev && prev.path === path ? internals.get(prev) : void 0;
15
111
  let sf;
16
112
  if (old && !old.reused) {
17
113
  old.reused = true;
18
- sf = ts.updateSourceFile(old.sf, text, changeRange(prev.text, text));
114
+ sf = ts3.updateSourceFile(old.sf, text, changeRange(prev.text, text));
19
115
  } else {
20
- sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kind);
116
+ sf = ts3.createSourceFile(path, text, ts3.ScriptTarget.Latest, true, kind);
21
117
  }
22
- const b = new Builder(sf, text);
118
+ const language = fileLanguageOf(path, sf);
119
+ const b = new Builder(sf, text, language.isReact);
23
120
  b.collectStatements(sf.statements, null, 0);
24
121
  const syntax = sf.parseDiagnostics ?? [];
25
122
  const model = {
@@ -31,7 +128,8 @@ function parseStructure(path, text, prev) {
31
128
  syntaxErrors: syntax.length,
32
129
  firstErrorLine: -1,
33
130
  diagnostics: [],
34
- indentUnit: detectIndentUnit(text)
131
+ indentUnit: detectIndentUnit(text),
132
+ language
35
133
  };
36
134
  model.diagnostics = syntax.map((d) => toCodeDiagnostic(model, sf, d, "syntax"));
37
135
  model.firstErrorLine = model.diagnostics[0]?.line ?? -1;
@@ -54,9 +152,9 @@ function toCodeDiagnostic(model, sf, d, source) {
54
152
  start,
55
153
  length: d.length ?? 0,
56
154
  line: sf.getLineAndCharacterOfPosition(Math.min(start, sf.text.length)).line + 1,
57
- message: ts.flattenDiagnosticMessageText(d.messageText, "\n"),
155
+ message: ts3.flattenDiagnosticMessageText(d.messageText, "\n"),
58
156
  source,
59
- severity: d.category === ts.DiagnosticCategory.Error ? "error" : "warning",
157
+ severity: d.category === ts3.DiagnosticCategory.Error ? "error" : "warning",
60
158
  blockId: blockAt(model, start)?.id ?? null
61
159
  };
62
160
  }
@@ -77,15 +175,29 @@ function changeRange(a, b) {
77
175
  while (s < n && a.charCodeAt(s) === b.charCodeAt(s)) s++;
78
176
  let e = 0;
79
177
  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);
178
+ return ts3.createTextChangeRange(ts3.createTextSpan(s, a.length - e - s), b.length - e - s);
179
+ }
180
+ function commonJsExportedFunction(s) {
181
+ if (!ts3.isExpressionStatement(s) || !ts3.isBinaryExpression(s.expression) || s.expression.operatorToken.kind !== ts3.SyntaxKind.EqualsToken) return null;
182
+ const { left, right } = s.expression;
183
+ const fn = unwrap(right);
184
+ const isFunction = ts3.isFunctionExpression(fn) || ts3.isArrowFunction(fn);
185
+ if (!isFunction || !ts3.isPropertyAccessExpression(left) || !ts3.isIdentifier(left.name)) return null;
186
+ const target = left.expression.getText();
187
+ const isExportsObject = target === "exports" || target === "module.exports";
188
+ return isExportsObject ? { assignment: s.expression, exportedName: left.name, fn } : null;
189
+ }
190
+ function scriptKindOf(path) {
191
+ if (/\.[cm]?jsx?$/.test(path)) return path.endsWith("x") ? ts3.ScriptKind.JSX : ts3.ScriptKind.JS;
192
+ return path.endsWith("x") ? ts3.ScriptKind.TSX : ts3.ScriptKind.TS;
81
193
  }
82
194
  function createProgram(sf) {
83
195
  const options = {
84
196
  noLib: true,
85
197
  noResolve: true,
86
198
  allowJs: true,
87
- target: ts.ScriptTarget.Latest,
88
- jsx: ts.JsxEmit.Preserve,
199
+ target: ts3.ScriptTarget.Latest,
200
+ jsx: ts3.JsxEmit.Preserve,
89
201
  types: []
90
202
  };
91
203
  const host = {
@@ -101,7 +213,7 @@ function createProgram(sf) {
101
213
  readFile: () => void 0,
102
214
  getDirectories: () => []
103
215
  };
104
- return ts.createProgram({ rootNames: [sf.fileName], options, host });
216
+ return ts3.createProgram({ rootNames: [sf.fileName], options, host });
105
217
  }
106
218
  function detectIndentUnit(text) {
107
219
  const m = /\n(\t+|[ ]{2,8})\S/.exec(text);
@@ -109,12 +221,14 @@ function detectIndentUnit(text) {
109
221
  return m[1].startsWith(" ") ? " " : m[1].length >= 4 && m[1].length % 4 === 0 ? " " : " ";
110
222
  }
111
223
  var Builder = class {
112
- constructor(sf, text) {
224
+ constructor(sf, text, isReact) {
113
225
  this.sf = sf;
114
226
  this.text = text;
227
+ this.isReact = isReact;
115
228
  }
116
229
  sf;
117
230
  text;
231
+ isReact;
118
232
  roots = [];
119
233
  byId = /* @__PURE__ */ new Map();
120
234
  /**
@@ -126,6 +240,8 @@ var Builder = class {
126
240
  rangeToBlock = /* @__PURE__ */ new Map();
127
241
  /** Every name a usage could resolve to. Other identifiers skip the (slow) checker. */
128
242
  names = /* @__PURE__ */ new Set();
243
+ /** Setters of React state (`setCount`), keyed like `declToBlock`: using one writes the state. */
244
+ stateSetters = /* @__PURE__ */ new Set();
129
245
  // ---------- structure ----------
130
246
  collectStatements(stmts, parent, depth, declarationsOnly = false) {
131
247
  let group = [];
@@ -154,33 +270,40 @@ var Builder = class {
154
270
  };
155
271
  for (let i = 0; i < stmts.length; i++) {
156
272
  const s = stmts[i];
157
- if (ts.isFunctionDeclaration(s)) {
273
+ if (ts3.isFunctionDeclaration(s)) {
158
274
  flushGroup();
159
275
  const overloads = [s];
160
276
  while (!overloads[overloads.length - 1].body) {
161
277
  const next = stmts[i + 1];
162
- if (next && ts.isFunctionDeclaration(next) && next.name?.text === s.name?.text) {
278
+ if (next && ts3.isFunctionDeclaration(next) && next.name?.text === s.name?.text) {
163
279
  overloads.push(next);
164
280
  i++;
165
281
  } else break;
166
282
  }
167
- this.addFunctionLike(overloads, s.name?.text ?? "default", parent, depth);
168
- } else if (ts.isClassDeclaration(s)) {
283
+ const name = s.name?.text ?? "default";
284
+ const isComponent = this.isReact && isComponentName(name) && returnsJsx(overloads[overloads.length - 1]);
285
+ this.addFunctionLike(overloads, name, parent, depth, { kind: isComponent ? "component" : "function" });
286
+ } else if (ts3.isClassDeclaration(s)) {
169
287
  flushGroup();
170
288
  this.addClass(s, s, s.name?.text ?? "default", parent, depth);
171
- } else if (ts.isVariableStatement(s)) {
289
+ } else if (ts3.isVariableStatement(s)) {
172
290
  flushGroup();
173
291
  this.addVariableStatement(s, parent, depth, declarationsOnly);
292
+ } else if (commonJsExportedFunction(s)) {
293
+ flushGroup();
294
+ const { assignment, exportedName, fn } = commonJsExportedFunction(s);
295
+ const declNodes = [s, assignment, assignment.left];
296
+ this.addFunctionLike([fn], exportedName.text, parent, depth, { rangeNodes: [s], declNodes, extraTags: ["export"], nameNode: exportedName });
174
297
  } else if (declarationsOnly) {
175
298
  continue;
176
- } else if (ts.isImportDeclaration(s) || ts.isImportEqualsDeclaration(s)) {
299
+ } else if (ts3.isImportDeclaration(s) || ts3.isImportEqualsDeclaration(s)) {
177
300
  flushGroup();
178
301
  this.addImport(s, parent, depth);
179
- } else if (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) || ts.isEnumDeclaration(s)) {
302
+ } else if (ts3.isInterfaceDeclaration(s) || ts3.isTypeAliasDeclaration(s) || ts3.isEnumDeclaration(s)) {
180
303
  flushGroup();
181
- const keyword = ts.isInterfaceDeclaration(s) ? "interface" : ts.isEnumDeclaration(s) ? "enum" : "type";
304
+ const keyword = ts3.isInterfaceDeclaration(s) ? "interface" : ts3.isEnumDeclaration(s) ? "enum" : "type";
182
305
  const typeParams = "typeParameters" in s && s.typeParameters ? `<${s.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
183
- const bodyStart = ts.isTypeAliasDeclaration(s) ? s.type.getStart(this.sf) : this.text.indexOf("{", s.name.end);
306
+ const bodyStart = ts3.isTypeAliasDeclaration(s) ? s.type.getStart(this.sf) : this.text.indexOf("{", s.name.end);
184
307
  this.add(
185
308
  {
186
309
  kind: "type",
@@ -198,7 +321,7 @@ var Builder = class {
198
321
  parent,
199
322
  depth
200
323
  );
201
- } else if (ts.isModuleDeclaration(s) && s.body && ts.isModuleBlock(s.body)) {
324
+ } else if (ts3.isModuleDeclaration(s) && s.body && ts3.isModuleBlock(s.body)) {
202
325
  flushGroup();
203
326
  const body = s.body;
204
327
  this.add(
@@ -228,13 +351,13 @@ var Builder = class {
228
351
  addImport(s, parent, depth) {
229
352
  let spec;
230
353
  let names = [];
231
- if (ts.isImportDeclaration(s)) {
354
+ if (ts3.isImportDeclaration(s)) {
232
355
  spec = s.moduleSpecifier.text;
233
356
  const clause = s.importClause;
234
357
  if (clause?.name) names.push(clause.name.text);
235
358
  const nb = clause?.namedBindings;
236
- if (nb && ts.isNamespaceImport(nb)) names.push("* as " + nb.name.text);
237
- if (nb && ts.isNamedImports(nb)) names.push(...nb.elements.map((e) => e.name.text));
359
+ if (nb && ts3.isNamespaceImport(nb)) names.push("* as " + nb.name.text);
360
+ if (nb && ts3.isNamedImports(nb)) names.push(...nb.elements.map((e) => e.name.text));
238
361
  if (!clause) names = ["(side effect)"];
239
362
  } else {
240
363
  spec = s.name.text;
@@ -248,7 +371,7 @@ var Builder = class {
248
371
  key: "import:" + spec,
249
372
  name: spec,
250
373
  detail: "",
251
- tags: ts.isImportDeclaration(s) && s.importClause?.isTypeOnly ? ["type"] : [],
374
+ tags: ts3.isImportDeclaration(s) && s.importClause?.isTypeOnly ? ["type"] : [],
252
375
  rangeNodes: [s],
253
376
  declNodes: [s],
254
377
  previewRange: null,
@@ -267,11 +390,11 @@ var Builder = class {
267
390
  let previewRange = null;
268
391
  let insert = null;
269
392
  let insertAt = -1;
270
- if (body && ts.isBlock(body)) {
393
+ if (body && ts3.isBlock(body)) {
271
394
  previewRange = [body.getStart(this.sf) + 1, body.end - 1];
272
395
  insert = "body";
273
396
  const last = body.statements[body.statements.length - 1];
274
- insertAt = last && ts.isReturnStatement(last) ? this.lineStartOf(last.getStart(this.sf)) : body.end - 1;
397
+ insertAt = last && ts3.isReturnStatement(last) ? this.lineStartOf(last.getStart(this.sf)) : body.end - 1;
275
398
  } else if (body) {
276
399
  previewRange = [body.getStart(this.sf), body.end];
277
400
  }
@@ -279,22 +402,23 @@ var Builder = class {
279
402
  const ret = sig.type ? ": " + sig.type.getText() : "";
280
403
  const detail = squash(`${typeParams}(${sig.parameters.map((p) => p.getText()).join(", ")})${ret}`);
281
404
  const tags = [...opts.extraTags ?? [], ...modifierTags(nodes[0])];
405
+ if (this.isReact && isHookName(name)) tags.push("hook");
282
406
  if (nodes.length > 1) tags.push(`${nodes.length - 1} overload${nodes.length > 2 ? "s" : ""}`);
283
407
  if (impl.asteriskToken) tags.push("generator");
284
408
  this.add(
285
409
  {
286
- kind: "function",
410
+ kind: opts.kind ?? "function",
287
411
  key: opts.key ?? name,
288
412
  name,
289
413
  detail,
290
414
  tags,
291
415
  rangeNodes: opts.rangeNodes ?? nodes,
292
416
  declNodes: opts.declNodes ?? nodes,
293
- nameNode: impl.name,
417
+ nameNode: opts.nameNode ?? impl.name,
294
418
  previewRange,
295
419
  insert,
296
420
  insertAt,
297
- children: body && ts.isBlock(body) ? (blk) => this.collectStatements(body.statements, blk, depth + 1, true) : void 0
421
+ children: body && ts3.isBlock(body) ? (blk) => this.collectStatements(body.statements, blk, depth + 1, true) : void 0
298
422
  },
299
423
  parent,
300
424
  depth
@@ -306,7 +430,7 @@ var Builder = class {
306
430
  const typeParams = cls.typeParameters?.length ? `<${cls.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
307
431
  this.add(
308
432
  {
309
- kind: "class",
433
+ kind: this.isReact && isComponentClass(cls) ? "component" : "class",
310
434
  key: name,
311
435
  name,
312
436
  detail: squash(typeParams + (heritage ? " " + heritage : "")),
@@ -326,27 +450,27 @@ var Builder = class {
326
450
  collectMembers(members, parent, depth) {
327
451
  for (let i = 0; i < members.length; i++) {
328
452
  const m = members[i];
329
- if (ts.isMethodDeclaration(m) || ts.isConstructorDeclaration(m)) {
330
- const name = ts.isConstructorDeclaration(m) ? "constructor" : m.name.getText();
453
+ if (ts3.isMethodDeclaration(m) || ts3.isConstructorDeclaration(m)) {
454
+ const name = ts3.isConstructorDeclaration(m) ? "constructor" : m.name.getText();
331
455
  const overloads = [m];
332
456
  while (!overloads[overloads.length - 1].body) {
333
457
  const next = members[i + 1];
334
- const nextName = next && (ts.isConstructorDeclaration(next) ? "constructor" : ts.isMethodDeclaration(next) ? next.name.getText() : null);
458
+ const nextName = next && (ts3.isConstructorDeclaration(next) ? "constructor" : ts3.isMethodDeclaration(next) ? next.name.getText() : null);
335
459
  if (nextName === name) {
336
460
  overloads.push(next);
337
461
  i++;
338
462
  } else break;
339
463
  }
340
464
  this.addFunctionLike(overloads, name, parent, depth);
341
- } else if (ts.isGetAccessor(m) || ts.isSetAccessor(m)) {
465
+ } else if (ts3.isGetAccessor(m) || ts3.isSetAccessor(m)) {
342
466
  const name = m.name.getText();
343
- const word = ts.isGetAccessor(m) ? "get" : "set";
467
+ const word = ts3.isGetAccessor(m) ? "get" : "set";
344
468
  this.addFunctionLike([m], name, parent, depth, { key: `${word} ${name}`, extraTags: [word] });
345
- } else if (ts.isPropertyDeclaration(m)) {
469
+ } else if (ts3.isPropertyDeclaration(m)) {
346
470
  const name = m.name.getText();
347
471
  const init = m.initializer && unwrap(m.initializer);
348
- if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
349
- this.addFunctionLike([init], name, parent, depth, { rangeNodes: [m], declNodes: [m], extraTags: modifierTags(m) });
472
+ if (init && (ts3.isArrowFunction(init) || ts3.isFunctionExpression(init))) {
473
+ this.addFunctionLike([init], name, parent, depth, { rangeNodes: [m], declNodes: [m], extraTags: modifierTags(m), nameNode: m.name });
350
474
  } else {
351
475
  this.addVariable(m, m, name, m.type ? ": " + m.type.getText() : "", modifierTags(m), parent, depth, m.initializer);
352
476
  }
@@ -355,25 +479,61 @@ var Builder = class {
355
479
  }
356
480
  addVariableStatement(s, parent, depth, functionsOnly) {
357
481
  const decls = s.declarationList.declarations;
358
- const keyword = s.declarationList.flags & ts.NodeFlags.Const ? "const" : s.declarationList.flags & ts.NodeFlags.Let ? "let" : "var";
482
+ const keyword = s.declarationList.flags & ts3.NodeFlags.Const ? "const" : s.declarationList.flags & ts3.NodeFlags.Let ? "let" : "var";
359
483
  for (const d of decls) {
360
484
  const rangeNode = decls.length === 1 ? s : d;
361
485
  const name = d.name.getText();
362
486
  const tags = [...modifierTags(s), keyword];
487
+ const stateHook = this.isReact ? stateHookOf(d) : void 0;
488
+ if (stateHook) {
489
+ this.addState(stateHook, rangeNode, tags, parent, depth);
490
+ continue;
491
+ }
363
492
  const init = d.initializer && unwrap(d.initializer);
364
- if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
365
- this.addFunctionLike([init], name, parent, depth, { rangeNodes: [rangeNode], declNodes: [d], extraTags: tags });
366
- } else if (init && ts.isClassExpression(init)) {
493
+ const componentFunction = this.isReact && init && isComponentName(name) ? componentFunctionOf(init) : void 0;
494
+ if (componentFunction) {
495
+ this.addFunctionLike([componentFunction], name, parent, depth, { kind: "component", rangeNodes: [rangeNode], declNodes: [d], extraTags: tags, nameNode: d.name });
496
+ } else if (init && (ts3.isArrowFunction(init) || ts3.isFunctionExpression(init))) {
497
+ this.addFunctionLike([init], name, parent, depth, { rangeNodes: [rangeNode], declNodes: [d], extraTags: tags, nameNode: d.name });
498
+ } else if (init && ts3.isClassExpression(init)) {
367
499
  this.addClass(init, rangeNode, name, parent, depth, [d, init], tags);
368
500
  } else if (!functionsOnly) {
369
501
  this.addVariable(d, rangeNode, name, d.type ? ": " + d.type.getText() : "", tags, parent, depth, d.initializer);
370
502
  }
371
503
  }
372
504
  }
505
+ /** `const [count, setCount] = useState(0)`: a state block named `count`; using `setCount` writes it. */
506
+ addState(hook, rangeNode, tags, parent, depth) {
507
+ const named = hook.value ?? hook.setter;
508
+ const setterName = hook.setter?.name.getText();
509
+ const [firstArg, ...otherArgs] = hook.call.arguments;
510
+ const args = firstArg ? truncate(squash(firstArg.getText()), 24) + (otherArgs.length ? ", \u2026" : "") : "";
511
+ if (hook.setter && setterName) {
512
+ this.stateSetters.add(nodeKey(hook.setter));
513
+ this.names.add(setterName);
514
+ }
515
+ this.add(
516
+ {
517
+ kind: "state",
518
+ key: named.name.getText(),
519
+ name: named.name.getText(),
520
+ detail: `${hook.call.expression.getText()}(${args})`,
521
+ tags: setterName && hook.value ? [...tags, setterName] : tags,
522
+ rangeNodes: [rangeNode],
523
+ declNodes: [hook.value, hook.setter].filter((declaration) => !!declaration),
524
+ nameNode: named.name,
525
+ previewRange: [hook.call.getStart(this.sf), hook.call.end],
526
+ insert: null,
527
+ insertAt: -1
528
+ },
529
+ parent,
530
+ depth
531
+ );
532
+ }
373
533
  addVariable(decl, rangeNode, name, type, tags, parent, depth, initializer) {
374
534
  const init = initializer && unwrap(initializer);
375
- const objectChildren = init && ts.isObjectLiteralExpression(init) ? init.properties.filter(
376
- (p) => ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p) && (ts.isArrowFunction(unwrap(p.initializer)) || ts.isFunctionExpression(unwrap(p.initializer)))
535
+ const objectChildren = init && ts3.isObjectLiteralExpression(init) ? init.properties.filter(
536
+ (p) => ts3.isMethodDeclaration(p) || ts3.isPropertyAssignment(p) && (ts3.isArrowFunction(unwrap(p.initializer)) || ts3.isFunctionExpression(unwrap(p.initializer)))
377
537
  ) : [];
378
538
  this.add(
379
539
  {
@@ -391,10 +551,10 @@ var Builder = class {
391
551
  children: objectChildren.length ? (blk) => {
392
552
  for (const p of objectChildren) {
393
553
  const pname = p.name.getText();
394
- if (ts.isMethodDeclaration(p)) this.addFunctionLike([p], pname, blk, depth + 1);
554
+ if (ts3.isMethodDeclaration(p)) this.addFunctionLike([p], pname, blk, depth + 1);
395
555
  else {
396
556
  const fn = unwrap(p.initializer);
397
- this.addFunctionLike([fn], pname, blk, depth + 1, { rangeNodes: [p], declNodes: [p] });
557
+ this.addFunctionLike([fn], pname, blk, depth + 1, { rangeNodes: [p], declNodes: [p], nameNode: p.name });
398
558
  }
399
559
  }
400
560
  } : void 0
@@ -453,7 +613,7 @@ var Builder = class {
453
613
  /** Include doc comments directly above the node (no blank line in between). */
454
614
  startWithComments(node) {
455
615
  let start = node.getStart(this.sf);
456
- const comments = ts.getLeadingCommentRanges(this.text, node.pos) ?? [];
616
+ const comments = ts3.getLeadingCommentRanges(this.text, node.pos) ?? [];
457
617
  for (let i = comments.length - 1; i >= 0; i--) {
458
618
  const gap = this.text.slice(comments[i].end, start);
459
619
  if (/\S/.test(gap) || (gap.match(/\n/g)?.length ?? 0) > 1) break;
@@ -481,8 +641,8 @@ var Builder = class {
481
641
  const visit = (node, current) => {
482
642
  const blk = this.rangeToBlock.get(nodeKey(node));
483
643
  if (blk) current = blk;
484
- if (ts.isIdentifier(node) && current) this.recordUsage(node, current, checker, byPair, otherFiles);
485
- ts.forEachChild(node, (c) => visit(c, current));
644
+ if (ts3.isIdentifier(node) && current) this.recordUsage(node, current, checker, byPair, otherFiles);
645
+ ts3.forEachChild(node, (c) => visit(c, current));
486
646
  };
487
647
  visit(sf, null);
488
648
  return [...byPair.values()];
@@ -491,29 +651,30 @@ var Builder = class {
491
651
  const isKnownName = this.names.has(id.text) || !!otherFiles?.projectNames.has(id.text);
492
652
  if (!isKnownName) return;
493
653
  const parent = id.parent;
494
- const isDeclarationName = !!parent && parent.name === id && !ts.isPropertyAccessExpression(parent) && !ts.isShorthandPropertyAssignment(parent);
654
+ const isDeclarationName = !!parent && parent.name === id && !ts3.isPropertyAccessExpression(parent) && !ts3.isShorthandPropertyAssignment(parent);
495
655
  if (isDeclarationName) return;
496
656
  let sym;
497
657
  try {
498
- sym = parent && ts.isShorthandPropertyAssignment(parent) ? checker.getShorthandAssignmentValueSymbol(parent) : checker.getSymbolAtLocation(id);
658
+ sym = parent && ts3.isShorthandPropertyAssignment(parent) ? checker.getShorthandAssignmentValueSymbol(parent) : checker.getSymbolAtLocation(id);
499
659
  } catch {
500
660
  return;
501
661
  }
502
662
  if (!sym?.declarations) return;
503
- const kind = usageKind(id);
663
+ let kind = usageKind(id);
504
664
  let to;
505
665
  let importBlockId;
506
666
  for (const d of sym.declarations) {
507
667
  to = this.declToBlock.get(nodeKey(d));
668
+ if (this.stateSetters.has(nodeKey(d))) kind = "write";
508
669
  if (!to) {
509
670
  let n = d;
510
- while (n && !ts.isImportDeclaration(n) && !ts.isImportEqualsDeclaration(n) && !ts.isSourceFile(n)) n = n.parent;
511
- if (n && !ts.isSourceFile(n)) importBlockId = to = this.declToBlock.get(nodeKey(n));
671
+ while (n && !ts3.isImportDeclaration(n) && !ts3.isImportEqualsDeclaration(n) && !ts3.isSourceFile(n)) n = n.parent;
672
+ if (n && !ts3.isSourceFile(n)) importBlockId = to = this.declToBlock.get(nodeKey(n));
512
673
  }
513
674
  if (to) break;
514
675
  }
515
676
  if (otherFiles) {
516
- const isImportedName = !!importBlockId && (sym.flags & ts.SymbolFlags.Alias) !== 0;
677
+ const isImportedName = !!importBlockId && (sym.flags & ts3.SymbolFlags.Alias) !== 0;
517
678
  const target = isImportedName ? safeAliasedSymbol(checker, sym) : to ? void 0 : sym;
518
679
  const declaredElsewhere = target?.declarations?.find((d) => d.getSourceFile() !== id.getSourceFile());
519
680
  const external = declaredElsewhere && otherFiles.blockInOtherFile(declaredElsewhere);
@@ -530,6 +691,7 @@ function addUsage(out, usage) {
530
691
  if (!existing) return void out.set(pairKey, usage);
531
692
  existing.count++;
532
693
  if (usage.kind === "call") existing.kind = "call";
694
+ else if (usage.kind === "write" && existing.kind === "read") existing.kind = "write";
533
695
  }
534
696
  function safeAliasedSymbol(checker, symbol) {
535
697
  try {
@@ -542,23 +704,26 @@ function nodeKey(node) {
542
704
  return `${node.kind}:${node.pos}:${node.end}`;
543
705
  }
544
706
  function usageKind(id) {
707
+ if (isJsxTagName(id)) return "render";
545
708
  let n = id;
546
- if (ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) n = n.parent;
709
+ if (ts3.isPropertyAccessExpression(n.parent) && n.parent.name === n) n = n.parent;
547
710
  const p = n.parent;
548
- if ((ts.isCallExpression(p) || ts.isNewExpression(p)) && p.expression === n) return "call";
549
- if (ts.isTaggedTemplateExpression(p) && p.tag === n) return "call";
550
- if (ts.isJsxOpeningLikeElement(p) && p.tagName === n) return "call";
551
- if (ts.isBinaryExpression(p) && p.left === n && p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts.SyntaxKind.LastAssignment) return "write";
552
- if ((ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) && (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) return "write";
553
- if (ts.isTypeReferenceNode(p) || ts.isExpressionWithTypeArguments(p) || ts.isTypeQueryNode(p) || ts.isQualifiedName(p)) return "type";
711
+ if ((ts3.isCallExpression(p) || ts3.isNewExpression(p)) && p.expression === n) return "call";
712
+ if (ts3.isTaggedTemplateExpression(p) && p.tag === n) return "call";
713
+ if (ts3.isJsxOpeningLikeElement(p) && p.tagName === n) return "render";
714
+ if (ts3.isBinaryExpression(p) && p.left === n && p.operatorToken.kind >= ts3.SyntaxKind.FirstAssignment && p.operatorToken.kind <= ts3.SyntaxKind.LastAssignment) return "write";
715
+ if ((ts3.isPrefixUnaryExpression(p) || ts3.isPostfixUnaryExpression(p)) && (p.operator === ts3.SyntaxKind.PlusPlusToken || p.operator === ts3.SyntaxKind.MinusMinusToken)) return "write";
716
+ if (ts3.isTypeReferenceNode(p) || ts3.isExpressionWithTypeArguments(p) || ts3.isTypeQueryNode(p) || ts3.isQualifiedName(p)) return "type";
554
717
  return "read";
555
718
  }
556
- function unwrap(e) {
557
- while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e) || ts.isTypeAssertionExpression(e)) e = e.expression;
558
- return e;
719
+ function isJsxTagName(id) {
720
+ let tag = id;
721
+ while (ts3.isPropertyAccessExpression(tag.parent) && tag.parent.expression === tag) tag = tag.parent;
722
+ const element = tag.parent;
723
+ return (ts3.isJsxOpeningLikeElement(element) || ts3.isJsxClosingElement(element)) && element.tagName === tag;
559
724
  }
560
725
  function modifierTags(n) {
561
- const mods = ts.canHaveModifiers(n) ? ts.getModifiers(n) ?? [] : [];
726
+ const mods = ts3.canHaveModifiers(n) ? ts3.getModifiers(n) ?? [] : [];
562
727
  return mods.map((m) => m.getText());
563
728
  }
564
729
  function squash(s) {
@@ -606,9 +771,106 @@ function makePreview(text, range, children, max) {
606
771
  }
607
772
 
608
773
  // src/project.ts
609
- import ts2 from "typescript";
774
+ import ts5 from "typescript";
775
+
776
+ // src/workspace.ts
777
+ import ts4 from "typescript";
778
+ function parseConfig(config, projectPaths) {
779
+ const configFileName = "/" + config.path;
780
+ const json = ts4.parseConfigFileTextToJson(configFileName, config.text);
781
+ if (json.error || !json.config) return null;
782
+ const readConfig = (name) => name === configFileName ? config.text : config.extendedFiles?.[name.slice(1)];
783
+ const host = {
784
+ useCaseSensitiveFileNames: true,
785
+ readDirectory: (rootDir, extensions, excludes, includes) => matchingPaths(projectPaths, rootDir, extensions, excludes ?? [], includes ?? ["**/*"]),
786
+ fileExists: (name) => readConfig(name) !== void 0,
787
+ readFile: readConfig
788
+ };
789
+ const folder = config.path.slice(0, config.path.lastIndexOf("/") + 1);
790
+ const parsed = ts4.parseJsonConfigFileContent(json.config, host, "/" + folder, void 0, configFileName);
791
+ const included = new Set(parsed.fileNames.map((name) => name.slice(1)));
792
+ return { path: config.path, folder, options: parsed.options, includes: (projectPath) => included.has(projectPath) };
793
+ }
794
+ function configForFile(projectPath, configs) {
795
+ const above = configs.filter((config) => projectPath.startsWith(config.folder));
796
+ const nearestFirst = [...above].sort((a, b) => b.folder.length - a.folder.length);
797
+ return nearestFirst.find((config) => config.includes(projectPath)) ?? nearestFirst[0] ?? null;
798
+ }
799
+ function workspacePackagePaths(packageFiles, hasFile) {
800
+ const paths = {};
801
+ for (const [packagePath, text] of Object.entries(packageFiles)) {
802
+ const manifest = parsePackageJson(text);
803
+ if (!manifest?.name) continue;
804
+ const folder = packagePath.slice(0, packagePath.lastIndexOf("/") + 1);
805
+ 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);
806
+ if (entry) paths[manifest.name] = ["/" + entry];
807
+ paths[manifest.name + "/*"] = ["/" + folder + "src/*", "/" + folder + "*"];
808
+ }
809
+ return paths;
810
+ }
811
+ function parsePackageJson(text) {
812
+ try {
813
+ const value = JSON.parse(text);
814
+ return value && typeof value === "object" ? value : null;
815
+ } catch {
816
+ return null;
817
+ }
818
+ }
819
+ function packageEntries(manifest) {
820
+ const rootExport = manifest.exports && typeof manifest.exports === "object" && "." in manifest.exports ? manifest.exports["."] : manifest.exports;
821
+ return [manifest.types, manifest.typings, ...exportTargets(rootExport), manifest.module, manifest.main].filter((entry) => typeof entry === "string");
822
+ }
823
+ function exportTargets(target) {
824
+ if (typeof target === "string") return [target];
825
+ if (Array.isArray(target)) return target.flatMap(exportTargets);
826
+ if (target && typeof target === "object") return Object.values(target).flatMap(exportTargets);
827
+ return [];
828
+ }
829
+ function sourceOfBuildOutput(entry) {
830
+ const isBuildOutput = /\.(d\.ts|d\.mts|js|mjs|cjs|jsx)$/.test(entry);
831
+ if (!isBuildOutput) return [];
832
+ const withoutExtension = entry.replace(/\.(d\.ts|d\.mts|js|mjs|cjs|jsx)$/, "");
833
+ const inSourceFolder = withoutExtension.replace(/(^|\/)(dist|lib|build|out|esm|cjs)\//, "$1src/");
834
+ return [inSourceFolder, withoutExtension].flatMap((base) => [base + ".ts", base + ".tsx"]);
835
+ }
836
+ function matchingPaths(projectPaths, rootDir, extensions, excludes, includes) {
837
+ const base = rootDir.replace(/^\//, "").replace(/\/?$/, "/").replace(/^\/$/, "");
838
+ const includePatterns = includes.map((spec) => configPattern(base, spec, "files"));
839
+ const excludePatterns = excludes.map((spec) => configPattern(base, spec, "filesAndFolders"));
840
+ 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);
841
+ }
842
+ function configPattern(base, spec, matching) {
843
+ const absolute = spec.startsWith("/") ? spec.slice(1) : normalizePath(base + spec.replace(/^\.\//, ""));
844
+ const lastPart = absolute.slice(absolute.lastIndexOf("/") + 1);
845
+ const isFolder = !/[*?]/.test(lastPart) && !/\.[^.]+$/.test(lastPart);
846
+ const pattern = isFolder ? absolute.replace(/\/?$/, "/**/*") : absolute;
847
+ let source = "";
848
+ for (let index = 0; index < pattern.length; index++) {
849
+ const char = pattern[index];
850
+ if (pattern.startsWith("**/", index)) {
851
+ source += "(?:[^/]+/)*";
852
+ index += 2;
853
+ } else if (char === "*") source += "[^/]*";
854
+ else if (char === "?") source += "[^/]";
855
+ else source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
856
+ }
857
+ return new RegExp(`^${source}${matching === "filesAndFolders" ? "(?:/.*)?" : ""}$`);
858
+ }
859
+ function normalizePath(path) {
860
+ const parts = [];
861
+ for (const part of path.split("/")) {
862
+ if (part === "..") parts.pop();
863
+ else if (part !== "." && part !== "") parts.push(part);
864
+ }
865
+ return parts.join("/");
866
+ }
867
+
868
+ // src/project.ts
610
869
  var LIB_DIR = "/__lib__/";
611
870
  var DEFAULT_LIB = ["lib.es2022.d.ts", "lib.dom.d.ts"];
871
+ var NO_CONFIG = "";
872
+ var REQUIRE_CALL = /\brequire\s*\(\s*(['"])([^'"]+)\1\s*\)/;
873
+ var RE_EXPORT = /^export\s+(?:type\s+)?(?:\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s*from\s*(['"])([^'"]+)\1/;
612
874
  var Project = class {
613
875
  constructor(files = {}, projectOptions = {}) {
614
876
  this.projectOptions = projectOptions;
@@ -620,11 +882,14 @@ var Project = class {
620
882
  projectVersion = 0;
621
883
  libFiles = {};
622
884
  hasLib = false;
623
- options = {};
624
- service;
885
+ libOptions = {};
625
886
  projectNamesCache = null;
626
- /** Options from the folder's tsconfig.json, between the defaults and `ProjectOptions.compilerOptions`. */
627
- configFileOptions = {};
887
+ workspace = null;
888
+ configPrograms = null;
889
+ /** Language services by config path, created when a file of that config is first resolved. */
890
+ services = /* @__PURE__ */ new Map();
891
+ /** Shared by the services, so a file several programs include is parsed once. */
892
+ documentRegistry = ts5.createDocumentRegistry(true, "/");
628
893
  /** Changes when files are added or removed, or the settings change: where imports point may change then. */
629
894
  fileSetVersion = 0;
630
895
  /** Load the standard library later, e.g. once it has been downloaded. Type errors show from then on. */
@@ -634,11 +899,16 @@ var Project = class {
634
899
  }
635
900
  /** Use the folder's tsconfig.json (or none, with null). */
636
901
  setConfigFile(config) {
637
- this.configFileOptions = config ? compilerOptionsOf(config) : {};
902
+ this.setWorkspace(config ? { configFiles: [config] } : null);
903
+ }
904
+ /** Use the folder's config files and workspace packages (or none, with null). */
905
+ setWorkspace(workspace) {
906
+ this.workspace = workspace;
638
907
  this.reconfigure();
639
908
  }
640
909
  reconfigure() {
641
- this.service.dispose();
910
+ for (const service of this.services.values()) service.dispose();
911
+ this.services.clear();
642
912
  this.configure(this.projectOptions);
643
913
  this.projectVersion++;
644
914
  this.fileSetVersion++;
@@ -647,23 +917,66 @@ var Project = class {
647
917
  this.libFiles = options.libFiles ?? {};
648
918
  const libEntries = (options.lib ?? DEFAULT_LIB).filter((name) => name in this.libFiles);
649
919
  this.hasLib = libEntries.length > 0;
650
- this.options = {
651
- target: ts2.ScriptTarget.ES2022,
652
- module: ts2.ModuleKind.ESNext,
653
- moduleResolution: ts2.ModuleResolutionKind.Bundler,
920
+ this.libOptions = this.hasLib ? { lib: libEntries, noLib: false } : { noLib: true };
921
+ }
922
+ /** Options for a program: the defaults, then the config's, then `ProjectOptions.compilerOptions`. */
923
+ compilerOptions(configOptions, packagePaths) {
924
+ const paths = { ...packagePaths, ...configOptions.paths };
925
+ return {
926
+ target: ts5.ScriptTarget.ES2022,
927
+ module: ts5.ModuleKind.ESNext,
928
+ moduleResolution: ts5.ModuleResolutionKind.Bundler,
654
929
  strict: true,
655
- allowJs: true,
656
- jsx: ts2.JsxEmit.Preserve,
930
+ jsx: ts5.JsxEmit.Preserve,
657
931
  skipLibCheck: true,
658
932
  allowImportingTsExtensions: true,
659
- ...this.configFileOptions,
660
- ...options.compilerOptions,
933
+ ...configOptions,
934
+ ...Object.keys(paths).length ? { paths } : {},
935
+ ...this.projectOptions.compilerOptions,
936
+ // JavaScript files are part of the project even where a tsconfig.json leaves `allowJs` off.
937
+ allowJs: true,
661
938
  // The project loads no @types packages, its standard library is the one given here, and it never emits.
662
939
  types: [],
663
940
  noEmit: true,
664
- ...this.hasLib ? { lib: libEntries, noLib: false } : { noLib: true }
941
+ ...this.libOptions
665
942
  };
666
- this.service = ts2.createLanguageService(this.createHost(), ts2.createDocumentRegistry(true, "/"));
943
+ }
944
+ /** Which config each file uses, and the options of each. Worked out again when files are added or removed. */
945
+ programs() {
946
+ if (this.configPrograms?.fileSetVersion === this.fileSetVersion) return this.configPrograms;
947
+ const projectPaths = [...this.files.keys()];
948
+ const packagePaths = workspacePackagePaths(this.workspace?.packageFiles ?? {}, (path) => this.files.has(path));
949
+ const configs = (this.workspace?.configFiles ?? []).map((config) => parseConfig(config, projectPaths)).filter((config) => !!config);
950
+ const optionsByConfig = /* @__PURE__ */ new Map([[NO_CONFIG, this.compilerOptions({}, packagePaths)]]);
951
+ for (const config of configs) optionsByConfig.set(config.path, this.compilerOptions(config.options, packagePaths));
952
+ const pathsByConfig = /* @__PURE__ */ new Map();
953
+ const configOfPath = /* @__PURE__ */ new Map();
954
+ for (const path of projectPaths) {
955
+ const configPath = configForFile(path, configs)?.path ?? NO_CONFIG;
956
+ configOfPath.set(path, configPath);
957
+ pathsByConfig.set(configPath, [...pathsByConfig.get(configPath) ?? [], path]);
958
+ }
959
+ this.configPrograms = { fileSetVersion: this.fileSetVersion, optionsByConfig, pathsByConfig, configOfPath };
960
+ return this.configPrograms;
961
+ }
962
+ configOf(path) {
963
+ return this.programs().configOfPath.get(path) ?? NO_CONFIG;
964
+ }
965
+ /** The options a file's imports resolve with: its own config's, whichever program includes it. */
966
+ optionsOfFile(path) {
967
+ const { optionsByConfig } = this.programs();
968
+ return optionsByConfig.get(path ? this.configOf(path) : NO_CONFIG) ?? optionsByConfig.get(NO_CONFIG);
969
+ }
970
+ serviceOf(path) {
971
+ return this.serviceOfConfig(this.configOf(path));
972
+ }
973
+ serviceOfConfig(configPath) {
974
+ let service = this.services.get(configPath);
975
+ if (!service) {
976
+ service = ts5.createLanguageService(this.createHost(configPath), this.documentRegistry);
977
+ this.services.set(configPath, service);
978
+ }
979
+ return service;
667
980
  }
668
981
  // ---------------------------------------------------------------- files (fast path)
669
982
  /** Add or update a file. Parses only this file, incrementally from its previous version. */
@@ -702,7 +1015,7 @@ var Project = class {
702
1015
  const file = this.files.get(path);
703
1016
  if (!file) return [];
704
1017
  if (file.usages && file.usagesProjectVersion === this.projectVersion) return file.usages;
705
- const program = this.service.getProgram();
1018
+ const program = this.serviceOf(path).getProgram();
706
1019
  const sf = program?.getSourceFile(toFileName(path));
707
1020
  const builder = builderOf(file.model);
708
1021
  if (!program || !sf || !builder || sf.text !== file.text) return [];
@@ -713,7 +1026,7 @@ var Project = class {
713
1026
  /** Usages in other files that point at blocks in this file. */
714
1027
  usagesInto(path) {
715
1028
  const incoming = [];
716
- for (const otherPath of this.importersOf(path)) {
1029
+ for (const otherPath of this.importersThroughReExports(path)) {
717
1030
  for (const usage of this.usages(otherPath)) {
718
1031
  if (usage.toPath === path) incoming.push({ ...usage, fromPath: otherPath });
719
1032
  }
@@ -726,9 +1039,10 @@ var Project = class {
726
1039
  if (!file) return [];
727
1040
  const hasSyntaxErrors = file.model.diagnostics.length > 0;
728
1041
  if (hasSyntaxErrors || !this.hasLib) return file.model.diagnostics;
729
- const sf = this.service.getProgram()?.getSourceFile(toFileName(path));
1042
+ const service = this.serviceOf(path);
1043
+ const sf = service.getProgram()?.getSourceFile(toFileName(path));
730
1044
  if (!sf || sf.text !== file.text) return [];
731
- return this.service.getSemanticDiagnostics(toFileName(path)).map((d) => toCodeDiagnostic(file.model, sf, d, "type"));
1045
+ return service.getSemanticDiagnostics(toFileName(path)).map((d) => toCodeDiagnostic(file.model, sf, d, "type"));
732
1046
  }
733
1047
  /**
734
1048
  * Text edits that rename a block and every usage of it, in every file of the project.
@@ -745,29 +1059,54 @@ var Project = class {
745
1059
  if (namesVisibleFrom(file.model, block).has(newName)) return { ok: false, error: `${newName} already exists here` };
746
1060
  if (file.model.diagnostics.length) return { ok: false, error: `Fix the syntax error on line ${file.model.firstErrorLine} first` };
747
1061
  const fileName2 = toFileName(path);
748
- const info = this.service.getRenameInfo(fileName2, block.nameStart, { allowRenameOfImportPath: false });
1062
+ const info = this.serviceOf(path).getRenameInfo(fileName2, block.nameStart, { allowRenameOfImportPath: false });
749
1063
  if (!info.canRename) return { ok: false, error: info.localizedErrorMessage };
750
- const locations = this.service.findRenameLocations(fileName2, block.nameStart, false, false, { providePrefixAndSuffixTextForRename: true }) ?? [];
751
1064
  const edits = {};
752
- for (const location of locations) {
753
- const locationPath = fromFileName(location.fileName);
754
- if (!locationPath) continue;
755
- const text = this.withoutImportAlias(location, newName);
756
- (edits[locationPath] ??= []).push({ start: location.textSpan.start, end: location.textSpan.start + location.textSpan.length, text });
1065
+ const renamedLocations = /* @__PURE__ */ new Set();
1066
+ for (const configPath of this.configsUsing(path)) {
1067
+ const service = this.serviceOfConfig(configPath);
1068
+ if (!service.getProgram()?.getSourceFile(fileName2)) continue;
1069
+ const renameStarts = [{ fileName: fileName2, position: block.nameStart }];
1070
+ const visitedStarts = /* @__PURE__ */ new Set();
1071
+ for (let start = renameStarts.pop(); start; start = renameStarts.pop()) {
1072
+ const startKey = `${start.fileName}:${start.position}`;
1073
+ if (visitedStarts.has(startKey)) continue;
1074
+ visitedStarts.add(startKey);
1075
+ const locations = service.findRenameLocations(start.fileName, start.position, false, false, { providePrefixAndSuffixTextForRename: true }) ?? [];
1076
+ for (const location of locations) {
1077
+ const locationPath = fromFileName(location.fileName);
1078
+ if (!locationPath) continue;
1079
+ if (isAliasInSpecifier(location)) renameStarts.push({ fileName: location.fileName, position: location.textSpan.start });
1080
+ const locationKey = `${location.fileName}:${location.textSpan.start}`;
1081
+ if (renamedLocations.has(locationKey)) continue;
1082
+ renamedLocations.add(locationKey);
1083
+ const text = isAliasInSpecifier(location) ? newName : (location.prefixText ?? "") + newName + (location.suffixText ?? "");
1084
+ (edits[locationPath] ??= []).push({ start: location.textSpan.start, end: location.textSpan.start + location.textSpan.length, text });
1085
+ }
1086
+ }
757
1087
  }
1088
+ for (const fileEdits of Object.values(edits)) fileEdits.sort((a, b) => a.start - b.start);
758
1089
  return { ok: true, edits };
759
1090
  }
760
1091
  // ---------------------------------------------------------------- internals
761
- /**
762
- * TypeScript keeps local names stable when renaming an export (`import { newName as old }`).
763
- * Here a rename should update every usage, so import/export specifiers get the plain new name.
764
- */
765
- withoutImportAlias(location, newName) {
766
- const prefix = location.prefixText ?? "";
767
- const suffix = location.suffixText ?? "";
768
- const isAliasInSpecifier = /\bas\s*$/.test(prefix) || /^\s*as\b/.test(suffix);
769
- if (isAliasInSpecifier) return newName;
770
- return prefix + newName + suffix;
1092
+ /** The configs of a file and of the files that can use its blocks: its importers, also through re-exports. */
1093
+ configsUsing(path) {
1094
+ const owner = this.configOf(path);
1095
+ const importerConfigs = this.importersThroughReExports(path).map((importer) => this.configOf(importer));
1096
+ return [owner, ...new Set(importerConfigs.filter((configPath) => configPath !== owner))];
1097
+ }
1098
+ /** Files that import `path`, or import a file that re-exports from it (an `index.ts` of a folder or package). */
1099
+ importersThroughReExports(path) {
1100
+ const importers = /* @__PURE__ */ new Set();
1101
+ const reExporters = [path];
1102
+ for (let reExported = reExporters.pop(); reExported !== void 0; reExported = reExporters.pop()) {
1103
+ for (const importer of this.importersOf(reExported)) {
1104
+ if (importers.has(importer) || importer === path) continue;
1105
+ importers.add(importer);
1106
+ if (this.importsOf(importer).reExportedPaths.has(reExported)) reExporters.push(importer);
1107
+ }
1108
+ }
1109
+ return [...importers];
771
1110
  }
772
1111
  otherFileResolver() {
773
1112
  return {
@@ -793,29 +1132,38 @@ var Project = class {
793
1132
  importersOf(path) {
794
1133
  const importers = [];
795
1134
  for (const [otherPath, file] of this.files) {
796
- if (otherPath !== path && this.importedPaths(otherPath, file).has(path)) importers.push(otherPath);
1135
+ if (otherPath !== path && this.importsOf(otherPath, file).importedPaths.has(path)) importers.push(otherPath);
797
1136
  }
798
1137
  return importers;
799
1138
  }
800
- importedPaths(path, file) {
801
- if (file.importedPaths?.fileSetVersion === this.fileSetVersion) return file.importedPaths.paths;
802
- const resolved = /* @__PURE__ */ new Set();
1139
+ importsOf(path, file = this.files.get(path)) {
1140
+ if (file.imports?.fileSetVersion === this.fileSetVersion && file.imports.fileVersion === file.version) return file.imports;
1141
+ const importedPaths = /* @__PURE__ */ new Set();
1142
+ const reExportedPaths = /* @__PURE__ */ new Set();
803
1143
  const host = this.moduleResolutionHost();
1144
+ const resolve = (specifier) => {
1145
+ const result = ts5.resolveModuleName(specifier, toFileName(path), this.optionsOfFile(path), host).resolvedModule;
1146
+ return result ? fromFileName(result.resolvedFileName) : null;
1147
+ };
804
1148
  for (const block of file.model.roots) {
805
- if (block.kind !== "import") continue;
806
- const specifier = block.name;
807
- const result = ts2.resolveModuleName(specifier, toFileName(path), this.options, host).resolvedModule;
808
- const target = result && fromFileName(result.resolvedFileName);
809
- if (target) resolved.add(target);
810
- }
811
- file.importedPaths = { paths: resolved, fileSetVersion: this.fileSetVersion };
812
- return resolved;
1149
+ const code = block.kind === "statement" || block.kind === "variable" ? file.text.slice(block.start, block.end) : "";
1150
+ const reExportSpecifier = block.kind === "statement" ? RE_EXPORT.exec(code)?.[2] : void 0;
1151
+ const requiredSpecifier = block.kind === "variable" ? REQUIRE_CALL.exec(code)?.[2] : void 0;
1152
+ const specifier = block.kind === "import" ? block.name : reExportSpecifier ?? requiredSpecifier;
1153
+ const target = specifier ? resolve(specifier) : null;
1154
+ if (!target) continue;
1155
+ importedPaths.add(target);
1156
+ if (reExportSpecifier) reExportedPaths.add(target);
1157
+ }
1158
+ file.imports = { importedPaths, reExportedPaths, fileSetVersion: this.fileSetVersion, fileVersion: file.version };
1159
+ return file.imports;
813
1160
  }
814
1161
  moduleResolutionHost() {
815
1162
  return {
816
1163
  fileExists: (name) => this.fileExists(name),
817
1164
  readFile: (name) => this.readFile(name),
818
- directoryExists: (dir) => this.directoryExists(dir)
1165
+ directoryExists: (dir) => this.directoryExists(dir),
1166
+ getCurrentDirectory: () => "/"
819
1167
  };
820
1168
  }
821
1169
  fileExists(name) {
@@ -834,11 +1182,12 @@ var Project = class {
834
1182
  for (const path of this.files.keys()) if (path.startsWith(prefix)) return true;
835
1183
  return false;
836
1184
  }
837
- createHost() {
1185
+ /** The host of one config's program. Its files are those that use the config; their imports come in from the rest. */
1186
+ createHost(configPath) {
838
1187
  return {
839
- getCompilationSettings: () => this.options,
1188
+ getCompilationSettings: () => this.programs().optionsByConfig.get(configPath) ?? this.programs().optionsByConfig.get(NO_CONFIG),
840
1189
  getProjectVersion: () => String(this.projectVersion),
841
- getScriptFileNames: () => [...this.files.keys()].map(toFileName),
1190
+ getScriptFileNames: () => (this.programs().pathsByConfig.get(configPath) ?? []).map(toFileName),
842
1191
  getScriptVersion: (name) => {
843
1192
  if (name.startsWith(LIB_DIR)) return "0";
844
1193
  const path = fromFileName(name);
@@ -847,7 +1196,7 @@ var Project = class {
847
1196
  getScriptSnapshot: (name) => {
848
1197
  if (name.startsWith(LIB_DIR)) {
849
1198
  const text = this.libFiles[name.slice(LIB_DIR.length)];
850
- return text === void 0 ? void 0 : ts2.ScriptSnapshot.fromString(text);
1199
+ return text === void 0 ? void 0 : ts5.ScriptSnapshot.fromString(text);
851
1200
  }
852
1201
  const path = fromFileName(name);
853
1202
  const file = path ? this.files.get(path) : void 0;
@@ -856,6 +1205,13 @@ var Project = class {
856
1205
  return file.snapshot;
857
1206
  },
858
1207
  getCurrentDirectory: () => "/",
1208
+ // A file another package's program pulls in keeps its own package's aliases.
1209
+ resolveModuleNameLiterals: (moduleLiterals, containingFile) => {
1210
+ const containingPath = fromFileName(containingFile);
1211
+ const options = this.optionsOfFile(containingPath);
1212
+ const host = this.moduleResolutionHost();
1213
+ return moduleLiterals.map((literal) => ts5.resolveModuleName(literal.text, containingFile, options, host));
1214
+ },
859
1215
  getDefaultLibFileName: () => LIB_DIR + "lib.d.ts",
860
1216
  fileExists: (name) => this.fileExists(name),
861
1217
  readFile: (name) => this.readFile(name),
@@ -865,6 +1221,9 @@ var Project = class {
865
1221
  };
866
1222
  }
867
1223
  };
1224
+ function isAliasInSpecifier(location) {
1225
+ return /\bas\s*$/.test(location.prefixText ?? "") || /^\s*as\b/.test(location.suffixText ?? "");
1226
+ }
868
1227
  var TextSnapshot = class _TextSnapshot {
869
1228
  constructor(text) {
870
1229
  this.text = text;
@@ -887,7 +1246,7 @@ function changedRegion(before, after) {
887
1246
  while (start < shorter && before.charCodeAt(start) === after.charCodeAt(start)) start++;
888
1247
  let fromEnd = 0;
889
1248
  while (fromEnd < shorter - start && before.charCodeAt(before.length - 1 - fromEnd) === after.charCodeAt(after.length - 1 - fromEnd)) fromEnd++;
890
- return ts2.createTextChangeRange(ts2.createTextSpan(start, before.length - fromEnd - start), after.length - fromEnd - start);
1249
+ return ts5.createTextChangeRange(ts5.createTextSpan(start, before.length - fromEnd - start), after.length - fromEnd - start);
891
1250
  }
892
1251
  function toFileName(path) {
893
1252
  return "/" + path;
@@ -911,26 +1270,12 @@ function namesVisibleFrom(model, block) {
911
1270
  return names;
912
1271
  }
913
1272
  function isValidIdentifier(name) {
914
- const scanner = ts2.createScanner(ts2.ScriptTarget.Latest, true, ts2.LanguageVariant.Standard, name);
1273
+ const scanner = ts5.createScanner(ts5.ScriptTarget.Latest, true, ts5.LanguageVariant.Standard, name);
915
1274
  const token = scanner.scan();
916
1275
  const isWholeText = scanner.getTokenEnd() === name.length && name.length > 0;
917
- const isNameToken = token === ts2.SyntaxKind.Identifier || token > ts2.SyntaxKind.LastReservedWord && token <= ts2.SyntaxKind.LastKeyword;
1276
+ const isNameToken = token === ts5.SyntaxKind.Identifier || token > ts5.SyntaxKind.LastReservedWord && token <= ts5.SyntaxKind.LastKeyword;
918
1277
  return isWholeText && isNameToken;
919
1278
  }
920
- function compilerOptionsOf(config) {
921
- const configFileName = toFileName(config.path);
922
- const json = ts2.parseConfigFileTextToJson(configFileName, config.text);
923
- if (json.error || !json.config) return {};
924
- const readConfig = (name) => name === configFileName ? config.text : config.extendedFiles?.[fromFileName(name) ?? ""];
925
- const host = {
926
- useCaseSensitiveFileNames: true,
927
- readDirectory: () => [],
928
- fileExists: (name) => readConfig(name) !== void 0,
929
- readFile: readConfig
930
- };
931
- const configFolder = configFileName.slice(0, configFileName.lastIndexOf("/") + 1);
932
- return ts2.parseJsonConfigFileContent(json.config, host, configFolder, void 0, configFileName).options;
933
- }
934
1279
 
935
1280
  // src/remoteProject.ts
936
1281
  var RemoteProject = class {
@@ -966,6 +1311,10 @@ var RemoteProject = class {
966
1311
  setConfigFile(config) {
967
1312
  void this.callServer("setConfigFile", [config]);
968
1313
  }
1314
+ /** The folder's config files (one per package in a monorepo) and package.json files (or none, with null). */
1315
+ setWorkspace(workspace) {
1316
+ void this.callServer("setWorkspace", [workspace]);
1317
+ }
969
1318
  getModel(path) {
970
1319
  const file = this.files.get(path);
971
1320
  if (!file) return void 0;
@@ -988,7 +1337,7 @@ var RemoteProject = class {
988
1337
  };
989
1338
 
990
1339
  // src/projectServer.ts
991
- var SERVER_METHODS = ["setLibFiles", "setConfigFile", "setFiles", "setFile", "removeFile", "resolve", "renameBlock"];
1340
+ var SERVER_METHODS = ["setLibFiles", "setConfigFile", "setWorkspace", "setFiles", "setFile", "removeFile", "resolve", "renameBlock"];
992
1341
  var ProjectServer = class {
993
1342
  project = new Project();
994
1343
  setLibFiles(libFiles) {
@@ -997,6 +1346,9 @@ var ProjectServer = class {
997
1346
  setConfigFile(config) {
998
1347
  this.project.setConfigFile(config);
999
1348
  }
1349
+ setWorkspace(workspace) {
1350
+ this.project.setWorkspace(workspace);
1351
+ }
1000
1352
  setFiles(files) {
1001
1353
  for (const [path, text] of Object.entries(files)) this.project.setFile(path, text);
1002
1354
  }
@@ -1051,7 +1403,7 @@ function connectProjectWorker(port) {
1051
1403
  }
1052
1404
 
1053
1405
  // src/edits.ts
1054
- import ts3 from "typescript";
1406
+ import ts6 from "typescript";
1055
1407
  function applyEdit(text, e) {
1056
1408
  return text.slice(0, e.start) + e.text + text.slice(e.end);
1057
1409
  }
@@ -1178,13 +1530,13 @@ function toTopLevel(code) {
1178
1530
  function renameDeclaration(code, inClass, name) {
1179
1531
  const prefix = inClass ? "class __cc__ {\n" : "";
1180
1532
  const src = prefix + code + (inClass ? "\n}" : "");
1181
- const sf = ts3.createSourceFile("snippet.ts", src, ts3.ScriptTarget.Latest, true);
1533
+ const sf = ts6.createSourceFile("snippet.ts", src, ts6.ScriptTarget.Latest, true);
1182
1534
  let target;
1183
1535
  const first = sf.statements[0];
1184
- if (inClass && first && ts3.isClassDeclaration(first)) target = first.members[0]?.name;
1185
- else if (first && (ts3.isFunctionDeclaration(first) || ts3.isClassDeclaration(first) || ts3.isInterfaceDeclaration(first) || ts3.isTypeAliasDeclaration(first) || ts3.isEnumDeclaration(first)))
1536
+ if (inClass && first && ts6.isClassDeclaration(first)) target = first.members[0]?.name;
1537
+ else if (first && (ts6.isFunctionDeclaration(first) || ts6.isClassDeclaration(first) || ts6.isInterfaceDeclaration(first) || ts6.isTypeAliasDeclaration(first) || ts6.isEnumDeclaration(first)))
1186
1538
  target = first.name;
1187
- else if (first && ts3.isVariableStatement(first)) target = first.declarationList.declarations[0]?.name;
1539
+ else if (first && ts6.isVariableStatement(first)) target = first.declarationList.declarations[0]?.name;
1188
1540
  if (!target) return code;
1189
1541
  const s = target.getStart(sf) - prefix.length;
1190
1542
  return code.slice(0, s) + name + code.slice(target.end - prefix.length);
@@ -1196,26 +1548,26 @@ function must(model, id) {
1196
1548
  }
1197
1549
  function clipFromText(code) {
1198
1550
  const text = code.replace(/\s+$/, "");
1199
- const sf = ts3.createSourceFile("clip.ts", text, ts3.ScriptTarget.Latest, true);
1551
+ const sf = ts6.createSourceFile("clip.ts", text, ts6.ScriptTarget.Latest, true);
1200
1552
  const first = sf.statements[0];
1201
1553
  const diags = sf.parseDiagnostics ?? [];
1202
1554
  if (first && !diags.length) {
1203
1555
  let name;
1204
1556
  let kind = "statement";
1205
- if (ts3.isFunctionDeclaration(first)) name = first.name?.text, kind = "function";
1206
- else if (ts3.isClassDeclaration(first)) name = first.name?.text, kind = "class";
1207
- else if (ts3.isVariableStatement(first)) name = first.declarationList.declarations[0]?.name.getText(), kind = "variable";
1208
- else if (ts3.isInterfaceDeclaration(first) || ts3.isTypeAliasDeclaration(first) || ts3.isEnumDeclaration(first)) name = first.name.text, kind = "type";
1557
+ if (ts6.isFunctionDeclaration(first)) name = first.name?.text, kind = "function";
1558
+ else if (ts6.isClassDeclaration(first)) name = first.name?.text, kind = "class";
1559
+ else if (ts6.isVariableStatement(first)) name = first.declarationList.declarations[0]?.name.getText(), kind = "variable";
1560
+ else if (ts6.isInterfaceDeclaration(first) || ts6.isTypeAliasDeclaration(first) || ts6.isEnumDeclaration(first)) name = first.name.text, kind = "type";
1209
1561
  if (name) return { code: text, name, kind, fromClass: false };
1210
1562
  }
1211
- const wrapped = ts3.createSourceFile("clip.ts", `class __cc__ {
1563
+ const wrapped = ts6.createSourceFile("clip.ts", `class __cc__ {
1212
1564
  ${text}
1213
- }`, ts3.ScriptTarget.Latest, true);
1565
+ }`, ts6.ScriptTarget.Latest, true);
1214
1566
  const cls = wrapped.statements[0];
1215
1567
  const wd = wrapped.parseDiagnostics ?? [];
1216
- if (cls && ts3.isClassDeclaration(cls) && !wd.length && cls.members[0]?.name) {
1568
+ if (cls && ts6.isClassDeclaration(cls) && !wd.length && cls.members[0]?.name) {
1217
1569
  const m = cls.members[0];
1218
- return { code: text, name: m.name.getText(wrapped), kind: ts3.isPropertyDeclaration(m) ? "variable" : "function", fromClass: true };
1570
+ return { code: text, name: m.name.getText(wrapped), kind: ts6.isPropertyDeclaration(m) ? "variable" : "function", fromClass: true };
1219
1571
  }
1220
1572
  return { code: text, name: "", kind: "statement", fromClass: false };
1221
1573
  }
@@ -1373,7 +1725,7 @@ function resolveConflict(text, hunk, choice) {
1373
1725
  var BAND_ORDER = ["data", "types", "main", "bottom"];
1374
1726
  var ZONE_RULES = [
1375
1727
  { zone: "left", description: "Imports on the left", matches: (block) => block.kind === "import" },
1376
- { zone: "data", description: "Constants and other data at the top", matches: (block) => block.kind === "variable" },
1728
+ { zone: "data", description: "Constants, other data and React state at the top", matches: (block) => block.kind === "variable" || block.kind === "state" },
1377
1729
  { zone: "types", description: "Then interfaces, types and enums", matches: (block) => block.kind === "type" },
1378
1730
  { zone: "bottom", description: "Code that runs when the file loads, at the bottom", matches: (block) => block.kind === "statement" },
1379
1731
  { zone: "main", description: "Functions, classes and the rest in between", matches: () => true }
@@ -1406,8 +1758,64 @@ var RELATEDNESS_RULES = [
1406
1758
  score: (a, b) => firstWord(a.name) && firstWord(a.name) === firstWord(b.name) ? 1 : 0
1407
1759
  }
1408
1760
  ];
1761
+ var PLACEMENT_RULES = [
1762
+ {
1763
+ description: "A getter right above its setter, a setter right below its getter",
1764
+ spot(block, placed) {
1765
+ const siblings = [block, ...placed.map((sibling) => sibling.block)];
1766
+ for (const rule of STACK_RULES) {
1767
+ for (const [top, bottom] of rule.pairs(siblings)) {
1768
+ const placedBottom = top === block && placed.find((sibling) => sibling.block === bottom);
1769
+ if (placedBottom) return { sibling: placedBottom, side: "above" };
1770
+ const placedTop = bottom === block && placed.find((sibling) => sibling.block === top);
1771
+ if (placedTop) return { sibling: placedTop, side: "below" };
1772
+ }
1773
+ }
1774
+ return void 0;
1775
+ }
1776
+ },
1777
+ {
1778
+ description: "Like a newspaper: a function goes in the column right of its caller, after the function called before it",
1779
+ spot(block, placed, factsOf) {
1780
+ if (!isFunctionLike(block)) return void 0;
1781
+ const caller = placed.find((sibling) => isFunctionLike(sibling.block) && factsOf(sibling.block).calls.includes(block.id));
1782
+ if (!caller) return void 0;
1783
+ const calls = factsOf(caller.block).calls;
1784
+ const isInCallerColumn = (sibling) => sibling !== caller && sibling.x >= caller.x + caller.w;
1785
+ const calledBefore = calls.slice(0, calls.indexOf(block.id)).reverse();
1786
+ const previousInColumn = calledBefore.map((id) => placed.find((sibling) => sibling.block.id === id && isInCallerColumn(sibling))).find(Boolean);
1787
+ return previousInColumn ? { sibling: previousInColumn, side: "below" } : { sibling: caller, side: "right" };
1788
+ }
1789
+ },
1790
+ {
1791
+ description: "An import, constant or type below the last one of its kind before it in the file",
1792
+ spot(block, placed) {
1793
+ const zone = zoneOf(block);
1794
+ if (zone === "main") return void 0;
1795
+ const sameZone = placed.filter((sibling) => zoneOf(sibling.block) === zone);
1796
+ const lastBefore = sameZone.filter((sibling) => sibling.block.start < block.start).at(-1) ?? sameZone[0];
1797
+ return lastBefore ? { sibling: lastBefore, side: "below" } : void 0;
1798
+ }
1799
+ },
1800
+ {
1801
+ description: "Below the block it is most related to",
1802
+ spot(block, placed, factsOf) {
1803
+ let best;
1804
+ let bestScore = 0;
1805
+ for (const sibling of placed) {
1806
+ const score = RELATEDNESS_RULES.reduce((total, rule) => total + rule.score(block, sibling.block, factsOf), 0);
1807
+ if (score > bestScore) {
1808
+ best = sibling;
1809
+ bestScore = score;
1810
+ }
1811
+ }
1812
+ return best ? { sibling: best, side: "below" } : void 0;
1813
+ }
1814
+ }
1815
+ ];
1816
+ var zoneOf = (block) => ZONE_RULES.find((rule) => rule.matches(block)).zone;
1409
1817
  var NEWSPAPER_DEPTH = 3;
1410
- var isFunctionLike = (block) => block.kind === "function";
1818
+ var isFunctionLike = (block) => block.kind === "function" || block.kind === "component";
1411
1819
  function accessedName(block, accessor) {
1412
1820
  if (block.kind !== "function") return void 0;
1413
1821
  if (block.tags.includes(accessor)) return block.name;
@@ -1467,8 +1875,19 @@ function arrangeSiblings(blocks, sizeOf, text, gap, isFile) {
1467
1875
  }
1468
1876
  return positions;
1469
1877
  }
1878
+ function newBlockSpots(siblings, text) {
1879
+ let factsById;
1880
+ const factsOf = (sibling) => factsById.get(sibling.id);
1881
+ return (block, placed) => {
1882
+ factsById ??= siblingFacts(siblings, text);
1883
+ for (const rule of PLACEMENT_RULES) {
1884
+ const spot = rule.spot(block, placed, factsOf);
1885
+ if (spot) return spot;
1886
+ }
1887
+ return void 0;
1888
+ };
1889
+ }
1470
1890
  function stackedUnits(blocks) {
1471
- const zoneOf = (block) => ZONE_RULES.find((rule) => rule.matches(block)).zone;
1472
1891
  const unitOf = /* @__PURE__ */ new Map();
1473
1892
  const units = blocks.map((block, order) => {
1474
1893
  const unit = { members: [block], arrangement: { block: block.id }, zone: zoneOf(block), order };
@@ -1550,6 +1969,7 @@ function orderByRelatedness(units, factsOf) {
1550
1969
  function siblingFacts(blocks, text) {
1551
1970
  const idsByName = /* @__PURE__ */ new Map();
1552
1971
  for (const block of blocks) idsByName.set(block.name, [...idsByName.get(block.name) ?? [], block.id]);
1972
+ const componentIds = new Set(blocks.filter((block) => block.kind === "component").map((block) => block.id));
1553
1973
  const facts = /* @__PURE__ */ new Map();
1554
1974
  for (const block of blocks) {
1555
1975
  const code = codeWithoutChildren(text, [block.start, block.end], block.children);
@@ -1559,9 +1979,11 @@ function siblingFacts(blocks, text) {
1559
1979
  const ids = idsByName.get(match[0])?.filter((id) => id !== block.id);
1560
1980
  if (!ids?.length) continue;
1561
1981
  const isCall = /^\s*\(/.test(code.slice(match.index + match[0].length, match.index + match[0].length + 40));
1982
+ const isJsxTag = /<\/?$/.test(code.slice(Math.max(0, match.index - 2), match.index));
1562
1983
  for (const id of ids) {
1563
1984
  uses.add(id);
1564
- if (isCall && !calls.includes(id)) calls.push(id);
1985
+ const isRender = isJsxTag && componentIds.has(id);
1986
+ if ((isCall || isRender) && !calls.includes(id)) calls.push(id);
1565
1987
  }
1566
1988
  }
1567
1989
  facts.set(block.id, { calls, uses });
@@ -1672,11 +2094,18 @@ function computeGeometry(model, layout, hints = {}, opts = {}) {
1672
2094
  placed.push(g);
1673
2095
  } else unplaced.push(g);
1674
2096
  }
2097
+ const spotByRules = newBlockSpots(sibs.map((sibling) => sibling.block), model.text);
1675
2098
  for (const g of unplaced) {
1676
- const idx = sibs.indexOf(g);
1677
- const prev = sibs.slice(0, idx).reverse().find((s) => placed.includes(s));
1678
2099
  const hint = hints[g.id];
1679
- const p = hint ? freeSpotNear(hint, g, placed, !isFile) : findFreeSpot(g, placed, prev, !isFile);
2100
+ const ruleSpot = hint || !placed.length ? void 0 : spotByRules(g.block, placed);
2101
+ let p;
2102
+ if (hint) p = freeSpotNear(hint, g, placed, !isFile);
2103
+ else if (ruleSpot?.side === "below") p = freeSpotDown(pointBeside(ruleSpot, g), g, placed, !isFile);
2104
+ else if (ruleSpot) p = freeSpotNear(pointBeside(ruleSpot, g), g, placed, !isFile);
2105
+ else {
2106
+ const previousPlacedInFile = sibs.slice(0, sibs.indexOf(g)).reverse().find((sibling) => placed.includes(sibling));
2107
+ p = findFreeSpot(g, placed, previousPlacedInFile, !isFile);
2108
+ }
1680
2109
  g.x = p.x;
1681
2110
  g.y = p.y;
1682
2111
  assigned[g.id] = p;
@@ -1709,6 +2138,26 @@ function computeGeometry(model, layout, hints = {}, opts = {}) {
1709
2138
  };
1710
2139
  return { geo, order, frame, assigned };
1711
2140
  }
2141
+ function placeGhostBlocks(ghosts, ghostGeometry, currentModel, current) {
2142
+ const positions = /* @__PURE__ */ new Map();
2143
+ const placedGhostsByParent = /* @__PURE__ */ new Map();
2144
+ for (const ghost of ghosts) {
2145
+ const ghostGeo = ghostGeometry.geo.get(ghost.id);
2146
+ if (!ghostGeo) continue;
2147
+ const parentId = ghost.parentId ?? null;
2148
+ const parentGeo = parentId ? current.geo.get(parentId) : void 0;
2149
+ if (parentId && !parentGeo) continue;
2150
+ if (parentGeo && (parentGeo.hidden || parentGeo.collapsed)) continue;
2151
+ const currentSiblings = parentId ? currentModel.blocks.get(parentId).children : currentModel.roots;
2152
+ const placedGhosts = placedGhostsByParent.get(parentId) ?? [];
2153
+ const taken = [...currentSiblings.map((sibling) => current.geo.get(sibling.id)), ...placedGhosts];
2154
+ const spot = freeSpotNear({ x: ghostGeo.x, y: ghostGeo.y }, ghostGeo, taken, !!parentGeo);
2155
+ placedGhosts.push({ ...spot, w: ghostGeo.w, h: ghostGeo.h });
2156
+ placedGhostsByParent.set(parentId, placedGhosts);
2157
+ positions.set(ghost.id, parentGeo ? { x: parentGeo.ax + M.pad + spot.x, y: parentGeo.ay + parentGeo.contentTop + spot.y } : spot);
2158
+ }
2159
+ return positions;
2160
+ }
1712
2161
  function pushApart(sibs, assigned) {
1713
2162
  for (let iter = 0; iter < 100; iter++) {
1714
2163
  let moved = false;
@@ -1729,6 +2178,12 @@ function pushApart(sibs, assigned) {
1729
2178
  if (!moved) break;
1730
2179
  }
1731
2180
  }
2181
+ function pointBeside(spot, size) {
2182
+ const { sibling, side } = spot;
2183
+ if (side === "right") return { x: sibling.x + sibling.w + M.gap, y: sibling.y };
2184
+ if (side === "below") return { x: sibling.x, y: sibling.y + sibling.h + M.gap };
2185
+ return { x: sibling.x, y: sibling.y - size.h - M.gap };
2186
+ }
1732
2187
  function headerChars(b) {
1733
2188
  const tags = b.tags.filter((t) => t !== "export").join(" ");
1734
2189
  return 4 + b.name.length + Math.min(b.detail.length, 40) + (tags ? tags.length + 2 : 0) + 3;
@@ -1771,6 +2226,12 @@ function freeSpotNear(at, g, placed, clampPositive) {
1771
2226
  const bottom = Math.max(...others.map((p) => p.y + p.h));
1772
2227
  return round({ x: start.x, y: bottom + M.gap });
1773
2228
  }
2229
+ function freeSpotDown(at, g, placed, clampPositive) {
2230
+ const spot = clampPositive ? { x: Math.max(0, at.x), y: Math.max(0, at.y) } : { ...at };
2231
+ const blockCoveringSpot = () => placed.find((p) => overlaps({ x: spot.x, y: spot.y, w: g.w, h: g.h }, p));
2232
+ for (let covering = blockCoveringSpot(); covering; covering = blockCoveringSpot()) spot.y = covering.y + covering.h + M.gap;
2233
+ return round(spot);
2234
+ }
1774
2235
  function dist(a, b) {
1775
2236
  return Math.hypot(a.x - b.x, a.y - b.y);
1776
2237
  }
@@ -1859,6 +2320,8 @@ var CSS = (
1859
2320
  --k-import: #6b7280; --k-import-head: #3b3f46; --k-import-bg: #25272c;
1860
2321
  --k-class: #34b27a; --k-class-head: #1d6b4a; --k-class-bg: rgba(52,178,122,.08);
1861
2322
  --k-function: #ec5fa5; --k-function-head: #8f2f62; --k-function-bg: rgba(236,95,165,.09);
2323
+ --k-component: #22b8cf; --k-component-head: #146c7a; --k-component-bg: rgba(34,184,207,.09);
2324
+ --k-state: #f0883e; --k-state-head: #8a4614; --k-state-bg: rgba(240,136,62,.09);
1862
2325
  --k-variable: #4b8ef7; --k-variable-head: #23508f; --k-variable-bg: rgba(75,142,247,.09);
1863
2326
  --k-type: #d4a92a; --k-type-head: #6e5714; --k-type-bg: rgba(212,169,42,.08);
1864
2327
  --k-namespace: #9b8cf8; --k-namespace-head: #4d4291; --k-namespace-bg: rgba(155,140,248,.08);
@@ -1881,6 +2344,8 @@ var CSS = (
1881
2344
  --k-import: #6b7280; --k-import-head: #4b5058; --k-import-bg: #eceef1;
1882
2345
  --k-class: #1f9963; --k-class-head: #1f9963; --k-class-bg: rgba(31,153,99,.07);
1883
2346
  --k-function: #d63e8c; --k-function-head: #d63e8c; --k-function-bg: rgba(214,62,140,.07);
2347
+ --k-component: #0f8fa3; --k-component-head: #0f8fa3; --k-component-bg: rgba(15,143,163,.07);
2348
+ --k-state: #d4691c; --k-state-head: #d4691c; --k-state-bg: rgba(212,105,28,.07);
1884
2349
  --k-variable: #2f6fe0; --k-variable-head: #2f6fe0; --k-variable-bg: rgba(47,111,224,.07);
1885
2350
  --k-type: #b58a0c; --k-type-head: #a87f08; --k-type-bg: rgba(181,138,12,.08);
1886
2351
  --k-namespace: #6f5ce6; --k-namespace-head: #6f5ce6; --k-namespace-bg: rgba(111,92,230,.07);
@@ -1900,6 +2365,8 @@ var CSS = (
1900
2365
  .cc-block.k-import { --k: var(--k-import); --kh: var(--k-import-head); --kbg: var(--k-import-bg); }
1901
2366
  .cc-block.k-class { --k: var(--k-class); --kh: var(--k-class-head); --kbg: var(--k-class-bg); }
1902
2367
  .cc-block.k-function { --k: var(--k-function); --kh: var(--k-function-head); --kbg: var(--k-function-bg); }
2368
+ .cc-block.k-component { --k: var(--k-component); --kh: var(--k-component-head); --kbg: var(--k-component-bg); }
2369
+ .cc-block.k-state { --k: var(--k-state); --kh: var(--k-state-head); --kbg: var(--k-state-bg); }
1903
2370
  .cc-block.k-variable { --k: var(--k-variable); --kh: var(--k-variable-head); --kbg: var(--k-variable-bg); }
1904
2371
  .cc-block.k-type { --k: var(--k-type); --kh: var(--k-type-head); --kbg: var(--k-type-bg); }
1905
2372
  .cc-block.k-namespace { --k: var(--k-namespace); --kh: var(--k-namespace-head); --kbg: var(--k-namespace-bg); }
@@ -1929,6 +2396,7 @@ var CSS = (
1929
2396
  .cc-mini { position: relative; margin: 6px 12px 0; height: 34px; opacity: .45; }
1930
2397
  .cc-mini > i { position: absolute; border-radius: 2px; border: 1px solid var(--cc-faint); background: rgba(255,255,255,.06); }
1931
2398
  .cc-mini > i.k-class { border-color: var(--k-class); } .cc-mini > i.k-function { border-color: var(--k-function); }
2399
+ .cc-mini > i.k-component { border-color: var(--k-component); } .cc-mini > i.k-state { border-color: var(--k-state); }
1932
2400
  .cc-mini > i.k-variable { border-color: var(--k-variable); } .cc-mini > i.k-type { border-color: var(--k-type); }
1933
2401
 
1934
2402
  .cc-err {
@@ -2024,6 +2492,7 @@ var CSS = (
2024
2492
  .cc-btn:hover { background: rgba(127,127,127,.16); }
2025
2493
  .cc-btn.danger { color: var(--cc-danger); }
2026
2494
  .cc-btn.primary { background: var(--cc-accent); color: #fff; }
2495
+ .cc-language { padding: 0 8px; font: 600 11px/30px var(--cc-font, "JetBrains Mono", ui-monospace, Menlo, monospace); color: var(--cc-muted); white-space: nowrap; }
2027
2496
  .cc-sep { width: 1px; height: 20px; background: var(--cc-panel-border); margin: 0 2px; flex: none; }
2028
2497
  .cc-refs {
2029
2498
  position: absolute; left: 50%; bottom: 64px; transform: translateX(-50%); z-index: 21; width: min(360px, calc(100% - 24px)); max-height: 45%;
@@ -2073,6 +2542,8 @@ var KIND_LABEL = {
2073
2542
  import: "import",
2074
2543
  class: "class",
2075
2544
  function: "fn",
2545
+ component: "component",
2546
+ state: "state",
2076
2547
  variable: "var",
2077
2548
  type: "type",
2078
2549
  namespace: "ns",
@@ -2102,7 +2573,7 @@ var CodeCanvas = class {
2102
2573
  this.blocksEl = el("div", "cc-blocks");
2103
2574
  this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2104
2575
  this.svg.classList.add("cc-edges");
2105
- setHTML(this.svg, `<defs>${["import", "class", "function", "variable", "type", "namespace", "statement"].map(
2576
+ setHTML(this.svg, `<defs>${["import", "class", "function", "component", "state", "variable", "type", "namespace", "statement"].map(
2106
2577
  (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>`
2107
2578
  ).join("")}</defs>`);
2108
2579
  this.world.append(this.frameEl, this.blocksEl, this.svg);
@@ -2166,8 +2637,7 @@ var CodeCanvas = class {
2166
2637
  diffCache = null;
2167
2638
  /** Merge conflicts in the text. The canvas then shows its current side, marking the blocks they touch. */
2168
2639
  conflicts = null;
2169
- incomingBlockEls = [];
2170
- removedBlockEls = [];
2640
+ ghostBlockEls = { removed: [], incoming: [] };
2171
2641
  /** Errors from the host (`setDiagnostics`), shown instead of the project's type errors. */
2172
2642
  hostDiagnostics = null;
2173
2643
  renaming = null;
@@ -2549,25 +3019,10 @@ var CodeCanvas = class {
2549
3019
  }
2550
3020
  /** Blocks only the incoming side of a conflict has, drawn dashed where they would go. */
2551
3021
  renderIncomingBlocks() {
2552
- for (const incomingEl of this.incomingBlockEls) incomingEl.remove();
2553
- this.incomingBlockEls = [];
2554
3022
  const conflicts = this.conflicts;
2555
- const geometry = this.geom;
2556
- if (!conflicts?.incomingOnly.length || !geometry) return;
3023
+ if (!conflicts?.incomingOnly.length || !this.geom) return this.drawGhostBlocks("incoming", [], null);
2557
3024
  conflicts.geometry ??= computeGeometry(conflicts.incomingModel, this.layout, {}, { resolveOverlaps: false });
2558
- for (const block of conflicts.incomingOnly) {
2559
- const incomingGeo = conflicts.geometry.geo.get(block.id);
2560
- const parentGeo = block.parentId ? geometry.geo.get(block.parentId) : void 0;
2561
- if (!incomingGeo || parentGeo && (parentGeo.hidden || parentGeo.collapsed)) continue;
2562
- const x = parentGeo ? parentGeo.ax + M.pad + incomingGeo.x : incomingGeo.ax;
2563
- const y = parentGeo ? parentGeo.ay + parentGeo.contentTop + incomingGeo.y : incomingGeo.ay;
2564
- const incomingEl = el("div", `cc-block k-${block.kind} cc-conflict-incoming`);
2565
- setHTML(incomingEl, this.blockHTML(block, { ...incomingGeo, collapsed: true }, "", "incoming"));
2566
- Object.assign(incomingEl.style, { transform: `translate(${x}px,${y}px)`, width: incomingGeo.w + "px", height: incomingGeo.h + "px", zIndex: String(block.depth + 1) });
2567
- incomingEl.title = `${block.name}: only in the incoming side of a merge conflict`;
2568
- this.blocksEl.appendChild(incomingEl);
2569
- this.incomingBlockEls.push(incomingEl);
2570
- }
3025
+ this.drawGhostBlocks("incoming", conflicts.incomingOnly, conflicts.geometry);
2571
3026
  }
2572
3027
  /** A changed block's preview: its changed lines with a line of context, as many as fit. */
2573
3028
  changedLinesPreview(diff, block) {
@@ -2582,24 +3037,26 @@ var CodeCanvas = class {
2582
3037
  }
2583
3038
  /** Removed blocks stay visible, in red, where they were in the base version. They can't be selected. */
2584
3039
  renderRemovedBlocks(diff) {
2585
- for (const removedEl of this.removedBlockEls) removedEl.remove();
2586
- this.removedBlockEls = [];
2587
- const geometry = this.geom;
2588
- if (!diff?.removed.length || !this.diffBase || !geometry) return;
3040
+ if (!diff?.removed.length || !this.diffBase || !this.geom) return this.drawGhostBlocks("removed", [], null);
2589
3041
  this.diffBase.geometry ??= computeGeometry(diff.base, this.diffBase.layout ?? this.layout, {}, { resolveOverlaps: false });
2590
- for (const block of diff.removed) {
2591
- const baseGeo = this.diffBase.geometry.geo.get(block.id);
2592
- const parentGeo = block.parentId ? geometry.geo.get(block.parentId) : void 0;
2593
- const isInsideHiddenParent = !!parentGeo && (parentGeo.hidden || parentGeo.collapsed);
2594
- if (!baseGeo || isInsideHiddenParent) continue;
2595
- const x = parentGeo ? parentGeo.ax + M.pad + baseGeo.x : baseGeo.ax;
2596
- const y = parentGeo ? parentGeo.ay + parentGeo.contentTop + baseGeo.y : baseGeo.ay;
2597
- const removedEl = el("div", `cc-block k-${block.kind} cc-diff-removed`);
2598
- setHTML(removedEl, this.blockHTML(block, { ...baseGeo, collapsed: true }, "", "removed"));
2599
- Object.assign(removedEl.style, { transform: `translate(${x}px,${y}px)`, width: baseGeo.w + "px", height: baseGeo.h + "px", zIndex: String(block.depth + 1) });
2600
- removedEl.title = `${block.name}: removed`;
2601
- this.blocksEl.appendChild(removedEl);
2602
- this.removedBlockEls.push(removedEl);
3042
+ this.drawGhostBlocks("removed", diff.removed, this.diffBase.geometry);
3043
+ }
3044
+ /** Blocks of another version (removed, or incoming in a conflict): drawn, but not part of this file. */
3045
+ drawGhostBlocks(kind, ghosts, ghostGeometry) {
3046
+ for (const ghostEl of this.ghostBlockEls[kind]) ghostEl.remove();
3047
+ this.ghostBlockEls[kind] = [];
3048
+ if (!ghostGeometry || !this.geom || !this.view) return;
3049
+ const positions = placeGhostBlocks(ghosts, ghostGeometry, this.view, this.geom);
3050
+ for (const block of ghosts) {
3051
+ const position = positions.get(block.id);
3052
+ const ghostGeo = ghostGeometry.geo.get(block.id);
3053
+ if (!position || !ghostGeo) continue;
3054
+ const ghostEl = el("div", `cc-block k-${block.kind} ${kind === "removed" ? "cc-diff-removed" : "cc-conflict-incoming"}`);
3055
+ setHTML(ghostEl, this.blockHTML(block, { ...ghostGeo, collapsed: true }, "", kind));
3056
+ 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) });
3057
+ ghostEl.title = kind === "removed" ? `${block.name}: removed` : `${block.name}: only in the incoming side of a merge conflict`;
3058
+ this.blocksEl.appendChild(ghostEl);
3059
+ this.ghostBlockEls[kind].push(ghostEl);
2603
3060
  }
2604
3061
  }
2605
3062
  /** Diagnostics grouped by the block that shows them: the innermost visible block containing each error. */
@@ -3378,7 +3835,8 @@ var CodeCanvas = class {
3378
3835
  el("div", "cc-sep"),
3379
3836
  ...firstHunk ? [keep("Keep current", "current", firstHunk.currentLabel), keep("Keep incoming", "incoming", firstHunk.incomingLabel), keep("Keep both", "both"), el("div", "cc-sep")] : [],
3380
3837
  button("Edit", () => this.openEditor(b.id)),
3381
- button("Copy", () => this.copy(b.id))
3838
+ button("Copy", () => this.copy(b.id)),
3839
+ ...this.languageLabelParts()
3382
3840
  );
3383
3841
  return this.renderRefs();
3384
3842
  }
@@ -3409,8 +3867,15 @@ var CodeCanvas = class {
3409
3867
  button("+", () => this.zoomBy(1.25))
3410
3868
  );
3411
3869
  }
3870
+ bar.append(...this.languageLabelParts());
3412
3871
  this.renderRefs();
3413
3872
  }
3873
+ languageLabelParts() {
3874
+ if (!this.view) return [];
3875
+ const label = el("span", "cc-language");
3876
+ label.textContent = this.view.language.label;
3877
+ return [el("div", "cc-sep"), label];
3878
+ }
3414
3879
  toggleRefs() {
3415
3880
  this.refsOpen = !this.refsOpen;
3416
3881
  this.renderBar();
@@ -3450,7 +3915,7 @@ var CodeCanvas = class {
3450
3915
  btn.style.setProperty("--k", `var(--k-${block?.kind ?? "statement"})`);
3451
3916
  const name = block && model ? displayName(model, block) : blockId;
3452
3917
  const label = isOtherFile ? `<span class="cc-file">${esc(fileName(path))} \u203A</span> ${esc(name)}` : esc(name);
3453
- setHTML(btn, `<i></i><span>${label}</span><span>${usage.kind}${usage.count > 1 ? " \xD7" + usage.count : ""}</span>`);
3918
+ setHTML(btn, `<i></i><span>${label}</span><span>${usage.kind === "render" ? "renders" : usage.kind}${usage.count > 1 ? " \xD7" + usage.count : ""}</span>`);
3454
3919
  btn.onclick = () => isOtherFile ? this.opts.onOpenBlock?.(path, blockId) : this.reveal(blockId);
3455
3920
  btn.disabled = isOtherFile && !this.opts.onOpenBlock;
3456
3921
  return btn;