@aihu/language-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js ADDED
@@ -0,0 +1,2119 @@
1
+ #!/usr/bin/env node
2
+ import { CodeActionKind, CompletionList, DiagnosticSeverity, MarkupKind, ProposedFeatures, TextDocumentSyncKind, TextDocuments, TextEdit, createConnection } from "vscode-languageserver/node.js";
3
+ import { TextDocument } from "vscode-languageserver-textdocument";
4
+ import { execFile } from "node:child_process";
5
+ import { dirname, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+ //#region ../compiler/js/codemods/macro-simplification/migrate.ts
9
+ const INDENT = " ";
10
+ function migrate(source) {
11
+ const warnings = [];
12
+ const stateBlock = findBlock(source, "state");
13
+ const agentBlock = findBlock(source, "agent");
14
+ const sidecar = /* @__PURE__ */ new Map();
15
+ const preservedAgentLines = [];
16
+ if (agentBlock) parseAgent(agentBlock.body, sidecar, preservedAgentLines, warnings);
17
+ if (!stateBlock) return {
18
+ rewritten: source,
19
+ warnings
20
+ };
21
+ const buckets = walkState(stateBlock.body, sidecar, warnings);
22
+ const newStateBlock = `@state {\n${emitStateBody(buckets)}\n}`;
23
+ const newAgentBlock = preservedAgentLines.length > 0 ? `@agent {\n${preservedAgentLines.join("\n")}\n}` : null;
24
+ let out = source.slice(0, stateBlock.start) + newStateBlock + source.slice(stateBlock.end);
25
+ if (agentBlock) {
26
+ const agentRelocated = findBlock(out, "agent");
27
+ if (agentRelocated) if (newAgentBlock) out = out.slice(0, agentRelocated.start) + newAgentBlock + out.slice(agentRelocated.end);
28
+ else {
29
+ const dropStart = expandLeftToLineStart(out, agentRelocated.start);
30
+ const dropEnd = expandRightThroughBlankLines(out, agentRelocated.end);
31
+ out = out.slice(0, dropStart) + out.slice(dropEnd);
32
+ }
33
+ }
34
+ const stateNames = new Set([
35
+ ...buckets.props.map((p) => p.name),
36
+ ...buckets.computed.map((c) => c.name),
37
+ ...buckets.action.map((a) => a.name),
38
+ ...buckets.resource.map((r) => r.name),
39
+ ...buckets.effectNamed.map((e) => e.name),
40
+ ...buckets.plain.map((p) => p.name).filter((x) => !!x)
41
+ ]);
42
+ for (const [name] of sidecar) if (!stateNames.has(name)) warnings.push(`@agent references '${name}' but no @state declaration found`);
43
+ return {
44
+ rewritten: out,
45
+ warnings
46
+ };
47
+ }
48
+ function findBlock(source, name) {
49
+ const m = new RegExp(`(^|\\n)([ \\t]*)@${name}\\s*\\{`, "g").exec(source);
50
+ if (!m) return null;
51
+ const startMatch = m.index + (m[1] === "\n" ? 1 : 0);
52
+ const openIdx = m.index + m[0].length - 1;
53
+ const closeIdx = matchBrace(source, openIdx);
54
+ if (closeIdx < 0) return null;
55
+ return {
56
+ start: startMatch,
57
+ end: closeIdx + 1,
58
+ bodyStart: openIdx + 1,
59
+ bodyEnd: closeIdx,
60
+ body: source.slice(openIdx + 1, closeIdx)
61
+ };
62
+ }
63
+ function matchBrace(s, i) {
64
+ if (s[i] !== "{") return -1;
65
+ let depth = 0;
66
+ let j = i;
67
+ while (j < s.length) {
68
+ const c = s[j];
69
+ if (c === "/" && s[j + 1] === "/") {
70
+ const nl = s.indexOf("\n", j);
71
+ j = nl < 0 ? s.length : nl;
72
+ continue;
73
+ }
74
+ if (c === "/" && s[j + 1] === "*") {
75
+ const end = s.indexOf("*/", j + 2);
76
+ j = end < 0 ? s.length : end + 2;
77
+ continue;
78
+ }
79
+ if (c === "\"" || c === "'" || c === "`") {
80
+ j = skipString(s, j);
81
+ continue;
82
+ }
83
+ if (c === "{") depth++;
84
+ else if (c === "}") {
85
+ depth--;
86
+ if (depth === 0) return j;
87
+ }
88
+ j++;
89
+ }
90
+ return -1;
91
+ }
92
+ function skipString(s, i) {
93
+ const quote = s[i];
94
+ let j = i + 1;
95
+ while (j < s.length) {
96
+ const c = s[j];
97
+ if (c === "\\") {
98
+ j += 2;
99
+ continue;
100
+ }
101
+ if (quote === "`" && c === "$" && s[j + 1] === "{") {
102
+ const close = matchBrace(s, j + 1);
103
+ j = close < 0 ? s.length : close + 1;
104
+ continue;
105
+ }
106
+ if (c === quote) return j + 1;
107
+ j++;
108
+ }
109
+ return s.length;
110
+ }
111
+ function matchParen(s, i) {
112
+ if (s[i] !== "(") return -1;
113
+ let depth = 0;
114
+ let j = i;
115
+ while (j < s.length) {
116
+ const c = s[j];
117
+ if (c === "/" && s[j + 1] === "/") {
118
+ const nl = s.indexOf("\n", j);
119
+ j = nl < 0 ? s.length : nl;
120
+ continue;
121
+ }
122
+ if (c === "/" && s[j + 1] === "*") {
123
+ const end = s.indexOf("*/", j + 2);
124
+ j = end < 0 ? s.length : end + 2;
125
+ continue;
126
+ }
127
+ if (c === "\"" || c === "'" || c === "`") {
128
+ j = skipString(s, j);
129
+ continue;
130
+ }
131
+ if (c === "{") {
132
+ const close = matchBrace(s, j);
133
+ j = close < 0 ? s.length : close + 1;
134
+ continue;
135
+ }
136
+ if (c === "(") depth++;
137
+ else if (c === ")") {
138
+ depth--;
139
+ if (depth === 0) return j;
140
+ }
141
+ j++;
142
+ }
143
+ return -1;
144
+ }
145
+ function parseAgent(body, sidecar, preservedAgentLines, _warnings) {
146
+ const upsert = (name, patch) => {
147
+ sidecar.set(name, {
148
+ ...sidecar.get(name) ?? {},
149
+ ...patch
150
+ });
151
+ };
152
+ for (const rawLine of body.split("\n")) {
153
+ const line = rawLine.trim();
154
+ if (line === "" || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) continue;
155
+ if (line.startsWith("$expose.write ")) {
156
+ const names = parseNameList(line.slice(14));
157
+ for (const n of names) upsert(n, {
158
+ exposeRead: true,
159
+ exposeWrite: true
160
+ });
161
+ continue;
162
+ }
163
+ if (line.startsWith("$expose ")) {
164
+ const names = parseNameList(line.slice(8));
165
+ for (const n of names) upsert(n, { exposeRead: true });
166
+ continue;
167
+ }
168
+ if (line.startsWith("$action ")) {
169
+ const rest = line.slice(8).trim();
170
+ const m = /^([A-Za-z_$][\w$]*)/.exec(rest);
171
+ if (m) upsert(m[1], {
172
+ isAction: true,
173
+ exposeRead: true,
174
+ exposeWrite: true
175
+ });
176
+ continue;
177
+ }
178
+ if (line.startsWith("$describe ")) {
179
+ const rest = line.slice(10).trim();
180
+ const nameMatch = /^([A-Za-z_$][\w$]*)\s+(["'])([\s\S]*)\2\s*$/.exec(rest);
181
+ if (nameMatch) upsert(nameMatch[1], { describe: nameMatch[3] });
182
+ continue;
183
+ }
184
+ if (line.startsWith("$scope ")) {
185
+ preservedAgentLines.push(` ${line}`);
186
+ continue;
187
+ }
188
+ if (line.startsWith("$rate-limit ")) preservedAgentLines.push(` ${line}`);
189
+ }
190
+ }
191
+ function parseNameList(s) {
192
+ return s.split(",").map((x) => x.trim()).filter((x) => /^[A-Za-z_$][\w$]*$/.test(x));
193
+ }
194
+ function walkState(body, sidecar, warnings) {
195
+ const buckets = {
196
+ props: [],
197
+ computed: [],
198
+ action: [],
199
+ resource: [],
200
+ effectNamed: [],
201
+ effectAnon: [],
202
+ lifecycle: [],
203
+ plain: [],
204
+ passthrough: []
205
+ };
206
+ let i = 0;
207
+ while (i < body.length && (body[i] === "\n" || body[i] === "\r")) i++;
208
+ let leadingBuf = "";
209
+ let position = 0;
210
+ const consumeWS = () => {
211
+ while (i < body.length) {
212
+ const lineEnd = body.indexOf("\n", i);
213
+ const end = lineEnd < 0 ? body.length : lineEnd;
214
+ const line = body.slice(i, end);
215
+ const trimmed = line.trim();
216
+ if (trimmed === "") {
217
+ leadingBuf += `${line}\n`;
218
+ i = end + 1;
219
+ continue;
220
+ }
221
+ if (trimmed.startsWith("//")) {
222
+ leadingBuf += `${line}\n`;
223
+ i = end + 1;
224
+ continue;
225
+ }
226
+ if (trimmed.startsWith("/*")) {
227
+ const close = body.indexOf("*/", i);
228
+ if (close < 0) {
229
+ leadingBuf += body.slice(i);
230
+ i = body.length;
231
+ return;
232
+ }
233
+ const commentEnd = body.indexOf("\n", close);
234
+ const realEnd = commentEnd < 0 ? body.length : commentEnd + 1;
235
+ leadingBuf += body.slice(i, realEnd);
236
+ i = realEnd;
237
+ continue;
238
+ }
239
+ return;
240
+ }
241
+ };
242
+ while (i < body.length) {
243
+ consumeWS();
244
+ if (i >= body.length) break;
245
+ const lineStart = i;
246
+ const lineEnd = body.indexOf("\n", i);
247
+ const lineEndIdx = lineEnd < 0 ? body.length : lineEnd;
248
+ const lineText = body.slice(lineStart, lineEndIdx);
249
+ const trimmed = lineText.trim();
250
+ const leading = leadingBuf;
251
+ leadingBuf = "";
252
+ const trimStart = lineStart + (lineText.length - lineText.trimStart().length);
253
+ if (trimmed.startsWith("import ")) {
254
+ buckets.passthrough.push({
255
+ raw: lineText.trim(),
256
+ leading,
257
+ position: position++
258
+ });
259
+ i = lineEndIdx + 1;
260
+ continue;
261
+ }
262
+ {
263
+ const v2 = tryParseV2Collection(body, trimStart, sidecar, buckets, leading, () => position++);
264
+ if (v2 !== null) {
265
+ i = v2;
266
+ continue;
267
+ }
268
+ }
269
+ if (/^\$effect\s*:\s*(?:\(|async\b)/.test(trimmed)) {
270
+ const handled = tryParseV2AnonEffect(body, trimStart, buckets, leading, position++);
271
+ if (handled !== null) {
272
+ i = handled;
273
+ continue;
274
+ }
275
+ }
276
+ if (trimmed.startsWith("$prop ")) {
277
+ const handled = handleProp(body, trimStart, sidecar, buckets, leading);
278
+ if (handled !== null) {
279
+ i = handled;
280
+ continue;
281
+ }
282
+ buckets.passthrough.push({
283
+ raw: trimmed,
284
+ leading,
285
+ position: position++
286
+ });
287
+ i = lineEndIdx + 1;
288
+ continue;
289
+ }
290
+ if (trimmed.startsWith("$computed ")) {
291
+ const handled = handleComputed(body, trimStart, sidecar, buckets, leading);
292
+ if (handled !== null) {
293
+ i = handled;
294
+ continue;
295
+ }
296
+ buckets.passthrough.push({
297
+ raw: trimmed,
298
+ leading,
299
+ position: position++
300
+ });
301
+ i = lineEndIdx + 1;
302
+ continue;
303
+ }
304
+ if (trimmed.startsWith("$resource ")) {
305
+ const handled = handleResource(body, trimStart, sidecar, buckets, leading);
306
+ if (handled !== null) {
307
+ i = handled;
308
+ continue;
309
+ }
310
+ buckets.passthrough.push({
311
+ raw: trimmed,
312
+ leading,
313
+ position: position++
314
+ });
315
+ i = lineEndIdx + 1;
316
+ continue;
317
+ }
318
+ if (trimmed.startsWith("$action ")) {
319
+ const handled = handleAction(body, trimStart, sidecar, buckets, leading);
320
+ if (handled !== null) {
321
+ i = handled;
322
+ continue;
323
+ }
324
+ buckets.passthrough.push({
325
+ raw: trimmed,
326
+ leading,
327
+ position: position++
328
+ });
329
+ i = lineEndIdx + 1;
330
+ continue;
331
+ }
332
+ if (trimmed.startsWith("$lifecycle.mount(") || trimmed.startsWith("$lifecycle.dispose(")) {
333
+ const handled = handleLifecycle(body, trimStart, buckets, leading, position++);
334
+ if (handled !== null) {
335
+ i = handled;
336
+ continue;
337
+ }
338
+ buckets.passthrough.push({
339
+ raw: trimmed,
340
+ leading,
341
+ position: position++
342
+ });
343
+ i = lineEndIdx + 1;
344
+ continue;
345
+ }
346
+ if (trimmed.startsWith("$effect(")) {
347
+ const handled = handleEffectAnon(body, trimStart, buckets, leading, position++, warnings);
348
+ if (handled !== null) {
349
+ i = handled;
350
+ continue;
351
+ }
352
+ buckets.passthrough.push({
353
+ raw: trimmed,
354
+ leading,
355
+ position: position++
356
+ });
357
+ i = lineEndIdx + 1;
358
+ continue;
359
+ }
360
+ if (trimmed.startsWith("$effect.on(")) {
361
+ const handled = consumePassthroughCall(body, trimStart);
362
+ buckets.passthrough.push({
363
+ raw: body.slice(trimStart, handled).trim(),
364
+ leading,
365
+ position: position++
366
+ });
367
+ i = handled;
368
+ continue;
369
+ }
370
+ if (/^\$[A-Za-z_][\w.$]*\s*\(/.test(trimmed)) {
371
+ const handled = consumePassthroughCall(body, trimStart);
372
+ buckets.passthrough.push({
373
+ raw: body.slice(trimStart, handled).trimEnd(),
374
+ leading,
375
+ position: position++
376
+ });
377
+ i = handled;
378
+ continue;
379
+ }
380
+ if (/^\$[A-Za-z_][\w.$-]*\s+[A-Za-z_$]/.test(trimmed)) {
381
+ buckets.passthrough.push({
382
+ raw: trimmed,
383
+ leading,
384
+ position: position++
385
+ });
386
+ i = lineEndIdx + 1;
387
+ continue;
388
+ }
389
+ const plain = tryParsePlainDecl(body, trimStart);
390
+ if (plain !== null) {
391
+ const sc = plain.name ? sidecar.get(plain.name) : void 0;
392
+ if (sc) buckets.props.push({
393
+ name: plain.name,
394
+ rawType: plain.rawType,
395
+ rawDefault: plain.rawDefault,
396
+ describe: sc.describe,
397
+ expose: sidecarToExpose(sc),
398
+ leading
399
+ });
400
+ else buckets.plain.push({
401
+ raw: plain.raw,
402
+ leading,
403
+ name: plain.name,
404
+ rawType: plain.rawType,
405
+ rawDefault: plain.rawDefault
406
+ });
407
+ i = plain.endIdx;
408
+ continue;
409
+ }
410
+ buckets.passthrough.push({
411
+ raw: trimmed,
412
+ leading,
413
+ position: position++
414
+ });
415
+ i = lineEndIdx + 1;
416
+ }
417
+ return buckets;
418
+ }
419
+ function sidecarToExpose(sc) {
420
+ if (!sc.exposeRead && !sc.exposeWrite && !sc.isAction) return void 0;
421
+ return {
422
+ read: !!sc.exposeRead,
423
+ write: !!sc.exposeWrite
424
+ };
425
+ }
426
+ function handleProp(body, start, sidecar, buckets, leading) {
427
+ const after = start + 6;
428
+ const nameMatch = /^\s*([A-Za-z_$][\w$]*)\s*:/.exec(body.slice(after));
429
+ if (!nameMatch) return null;
430
+ const name = nameMatch[1];
431
+ let cursor = after + nameMatch[0].length;
432
+ const typeStart = cursor;
433
+ const typeEnd = scanUntilTopLevel(body, cursor, [
434
+ "=",
435
+ "\n",
436
+ ";"
437
+ ]);
438
+ const rawType = body.slice(typeStart, typeEnd).trim();
439
+ cursor = typeEnd;
440
+ let rawDefault;
441
+ if (body[cursor] === "=") {
442
+ cursor++;
443
+ const defStart = cursor;
444
+ const defEnd = scanUntilTopLevel(body, cursor, ["\n", ";"]);
445
+ rawDefault = body.slice(defStart, defEnd).trim();
446
+ cursor = defEnd;
447
+ }
448
+ if (body[cursor] === ";" || body[cursor] === "\n") cursor++;
449
+ const sc = sidecar.get(name);
450
+ buckets.props.push({
451
+ name,
452
+ rawType: flattenWhitespace(rawType),
453
+ rawDefault: rawDefault !== void 0 ? flattenWhitespace(rawDefault) : void 0,
454
+ describe: sc?.describe,
455
+ expose: sc ? sidecarToExpose(sc) : void 0,
456
+ leading
457
+ });
458
+ return cursor;
459
+ }
460
+ function handleComputed(body, start, sidecar, buckets, leading) {
461
+ const after = start + 10;
462
+ const nameMatch = /^\s*([A-Za-z_$][\w$]*)\s*=/.exec(body.slice(after));
463
+ if (!nameMatch) return null;
464
+ const name = nameMatch[1];
465
+ let cursor = after + nameMatch[0].length;
466
+ const exprStart = cursor;
467
+ const exprEnd = scanComputedOrResourceExpr(body, cursor);
468
+ const expr = body.slice(exprStart, exprEnd).trim();
469
+ cursor = exprEnd;
470
+ const sc = sidecar.get(name);
471
+ buckets.computed.push({
472
+ name,
473
+ expr,
474
+ describe: sc?.describe,
475
+ expose: sc ? sidecarToExpose(sc) : void 0,
476
+ leading
477
+ });
478
+ return cursor;
479
+ }
480
+ function handleResource(body, start, sidecar, buckets, leading) {
481
+ const after = start + 10;
482
+ const nameMatch = /^\s*([A-Za-z_$][\w$]*)\s*=/.exec(body.slice(after));
483
+ if (!nameMatch) return null;
484
+ const name = nameMatch[1];
485
+ let cursor = after + nameMatch[0].length;
486
+ const exprStart = cursor;
487
+ const exprEnd = scanComputedOrResourceExpr(body, cursor);
488
+ const expr = body.slice(exprStart, exprEnd).trim();
489
+ cursor = exprEnd;
490
+ const sc = sidecar.get(name);
491
+ buckets.resource.push({
492
+ name,
493
+ expr,
494
+ describe: sc?.describe,
495
+ expose: sc ? sidecarToExpose(sc) : void 0,
496
+ leading
497
+ });
498
+ return cursor;
499
+ }
500
+ function handleAction(body, start, sidecar, buckets, leading) {
501
+ const after = start + 8;
502
+ const nameMatch = /^\s*([A-Za-z_$][\w$]*)\s*\(/.exec(body.slice(after));
503
+ if (!nameMatch) return null;
504
+ const name = nameMatch[1];
505
+ const parenIdx = after + nameMatch[0].length - 1;
506
+ const parenClose = matchParen(body, parenIdx);
507
+ if (parenClose < 0) return null;
508
+ const args = body.slice(parenIdx + 1, parenClose).trim();
509
+ let cursor = parenClose + 1;
510
+ let retType;
511
+ const retStart = cursor;
512
+ while (cursor < body.length && body[cursor] !== "{") {
513
+ if (body[cursor] === "\n") break;
514
+ cursor++;
515
+ }
516
+ const between = body.slice(retStart, cursor).trim();
517
+ if (between.startsWith(":")) retType = between.slice(1).trim();
518
+ if (body[cursor] !== "{") return null;
519
+ const braceClose = matchBrace(body, cursor);
520
+ if (braceClose < 0) return null;
521
+ const bodySrc = body.slice(cursor + 1, braceClose);
522
+ cursor = braceClose + 1;
523
+ const sc = sidecar.get(name);
524
+ buckets.action.push({
525
+ name,
526
+ args,
527
+ retType,
528
+ body: bodySrc,
529
+ describe: sc?.describe,
530
+ expose: sc ? sidecarToExpose(sc) : void 0,
531
+ leading
532
+ });
533
+ return cursor;
534
+ }
535
+ function handleLifecycle(body, start, buckets, leading, position) {
536
+ const kind = body.slice(start).trim().startsWith("$lifecycle.mount(") ? "mount" : "dispose";
537
+ const parenIdx = body.indexOf("(", start);
538
+ if (parenIdx < 0) return null;
539
+ const parenClose = matchParen(body, parenIdx);
540
+ if (parenClose < 0) return null;
541
+ const cb = body.slice(parenIdx + 1, parenClose).trim();
542
+ const fn = parseArrowOrFunctionExpr(cb);
543
+ buckets.lifecycle.push({
544
+ kind,
545
+ body: fn ? fn.body : cb,
546
+ leading,
547
+ position
548
+ });
549
+ let cursor = parenClose + 1;
550
+ if (body[cursor] === ";" || body[cursor] === "\n") cursor++;
551
+ return cursor;
552
+ }
553
+ function handleEffectAnon(body, start, buckets, leading, position, _warnings) {
554
+ const parenIdx = body.indexOf("(", start);
555
+ if (parenIdx < 0) return null;
556
+ const parenClose = matchParen(body, parenIdx);
557
+ if (parenClose < 0) return null;
558
+ const cb = body.slice(parenIdx + 1, parenClose).trim();
559
+ const fn = parseArrowOrFunctionExpr(cb);
560
+ buckets.effectAnon.push({
561
+ body: fn ? fn.body : cb,
562
+ leading,
563
+ position
564
+ });
565
+ let cursor = parenClose + 1;
566
+ if (body[cursor] === ";" || body[cursor] === "\n") cursor++;
567
+ return cursor;
568
+ }
569
+ function consumePassthroughCall(body, start) {
570
+ const parenIdx = body.indexOf("(", start);
571
+ if (parenIdx < 0) {
572
+ const nl = body.indexOf("\n", start);
573
+ return nl < 0 ? body.length : nl + 1;
574
+ }
575
+ const close = matchParen(body, parenIdx);
576
+ if (close < 0) {
577
+ const nl = body.indexOf("\n", start);
578
+ return nl < 0 ? body.length : nl + 1;
579
+ }
580
+ let cursor = close + 1;
581
+ if (body[cursor] === ";" || body[cursor] === "\n") cursor++;
582
+ return cursor;
583
+ }
584
+ function tryParsePlainDecl(body, start) {
585
+ const m = /^([ \t]*)([A-Za-z_$][\w$]*)\s*:/.exec(body.slice(start));
586
+ if (!m) return null;
587
+ const name = m[2];
588
+ let cursor = start + m[0].length;
589
+ const typeStart = cursor;
590
+ const typeEnd = scanUntilTopLevel(body, cursor, [
591
+ "=",
592
+ "\n",
593
+ ";"
594
+ ]);
595
+ const rawType = body.slice(typeStart, typeEnd).trim();
596
+ cursor = typeEnd;
597
+ let rawDefault;
598
+ if (body[cursor] === "=") {
599
+ cursor++;
600
+ const defStart = cursor;
601
+ const defEnd = scanUntilTopLevel(body, cursor, ["\n", ";"]);
602
+ rawDefault = body.slice(defStart, defEnd).trim();
603
+ cursor = defEnd;
604
+ }
605
+ if (body[cursor] === ";") cursor++;
606
+ if (body[cursor] === "\n") cursor++;
607
+ return {
608
+ name,
609
+ rawType,
610
+ rawDefault,
611
+ raw: body.slice(start, cursor).replace(/\n+$/, ""),
612
+ endIdx: cursor
613
+ };
614
+ }
615
+ function parseArrowOrFunctionExpr(s) {
616
+ s = s.trim();
617
+ if (s.startsWith("(")) {
618
+ const closeIdx = matchParen(s, 0);
619
+ if (closeIdx < 0) return null;
620
+ const args = s.slice(1, closeIdx);
621
+ let cur = closeIdx + 1;
622
+ while (cur < s.length && /\s/.test(s[cur])) cur++;
623
+ let retType;
624
+ if (s[cur] === ":") {
625
+ cur++;
626
+ const arrowAt = s.indexOf("=>", cur);
627
+ if (arrowAt < 0) return null;
628
+ retType = s.slice(cur, arrowAt).trim();
629
+ cur = arrowAt;
630
+ }
631
+ if (s.slice(cur, cur + 2) !== "=>") return null;
632
+ cur += 2;
633
+ while (cur < s.length && /\s/.test(s[cur])) cur++;
634
+ if (s[cur] === "{") {
635
+ const close = matchBrace(s, cur);
636
+ if (close < 0) return null;
637
+ return {
638
+ args,
639
+ body: s.slice(cur + 1, close),
640
+ retType,
641
+ isArrow: true
642
+ };
643
+ }
644
+ return {
645
+ args,
646
+ body: ` return ${s.slice(cur).trim()} `,
647
+ retType,
648
+ isArrow: true
649
+ };
650
+ }
651
+ const fnMatch = /^function\s*(?:[A-Za-z_$][\w$]*)?\s*\(/.exec(s);
652
+ if (fnMatch) {
653
+ const parenIdx = fnMatch[0].length - 1;
654
+ const parenClose = matchParen(s, parenIdx);
655
+ if (parenClose < 0) return null;
656
+ const args = s.slice(parenIdx + 1, parenClose);
657
+ let cur = parenClose + 1;
658
+ while (cur < s.length && /\s/.test(s[cur])) cur++;
659
+ let retType;
660
+ if (s[cur] === ":") {
661
+ cur++;
662
+ const braceAt = s.indexOf("{", cur);
663
+ if (braceAt < 0) return null;
664
+ retType = s.slice(cur, braceAt).trim();
665
+ cur = braceAt;
666
+ }
667
+ if (s[cur] !== "{") return null;
668
+ const close = matchBrace(s, cur);
669
+ if (close < 0) return null;
670
+ return {
671
+ args,
672
+ body: s.slice(cur + 1, close),
673
+ retType,
674
+ isArrow: false
675
+ };
676
+ }
677
+ return null;
678
+ }
679
+ function scanUntilTopLevel(s, start, stops) {
680
+ let j = start;
681
+ while (j < s.length) {
682
+ const c = s[j];
683
+ if (c === "/" && s[j + 1] === "/") {
684
+ const nl = s.indexOf("\n", j);
685
+ if (stops.includes("\n")) return nl < 0 ? s.length : nl;
686
+ j = nl < 0 ? s.length : nl + 1;
687
+ continue;
688
+ }
689
+ if (c === "/" && s[j + 1] === "*") {
690
+ const end = s.indexOf("*/", j + 2);
691
+ j = end < 0 ? s.length : end + 2;
692
+ continue;
693
+ }
694
+ if (c === "\"" || c === "'" || c === "`") {
695
+ j = skipString(s, j);
696
+ continue;
697
+ }
698
+ if (c === "{") {
699
+ const close = matchBrace(s, j);
700
+ j = close < 0 ? s.length : close + 1;
701
+ continue;
702
+ }
703
+ if (c === "(") {
704
+ const close = matchParen(s, j);
705
+ j = close < 0 ? s.length : close + 1;
706
+ continue;
707
+ }
708
+ if (c === "<") {
709
+ const close = scanGeneric(s, j);
710
+ if (close > j) {
711
+ j = close;
712
+ continue;
713
+ }
714
+ }
715
+ if (stops.includes(c)) return j;
716
+ j++;
717
+ }
718
+ return s.length;
719
+ }
720
+ function scanGeneric(s, start) {
721
+ if (s[start] !== "<") return start;
722
+ let depth = 0;
723
+ let j = start;
724
+ while (j < s.length) {
725
+ const c = s[j];
726
+ if (c === "<") depth++;
727
+ else if (c === ">") {
728
+ depth--;
729
+ if (depth === 0) return j + 1;
730
+ } else if (c === "\n") return start;
731
+ else if (c === "(" || c === "{") return start;
732
+ else if (c === "=" || c === ";") return start;
733
+ j++;
734
+ }
735
+ return start;
736
+ }
737
+ function scanComputedOrResourceExpr(body, start) {
738
+ const startLineIndent = countIndent(body, body.lastIndexOf("\n", start - 1) + 1);
739
+ let j = start;
740
+ const lineEnd = body.indexOf("\n", j);
741
+ if (lineEnd < 0) return body.length;
742
+ j = lineEnd + 1;
743
+ while (j < body.length) {
744
+ const nextLineEnd = body.indexOf("\n", j);
745
+ const lineEndIdx = nextLineEnd < 0 ? body.length : nextLineEnd;
746
+ const trimmed = body.slice(j, lineEndIdx).trim();
747
+ if (trimmed === "") return j;
748
+ const indent = countIndent(body, j);
749
+ if (indent > startLineIndent || indent === startLineIndent && /^[?:&|+\-*/<>=,.[\](){}]/.test(trimmed) && !trimmed.startsWith("//")) {
750
+ j = nextLineEnd < 0 ? body.length : nextLineEnd + 1;
751
+ continue;
752
+ }
753
+ return j;
754
+ }
755
+ return j;
756
+ }
757
+ function countIndent(body, lineStart) {
758
+ let n = 0;
759
+ while (lineStart + n < body.length) {
760
+ const c = body[lineStart + n];
761
+ if (c === " " || c === " ") n++;
762
+ else break;
763
+ }
764
+ return n;
765
+ }
766
+ function emitStateBody(buckets) {
767
+ const out = [];
768
+ const imports = buckets.passthrough.filter((p) => p.raw.startsWith("import "));
769
+ const otherPassthrough = buckets.passthrough.filter((p) => !p.raw.startsWith("import "));
770
+ for (const imp of imports) {
771
+ if (imp.leading.trim()) out.push(rstrip(imp.leading));
772
+ out.push(`${INDENT}${imp.raw}`);
773
+ }
774
+ if (imports.length > 0) out.push("");
775
+ if (buckets.props.length > 0) pushBlock(out, emitProps(buckets.props));
776
+ for (const p of buckets.plain) {
777
+ if (p.leading.trim()) out.push(rstrip(p.leading));
778
+ out.push(rebaseLeadingIndent(p.raw, INDENT));
779
+ out.push("");
780
+ }
781
+ trimTrailingBlanks(out);
782
+ if (buckets.computed.length > 0) pushBlock(out, emitComputed(buckets.computed));
783
+ if (buckets.action.length > 0) pushBlock(out, emitAction(buckets.action));
784
+ if (buckets.resource.length > 0) pushBlock(out, emitResource(buckets.resource));
785
+ if (buckets.effectNamed.length > 0) pushBlock(out, emitEffectNamed(buckets.effectNamed));
786
+ if (buckets.lifecycle.length > 0) pushBlock(out, emitLifecycle(buckets.lifecycle));
787
+ if (buckets.effectAnon.length > 0) pushBlock(out, emitEffectAnon(buckets.effectAnon));
788
+ for (const p of otherPassthrough) {
789
+ if (p.leading.trim()) out.push(rstrip(p.leading));
790
+ if (p.raw.includes("\n")) out.push(rebaseLeadingIndent(p.raw, INDENT));
791
+ else out.push(`${INDENT}${p.raw}`);
792
+ out.push("");
793
+ }
794
+ trimTrailingBlanks(out);
795
+ return out.join("\n");
796
+ }
797
+ function pushBlock(out, block) {
798
+ if (out.length > 0 && out[out.length - 1] !== "") out.push("");
799
+ out.push(block);
800
+ out.push("");
801
+ }
802
+ function trimTrailingBlanks(out) {
803
+ while (out.length > 0 && out[out.length - 1] === "") out.pop();
804
+ }
805
+ function emitProps(entries) {
806
+ const lines = [];
807
+ const firstLeading = entries[0]?.leading;
808
+ const headerLeading = firstLeading?.trim() ? firstLeading : "";
809
+ const inner = [];
810
+ for (const [idx, e] of entries.entries()) {
811
+ if (idx > 0 && e.leading.trim()) inner.push(rstrip(indentBlock(e.leading, `${INDENT}${INDENT}`)));
812
+ inner.push(formatPropEntry(e));
813
+ }
814
+ if (headerLeading) lines.push(rstrip(headerLeading));
815
+ lines.push(`${INDENT}$prop: {`);
816
+ lines.push(...inner);
817
+ lines.push(`${INDENT}}`);
818
+ return lines.join("\n");
819
+ }
820
+ function formatPropEntry(e) {
821
+ const keys = [];
822
+ if (shouldKeepType(e.rawType, e.rawDefault) && e.rawType) keys.push({
823
+ k: "type",
824
+ v: e.rawType
825
+ });
826
+ if (e.rawDefault !== void 0) keys.push({
827
+ k: "default",
828
+ v: e.rawDefault
829
+ });
830
+ if (e.describe) keys.push({
831
+ k: "describe",
832
+ v: quoteSingle(e.describe)
833
+ });
834
+ if (e.expose) keys.push({
835
+ k: "expose",
836
+ v: formatExpose(e.expose)
837
+ });
838
+ const inline = `${e.name}: { ${keys.map(({ k, v }) => `${k}: ${v}`).join(", ")} }`;
839
+ const baseIndent = 4;
840
+ const linePrefix = " ".repeat(baseIndent);
841
+ if (keys.length <= 3 && baseIndent + inline.length + 1 <= 100) return `${linePrefix}${inline},`;
842
+ const lines = [`${linePrefix}${e.name}: {`];
843
+ const inner = " ".repeat(baseIndent + 2);
844
+ for (const { k, v } of keys) lines.push(`${inner}${k}: ${v},`);
845
+ lines.push(`${linePrefix}},`);
846
+ return lines.join("\n");
847
+ }
848
+ function shouldKeepType(rawType, rawDefault) {
849
+ if (!rawType) return false;
850
+ if (rawDefault === void 0) return true;
851
+ const trimmed = rawDefault.trim();
852
+ if (trimmed === "null" || trimmed === "undefined") return true;
853
+ if (trimmed === "[]") return true;
854
+ if (trimmed === "{}") return true;
855
+ if (/^['"`]/.test(trimmed) && rawType.includes("|")) return true;
856
+ if (/[<>{}]/.test(rawType)) {
857
+ if (/^[0-9.+-]/.test(trimmed) || trimmed === "true" || trimmed === "false") return true;
858
+ if (/^['"`]/.test(trimmed)) return true;
859
+ return true;
860
+ }
861
+ return false;
862
+ }
863
+ function emitComputed(entries) {
864
+ return emitValueCollection("$computed", entries);
865
+ }
866
+ function emitResource(entries) {
867
+ return emitValueCollection("$resource", entries);
868
+ }
869
+ function emitEffectNamed(entries) {
870
+ return emitValueCollection("$effect", entries);
871
+ }
872
+ function emitValueCollection(macro, entries) {
873
+ const firstLeading = entries[0]?.leading;
874
+ const headerLeading = firstLeading?.trim() ? firstLeading : "";
875
+ const inner = [];
876
+ for (const [idx, e] of entries.entries()) {
877
+ if (idx > 0 && e.leading.trim()) inner.push(rstrip(indentBlock(e.leading, `${INDENT}${INDENT}`)));
878
+ inner.push(formatValueEntry(e));
879
+ }
880
+ const lines = [];
881
+ if (headerLeading) lines.push(rstrip(headerLeading));
882
+ lines.push(`${INDENT}${macro}: {`);
883
+ lines.push(...inner);
884
+ lines.push(`${INDENT}}`);
885
+ return lines.join("\n");
886
+ }
887
+ function formatValueEntry(e) {
888
+ const baseIndent = 4;
889
+ const linePrefix = " ".repeat(baseIndent);
890
+ if (!e.describe && !e.expose) {
891
+ if (!e.expr.includes("\n")) {
892
+ const inline = `${linePrefix}${e.name}: () => ${e.expr},`;
893
+ if (inline.length <= 100) return inline;
894
+ }
895
+ const indented = reindentExpr(e.expr, baseIndent + 2);
896
+ return `${linePrefix}${e.name}: () => ${indented},`;
897
+ }
898
+ const keys = [];
899
+ if (e.describe) keys.push({
900
+ k: "describe",
901
+ v: quoteSingle(e.describe)
902
+ });
903
+ if (e.expose) keys.push({
904
+ k: "expose",
905
+ v: formatExpose(e.expose)
906
+ });
907
+ const valueExpr = e.expr.includes("\n") ? `() => ${reindentExpr(e.expr, baseIndent + 4)}` : `() => ${e.expr}`;
908
+ keys.push({
909
+ k: "value",
910
+ v: valueExpr
911
+ });
912
+ const lines = [`${linePrefix}${e.name}: {`];
913
+ const inner = " ".repeat(baseIndent + 2);
914
+ for (const { k, v } of keys) lines.push(`${inner}${k}: ${v},`);
915
+ lines.push(`${linePrefix}},`);
916
+ return lines.join("\n");
917
+ }
918
+ function emitAction(entries) {
919
+ const firstLeading = entries[0]?.leading;
920
+ const headerLeading = firstLeading?.trim() ? firstLeading : "";
921
+ const inner = [];
922
+ for (const [idx, e] of entries.entries()) {
923
+ if (idx > 0 && e.leading.trim()) inner.push(rstrip(indentBlock(e.leading, `${INDENT}${INDENT}`)));
924
+ inner.push(formatActionEntry(e));
925
+ }
926
+ const lines = [];
927
+ if (headerLeading) lines.push(rstrip(headerLeading));
928
+ lines.push(`${INDENT}$action: {`);
929
+ lines.push(...inner);
930
+ lines.push(`${INDENT}}`);
931
+ return lines.join("\n");
932
+ }
933
+ function formatActionEntry(e) {
934
+ const ret = e.retType ? `: ${e.retType}` : "";
935
+ const arrow = `(${e.args})${ret} => `;
936
+ const baseIndent = 4;
937
+ const linePrefix = " ".repeat(baseIndent);
938
+ if (e.describe || e.expose) {
939
+ const keys = [];
940
+ if (e.describe) keys.push({
941
+ k: "describe",
942
+ v: quoteSingle(e.describe)
943
+ });
944
+ if (e.expose) keys.push({
945
+ k: "expose",
946
+ v: formatExpose(e.expose)
947
+ });
948
+ const bodyIndented = reindentBlockBody(e.body, baseIndent + 4);
949
+ keys.push({
950
+ k: "handler",
951
+ v: `${arrow}{${bodyIndented}}`
952
+ });
953
+ const lines = [`${linePrefix}${e.name}: {`];
954
+ const inner = " ".repeat(baseIndent + 2);
955
+ for (const { k, v } of keys) lines.push(`${inner}${k}: ${v},`);
956
+ lines.push(`${linePrefix}},`);
957
+ return lines.join("\n");
958
+ }
959
+ const bodyIndented = reindentBlockBody(e.body, baseIndent + 2);
960
+ return `${linePrefix}${e.name}: ${arrow}{${bodyIndented}},`;
961
+ }
962
+ function emitLifecycle(hooks) {
963
+ const firstLeading = hooks[0]?.leading;
964
+ const headerLeading = firstLeading?.trim() ? firstLeading : "";
965
+ const inner = [];
966
+ const baseIndent = 4;
967
+ const linePrefix = " ".repeat(baseIndent);
968
+ for (const [idx, h] of hooks.entries()) {
969
+ if (idx > 0 && h.leading.trim()) inner.push(rstrip(indentBlock(h.leading, linePrefix)));
970
+ const bodyIndented = reindentBlockBody(h.body, baseIndent + 2);
971
+ inner.push(`${linePrefix}${h.kind}: () => {${bodyIndented}},`);
972
+ }
973
+ const lines = [];
974
+ if (headerLeading) lines.push(rstrip(headerLeading));
975
+ lines.push(`${INDENT}$lifecycle: {`);
976
+ lines.push(...inner);
977
+ lines.push(`${INDENT}}`);
978
+ return lines.join("\n");
979
+ }
980
+ function emitEffectAnon(effects) {
981
+ const out = [];
982
+ const baseIndent = 2;
983
+ const linePrefix = " ".repeat(baseIndent);
984
+ for (const [idx, e] of effects.entries()) {
985
+ if (idx > 0) out.push("");
986
+ if (e.leading.trim()) out.push(rstrip(e.leading));
987
+ const bodyIndented = reindentBlockBody(e.body, baseIndent + 2);
988
+ out.push(`${linePrefix}$effect: () => {${bodyIndented}}`);
989
+ }
990
+ return out.join("\n");
991
+ }
992
+ function formatExpose(e) {
993
+ const parts = [];
994
+ if (e.read) parts.push("read: true");
995
+ if (e.write) parts.push("write: true");
996
+ return `{ ${parts.join(", ")} }`;
997
+ }
998
+ function quoteSingle(s) {
999
+ return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
1000
+ }
1001
+ function rstrip(s) {
1002
+ return s.replace(/[ \t]+(\n|$)/g, "$1").replace(/^\n+/, "").replace(/\n+$/g, "");
1003
+ }
1004
+ function rebaseLeadingIndent(s, indent) {
1005
+ const lines = s.split("\n");
1006
+ const first = lines[0] ?? "";
1007
+ let n = 0;
1008
+ while (n < first.length && (first[n] === " " || first[n] === " ")) n++;
1009
+ if (n === 0) return [indent + first, ...lines.slice(1)].join("\n");
1010
+ return lines.map((line) => {
1011
+ if (line.length === 0) return line;
1012
+ let m = 0;
1013
+ while (m < line.length && m < n && (line[m] === " " || line[m] === " ")) m++;
1014
+ return indent + line.slice(m);
1015
+ }).join("\n");
1016
+ }
1017
+ function indentBlock(s, indent) {
1018
+ return s.split("\n").map((line) => line.length > 0 ? indent + line : line).join("\n");
1019
+ }
1020
+ function flattenWhitespace(s) {
1021
+ return s.replace(/\s+/g, " ").trim();
1022
+ }
1023
+ function reindentBlockBody(bodyText, targetInner) {
1024
+ const lines = bodyText.split("\n");
1025
+ let firstReal = 0;
1026
+ while (firstReal < lines.length && lines[firstReal].trim() === "") firstReal++;
1027
+ let lastReal = lines.length - 1;
1028
+ while (lastReal >= 0 && lines[lastReal].trim() === "") lastReal--;
1029
+ if (firstReal > lastReal) return "";
1030
+ let minIndent = Infinity;
1031
+ for (let k = firstReal; k <= lastReal; k++) {
1032
+ const line = lines[k];
1033
+ if (line.trim() === "") continue;
1034
+ let n = 0;
1035
+ while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
1036
+ if (n < minIndent) minIndent = n;
1037
+ }
1038
+ if (!Number.isFinite(minIndent)) minIndent = 0;
1039
+ const rebased = [""];
1040
+ const targetSpaces = " ".repeat(targetInner);
1041
+ for (let k = firstReal; k <= lastReal; k++) {
1042
+ const line = lines[k];
1043
+ if (line.trim() === "") {
1044
+ rebased.push("");
1045
+ continue;
1046
+ }
1047
+ rebased.push((targetSpaces + line.slice(minIndent)).replace(/[ \t]+$/, ""));
1048
+ }
1049
+ rebased.push(" ".repeat(Math.max(targetInner - 2, 0)));
1050
+ return rebased.join("\n");
1051
+ }
1052
+ function reindentExpr(expr, targetInner) {
1053
+ const lines = expr.split("\n");
1054
+ if (lines.length === 1) return expr;
1055
+ const firstLine = lines[0];
1056
+ const rest = lines.slice(1);
1057
+ let minIndent = Infinity;
1058
+ for (const line of rest) {
1059
+ if (line.trim() === "") continue;
1060
+ let n = 0;
1061
+ while (n < line.length && (line[n] === " " || line[n] === " ")) n++;
1062
+ if (n < minIndent) minIndent = n;
1063
+ }
1064
+ if (!Number.isFinite(minIndent)) minIndent = 0;
1065
+ const targetSpaces = " ".repeat(targetInner);
1066
+ return [firstLine, ...rest.map((line) => line.trim() === "" ? "" : targetSpaces + line.slice(minIndent))].join("\n");
1067
+ }
1068
+ function expandLeftToLineStart(s, idx) {
1069
+ let j = idx;
1070
+ while (j > 0 && s[j - 1] !== "\n") j--;
1071
+ if (j > 0 && s[j - 1] === "\n") {
1072
+ let k = j - 1;
1073
+ while (k > 0 && (s[k - 1] === " " || s[k - 1] === " ")) k--;
1074
+ if (k > 0 && s[k - 1] === "\n") j = k;
1075
+ }
1076
+ return j;
1077
+ }
1078
+ function expandRightThroughBlankLines(s, idx) {
1079
+ let j = idx;
1080
+ while (j < s.length && (s[j] === " " || s[j] === " ")) j++;
1081
+ if (s[j] === "\n") j++;
1082
+ while (j < s.length) {
1083
+ let k = j;
1084
+ while (k < s.length && (s[k] === " " || s[k] === " ")) k++;
1085
+ if (s[k] === "\n") {
1086
+ j = k + 1;
1087
+ continue;
1088
+ }
1089
+ break;
1090
+ }
1091
+ return j;
1092
+ }
1093
+ function tryParseV2Collection(body, start, sidecar, buckets, leading, nextPosition) {
1094
+ const m = /^\$(prop|computed|action|resource|effect|lifecycle)\s*:\s*\{/.exec(body.slice(start));
1095
+ if (!m) return null;
1096
+ const macro = m[1];
1097
+ const braceIdx = start + m[0].length - 1;
1098
+ const closeIdx = matchBrace(body, braceIdx);
1099
+ if (closeIdx < 0) return null;
1100
+ const entries = splitTopLevelEntries(body.slice(braceIdx + 1, closeIdx));
1101
+ let outerLeadingConsumed = false;
1102
+ for (const e of entries) {
1103
+ const parsed = parseV2Entry(e.text);
1104
+ if (!parsed) continue;
1105
+ let entryLeading;
1106
+ if (!outerLeadingConsumed) {
1107
+ entryLeading = e.leading.trim() ? e.leading : leading;
1108
+ outerLeadingConsumed = true;
1109
+ } else entryLeading = e.leading.trim() ? e.leading : "";
1110
+ routeV2Entry(macro, parsed, sidecar, buckets, entryLeading, nextPosition);
1111
+ }
1112
+ let cursor = closeIdx + 1;
1113
+ if (body[cursor] === ";") cursor++;
1114
+ if (body[cursor] === "\n") cursor++;
1115
+ return cursor;
1116
+ }
1117
+ function tryParseV2AnonEffect(body, start, buckets, leading, position) {
1118
+ const colonIdx = body.indexOf(":", start);
1119
+ if (colonIdx < 0) return null;
1120
+ let cursor = colonIdx + 1;
1121
+ while (cursor < body.length && /[ \t]/.test(body[cursor])) cursor++;
1122
+ if (body.slice(cursor, cursor + 5) === "async") cursor += 5;
1123
+ while (cursor < body.length && /[ \t]/.test(body[cursor])) cursor++;
1124
+ if (body[cursor] !== "(") return null;
1125
+ const exprStart = cursor;
1126
+ const fn = parseArrowOrFunctionExpr(body.slice(exprStart));
1127
+ if (!fn) return null;
1128
+ const closeIdx = scanArrowExprEnd(body, exprStart);
1129
+ if (closeIdx < 0) return null;
1130
+ buckets.effectAnon.push({
1131
+ body: fn.body,
1132
+ leading,
1133
+ position
1134
+ });
1135
+ let c = closeIdx;
1136
+ if (body[c] === ";") c++;
1137
+ if (body[c] === "\n") c++;
1138
+ return c;
1139
+ }
1140
+ function scanArrowExprEnd(s, start) {
1141
+ if (s[start] !== "(") return -1;
1142
+ const parenClose = matchParen(s, start);
1143
+ if (parenClose < 0) return -1;
1144
+ let cur = parenClose + 1;
1145
+ while (cur < s.length && /[ \t]/.test(s[cur])) cur++;
1146
+ if (s[cur] === ":") {
1147
+ cur++;
1148
+ const arrowAt = s.indexOf("=>", cur);
1149
+ if (arrowAt < 0) return -1;
1150
+ cur = arrowAt;
1151
+ }
1152
+ if (s.slice(cur, cur + 2) !== "=>") return -1;
1153
+ cur += 2;
1154
+ while (cur < s.length && /[ \t\n]/.test(s[cur])) cur++;
1155
+ if (s[cur] === "{") {
1156
+ const close = matchBrace(s, cur);
1157
+ if (close < 0) return -1;
1158
+ return close + 1;
1159
+ }
1160
+ const nl = s.indexOf("\n", cur);
1161
+ return nl < 0 ? s.length : nl;
1162
+ }
1163
+ function splitTopLevelEntries(inner) {
1164
+ const out = [];
1165
+ let depthBrace = 0;
1166
+ let depthParen = 0;
1167
+ let depthBracket = 0;
1168
+ let start = 0;
1169
+ let j = 0;
1170
+ while (j < inner.length) {
1171
+ const c = inner[j];
1172
+ if (c === "/" && inner[j + 1] === "/") {
1173
+ const nl = inner.indexOf("\n", j);
1174
+ j = nl < 0 ? inner.length : nl;
1175
+ continue;
1176
+ }
1177
+ if (c === "/" && inner[j + 1] === "*") {
1178
+ const end = inner.indexOf("*/", j + 2);
1179
+ j = end < 0 ? inner.length : end + 2;
1180
+ continue;
1181
+ }
1182
+ if (c === "\"" || c === "'" || c === "`") {
1183
+ j = skipString(inner, j);
1184
+ continue;
1185
+ }
1186
+ if (c === "{") depthBrace++;
1187
+ else if (c === "}") depthBrace--;
1188
+ else if (c === "(") depthParen++;
1189
+ else if (c === ")") depthParen--;
1190
+ else if (c === "[") depthBracket++;
1191
+ else if (c === "]") depthBracket--;
1192
+ else if (c === "," && depthBrace === 0 && depthParen === 0 && depthBracket === 0) {
1193
+ pushSplitEntry(out, inner.slice(start, j));
1194
+ start = j + 1;
1195
+ }
1196
+ j++;
1197
+ }
1198
+ if (start < inner.length) pushSplitEntry(out, inner.slice(start));
1199
+ return out;
1200
+ }
1201
+ function pushSplitEntry(out, piece) {
1202
+ let head = "";
1203
+ let i = 0;
1204
+ while (i < piece.length) {
1205
+ const lineEnd = piece.indexOf("\n", i);
1206
+ const end = lineEnd < 0 ? piece.length : lineEnd;
1207
+ const line = piece.slice(i, end);
1208
+ if (line.trim() === "" || line.trim().startsWith("//") || line.trim().startsWith("/*")) {
1209
+ head += `${line}\n`;
1210
+ i = end + 1;
1211
+ if (lineEnd < 0) break;
1212
+ continue;
1213
+ }
1214
+ break;
1215
+ }
1216
+ const text = piece.slice(i).trim();
1217
+ if (text === "") return;
1218
+ out.push({
1219
+ text,
1220
+ leading: head
1221
+ });
1222
+ }
1223
+ function parseV2Entry(text) {
1224
+ const m = /^([A-Za-z_$][\w$]*)\s*:\s*/.exec(text);
1225
+ if (!m) return null;
1226
+ const name = m[1];
1227
+ const rest = text.slice(m[0].length).trim();
1228
+ if (rest.startsWith("{")) {
1229
+ const close = matchBrace(rest, 0);
1230
+ if (close < 0) return null;
1231
+ return {
1232
+ name,
1233
+ rhs: rest.slice(1, close).trim(),
1234
+ isWrapped: true
1235
+ };
1236
+ }
1237
+ return {
1238
+ name,
1239
+ rhs: rest,
1240
+ isWrapped: false
1241
+ };
1242
+ }
1243
+ function parseWrappedFields(body) {
1244
+ const out = {};
1245
+ const entries = splitTopLevelEntries(body);
1246
+ for (const e of entries) {
1247
+ const m = /^([A-Za-z_$][\w$]*)\s*:\s*/.exec(e.text);
1248
+ if (!m) continue;
1249
+ const k = m[1];
1250
+ const v = e.text.slice(m[0].length).trim();
1251
+ if (k === "describe") out.describe = unquoteString(v);
1252
+ else if (k === "expose") {
1253
+ const parsed = parseExposeLiteral(v);
1254
+ if (parsed) out.expose = parsed;
1255
+ } else if (k === "value") out.value = v;
1256
+ else if (k === "handler") out.handler = v;
1257
+ else if (k === "default") out.default = v;
1258
+ else if (k === "type") out.type = v;
1259
+ else if (k === "on") out.on = v;
1260
+ }
1261
+ return out;
1262
+ }
1263
+ function unquoteString(s) {
1264
+ s = s.trim();
1265
+ if (s.startsWith("'") && s.endsWith("'") || s.startsWith("\"") && s.endsWith("\"")) return s.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
1266
+ return s;
1267
+ }
1268
+ function parseExposeLiteral(s) {
1269
+ s = s.trim();
1270
+ if (!s.startsWith("{")) return void 0;
1271
+ const close = matchBrace(s, 0);
1272
+ if (close < 0) return void 0;
1273
+ const body = s.slice(1, close);
1274
+ return {
1275
+ read: /\bread\s*:\s*true\b/.test(body),
1276
+ write: /\bwrite\s*:\s*true\b/.test(body)
1277
+ };
1278
+ }
1279
+ function routeV2Entry(macro, entry, _sidecar, buckets, leading, nextPosition) {
1280
+ if (macro === "prop") {
1281
+ if (entry.isWrapped) {
1282
+ const f = parseWrappedFields(entry.rhs);
1283
+ buckets.props.push({
1284
+ name: entry.name,
1285
+ rawType: f.type,
1286
+ rawDefault: f.default,
1287
+ describe: f.describe,
1288
+ expose: f.expose,
1289
+ leading
1290
+ });
1291
+ }
1292
+ return;
1293
+ }
1294
+ if (macro === "computed" || macro === "resource") {
1295
+ let expr;
1296
+ let describe;
1297
+ let expose;
1298
+ if (entry.isWrapped) {
1299
+ const f = parseWrappedFields(entry.rhs);
1300
+ expr = stripThunk(f.value ?? "");
1301
+ describe = f.describe;
1302
+ expose = f.expose;
1303
+ } else expr = stripThunk(entry.rhs);
1304
+ (macro === "computed" ? buckets.computed : buckets.resource).push({
1305
+ name: entry.name,
1306
+ expr,
1307
+ describe,
1308
+ expose,
1309
+ leading
1310
+ });
1311
+ return;
1312
+ }
1313
+ if (macro === "action") {
1314
+ let handlerSrc;
1315
+ let describe;
1316
+ let expose;
1317
+ if (entry.isWrapped) {
1318
+ const f = parseWrappedFields(entry.rhs);
1319
+ handlerSrc = f.handler ?? "";
1320
+ describe = f.describe;
1321
+ expose = f.expose;
1322
+ } else handlerSrc = entry.rhs;
1323
+ const fn = parseArrowOrFunctionExpr(handlerSrc);
1324
+ if (!fn) return;
1325
+ buckets.action.push({
1326
+ name: entry.name,
1327
+ args: fn.args,
1328
+ retType: fn.retType,
1329
+ body: fn.body,
1330
+ describe,
1331
+ expose,
1332
+ leading
1333
+ });
1334
+ return;
1335
+ }
1336
+ if (macro === "effect") {
1337
+ let expr;
1338
+ let describe;
1339
+ let expose;
1340
+ if (entry.isWrapped) {
1341
+ const f = parseWrappedFields(entry.rhs);
1342
+ expr = stripThunk(f.value ?? "");
1343
+ describe = f.describe;
1344
+ expose = f.expose;
1345
+ } else expr = stripThunk(entry.rhs);
1346
+ buckets.effectNamed.push({
1347
+ name: entry.name,
1348
+ expr,
1349
+ describe,
1350
+ expose,
1351
+ leading
1352
+ });
1353
+ return;
1354
+ }
1355
+ if (macro === "lifecycle") {
1356
+ if (entry.name !== "mount" && entry.name !== "dispose") return;
1357
+ const fn = parseArrowOrFunctionExpr(entry.isWrapped ? entry.rhs : entry.rhs);
1358
+ if (!fn) return;
1359
+ buckets.lifecycle.push({
1360
+ kind: entry.name,
1361
+ body: fn.body,
1362
+ leading,
1363
+ position: nextPosition()
1364
+ });
1365
+ return;
1366
+ }
1367
+ }
1368
+ function stripThunk(s) {
1369
+ s = s.trim();
1370
+ const m = /^\(\s*\)\s*(?::\s*[^=]+)?=>\s*/.exec(s);
1371
+ if (!m) return s;
1372
+ const rest = s.slice(m[0].length);
1373
+ if (rest.startsWith("{")) {
1374
+ const close = matchBrace(rest, 0);
1375
+ if (close < 0) return rest;
1376
+ return rest.slice(1, close).trim();
1377
+ }
1378
+ return rest.trim();
1379
+ }
1380
+ //#endregion
1381
+ //#region src/core/code-action.ts
1382
+ /**
1383
+ * packages/language-server/src/core/code-action.ts
1384
+ *
1385
+ * QuickFix code-action backing — bridges compiler diagnostic codes C440-C444
1386
+ * (rejected old-spec macro forms) to the macro-simplification codemod
1387
+ * (`migrate()`), which rewrites a full document to v2 collection-form syntax.
1388
+ *
1389
+ * Editor-agnostic: this module computes WHAT the fix is (the rewritten text +
1390
+ * any warnings). The connection layer (src/server.ts) turns that into a protocol
1391
+ * `WorkspaceEdit`/`CodeAction`. This keeps the codemod bridge a clean seam for a
1392
+ * future Volar code-action provider.
1393
+ *
1394
+ * The migrate() codemod is internal monorepo source under @aihu/compiler (not a
1395
+ * public package export); imported via the workspace-relative path the same way
1396
+ * the original embedded server did.
1397
+ */
1398
+ /** Compiler diagnostic codes whose QuickFix is the v2 macro migration codemod. */
1399
+ const MIGRATE_CODES = new Set([
1400
+ "C440",
1401
+ "C441",
1402
+ "C442",
1403
+ "C443",
1404
+ "C444"
1405
+ ]);
1406
+ /**
1407
+ * Run the macro-simplification codemod over `source` and return the fix payload.
1408
+ * Returns `null` when the codemod throws (malformed source) so callers can fall
1409
+ * back to offering no action rather than surfacing a crash.
1410
+ */
1411
+ function buildMigrateFix(source) {
1412
+ let result;
1413
+ try {
1414
+ result = migrate(source);
1415
+ } catch {
1416
+ return null;
1417
+ }
1418
+ const { rewritten, warnings } = result;
1419
+ let title = "Migrate to v2 macro syntax (aihu codemod)";
1420
+ if (warnings.length > 0) title += ` (${warnings.length} warning(s) - see Output panel)`;
1421
+ return {
1422
+ rewritten,
1423
+ warnings,
1424
+ title
1425
+ };
1426
+ }
1427
+ //#endregion
1428
+ //#region src/core/completion.ts
1429
+ /**
1430
+ * packages/language-server/src/core/completion.ts
1431
+ *
1432
+ * LSP completion items for aihu v2 macro-kind collection-form snippets.
1433
+ * Triggered by '$' inside @state block, '@' at top level.
1434
+ *
1435
+ * Uses inline numeric constants for CompletionItemKind and InsertTextFormat
1436
+ * to avoid importing vscode-languageserver at module load time (enables testing
1437
+ * without the vscode-languageserver package installed, and keeps this module
1438
+ * editor-agnostic — the clean seam for a future Volar adoption).
1439
+ */
1440
+ const SNIPPET_KIND = 15;
1441
+ const SNIPPET_FORMAT = 2;
1442
+ /** v2 macro-kind snippet completions (triggered by '$' in @state block). */
1443
+ const STATE_MACRO_COMPLETIONS = [
1444
+ {
1445
+ label: "$prop",
1446
+ kind: SNIPPET_KIND,
1447
+ insertTextFormat: SNIPPET_FORMAT,
1448
+ detail: "v2 collection-form prop declarations",
1449
+ sortText: "0_$prop",
1450
+ insertText: [
1451
+ "$prop: {",
1452
+ " ${1:name}: { default: ${2:undefined}, describe: '${3:description}' },",
1453
+ "}"
1454
+ ].join("\n")
1455
+ },
1456
+ {
1457
+ label: "$computed",
1458
+ kind: SNIPPET_KIND,
1459
+ insertTextFormat: SNIPPET_FORMAT,
1460
+ detail: "v2 collection-form computed declarations",
1461
+ sortText: "0_$computed",
1462
+ insertText: [
1463
+ "$computed: {",
1464
+ " ${1:name}: () => ${2:expression},",
1465
+ "}"
1466
+ ].join("\n")
1467
+ },
1468
+ {
1469
+ label: "$action",
1470
+ kind: SNIPPET_KIND,
1471
+ insertTextFormat: SNIPPET_FORMAT,
1472
+ detail: "v2 collection-form action declarations",
1473
+ sortText: "0_$action",
1474
+ insertText: [
1475
+ "$action: {",
1476
+ " ${1:name}: (${2:args}) => { ${3:body} },",
1477
+ "}"
1478
+ ].join("\n")
1479
+ },
1480
+ {
1481
+ label: "$resource",
1482
+ kind: SNIPPET_KIND,
1483
+ insertTextFormat: SNIPPET_FORMAT,
1484
+ detail: "v2 collection-form resource declarations",
1485
+ sortText: "0_$resource",
1486
+ insertText: [
1487
+ "$resource: {",
1488
+ " ${1:name}: () => ${2:fetchExpr()},",
1489
+ "}"
1490
+ ].join("\n")
1491
+ },
1492
+ {
1493
+ label: "$effect",
1494
+ kind: SNIPPET_KIND,
1495
+ insertTextFormat: SNIPPET_FORMAT,
1496
+ detail: "v2 anonymous effect declaration",
1497
+ sortText: "0_$effect",
1498
+ insertText: [
1499
+ "$effect: () => {",
1500
+ " ${1:body}",
1501
+ "}"
1502
+ ].join("\n")
1503
+ },
1504
+ {
1505
+ label: "$lifecycle",
1506
+ kind: SNIPPET_KIND,
1507
+ insertTextFormat: SNIPPET_FORMAT,
1508
+ detail: "v2 collection-form lifecycle hooks",
1509
+ sortText: "0_$lifecycle",
1510
+ insertText: [
1511
+ "$lifecycle: {",
1512
+ " mount: () => { ${1:initBody} },",
1513
+ " dispose: () => { ${2:cleanupBody} },",
1514
+ "}"
1515
+ ].join("\n")
1516
+ },
1517
+ {
1518
+ label: "$emit",
1519
+ kind: SNIPPET_KIND,
1520
+ insertTextFormat: SNIPPET_FORMAT,
1521
+ detail: "typed custom event declarations",
1522
+ sortText: "0_$emit",
1523
+ insertText: [
1524
+ "$emit: {",
1525
+ " ${1:eventName}: (payload: ${2:PayloadType}) => void",
1526
+ "}"
1527
+ ].join("\n")
1528
+ },
1529
+ {
1530
+ label: "$on",
1531
+ kind: SNIPPET_KIND,
1532
+ insertTextFormat: SNIPPET_FORMAT,
1533
+ detail: "event listener attribute (template)",
1534
+ sortText: "0_$on",
1535
+ insertText: "$on.${1:click}={${2:handler}}"
1536
+ },
1537
+ {
1538
+ label: "$bind",
1539
+ kind: SNIPPET_KIND,
1540
+ insertTextFormat: SNIPPET_FORMAT,
1541
+ detail: "two-way attribute binding (template)",
1542
+ sortText: "0_$bind",
1543
+ insertText: "$bind.${1:value}={${2:signal}}"
1544
+ }
1545
+ ];
1546
+ /** Top-level block completions (triggered by '@' at top level). */
1547
+ const BLOCK_COMPLETIONS = [
1548
+ {
1549
+ label: "@state",
1550
+ kind: SNIPPET_KIND,
1551
+ insertTextFormat: SNIPPET_FORMAT,
1552
+ detail: "aihu @state block",
1553
+ sortText: "0_@state",
1554
+ insertText: "@state"
1555
+ },
1556
+ {
1557
+ label: "@template",
1558
+ kind: SNIPPET_KIND,
1559
+ insertTextFormat: SNIPPET_FORMAT,
1560
+ detail: "aihu @template block",
1561
+ sortText: "0_@template",
1562
+ insertText: "@template"
1563
+ },
1564
+ {
1565
+ label: "@style",
1566
+ kind: SNIPPET_KIND,
1567
+ insertTextFormat: SNIPPET_FORMAT,
1568
+ detail: "aihu @style block",
1569
+ sortText: "0_@style",
1570
+ insertText: "@style"
1571
+ },
1572
+ {
1573
+ label: "@agent",
1574
+ kind: SNIPPET_KIND,
1575
+ insertTextFormat: SNIPPET_FORMAT,
1576
+ detail: "aihu @agent block for MCP tool exposure",
1577
+ sortText: "0_@agent",
1578
+ insertText: "@agent"
1579
+ },
1580
+ {
1581
+ label: "@route",
1582
+ kind: SNIPPET_KIND,
1583
+ insertTextFormat: SNIPPET_FORMAT,
1584
+ detail: "aihu @route block for file-based routing",
1585
+ sortText: "0_@route",
1586
+ insertText: "@route"
1587
+ }
1588
+ ];
1589
+ //#endregion
1590
+ //#region src/core/diagnostics.ts
1591
+ /**
1592
+ * packages/language-server/src/core/diagnostics.ts
1593
+ *
1594
+ * Async wrapper around the aihu-compile Rust binary with --machine-errors support.
1595
+ * Invoked by the LSP server on every textDocument/didOpen and textDocument/didChange.
1596
+ *
1597
+ * NOTE: Uses node:child_process execFile (not exec) with argv arrays — safe from
1598
+ * shell injection. The LSP server process runs out-of-process for editor isolation.
1599
+ *
1600
+ * This module is editor-agnostic: it returns plain `AihuDiagnostic` records with
1601
+ * 0-based LSP positions. The transport/connection layer (src/server.ts) maps these
1602
+ * onto protocol `Diagnostic` objects. Keeping the parse logic here is the clean
1603
+ * seam for a future `@volar/language-core` virtual-code adoption (arch-4 §2.7).
1604
+ */
1605
+ const execFileAsync = promisify(execFile);
1606
+ const ext = process.platform === "win32" ? ".exe" : "";
1607
+ const binPath = process.env.AIHU_COMPILE_BIN ?? resolve(dirname(fileURLToPath(import.meta.url)), `../../../compiler/bin/aihu-compile${ext}`);
1608
+ function toAihuDiagnostic(raw) {
1609
+ let lspRange = null;
1610
+ if (raw.range && raw.range.line > 0) {
1611
+ const startLine = raw.range.line - 1;
1612
+ const endLine = raw.range.end_line - 1;
1613
+ lspRange = {
1614
+ start: {
1615
+ line: startLine,
1616
+ character: raw.range.col
1617
+ },
1618
+ end: {
1619
+ line: endLine,
1620
+ character: raw.range.end_col
1621
+ }
1622
+ };
1623
+ }
1624
+ return {
1625
+ code: raw.code,
1626
+ message: raw.message,
1627
+ hint: raw.hint,
1628
+ fix: raw.fix,
1629
+ fromText: raw.from ?? null,
1630
+ toText: raw.to ?? null,
1631
+ range: lspRange
1632
+ };
1633
+ }
1634
+ /**
1635
+ * Fallback: parse a plain-text compiler error into a synthetic diagnostic.
1636
+ * Used when the binary does not emit JSON (very old binary or stdin parse error).
1637
+ * TODO: remove when --machine-errors covers all error paths.
1638
+ */
1639
+ function parsePlainError(stderr, _filePath) {
1640
+ const m = /\b(\d+):\s*(.+)/.exec(stderr);
1641
+ const line = m ? Math.max(0, Number.parseInt(m[1], 10) - 1) : 0;
1642
+ return {
1643
+ code: "C000",
1644
+ message: m ? m[2] : stderr.trim() || "Unknown compiler error",
1645
+ fromText: null,
1646
+ toText: null,
1647
+ range: {
1648
+ start: {
1649
+ line,
1650
+ character: 0
1651
+ },
1652
+ end: {
1653
+ line,
1654
+ character: 0
1655
+ }
1656
+ }
1657
+ };
1658
+ }
1659
+ /**
1660
+ * Parse the Rust binary's `--machine-errors` stderr stream into structured
1661
+ * diagnostics. Each JSON line is one error; non-JSON lines are skipped. When no
1662
+ * JSON is present at all, falls back to a single synthetic plain-text diagnostic.
1663
+ *
1664
+ * Pure function — no I/O. Exposed so editors/tests can exercise the mapping
1665
+ * without spawning the compiler.
1666
+ */
1667
+ function parseMachineErrors(stderr, filePath) {
1668
+ const diagnostics = [];
1669
+ let jsonParsed = false;
1670
+ for (const line of stderr.split("\n")) {
1671
+ const trimmed = line.trim();
1672
+ if (!trimmed?.startsWith("{")) continue;
1673
+ try {
1674
+ const raw = JSON.parse(trimmed);
1675
+ diagnostics.push(toAihuDiagnostic(raw));
1676
+ jsonParsed = true;
1677
+ } catch {}
1678
+ }
1679
+ if (!jsonParsed) diagnostics.push(parsePlainError(stderr, filePath));
1680
+ return diagnostics;
1681
+ }
1682
+ /**
1683
+ * Compile a .aihu source string via stdin, returning structured diagnostics.
1684
+ */
1685
+ async function compileWithDiagnostics(source, filePath) {
1686
+ const stem = filePath.replace(/\\/g, "/").split("/").pop()?.replace(/\.aihu$/, "") ?? "component";
1687
+ try {
1688
+ const { stdout } = await execFileAsync(binPath, [
1689
+ "--stdin",
1690
+ "--tag",
1691
+ stem,
1692
+ "--path",
1693
+ filePath,
1694
+ "--machine-errors"
1695
+ ], {
1696
+ input: source,
1697
+ encoding: "utf8",
1698
+ maxBuffer: 8 * 1024 * 1024
1699
+ });
1700
+ return {
1701
+ code: stdout,
1702
+ diagnostics: []
1703
+ };
1704
+ } catch (err) {
1705
+ return {
1706
+ code: null,
1707
+ diagnostics: parseMachineErrors(err.stderr ?? "", filePath)
1708
+ };
1709
+ }
1710
+ }
1711
+ //#endregion
1712
+ //#region src/core/hover.ts
1713
+ /**
1714
+ * packages/language-server/src/core/hover.ts
1715
+ *
1716
+ * Static hover lookup table for aihu macro keywords.
1717
+ * Used by the LSP server textDocument/hover handler.
1718
+ *
1719
+ * Editor-agnostic: returns Markdown strings + does pure position math. The
1720
+ * connection layer wraps the result in a protocol `Hover`. Clean seam for a
1721
+ * future Volar virtual-code hover provider.
1722
+ */
1723
+ const HOVER_TABLE = {
1724
+ $prop: [
1725
+ "**aihu macro: `$prop`**",
1726
+ "",
1727
+ "Declares reactive prop signals from parent attributes.",
1728
+ "",
1729
+ "Compiles to:",
1730
+ "```typescript",
1731
+ "const name = computed(() => ctx.attrs.name)",
1732
+ "```",
1733
+ "",
1734
+ "[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1735
+ ].join("\n"),
1736
+ $computed: [
1737
+ "**aihu macro: `$computed`**",
1738
+ "",
1739
+ "Declares a derived signal.",
1740
+ "",
1741
+ "Compiles to:",
1742
+ "```typescript",
1743
+ "const name = computed(() => expr)",
1744
+ "```",
1745
+ "",
1746
+ "[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1747
+ ].join("\n"),
1748
+ $action: [
1749
+ "**aihu macro: `$action`**",
1750
+ "",
1751
+ "Declares a callable action in component scope.",
1752
+ "",
1753
+ "Compiles to:",
1754
+ "```typescript",
1755
+ "function name(args) { return batch(() => { body }) }",
1756
+ "```",
1757
+ "",
1758
+ "[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1759
+ ].join("\n"),
1760
+ $resource: [
1761
+ "**aihu macro: `$resource`**",
1762
+ "",
1763
+ "Declares an async data resource signal.",
1764
+ "",
1765
+ "Compiles to:",
1766
+ "```typescript",
1767
+ "const name = createResource(() => expr)",
1768
+ "```",
1769
+ "",
1770
+ "[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1771
+ ].join("\n"),
1772
+ $effect: [
1773
+ "**aihu macro: `$effect`**",
1774
+ "",
1775
+ "Declares a reactive side effect.",
1776
+ "",
1777
+ "Compiles to:",
1778
+ "```typescript",
1779
+ "effect(() => { body })",
1780
+ "```",
1781
+ "",
1782
+ "[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1783
+ ].join("\n"),
1784
+ $lifecycle: [
1785
+ "**aihu macro: `$lifecycle`**",
1786
+ "",
1787
+ "Declares lifecycle hooks (mount, dispose, adopt, attributeChange).",
1788
+ "",
1789
+ "Compiles to:",
1790
+ "```typescript",
1791
+ "onMount(() => { ... })",
1792
+ "onCleanup(() => { ... })",
1793
+ "```",
1794
+ "",
1795
+ "[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1796
+ ].join("\n"),
1797
+ $if: [
1798
+ "**aihu template directive: `$if` / `{#if}`**",
1799
+ "",
1800
+ "Conditional rendering.",
1801
+ "",
1802
+ "Compiles to:",
1803
+ "```typescript",
1804
+ "branch(condition, () => trueBranch)",
1805
+ "```",
1806
+ "",
1807
+ "[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1808
+ ].join("\n"),
1809
+ $each: [
1810
+ "**aihu template directive: `$each` / `{#each}`**",
1811
+ "",
1812
+ "List rendering.",
1813
+ "",
1814
+ "Compiles to:",
1815
+ "```typescript",
1816
+ "branch.each(items, (item) => leaf(...))",
1817
+ "```",
1818
+ "",
1819
+ "[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1820
+ ].join("\n"),
1821
+ $html: [
1822
+ "**aihu template directive: `$html` / `{@html}`**",
1823
+ "",
1824
+ "Raw HTML injection. Use with trusted content only.",
1825
+ "",
1826
+ "Compiles to:",
1827
+ "```typescript",
1828
+ "leaf({ nodeValue: htmlContent })",
1829
+ "```",
1830
+ "",
1831
+ "[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1832
+ ].join("\n"),
1833
+ $show: [
1834
+ "**aihu template directive: `$show`**",
1835
+ "",
1836
+ "Toggles element visibility without removing it from the DOM.",
1837
+ "",
1838
+ "Compiles to:",
1839
+ "```typescript",
1840
+ "el.style.display = condition ? \"\" : \"none\"",
1841
+ "```",
1842
+ "",
1843
+ "[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1844
+ ].join("\n"),
1845
+ $on: [
1846
+ "**aihu template directive: `$on`**",
1847
+ "",
1848
+ "Attaches an event listener to a DOM element.",
1849
+ "",
1850
+ "Compiles to:",
1851
+ "```typescript",
1852
+ "element.addEventListener(\"event\", handler)",
1853
+ "```",
1854
+ "",
1855
+ "Usage: `$on.click={handler}`",
1856
+ "",
1857
+ "[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1858
+ ].join("\n"),
1859
+ $bind: [
1860
+ "**aihu template directive: `$bind`**",
1861
+ "",
1862
+ "Two-way binding between a signal and an element attribute.",
1863
+ "",
1864
+ "Compiles to:",
1865
+ "```typescript",
1866
+ "// two-way binding via signal setter",
1867
+ "el.addEventListener(\"input\", e => setSignal(e.target.value))",
1868
+ "```",
1869
+ "",
1870
+ "Usage: `$bind.value={signal}`",
1871
+ "",
1872
+ "[Template spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1873
+ ].join("\n"),
1874
+ $emit: [
1875
+ "**aihu macro: `$emit`**",
1876
+ "",
1877
+ "Declares and dispatches typed custom events.",
1878
+ "",
1879
+ "Compiles to:",
1880
+ "```typescript",
1881
+ "this.dispatchEvent(new CustomEvent(name, { detail: payload }))",
1882
+ "```",
1883
+ "",
1884
+ "Usage: `$emit.name(payload)`",
1885
+ "",
1886
+ "[v2 spec](docs/superpowers/specs/2026-05-05-spec-macro-vocabulary-v2.md)"
1887
+ ].join("\n")
1888
+ };
1889
+ function getBlockContext(lines, lineIndex) {
1890
+ for (let i = lineIndex; i >= 0; i--) {
1891
+ const trimmed = lines[i].trimStart();
1892
+ if (/^@state\s*\{/.test(trimmed)) return "state";
1893
+ if (/^@template\s*\{/.test(trimmed)) return "template";
1894
+ if (/^@(style|agent|route)\s*\{/.test(trimmed)) return "unknown";
1895
+ }
1896
+ return "unknown";
1897
+ }
1898
+ function getMacroAtPosition(lineText, character) {
1899
+ let m;
1900
+ const namespacedRe = /\$(on|bind)(?:[.:][A-Za-z_$][\w$]*)?/g;
1901
+ while ((m = namespacedRe.exec(lineText)) !== null) if (character >= m.index && character <= m.index + m[0].length) return `$${m[1]}`;
1902
+ const bareRe = /\$(prop|computed|action|resource|effect|lifecycle|emit|if|each|html|show)\b/g;
1903
+ while ((m = bareRe.exec(lineText)) !== null) if (character >= m.index && character <= m.index + m[0].length) return `$${m[1]}`;
1904
+ const blockRe = /\{(?:#(if|each)|@(html))\b/g;
1905
+ while ((m = blockRe.exec(lineText)) !== null) if (character >= m.index && character <= m.index + m[0].length) return `$${m[1] ?? m[2]}`;
1906
+ return null;
1907
+ }
1908
+ function getHoverContent(macro) {
1909
+ return HOVER_TABLE[macro] ?? null;
1910
+ }
1911
+ //#endregion
1912
+ //#region src/server.ts
1913
+ /**
1914
+ * packages/language-server/src/server.ts
1915
+ *
1916
+ * The aihu cross-editor Language Server — transport/connection layer.
1917
+ *
1918
+ * Started as an out-of-process Node.js child by any LSP client (VS Code via
1919
+ * vscode-languageclient, Neovim, Helix, …) over stdio. The runnable binary
1920
+ * (`aihu-language-server`) is `src/bin.ts`, which calls `startServer()`.
1921
+ *
1922
+ * Features implemented:
1923
+ * - Diagnostics: shell out to aihu-compile --machine-errors (debounced 300ms)
1924
+ * - Code actions: QuickFix for C440-C444 via the migrate() codemod
1925
+ * - Hover: static lookup table for 13 macro keywords
1926
+ * - Completion: 9 macro-kind snippets ($ trigger) + 5 block names (@ trigger)
1927
+ *
1928
+ * All feature logic lives in ./core (editor-agnostic). This file only wires the
1929
+ * core functions onto the LSP connection — the clean seam for a future Volar
1930
+ * adoption (arch-4 §2.6/§2.7).
1931
+ */
1932
+ /**
1933
+ * Build and wire the LSP server onto a connection, returning it without calling
1934
+ * `.listen()`. Exposed so tests / alternative transports can drive the server.
1935
+ * The default `createConnection(ProposedFeatures.all)` reads stdio.
1936
+ */
1937
+ function createServer(connection = createConnection(ProposedFeatures.all)) {
1938
+ const documents = new TextDocuments(TextDocument);
1939
+ connection.onInitialize((_params) => {
1940
+ connection.console.log("Aihu LSP server started");
1941
+ return { capabilities: {
1942
+ textDocumentSync: TextDocumentSyncKind.Incremental,
1943
+ hoverProvider: true,
1944
+ completionProvider: { triggerCharacters: ["$", "@"] },
1945
+ codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix] }
1946
+ } };
1947
+ });
1948
+ const pendingValidations = /* @__PURE__ */ new Map();
1949
+ function scheduleValidation(doc) {
1950
+ const uri = doc.uri;
1951
+ const existing = pendingValidations.get(uri);
1952
+ if (existing !== void 0) clearTimeout(existing);
1953
+ const handle = setTimeout(() => {
1954
+ pendingValidations.delete(uri);
1955
+ validateDocument(doc);
1956
+ }, 300);
1957
+ pendingValidations.set(uri, handle);
1958
+ }
1959
+ async function validateDocument(doc) {
1960
+ const filePath = doc.uri.replace(/^file:\/\/\/([A-Za-z]:)/, "$1").replace(/^file:\/\//, "");
1961
+ let result;
1962
+ try {
1963
+ result = await compileWithDiagnostics(doc.getText(), filePath);
1964
+ } catch (err) {
1965
+ connection.console.error(`Aihu LSP: validation crashed for ${doc.uri}: ${String(err)}`);
1966
+ return;
1967
+ }
1968
+ const diagnostics = result.diagnostics.map(toDiagnostic);
1969
+ connection.sendDiagnostics({
1970
+ uri: doc.uri,
1971
+ diagnostics
1972
+ });
1973
+ }
1974
+ function toDiagnostic(d) {
1975
+ return {
1976
+ range: d.range ? {
1977
+ start: d.range.start,
1978
+ end: d.range.end
1979
+ } : {
1980
+ start: {
1981
+ line: 0,
1982
+ character: 0
1983
+ },
1984
+ end: {
1985
+ line: 0,
1986
+ character: 0
1987
+ }
1988
+ },
1989
+ message: d.message,
1990
+ severity: DiagnosticSeverity.Error,
1991
+ code: d.code,
1992
+ source: "aihu-compile"
1993
+ };
1994
+ }
1995
+ documents.onDidOpen((e) => scheduleValidation(e.document));
1996
+ documents.onDidChangeContent((e) => scheduleValidation(e.document));
1997
+ documents.onDidClose((e) => {
1998
+ const uri = e.document.uri;
1999
+ const pending = pendingValidations.get(uri);
2000
+ if (pending !== void 0) {
2001
+ clearTimeout(pending);
2002
+ pendingValidations.delete(uri);
2003
+ }
2004
+ connection.sendDiagnostics({
2005
+ uri,
2006
+ diagnostics: []
2007
+ });
2008
+ });
2009
+ connection.onCodeAction((params) => {
2010
+ const doc = documents.get(params.textDocument.uri);
2011
+ if (!doc) return [];
2012
+ const migrateDiags = params.context.diagnostics.filter((d) => typeof d.code === "string" && MIGRATE_CODES.has(d.code));
2013
+ if (migrateDiags.length === 0) return [];
2014
+ const fix = buildMigrateFix(doc.getText());
2015
+ if (!fix) {
2016
+ connection.console.error("Aihu LSP: migrate() threw — no code action offered");
2017
+ return [];
2018
+ }
2019
+ for (const w of fix.warnings) connection.console.warn(`Aihu codemod warning: ${w}`);
2020
+ const fullRange = {
2021
+ start: {
2022
+ line: 0,
2023
+ character: 0
2024
+ },
2025
+ end: {
2026
+ line: doc.lineCount,
2027
+ character: 0
2028
+ }
2029
+ };
2030
+ const edit = { changes: { [params.textDocument.uri]: [TextEdit.replace(fullRange, fix.rewritten)] } };
2031
+ return [{
2032
+ title: fix.title,
2033
+ kind: CodeActionKind.QuickFix,
2034
+ diagnostics: migrateDiags,
2035
+ edit,
2036
+ isPreferred: true
2037
+ }];
2038
+ });
2039
+ connection.onHover((params) => {
2040
+ const doc = documents.get(params.textDocument.uri);
2041
+ if (!doc) return null;
2042
+ const pos = params.position;
2043
+ const macro = getMacroAtPosition(doc.getText({
2044
+ start: {
2045
+ line: pos.line,
2046
+ character: 0
2047
+ },
2048
+ end: {
2049
+ line: pos.line,
2050
+ character: Number.MAX_SAFE_INTEGER
2051
+ }
2052
+ }), pos.character);
2053
+ if (!macro) return null;
2054
+ getBlockContext(doc.getText().split("\n"), pos.line);
2055
+ const content = getHoverContent(macro);
2056
+ if (!content) return null;
2057
+ return { contents: {
2058
+ kind: MarkupKind.Markdown,
2059
+ value: content
2060
+ } };
2061
+ });
2062
+ connection.onCompletion((params) => {
2063
+ const doc = documents.get(params.textDocument.uri);
2064
+ if (!doc) return null;
2065
+ const pos = params.position;
2066
+ const lineText = doc.getText({
2067
+ start: {
2068
+ line: pos.line,
2069
+ character: 0
2070
+ },
2071
+ end: {
2072
+ line: pos.line,
2073
+ character: pos.character
2074
+ }
2075
+ });
2076
+ const triggerChar = params.context?.triggerCharacter;
2077
+ if (triggerChar === "$" || lineText.trimEnd().endsWith("$")) {
2078
+ if (getBlockContext(doc.getText().split("\n"), pos.line) === "state") return CompletionList.create(STATE_MACRO_COMPLETIONS, false);
2079
+ const templateItems = STATE_MACRO_COMPLETIONS.filter((c) => [
2080
+ "$on",
2081
+ "$bind",
2082
+ "$show"
2083
+ ].includes(c.label));
2084
+ return CompletionList.create(templateItems, false);
2085
+ }
2086
+ if (triggerChar === "@" || /^\s*@$/.test(lineText)) return CompletionList.create(BLOCK_COMPLETIONS, false);
2087
+ return null;
2088
+ });
2089
+ documents.listen(connection);
2090
+ return connection;
2091
+ }
2092
+ /**
2093
+ * Start the language server on stdio and begin listening. This is the entry
2094
+ * point the `aihu-language-server` bin calls.
2095
+ *
2096
+ * Bind the connection to `process.stdin`/`process.stdout` explicitly so the
2097
+ * binary works when launched directly (e.g. `{ command: "aihu-language-server" }`
2098
+ * in Neovim/Helix). vscode-languageserver's bare `createConnection` otherwise
2099
+ * requires a `--stdio` / `--node-ipc` CLI flag and throws without one.
2100
+ */
2101
+ function startServer() {
2102
+ const connection = createServer(createConnection(ProposedFeatures.all, process.stdin, process.stdout));
2103
+ connection.listen();
2104
+ return connection;
2105
+ }
2106
+ //#endregion
2107
+ //#region src/bin.ts
2108
+ /**
2109
+ * packages/language-server/src/bin.ts
2110
+ *
2111
+ * Runnable entry for the `aihu-language-server` binary. Editors launch this over
2112
+ * stdio (e.g. VS Code via vscode-languageclient, or any LSP-aware editor with a
2113
+ * `{ command: "aihu-language-server" }` server spec).
2114
+ *
2115
+ * The rolldown build prepends a `#!/usr/bin/env node` shebang to dist/bin.js.
2116
+ */
2117
+ startServer();
2118
+ //#endregion
2119
+ export {};