@codecanvas-ide/engine 0.2.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;
@@ -15,11 +111,12 @@ function parseStructure(path, text, prev) {
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,29 +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);
81
179
  }
82
180
  function commonJsExportedFunction(s) {
83
- if (!ts.isExpressionStatement(s) || !ts.isBinaryExpression(s.expression) || s.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null;
181
+ if (!ts3.isExpressionStatement(s) || !ts3.isBinaryExpression(s.expression) || s.expression.operatorToken.kind !== ts3.SyntaxKind.EqualsToken) return null;
84
182
  const { left, right } = s.expression;
85
183
  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;
184
+ const isFunction = ts3.isFunctionExpression(fn) || ts3.isArrowFunction(fn);
185
+ if (!isFunction || !ts3.isPropertyAccessExpression(left) || !ts3.isIdentifier(left.name)) return null;
88
186
  const target = left.expression.getText();
89
187
  const isExportsObject = target === "exports" || target === "module.exports";
90
188
  return isExportsObject ? { assignment: s.expression, exportedName: left.name, fn } : null;
91
189
  }
92
190
  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;
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;
95
193
  }
96
194
  function createProgram(sf) {
97
195
  const options = {
98
196
  noLib: true,
99
197
  noResolve: true,
100
198
  allowJs: true,
101
- target: ts.ScriptTarget.Latest,
102
- jsx: ts.JsxEmit.Preserve,
199
+ target: ts3.ScriptTarget.Latest,
200
+ jsx: ts3.JsxEmit.Preserve,
103
201
  types: []
104
202
  };
105
203
  const host = {
@@ -115,7 +213,7 @@ function createProgram(sf) {
115
213
  readFile: () => void 0,
116
214
  getDirectories: () => []
117
215
  };
118
- return ts.createProgram({ rootNames: [sf.fileName], options, host });
216
+ return ts3.createProgram({ rootNames: [sf.fileName], options, host });
119
217
  }
120
218
  function detectIndentUnit(text) {
121
219
  const m = /\n(\t+|[ ]{2,8})\S/.exec(text);
@@ -123,12 +221,14 @@ function detectIndentUnit(text) {
123
221
  return m[1].startsWith(" ") ? " " : m[1].length >= 4 && m[1].length % 4 === 0 ? " " : " ";
124
222
  }
125
223
  var Builder = class {
126
- constructor(sf, text) {
224
+ constructor(sf, text, isReact) {
127
225
  this.sf = sf;
128
226
  this.text = text;
227
+ this.isReact = isReact;
129
228
  }
130
229
  sf;
131
230
  text;
231
+ isReact;
132
232
  roots = [];
133
233
  byId = /* @__PURE__ */ new Map();
134
234
  /**
@@ -140,6 +240,8 @@ var Builder = class {
140
240
  rangeToBlock = /* @__PURE__ */ new Map();
141
241
  /** Every name a usage could resolve to. Other identifiers skip the (slow) checker. */
142
242
  names = /* @__PURE__ */ new Set();
243
+ /** Setters of React state (`setCount`), keyed like `declToBlock`: using one writes the state. */
244
+ stateSetters = /* @__PURE__ */ new Set();
143
245
  // ---------- structure ----------
144
246
  collectStatements(stmts, parent, depth, declarationsOnly = false) {
145
247
  let group = [];
@@ -168,21 +270,23 @@ var Builder = class {
168
270
  };
169
271
  for (let i = 0; i < stmts.length; i++) {
170
272
  const s = stmts[i];
171
- if (ts.isFunctionDeclaration(s)) {
273
+ if (ts3.isFunctionDeclaration(s)) {
172
274
  flushGroup();
173
275
  const overloads = [s];
174
276
  while (!overloads[overloads.length - 1].body) {
175
277
  const next = stmts[i + 1];
176
- if (next && ts.isFunctionDeclaration(next) && next.name?.text === s.name?.text) {
278
+ if (next && ts3.isFunctionDeclaration(next) && next.name?.text === s.name?.text) {
177
279
  overloads.push(next);
178
280
  i++;
179
281
  } else break;
180
282
  }
181
- this.addFunctionLike(overloads, s.name?.text ?? "default", parent, depth);
182
- } 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)) {
183
287
  flushGroup();
184
288
  this.addClass(s, s, s.name?.text ?? "default", parent, depth);
185
- } else if (ts.isVariableStatement(s)) {
289
+ } else if (ts3.isVariableStatement(s)) {
186
290
  flushGroup();
187
291
  this.addVariableStatement(s, parent, depth, declarationsOnly);
188
292
  } else if (commonJsExportedFunction(s)) {
@@ -192,14 +296,14 @@ var Builder = class {
192
296
  this.addFunctionLike([fn], exportedName.text, parent, depth, { rangeNodes: [s], declNodes, extraTags: ["export"], nameNode: exportedName });
193
297
  } else if (declarationsOnly) {
194
298
  continue;
195
- } else if (ts.isImportDeclaration(s) || ts.isImportEqualsDeclaration(s)) {
299
+ } else if (ts3.isImportDeclaration(s) || ts3.isImportEqualsDeclaration(s)) {
196
300
  flushGroup();
197
301
  this.addImport(s, parent, depth);
198
- } else if (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) || ts.isEnumDeclaration(s)) {
302
+ } else if (ts3.isInterfaceDeclaration(s) || ts3.isTypeAliasDeclaration(s) || ts3.isEnumDeclaration(s)) {
199
303
  flushGroup();
200
- const keyword = ts.isInterfaceDeclaration(s) ? "interface" : ts.isEnumDeclaration(s) ? "enum" : "type";
304
+ const keyword = ts3.isInterfaceDeclaration(s) ? "interface" : ts3.isEnumDeclaration(s) ? "enum" : "type";
201
305
  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);
306
+ const bodyStart = ts3.isTypeAliasDeclaration(s) ? s.type.getStart(this.sf) : this.text.indexOf("{", s.name.end);
203
307
  this.add(
204
308
  {
205
309
  kind: "type",
@@ -217,7 +321,7 @@ var Builder = class {
217
321
  parent,
218
322
  depth
219
323
  );
220
- } else if (ts.isModuleDeclaration(s) && s.body && ts.isModuleBlock(s.body)) {
324
+ } else if (ts3.isModuleDeclaration(s) && s.body && ts3.isModuleBlock(s.body)) {
221
325
  flushGroup();
222
326
  const body = s.body;
223
327
  this.add(
@@ -247,13 +351,13 @@ var Builder = class {
247
351
  addImport(s, parent, depth) {
248
352
  let spec;
249
353
  let names = [];
250
- if (ts.isImportDeclaration(s)) {
354
+ if (ts3.isImportDeclaration(s)) {
251
355
  spec = s.moduleSpecifier.text;
252
356
  const clause = s.importClause;
253
357
  if (clause?.name) names.push(clause.name.text);
254
358
  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));
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));
257
361
  if (!clause) names = ["(side effect)"];
258
362
  } else {
259
363
  spec = s.name.text;
@@ -267,7 +371,7 @@ var Builder = class {
267
371
  key: "import:" + spec,
268
372
  name: spec,
269
373
  detail: "",
270
- tags: ts.isImportDeclaration(s) && s.importClause?.isTypeOnly ? ["type"] : [],
374
+ tags: ts3.isImportDeclaration(s) && s.importClause?.isTypeOnly ? ["type"] : [],
271
375
  rangeNodes: [s],
272
376
  declNodes: [s],
273
377
  previewRange: null,
@@ -286,11 +390,11 @@ var Builder = class {
286
390
  let previewRange = null;
287
391
  let insert = null;
288
392
  let insertAt = -1;
289
- if (body && ts.isBlock(body)) {
393
+ if (body && ts3.isBlock(body)) {
290
394
  previewRange = [body.getStart(this.sf) + 1, body.end - 1];
291
395
  insert = "body";
292
396
  const last = body.statements[body.statements.length - 1];
293
- 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;
294
398
  } else if (body) {
295
399
  previewRange = [body.getStart(this.sf), body.end];
296
400
  }
@@ -298,11 +402,12 @@ var Builder = class {
298
402
  const ret = sig.type ? ": " + sig.type.getText() : "";
299
403
  const detail = squash(`${typeParams}(${sig.parameters.map((p) => p.getText()).join(", ")})${ret}`);
300
404
  const tags = [...opts.extraTags ?? [], ...modifierTags(nodes[0])];
405
+ if (this.isReact && isHookName(name)) tags.push("hook");
301
406
  if (nodes.length > 1) tags.push(`${nodes.length - 1} overload${nodes.length > 2 ? "s" : ""}`);
302
407
  if (impl.asteriskToken) tags.push("generator");
303
408
  this.add(
304
409
  {
305
- kind: "function",
410
+ kind: opts.kind ?? "function",
306
411
  key: opts.key ?? name,
307
412
  name,
308
413
  detail,
@@ -313,7 +418,7 @@ var Builder = class {
313
418
  previewRange,
314
419
  insert,
315
420
  insertAt,
316
- 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
317
422
  },
318
423
  parent,
319
424
  depth
@@ -325,7 +430,7 @@ var Builder = class {
325
430
  const typeParams = cls.typeParameters?.length ? `<${cls.typeParameters.map((t) => t.getText()).join(", ")}>` : "";
326
431
  this.add(
327
432
  {
328
- kind: "class",
433
+ kind: this.isReact && isComponentClass(cls) ? "component" : "class",
329
434
  key: name,
330
435
  name,
331
436
  detail: squash(typeParams + (heritage ? " " + heritage : "")),
@@ -345,26 +450,26 @@ var Builder = class {
345
450
  collectMembers(members, parent, depth) {
346
451
  for (let i = 0; i < members.length; i++) {
347
452
  const m = members[i];
348
- if (ts.isMethodDeclaration(m) || ts.isConstructorDeclaration(m)) {
349
- 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();
350
455
  const overloads = [m];
351
456
  while (!overloads[overloads.length - 1].body) {
352
457
  const next = members[i + 1];
353
- 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);
354
459
  if (nextName === name) {
355
460
  overloads.push(next);
356
461
  i++;
357
462
  } else break;
358
463
  }
359
464
  this.addFunctionLike(overloads, name, parent, depth);
360
- } else if (ts.isGetAccessor(m) || ts.isSetAccessor(m)) {
465
+ } else if (ts3.isGetAccessor(m) || ts3.isSetAccessor(m)) {
361
466
  const name = m.name.getText();
362
- const word = ts.isGetAccessor(m) ? "get" : "set";
467
+ const word = ts3.isGetAccessor(m) ? "get" : "set";
363
468
  this.addFunctionLike([m], name, parent, depth, { key: `${word} ${name}`, extraTags: [word] });
364
- } else if (ts.isPropertyDeclaration(m)) {
469
+ } else if (ts3.isPropertyDeclaration(m)) {
365
470
  const name = m.name.getText();
366
471
  const init = m.initializer && unwrap(m.initializer);
367
- if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
472
+ if (init && (ts3.isArrowFunction(init) || ts3.isFunctionExpression(init))) {
368
473
  this.addFunctionLike([init], name, parent, depth, { rangeNodes: [m], declNodes: [m], extraTags: modifierTags(m), nameNode: m.name });
369
474
  } else {
370
475
  this.addVariable(m, m, name, m.type ? ": " + m.type.getText() : "", modifierTags(m), parent, depth, m.initializer);
@@ -374,25 +479,61 @@ var Builder = class {
374
479
  }
375
480
  addVariableStatement(s, parent, depth, functionsOnly) {
376
481
  const decls = s.declarationList.declarations;
377
- 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";
378
483
  for (const d of decls) {
379
484
  const rangeNode = decls.length === 1 ? s : d;
380
485
  const name = d.name.getText();
381
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
+ }
382
492
  const init = d.initializer && unwrap(d.initializer);
383
- if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(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))) {
384
497
  this.addFunctionLike([init], name, parent, depth, { rangeNodes: [rangeNode], declNodes: [d], extraTags: tags, nameNode: d.name });
385
- } else if (init && ts.isClassExpression(init)) {
498
+ } else if (init && ts3.isClassExpression(init)) {
386
499
  this.addClass(init, rangeNode, name, parent, depth, [d, init], tags);
387
500
  } else if (!functionsOnly) {
388
501
  this.addVariable(d, rangeNode, name, d.type ? ": " + d.type.getText() : "", tags, parent, depth, d.initializer);
389
502
  }
390
503
  }
391
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
+ }
392
533
  addVariable(decl, rangeNode, name, type, tags, parent, depth, initializer) {
393
534
  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)))
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)))
396
537
  ) : [];
397
538
  this.add(
398
539
  {
@@ -410,7 +551,7 @@ var Builder = class {
410
551
  children: objectChildren.length ? (blk) => {
411
552
  for (const p of objectChildren) {
412
553
  const pname = p.name.getText();
413
- if (ts.isMethodDeclaration(p)) this.addFunctionLike([p], pname, blk, depth + 1);
554
+ if (ts3.isMethodDeclaration(p)) this.addFunctionLike([p], pname, blk, depth + 1);
414
555
  else {
415
556
  const fn = unwrap(p.initializer);
416
557
  this.addFunctionLike([fn], pname, blk, depth + 1, { rangeNodes: [p], declNodes: [p], nameNode: p.name });
@@ -472,7 +613,7 @@ var Builder = class {
472
613
  /** Include doc comments directly above the node (no blank line in between). */
473
614
  startWithComments(node) {
474
615
  let start = node.getStart(this.sf);
475
- const comments = ts.getLeadingCommentRanges(this.text, node.pos) ?? [];
616
+ const comments = ts3.getLeadingCommentRanges(this.text, node.pos) ?? [];
476
617
  for (let i = comments.length - 1; i >= 0; i--) {
477
618
  const gap = this.text.slice(comments[i].end, start);
478
619
  if (/\S/.test(gap) || (gap.match(/\n/g)?.length ?? 0) > 1) break;
@@ -500,8 +641,8 @@ var Builder = class {
500
641
  const visit = (node, current) => {
501
642
  const blk = this.rangeToBlock.get(nodeKey(node));
502
643
  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));
644
+ if (ts3.isIdentifier(node) && current) this.recordUsage(node, current, checker, byPair, otherFiles);
645
+ ts3.forEachChild(node, (c) => visit(c, current));
505
646
  };
506
647
  visit(sf, null);
507
648
  return [...byPair.values()];
@@ -510,29 +651,30 @@ var Builder = class {
510
651
  const isKnownName = this.names.has(id.text) || !!otherFiles?.projectNames.has(id.text);
511
652
  if (!isKnownName) return;
512
653
  const parent = id.parent;
513
- 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);
514
655
  if (isDeclarationName) return;
515
656
  let sym;
516
657
  try {
517
- sym = parent && ts.isShorthandPropertyAssignment(parent) ? checker.getShorthandAssignmentValueSymbol(parent) : checker.getSymbolAtLocation(id);
658
+ sym = parent && ts3.isShorthandPropertyAssignment(parent) ? checker.getShorthandAssignmentValueSymbol(parent) : checker.getSymbolAtLocation(id);
518
659
  } catch {
519
660
  return;
520
661
  }
521
662
  if (!sym?.declarations) return;
522
- const kind = usageKind(id);
663
+ let kind = usageKind(id);
523
664
  let to;
524
665
  let importBlockId;
525
666
  for (const d of sym.declarations) {
526
667
  to = this.declToBlock.get(nodeKey(d));
668
+ if (this.stateSetters.has(nodeKey(d))) kind = "write";
527
669
  if (!to) {
528
670
  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));
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));
531
673
  }
532
674
  if (to) break;
533
675
  }
534
676
  if (otherFiles) {
535
- const isImportedName = !!importBlockId && (sym.flags & ts.SymbolFlags.Alias) !== 0;
677
+ const isImportedName = !!importBlockId && (sym.flags & ts3.SymbolFlags.Alias) !== 0;
536
678
  const target = isImportedName ? safeAliasedSymbol(checker, sym) : to ? void 0 : sym;
537
679
  const declaredElsewhere = target?.declarations?.find((d) => d.getSourceFile() !== id.getSourceFile());
538
680
  const external = declaredElsewhere && otherFiles.blockInOtherFile(declaredElsewhere);
@@ -549,6 +691,7 @@ function addUsage(out, usage) {
549
691
  if (!existing) return void out.set(pairKey, usage);
550
692
  existing.count++;
551
693
  if (usage.kind === "call") existing.kind = "call";
694
+ else if (usage.kind === "write" && existing.kind === "read") existing.kind = "write";
552
695
  }
553
696
  function safeAliasedSymbol(checker, symbol) {
554
697
  try {
@@ -561,23 +704,26 @@ function nodeKey(node) {
561
704
  return `${node.kind}:${node.pos}:${node.end}`;
562
705
  }
563
706
  function usageKind(id) {
707
+ if (isJsxTagName(id)) return "render";
564
708
  let n = id;
565
- 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;
566
710
  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";
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";
573
717
  return "read";
574
718
  }
575
- function unwrap(e) {
576
- while (ts.isParenthesizedExpression(e) || ts.isAsExpression(e) || ts.isSatisfiesExpression(e) || ts.isTypeAssertionExpression(e)) e = e.expression;
577
- 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;
578
724
  }
579
725
  function modifierTags(n) {
580
- const mods = ts.canHaveModifiers(n) ? ts.getModifiers(n) ?? [] : [];
726
+ const mods = ts3.canHaveModifiers(n) ? ts3.getModifiers(n) ?? [] : [];
581
727
  return mods.map((m) => m.getText());
582
728
  }
583
729
  function squash(s) {
@@ -625,13 +771,13 @@ function makePreview(text, range, children, max) {
625
771
  }
626
772
 
627
773
  // src/project.ts
628
- import ts3 from "typescript";
774
+ import ts5 from "typescript";
629
775
 
630
776
  // src/workspace.ts
631
- import ts2 from "typescript";
777
+ import ts4 from "typescript";
632
778
  function parseConfig(config, projectPaths) {
633
779
  const configFileName = "/" + config.path;
634
- const json = ts2.parseConfigFileTextToJson(configFileName, config.text);
780
+ const json = ts4.parseConfigFileTextToJson(configFileName, config.text);
635
781
  if (json.error || !json.config) return null;
636
782
  const readConfig = (name) => name === configFileName ? config.text : config.extendedFiles?.[name.slice(1)];
637
783
  const host = {
@@ -641,7 +787,7 @@ function parseConfig(config, projectPaths) {
641
787
  readFile: readConfig
642
788
  };
643
789
  const folder = config.path.slice(0, config.path.lastIndexOf("/") + 1);
644
- const parsed = ts2.parseJsonConfigFileContent(json.config, host, "/" + folder, void 0, configFileName);
790
+ const parsed = ts4.parseJsonConfigFileContent(json.config, host, "/" + folder, void 0, configFileName);
645
791
  const included = new Set(parsed.fileNames.map((name) => name.slice(1)));
646
792
  return { path: config.path, folder, options: parsed.options, includes: (projectPath) => included.has(projectPath) };
647
793
  }
@@ -743,7 +889,7 @@ var Project = class {
743
889
  /** Language services by config path, created when a file of that config is first resolved. */
744
890
  services = /* @__PURE__ */ new Map();
745
891
  /** Shared by the services, so a file several programs include is parsed once. */
746
- documentRegistry = ts3.createDocumentRegistry(true, "/");
892
+ documentRegistry = ts5.createDocumentRegistry(true, "/");
747
893
  /** Changes when files are added or removed, or the settings change: where imports point may change then. */
748
894
  fileSetVersion = 0;
749
895
  /** Load the standard library later, e.g. once it has been downloaded. Type errors show from then on. */
@@ -777,11 +923,11 @@ var Project = class {
777
923
  compilerOptions(configOptions, packagePaths) {
778
924
  const paths = { ...packagePaths, ...configOptions.paths };
779
925
  return {
780
- target: ts3.ScriptTarget.ES2022,
781
- module: ts3.ModuleKind.ESNext,
782
- moduleResolution: ts3.ModuleResolutionKind.Bundler,
926
+ target: ts5.ScriptTarget.ES2022,
927
+ module: ts5.ModuleKind.ESNext,
928
+ moduleResolution: ts5.ModuleResolutionKind.Bundler,
783
929
  strict: true,
784
- jsx: ts3.JsxEmit.Preserve,
930
+ jsx: ts5.JsxEmit.Preserve,
785
931
  skipLibCheck: true,
786
932
  allowImportingTsExtensions: true,
787
933
  ...configOptions,
@@ -827,7 +973,7 @@ var Project = class {
827
973
  serviceOfConfig(configPath) {
828
974
  let service = this.services.get(configPath);
829
975
  if (!service) {
830
- service = ts3.createLanguageService(this.createHost(configPath), this.documentRegistry);
976
+ service = ts5.createLanguageService(this.createHost(configPath), this.documentRegistry);
831
977
  this.services.set(configPath, service);
832
978
  }
833
979
  return service;
@@ -996,7 +1142,7 @@ var Project = class {
996
1142
  const reExportedPaths = /* @__PURE__ */ new Set();
997
1143
  const host = this.moduleResolutionHost();
998
1144
  const resolve = (specifier) => {
999
- const result = ts3.resolveModuleName(specifier, toFileName(path), this.optionsOfFile(path), host).resolvedModule;
1145
+ const result = ts5.resolveModuleName(specifier, toFileName(path), this.optionsOfFile(path), host).resolvedModule;
1000
1146
  return result ? fromFileName(result.resolvedFileName) : null;
1001
1147
  };
1002
1148
  for (const block of file.model.roots) {
@@ -1050,7 +1196,7 @@ var Project = class {
1050
1196
  getScriptSnapshot: (name) => {
1051
1197
  if (name.startsWith(LIB_DIR)) {
1052
1198
  const text = this.libFiles[name.slice(LIB_DIR.length)];
1053
- return text === void 0 ? void 0 : ts3.ScriptSnapshot.fromString(text);
1199
+ return text === void 0 ? void 0 : ts5.ScriptSnapshot.fromString(text);
1054
1200
  }
1055
1201
  const path = fromFileName(name);
1056
1202
  const file = path ? this.files.get(path) : void 0;
@@ -1064,7 +1210,7 @@ var Project = class {
1064
1210
  const containingPath = fromFileName(containingFile);
1065
1211
  const options = this.optionsOfFile(containingPath);
1066
1212
  const host = this.moduleResolutionHost();
1067
- return moduleLiterals.map((literal) => ts3.resolveModuleName(literal.text, containingFile, options, host));
1213
+ return moduleLiterals.map((literal) => ts5.resolveModuleName(literal.text, containingFile, options, host));
1068
1214
  },
1069
1215
  getDefaultLibFileName: () => LIB_DIR + "lib.d.ts",
1070
1216
  fileExists: (name) => this.fileExists(name),
@@ -1100,7 +1246,7 @@ function changedRegion(before, after) {
1100
1246
  while (start < shorter && before.charCodeAt(start) === after.charCodeAt(start)) start++;
1101
1247
  let fromEnd = 0;
1102
1248
  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);
1249
+ return ts5.createTextChangeRange(ts5.createTextSpan(start, before.length - fromEnd - start), after.length - fromEnd - start);
1104
1250
  }
1105
1251
  function toFileName(path) {
1106
1252
  return "/" + path;
@@ -1124,10 +1270,10 @@ function namesVisibleFrom(model, block) {
1124
1270
  return names;
1125
1271
  }
1126
1272
  function isValidIdentifier(name) {
1127
- const scanner = ts3.createScanner(ts3.ScriptTarget.Latest, true, ts3.LanguageVariant.Standard, name);
1273
+ const scanner = ts5.createScanner(ts5.ScriptTarget.Latest, true, ts5.LanguageVariant.Standard, name);
1128
1274
  const token = scanner.scan();
1129
1275
  const isWholeText = scanner.getTokenEnd() === name.length && name.length > 0;
1130
- const isNameToken = token === ts3.SyntaxKind.Identifier || token > ts3.SyntaxKind.LastReservedWord && token <= ts3.SyntaxKind.LastKeyword;
1276
+ const isNameToken = token === ts5.SyntaxKind.Identifier || token > ts5.SyntaxKind.LastReservedWord && token <= ts5.SyntaxKind.LastKeyword;
1131
1277
  return isWholeText && isNameToken;
1132
1278
  }
1133
1279
 
@@ -1257,7 +1403,7 @@ function connectProjectWorker(port) {
1257
1403
  }
1258
1404
 
1259
1405
  // src/edits.ts
1260
- import ts4 from "typescript";
1406
+ import ts6 from "typescript";
1261
1407
  function applyEdit(text, e) {
1262
1408
  return text.slice(0, e.start) + e.text + text.slice(e.end);
1263
1409
  }
@@ -1384,13 +1530,13 @@ function toTopLevel(code) {
1384
1530
  function renameDeclaration(code, inClass, name) {
1385
1531
  const prefix = inClass ? "class __cc__ {\n" : "";
1386
1532
  const src = prefix + code + (inClass ? "\n}" : "");
1387
- const sf = ts4.createSourceFile("snippet.ts", src, ts4.ScriptTarget.Latest, true);
1533
+ const sf = ts6.createSourceFile("snippet.ts", src, ts6.ScriptTarget.Latest, true);
1388
1534
  let target;
1389
1535
  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)))
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)))
1392
1538
  target = first.name;
1393
- else if (first && ts4.isVariableStatement(first)) target = first.declarationList.declarations[0]?.name;
1539
+ else if (first && ts6.isVariableStatement(first)) target = first.declarationList.declarations[0]?.name;
1394
1540
  if (!target) return code;
1395
1541
  const s = target.getStart(sf) - prefix.length;
1396
1542
  return code.slice(0, s) + name + code.slice(target.end - prefix.length);
@@ -1402,26 +1548,26 @@ function must(model, id) {
1402
1548
  }
1403
1549
  function clipFromText(code) {
1404
1550
  const text = code.replace(/\s+$/, "");
1405
- const sf = ts4.createSourceFile("clip.ts", text, ts4.ScriptTarget.Latest, true);
1551
+ const sf = ts6.createSourceFile("clip.ts", text, ts6.ScriptTarget.Latest, true);
1406
1552
  const first = sf.statements[0];
1407
1553
  const diags = sf.parseDiagnostics ?? [];
1408
1554
  if (first && !diags.length) {
1409
1555
  let name;
1410
1556
  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";
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";
1415
1561
  if (name) return { code: text, name, kind, fromClass: false };
1416
1562
  }
1417
- const wrapped = ts4.createSourceFile("clip.ts", `class __cc__ {
1563
+ const wrapped = ts6.createSourceFile("clip.ts", `class __cc__ {
1418
1564
  ${text}
1419
- }`, ts4.ScriptTarget.Latest, true);
1565
+ }`, ts6.ScriptTarget.Latest, true);
1420
1566
  const cls = wrapped.statements[0];
1421
1567
  const wd = wrapped.parseDiagnostics ?? [];
1422
- if (cls && ts4.isClassDeclaration(cls) && !wd.length && cls.members[0]?.name) {
1568
+ if (cls && ts6.isClassDeclaration(cls) && !wd.length && cls.members[0]?.name) {
1423
1569
  const m = cls.members[0];
1424
- return { code: text, name: m.name.getText(wrapped), kind: ts4.isPropertyDeclaration(m) ? "variable" : "function", fromClass: true };
1570
+ return { code: text, name: m.name.getText(wrapped), kind: ts6.isPropertyDeclaration(m) ? "variable" : "function", fromClass: true };
1425
1571
  }
1426
1572
  return { code: text, name: "", kind: "statement", fromClass: false };
1427
1573
  }
@@ -1579,7 +1725,7 @@ function resolveConflict(text, hunk, choice) {
1579
1725
  var BAND_ORDER = ["data", "types", "main", "bottom"];
1580
1726
  var ZONE_RULES = [
1581
1727
  { 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" },
1728
+ { zone: "data", description: "Constants, other data and React state at the top", matches: (block) => block.kind === "variable" || block.kind === "state" },
1583
1729
  { zone: "types", description: "Then interfaces, types and enums", matches: (block) => block.kind === "type" },
1584
1730
  { zone: "bottom", description: "Code that runs when the file loads, at the bottom", matches: (block) => block.kind === "statement" },
1585
1731
  { zone: "main", description: "Functions, classes and the rest in between", matches: () => true }
@@ -1669,7 +1815,7 @@ var PLACEMENT_RULES = [
1669
1815
  ];
1670
1816
  var zoneOf = (block) => ZONE_RULES.find((rule) => rule.matches(block)).zone;
1671
1817
  var NEWSPAPER_DEPTH = 3;
1672
- var isFunctionLike = (block) => block.kind === "function";
1818
+ var isFunctionLike = (block) => block.kind === "function" || block.kind === "component";
1673
1819
  function accessedName(block, accessor) {
1674
1820
  if (block.kind !== "function") return void 0;
1675
1821
  if (block.tags.includes(accessor)) return block.name;
@@ -1823,6 +1969,7 @@ function orderByRelatedness(units, factsOf) {
1823
1969
  function siblingFacts(blocks, text) {
1824
1970
  const idsByName = /* @__PURE__ */ new Map();
1825
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));
1826
1973
  const facts = /* @__PURE__ */ new Map();
1827
1974
  for (const block of blocks) {
1828
1975
  const code = codeWithoutChildren(text, [block.start, block.end], block.children);
@@ -1832,9 +1979,11 @@ function siblingFacts(blocks, text) {
1832
1979
  const ids = idsByName.get(match[0])?.filter((id) => id !== block.id);
1833
1980
  if (!ids?.length) continue;
1834
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));
1835
1983
  for (const id of ids) {
1836
1984
  uses.add(id);
1837
- 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);
1838
1987
  }
1839
1988
  }
1840
1989
  facts.set(block.id, { calls, uses });
@@ -2171,6 +2320,8 @@ var CSS = (
2171
2320
  --k-import: #6b7280; --k-import-head: #3b3f46; --k-import-bg: #25272c;
2172
2321
  --k-class: #34b27a; --k-class-head: #1d6b4a; --k-class-bg: rgba(52,178,122,.08);
2173
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);
2174
2325
  --k-variable: #4b8ef7; --k-variable-head: #23508f; --k-variable-bg: rgba(75,142,247,.09);
2175
2326
  --k-type: #d4a92a; --k-type-head: #6e5714; --k-type-bg: rgba(212,169,42,.08);
2176
2327
  --k-namespace: #9b8cf8; --k-namespace-head: #4d4291; --k-namespace-bg: rgba(155,140,248,.08);
@@ -2193,6 +2344,8 @@ var CSS = (
2193
2344
  --k-import: #6b7280; --k-import-head: #4b5058; --k-import-bg: #eceef1;
2194
2345
  --k-class: #1f9963; --k-class-head: #1f9963; --k-class-bg: rgba(31,153,99,.07);
2195
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);
2196
2349
  --k-variable: #2f6fe0; --k-variable-head: #2f6fe0; --k-variable-bg: rgba(47,111,224,.07);
2197
2350
  --k-type: #b58a0c; --k-type-head: #a87f08; --k-type-bg: rgba(181,138,12,.08);
2198
2351
  --k-namespace: #6f5ce6; --k-namespace-head: #6f5ce6; --k-namespace-bg: rgba(111,92,230,.07);
@@ -2212,6 +2365,8 @@ var CSS = (
2212
2365
  .cc-block.k-import { --k: var(--k-import); --kh: var(--k-import-head); --kbg: var(--k-import-bg); }
2213
2366
  .cc-block.k-class { --k: var(--k-class); --kh: var(--k-class-head); --kbg: var(--k-class-bg); }
2214
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); }
2215
2370
  .cc-block.k-variable { --k: var(--k-variable); --kh: var(--k-variable-head); --kbg: var(--k-variable-bg); }
2216
2371
  .cc-block.k-type { --k: var(--k-type); --kh: var(--k-type-head); --kbg: var(--k-type-bg); }
2217
2372
  .cc-block.k-namespace { --k: var(--k-namespace); --kh: var(--k-namespace-head); --kbg: var(--k-namespace-bg); }
@@ -2241,6 +2396,7 @@ var CSS = (
2241
2396
  .cc-mini { position: relative; margin: 6px 12px 0; height: 34px; opacity: .45; }
2242
2397
  .cc-mini > i { position: absolute; border-radius: 2px; border: 1px solid var(--cc-faint); background: rgba(255,255,255,.06); }
2243
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); }
2244
2400
  .cc-mini > i.k-variable { border-color: var(--k-variable); } .cc-mini > i.k-type { border-color: var(--k-type); }
2245
2401
 
2246
2402
  .cc-err {
@@ -2336,6 +2492,7 @@ var CSS = (
2336
2492
  .cc-btn:hover { background: rgba(127,127,127,.16); }
2337
2493
  .cc-btn.danger { color: var(--cc-danger); }
2338
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; }
2339
2496
  .cc-sep { width: 1px; height: 20px; background: var(--cc-panel-border); margin: 0 2px; flex: none; }
2340
2497
  .cc-refs {
2341
2498
  position: absolute; left: 50%; bottom: 64px; transform: translateX(-50%); z-index: 21; width: min(360px, calc(100% - 24px)); max-height: 45%;
@@ -2385,6 +2542,8 @@ var KIND_LABEL = {
2385
2542
  import: "import",
2386
2543
  class: "class",
2387
2544
  function: "fn",
2545
+ component: "component",
2546
+ state: "state",
2388
2547
  variable: "var",
2389
2548
  type: "type",
2390
2549
  namespace: "ns",
@@ -2414,7 +2573,7 @@ var CodeCanvas = class {
2414
2573
  this.blocksEl = el("div", "cc-blocks");
2415
2574
  this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2416
2575
  this.svg.classList.add("cc-edges");
2417
- 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(
2418
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>`
2419
2578
  ).join("")}</defs>`);
2420
2579
  this.world.append(this.frameEl, this.blocksEl, this.svg);
@@ -3676,7 +3835,8 @@ var CodeCanvas = class {
3676
3835
  el("div", "cc-sep"),
3677
3836
  ...firstHunk ? [keep("Keep current", "current", firstHunk.currentLabel), keep("Keep incoming", "incoming", firstHunk.incomingLabel), keep("Keep both", "both"), el("div", "cc-sep")] : [],
3678
3837
  button("Edit", () => this.openEditor(b.id)),
3679
- button("Copy", () => this.copy(b.id))
3838
+ button("Copy", () => this.copy(b.id)),
3839
+ ...this.languageLabelParts()
3680
3840
  );
3681
3841
  return this.renderRefs();
3682
3842
  }
@@ -3707,8 +3867,15 @@ var CodeCanvas = class {
3707
3867
  button("+", () => this.zoomBy(1.25))
3708
3868
  );
3709
3869
  }
3870
+ bar.append(...this.languageLabelParts());
3710
3871
  this.renderRefs();
3711
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
+ }
3712
3879
  toggleRefs() {
3713
3880
  this.refsOpen = !this.refsOpen;
3714
3881
  this.renderBar();
@@ -3748,7 +3915,7 @@ var CodeCanvas = class {
3748
3915
  btn.style.setProperty("--k", `var(--k-${block?.kind ?? "statement"})`);
3749
3916
  const name = block && model ? displayName(model, block) : blockId;
3750
3917
  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>`);
3918
+ setHTML(btn, `<i></i><span>${label}</span><span>${usage.kind === "render" ? "renders" : usage.kind}${usage.count > 1 ? " \xD7" + usage.count : ""}</span>`);
3752
3919
  btn.onclick = () => isOtherFile ? this.opts.onOpenBlock?.(path, blockId) : this.reveal(blockId);
3753
3920
  btn.disabled = isOtherFile && !this.opts.onOpenBlock;
3754
3921
  return btn;