@arviahq/language-server 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1746 @@
1
+ #!/usr/bin/env node
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let node_url = require("node:url");
25
+ let vscode_languageserver_node_js = require("vscode-languageserver/node.js");
26
+ let vscode_languageserver_textdocument = require("vscode-languageserver-textdocument");
27
+ let vscode_languageserver = require("vscode-languageserver");
28
+ let vscode_css_languageservice = require("vscode-css-languageservice");
29
+ let _arviahq_compiler = require("@arviahq/compiler");
30
+ let node_fs = require("node:fs");
31
+ node_fs = __toESM(node_fs);
32
+ let node_path = require("node:path");
33
+ node_path = __toESM(node_path);
34
+ //#region src/ast-query.ts
35
+ const inSpan = (offset, span) => offset >= span.start && offset < span.end;
36
+ /** Name spans are hit-tested inclusively so a cursor at the end still matches. */
37
+ const onSpan = (offset, span) => offset >= span.start && offset <= span.end;
38
+ function nodeAtOffset(ast, offset) {
39
+ for (const item of ast.items) {
40
+ if (!inSpan(offset, item.span) && !onSpan(offset, item.span)) continue;
41
+ switch (item.kind) {
42
+ case "theme":
43
+ for (const group of item.groups) {
44
+ const hit = tokenGroupTarget(group, offset, "theme", null);
45
+ if (hit) return hit;
46
+ }
47
+ break;
48
+ case "recipe": {
49
+ if (onSpan(offset, item.nameSpan)) return {
50
+ kind: "recipe-name",
51
+ recipe: item
52
+ };
53
+ const hit = styleItemsTarget(item.items, offset, null);
54
+ if (hit) return hit;
55
+ break;
56
+ }
57
+ case "keyframes":
58
+ if (onSpan(offset, item.nameSpan)) return {
59
+ kind: "keyframes-name",
60
+ keyframes: item
61
+ };
62
+ for (const step of item.steps) for (const decl of step.decls) {
63
+ const hit = declTarget(decl, offset, null);
64
+ if (hit) return hit;
65
+ }
66
+ break;
67
+ case "styledecl": {
68
+ if (onSpan(offset, item.nameSpan)) return {
69
+ kind: "style-name",
70
+ style: item
71
+ };
72
+ const hit = styleItemsTarget(item.items, offset, null);
73
+ if (hit) return hit;
74
+ break;
75
+ }
76
+ case "component": {
77
+ const hit = componentTarget(item, offset);
78
+ if (hit) return hit;
79
+ break;
80
+ }
81
+ case "global":
82
+ for (const rule of item.rules) for (const decl of rule.decls) {
83
+ const hit = declTarget(decl, offset, null);
84
+ if (hit) return hit;
85
+ }
86
+ break;
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+ function componentTarget(component, offset) {
92
+ if (onSpan(offset, component.nameSpan)) return {
93
+ kind: "component-name",
94
+ component
95
+ };
96
+ for (const item of component.items) switch (item.kind) {
97
+ case "decl": {
98
+ const hit = declTarget(item, offset, component);
99
+ if (hit) return hit;
100
+ break;
101
+ }
102
+ case "use":
103
+ if (onSpan(offset, item.recipeSpan)) return {
104
+ kind: "use-recipe",
105
+ name: item.recipe,
106
+ span: item.recipeSpan
107
+ };
108
+ break;
109
+ case "base": {
110
+ const hit = styleBodyTarget(item.body, offset, component);
111
+ if (hit) return hit;
112
+ break;
113
+ }
114
+ case "slots":
115
+ for (const slot of item.slots) {
116
+ if (onSpan(offset, slot.nameSpan)) return {
117
+ kind: "slot-name",
118
+ component,
119
+ name: slot.name,
120
+ span: slot.nameSpan
121
+ };
122
+ const hit = styleItemsTarget(slot.items, offset, component);
123
+ if (hit) return hit;
124
+ }
125
+ break;
126
+ case "variants":
127
+ for (const variant of item.variants) {
128
+ if (onSpan(offset, variant.nameSpan)) return {
129
+ kind: "variant-name",
130
+ component,
131
+ variant
132
+ };
133
+ for (const value of variant.values) {
134
+ if (onSpan(offset, value.nameSpan)) return {
135
+ kind: "variant-value-name",
136
+ component,
137
+ variant,
138
+ value
139
+ };
140
+ const hit = styleBodyTarget(value.body, offset, component);
141
+ if (hit) return hit;
142
+ }
143
+ }
144
+ break;
145
+ case "defaults": {
146
+ const hit = settingsTarget(item.entries, offset, component, "defaults");
147
+ if (hit) return hit;
148
+ break;
149
+ }
150
+ case "responsive":
151
+ for (const entry of item.entries) {
152
+ if (onSpan(offset, entry.breakpointSpan)) return {
153
+ kind: "conditional-key",
154
+ component,
155
+ key: entry.breakpoint,
156
+ span: entry.breakpointSpan,
157
+ context: "responsive"
158
+ };
159
+ const hit = settingsTarget(entry.variants, offset, component, "responsive");
160
+ if (hit) return hit;
161
+ }
162
+ break;
163
+ case "container":
164
+ for (const entry of item.entries) {
165
+ if (onSpan(offset, entry.containerSpan)) return {
166
+ kind: "conditional-key",
167
+ component,
168
+ key: entry.container,
169
+ span: entry.containerSpan,
170
+ context: "container"
171
+ };
172
+ const hit = settingsTarget(entry.variants, offset, component, "container");
173
+ if (hit) return hit;
174
+ }
175
+ break;
176
+ case "compound": {
177
+ const hit = settingsTarget(item.matchers, offset, component, "compound");
178
+ if (hit) return hit;
179
+ for (const slot of item.slots) {
180
+ if (onSpan(offset, slot.nameSpan)) return {
181
+ kind: "slot-name",
182
+ component,
183
+ name: slot.name,
184
+ span: slot.nameSpan
185
+ };
186
+ const inner = styleItemsTarget(slot.items, offset, component);
187
+ if (inner) return inner;
188
+ }
189
+ break;
190
+ }
191
+ case "tokens":
192
+ for (const group of item.groups) {
193
+ const hit = tokenGroupTarget(group, offset, "component", component);
194
+ if (hit) return hit;
195
+ }
196
+ break;
197
+ }
198
+ return null;
199
+ }
200
+ function tokenGroupTarget(group, offset, owner, component) {
201
+ const entries = [...group.entries, ...group.overrides.flatMap((o) => o.entries)];
202
+ for (const entry of entries) {
203
+ const valueHit = valueTarget(entry.value, offset, component);
204
+ if (valueHit) return valueHit;
205
+ if (onSpan(offset, entry.nameSpan)) return {
206
+ kind: "token-entry",
207
+ group,
208
+ name: entry.name,
209
+ span: entry.nameSpan,
210
+ value: entry.value.text,
211
+ doc: entry.doc,
212
+ owner,
213
+ component
214
+ };
215
+ }
216
+ return null;
217
+ }
218
+ function styleBodyTarget(body, offset, component) {
219
+ for (const item of body.items) if (item.kind === "slotblock") {
220
+ if (component && onSpan(offset, item.nameSpan)) return {
221
+ kind: "slot-name",
222
+ component,
223
+ name: item.name,
224
+ span: item.nameSpan
225
+ };
226
+ const hit = styleItemsTarget(item.items, offset, component);
227
+ if (hit) return hit;
228
+ } else {
229
+ const hit = styleItemsTarget([item], offset, component);
230
+ if (hit) return hit;
231
+ }
232
+ return null;
233
+ }
234
+ function styleItemsTarget(items, offset, component) {
235
+ for (const item of items) if (item.kind === "decl") {
236
+ const hit = declTarget(item, offset, component);
237
+ if (hit) return hit;
238
+ } else if (item.kind === "use") {
239
+ if (onSpan(offset, item.recipeSpan)) return {
240
+ kind: "use-recipe",
241
+ name: item.recipe,
242
+ span: item.recipeSpan
243
+ };
244
+ } else {
245
+ for (const decl of item.items) {
246
+ const hit = declTarget(decl, offset, component);
247
+ if (hit) return hit;
248
+ }
249
+ for (const slot of item.slots) {
250
+ if (component && onSpan(offset, slot.nameSpan)) return {
251
+ kind: "slot-name",
252
+ component,
253
+ name: slot.name,
254
+ span: slot.nameSpan
255
+ };
256
+ for (const decl of slot.items) {
257
+ if (decl.kind !== "decl") continue;
258
+ const hit = declTarget(decl, offset, component);
259
+ if (hit) return hit;
260
+ }
261
+ }
262
+ }
263
+ return null;
264
+ }
265
+ function declTarget(decl, offset, component) {
266
+ const propSpan = {
267
+ start: decl.span.start,
268
+ end: decl.span.start + decl.property.length,
269
+ line: decl.span.line,
270
+ col: decl.span.col
271
+ };
272
+ if (onSpan(offset, propSpan)) return {
273
+ kind: "css-property",
274
+ name: decl.property,
275
+ span: propSpan
276
+ };
277
+ return valueTarget(decl.value, offset, component);
278
+ }
279
+ function valueTarget(value, offset, component) {
280
+ if (!onSpan(offset, value.span)) return null;
281
+ for (const word of value.words) if (word.kind === "ref" && onSpan(offset, word.span)) return {
282
+ kind: "token-ref",
283
+ word,
284
+ component
285
+ };
286
+ return null;
287
+ }
288
+ function settingsTarget(entries, offset, component, context) {
289
+ for (const entry of entries) {
290
+ if (onSpan(offset, entry.variantSpan)) return {
291
+ kind: "variant-setting",
292
+ component,
293
+ entry,
294
+ part: "variant",
295
+ context
296
+ };
297
+ if (onSpan(offset, entry.valueSpan)) return {
298
+ kind: "variant-setting",
299
+ component,
300
+ entry,
301
+ part: "value",
302
+ context
303
+ };
304
+ }
305
+ return null;
306
+ }
307
+ //#endregion
308
+ //#region src/cssdata.ts
309
+ let propertyIndex = null;
310
+ /** MDN-sourced CSS property data (the dataset behind VS Code's CSS hover). */
311
+ function cssProperty(name) {
312
+ if (!propertyIndex) {
313
+ propertyIndex = /* @__PURE__ */ new Map();
314
+ for (const property of (0, vscode_css_languageservice.getDefaultCSSDataProvider)().provideProperties()) propertyIndex.set(property.name, property);
315
+ }
316
+ return propertyIndex.get(name);
317
+ }
318
+ function allCssProperties() {
319
+ cssProperty("color");
320
+ return [...propertyIndex.values()];
321
+ }
322
+ function propertyDescription(property) {
323
+ const desc = property.description;
324
+ if (!desc) return "";
325
+ return typeof desc === "string" ? desc : desc.value;
326
+ }
327
+ /** Markdown hover card for a CSS property (or custom property). */
328
+ function cssPropertyHover(name) {
329
+ if (name.startsWith("--")) return `**${name}** — CSS custom property`;
330
+ const property = cssProperty(name);
331
+ if (!property) return null;
332
+ const parts = [`**${name}**`];
333
+ const description = propertyDescription(property);
334
+ if (description) parts.push(description);
335
+ if (property.syntax) parts.push(`\`\`\`\n${property.syntax}\n\`\`\``);
336
+ const mdn = property.references?.find((r) => r.name.includes("MDN"));
337
+ if (mdn) parts.push(`[MDN Reference](${mdn.url})`);
338
+ return parts.join("\n\n");
339
+ }
340
+ //#endregion
341
+ //#region src/hover.ts
342
+ function getHover(analysis, offset, workspace) {
343
+ const target = nodeAtOffset(analysis.ast, offset);
344
+ if (!target) return null;
345
+ const markdown = renderHover(target, analysis, workspace);
346
+ if (!markdown) return null;
347
+ return {
348
+ contents: {
349
+ kind: vscode_languageserver.MarkupKind.Markdown,
350
+ value: markdown
351
+ },
352
+ range: rangeOf(analysis, targetSpan(target))
353
+ };
354
+ }
355
+ function targetSpan(target) {
356
+ switch (target.kind) {
357
+ case "token-ref": return target.word.span;
358
+ case "use-recipe":
359
+ case "slot-name":
360
+ case "conditional-key":
361
+ case "token-entry": return target.span;
362
+ case "component-name": return target.component.nameSpan;
363
+ case "style-name": return target.style.nameSpan;
364
+ case "recipe-name": return target.recipe.nameSpan;
365
+ case "keyframes-name": return target.keyframes.nameSpan;
366
+ case "variant-name": return target.variant.nameSpan;
367
+ case "variant-value-name": return target.value.nameSpan;
368
+ case "variant-setting": return target.part === "variant" ? target.entry.variantSpan : target.entry.valueSpan;
369
+ case "css-property": return target.span;
370
+ }
371
+ }
372
+ function rangeOf(analysis, span) {
373
+ const range = analysis.index.spanToRange(span);
374
+ return {
375
+ start: {
376
+ line: range.start.line - 1,
377
+ character: range.start.col - 1
378
+ },
379
+ end: {
380
+ line: range.end.line - 1,
381
+ character: range.end.col - 1
382
+ }
383
+ };
384
+ }
385
+ function renderHover(target, analysis, workspace) {
386
+ const env = analysis.env;
387
+ switch (target.kind) {
388
+ case "token-ref": return target.word.group === "keyframes" ? keyframesRefHover(target.word, analysis, workspace) : tokenRefHover(target.word, target.component, env);
389
+ case "use-recipe": return recipeHover(target.name, env);
390
+ case "recipe-name": return recipeHover(target.recipe.name, env);
391
+ case "token-entry": {
392
+ const heading = target.owner === "component" ? `**${target.group.name}.${target.name}** — local to \`${target.component?.name}\`` : `**${target.group.name}.${target.name}**`;
393
+ const doc = target.doc ? `\n\n${target.doc}` : "";
394
+ return `${heading}\n\n\`\`\`css\n${target.value}\n\`\`\`${doc}`;
395
+ }
396
+ case "component-name": return componentHover(target.component);
397
+ case "style-name": return `**style ${target.style.name}** — exported class\n\n\`\`\`ts\nexport const ${target.style.name}: string;\n\`\`\``;
398
+ case "keyframes-name": return keyframesDeclHover(target.keyframes.name, analysis.ast);
399
+ case "variant-name": {
400
+ const values = target.variant.values.map((v) => v.name).join(" | ");
401
+ return `**variant ${target.variant.name}** of \`${target.component.name}\`\n\n\`\`\`ts\n${target.variant.name}?: ${values || "never"}\n\`\`\``;
402
+ }
403
+ case "variant-value-name": return `**${target.variant.name}: ${target.value.name}** — variant value of \`${target.component.name}\``;
404
+ case "slot-name": return `**slot ${target.name}** of \`${target.component.name}\``;
405
+ case "variant-setting": {
406
+ const variant = findVariant(target.component, target.entry.variant);
407
+ if (!variant) return null;
408
+ if (target.part === "variant") {
409
+ const values = variant.values.map((v) => v.name).join(" | ");
410
+ return `**variant ${variant.name}** of \`${target.component.name}\`\n\n\`\`\`ts\n${variant.name}?: ${values || "never"}\n\`\`\``;
411
+ }
412
+ return `**${target.entry.variant}: ${target.entry.value}** (${target.context})`;
413
+ }
414
+ case "conditional-key": {
415
+ const size = target.context === "responsive" ? env.breakpoints[target.key] : env.containers[target.key];
416
+ const label = target.context === "responsive" ? "breakpoint" : "container size";
417
+ return size ? `**${label} ${target.key}**\n\n\`\`\`css\nmin-width: ${size}\n\`\`\`` : null;
418
+ }
419
+ case "css-property": return cssPropertyHover(target.name);
420
+ }
421
+ }
422
+ function tokenRefHover(word, component, env) {
423
+ const local = component ? findLocalToken(component, word.group, word.name) : null;
424
+ if (local) {
425
+ const doc = local.doc ? `\n\n${local.doc}` : "";
426
+ return `**${word.group}.${word.name}** — local to \`${component.name}\`\n\n\`\`\`css\n${local.value}\n\`\`\`${doc}`;
427
+ }
428
+ const entry = env.tokens[word.group]?.[word.name];
429
+ if (entry === void 0) return null;
430
+ let body;
431
+ if (typeof entry === "string") body = `\`\`\`css\n${entry}\n\`\`\``;
432
+ else body = `| mode | value |\n| --- | --- |\n${Object.entries(entry).map(([mode, value]) => `| ${mode} | \`${value}\` |`).join("\n")}`;
433
+ const doc = env.tokenDocs[word.group]?.[word.name];
434
+ const docLine = doc ? `\n\n${doc}` : "";
435
+ const cssVar = env.modes ? `\n\n\`var(--arvia-${word.group}-${word.name})\`` : "";
436
+ return `**${word.group}.${word.name}**\n\n${body}${docLine}${cssVar}`;
437
+ }
438
+ function recipeHover(name, env) {
439
+ const recipe = env.recipes[name];
440
+ if (!recipe) return null;
441
+ const shown = recipe.decls.slice(0, 8);
442
+ const lines = shown.map((d) => `${d.property}: ${d.value};`);
443
+ if (recipe.decls.length > shown.length) lines.push(`/* +${recipe.decls.length - shown.length} more */`);
444
+ const selectorList = recipe.states.flatMap((s) => s.selectors).map((sel) => `\`&${sel.trim()}\``).join(", ");
445
+ const states = recipe.states.length > 0 ? `\n\n${recipe.states.length} state${recipe.states.length === 1 ? "" : "s"}: ${selectorList}` : "";
446
+ return `**recipe ${name}**\n\n\`\`\`css\n${lines.join("\n")}\n\`\`\`${states}`;
447
+ }
448
+ function componentHover(component) {
449
+ const slots = new Set(["root"]);
450
+ const variants = [];
451
+ for (const item of component.items) {
452
+ if (item.kind === "slots") for (const slot of item.slots) slots.add(slot.name);
453
+ if (item.kind === "variants") for (const variant of item.variants) variants.push(`${variant.name}: ${variant.values.map((v) => v.name).join(" | ")}`);
454
+ }
455
+ const lines = [`slots: ${[...slots].join(", ")}`, ...variants.length > 0 ? variants : ["(no variants)"]];
456
+ return `**component ${component.name}**\n\n\`\`\`\n${lines.join("\n")}\n\`\`\``;
457
+ }
458
+ function keyframesRefHover(word, analysis, workspace) {
459
+ const local = keyframesDeclHover(word.name, analysis.ast);
460
+ if (local) return local;
461
+ const theme = workspace.themeFor(analysis.file);
462
+ return theme ? keyframesDeclHover(word.name, theme.ast) : null;
463
+ }
464
+ function keyframesDeclHover(name, ast) {
465
+ for (const item of ast.items) {
466
+ if (item.kind !== "keyframes" || item.name !== name) continue;
467
+ return `**keyframes ${name}**\n\n\`\`\`\n${item.steps.map((s) => s.selector).join(" → ") || "(no steps)"}\n\`\`\``;
468
+ }
469
+ return null;
470
+ }
471
+ function findVariant(component, name) {
472
+ for (const item of component.items) {
473
+ if (item.kind !== "variants") continue;
474
+ const variant = item.variants.find((v) => v.name === name);
475
+ if (variant) return variant;
476
+ }
477
+ }
478
+ function findLocalToken(component, group, name) {
479
+ for (const item of component.items) {
480
+ if (item.kind !== "tokens") continue;
481
+ for (const g of item.groups) {
482
+ if (g.name !== group) continue;
483
+ for (const entry of g.entries) if (entry.name === name) return {
484
+ value: entry.value.text,
485
+ doc: entry.doc,
486
+ span: entry.nameSpan
487
+ };
488
+ }
489
+ }
490
+ return null;
491
+ }
492
+ //#endregion
493
+ //#region src/walk.ts
494
+ /** Every CSS declaration in the file, with its owning component (if any). */
495
+ function walkDeclarations(ast) {
496
+ const out = [];
497
+ const visitItems = (items, component) => {
498
+ for (const item of items) if (item.kind === "decl") out.push({
499
+ decl: item,
500
+ component
501
+ });
502
+ else if (item.kind === "state") {
503
+ for (const decl of item.items) out.push({
504
+ decl,
505
+ component
506
+ });
507
+ for (const slot of item.slots) for (const decl of slot.items) if (decl.kind === "decl") out.push({
508
+ decl,
509
+ component
510
+ });
511
+ }
512
+ };
513
+ for (const top of ast.items) switch (top.kind) {
514
+ case "global":
515
+ for (const rule of top.rules) for (const decl of rule.decls) out.push({
516
+ decl,
517
+ component: null
518
+ });
519
+ break;
520
+ case "keyframes":
521
+ for (const step of top.steps) for (const decl of step.decls) out.push({
522
+ decl,
523
+ component: null
524
+ });
525
+ break;
526
+ case "recipe":
527
+ case "styledecl":
528
+ visitItems(top.items, null);
529
+ break;
530
+ case "component":
531
+ for (const item of top.items) switch (item.kind) {
532
+ case "decl":
533
+ out.push({
534
+ decl: item,
535
+ component: top
536
+ });
537
+ break;
538
+ case "base":
539
+ for (const part of item.body.items) if (part.kind === "slotblock") visitItems(part.items, top);
540
+ else visitItems([part], top);
541
+ break;
542
+ case "slots":
543
+ for (const slot of item.slots) visitItems(slot.items, top);
544
+ break;
545
+ case "variants":
546
+ for (const variant of item.variants) for (const value of variant.values) for (const part of value.body.items) if (part.kind === "slotblock") visitItems(part.items, top);
547
+ else visitItems([part], top);
548
+ break;
549
+ case "compound":
550
+ for (const slot of item.slots) visitItems(slot.items, top);
551
+ break;
552
+ }
553
+ break;
554
+ }
555
+ return out;
556
+ }
557
+ /** Every token entry (theme + component tokens, incl. mode overrides). */
558
+ function walkTokenEntries(ast) {
559
+ const out = [];
560
+ const visitGroup = (group, owner, component) => {
561
+ for (const entry of group.entries) out.push({
562
+ entry,
563
+ group,
564
+ owner,
565
+ component
566
+ });
567
+ for (const override of group.overrides) for (const entry of override.entries) out.push({
568
+ entry,
569
+ group,
570
+ owner,
571
+ component
572
+ });
573
+ };
574
+ for (const top of ast.items) {
575
+ if (top.kind === "theme") for (const group of top.groups) visitGroup(group, "theme", null);
576
+ if (top.kind === "component") for (const item of top.items) {
577
+ if (item.kind !== "tokens") continue;
578
+ for (const group of item.groups) visitGroup(group, "component", top);
579
+ }
580
+ }
581
+ return out;
582
+ }
583
+ /** Every RawValue in the file (declaration values + token entry values). */
584
+ function walkValues(ast) {
585
+ return [...walkDeclarations(ast).map(({ decl, component }) => ({
586
+ value: decl.value,
587
+ component
588
+ })), ...walkTokenEntries(ast).map(({ entry, component }) => ({
589
+ value: entry.value,
590
+ component
591
+ }))];
592
+ }
593
+ /** Every `use Recipe;` statement in the file. */
594
+ function walkUses(ast) {
595
+ const out = [];
596
+ const visitItems = (items) => {
597
+ for (const item of items) if (item.kind === "use") out.push({
598
+ recipe: item.recipe,
599
+ recipeSpan: item.recipeSpan
600
+ });
601
+ };
602
+ for (const top of ast.items) switch (top.kind) {
603
+ case "recipe":
604
+ case "styledecl":
605
+ visitItems(top.items);
606
+ break;
607
+ case "component":
608
+ for (const item of top.items) {
609
+ if (item.kind === "use") out.push({
610
+ recipe: item.recipe,
611
+ recipeSpan: item.recipeSpan
612
+ });
613
+ if (item.kind === "base") for (const part of item.body.items) if (part.kind === "slotblock") visitItems(part.items);
614
+ else visitItems([part]);
615
+ if (item.kind === "slots") for (const slot of item.slots) visitItems(slot.items);
616
+ if (item.kind === "variants") for (const variant of item.variants) for (const value of variant.values) for (const part of value.body.items) if (part.kind === "slotblock") visitItems(part.items);
617
+ else visitItems([part]);
618
+ if (item.kind === "compound") for (const slot of item.slots) visitItems(slot.items);
619
+ }
620
+ break;
621
+ }
622
+ return out;
623
+ }
624
+ //#endregion
625
+ //#region src/colors.ts
626
+ const HEX_RE = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
627
+ const FN_RE = /^(rgba?|hsla?)\(([^)]*)\)$/;
628
+ const to255 = (v) => Math.round(v * 255);
629
+ const rgbChannel = (raw) => raw.endsWith("%") ? parseFloat(raw) / 100 : parseFloat(raw) / 255;
630
+ /** Inline color swatches for literal color values (hex / rgb / hsl). */
631
+ function getDocumentColors(analysis) {
632
+ const out = [];
633
+ for (const { value } of walkValues(analysis.ast)) for (const word of value.words) {
634
+ if (word.kind !== "literal") continue;
635
+ const color = parseColor(word.text);
636
+ if (color) out.push({
637
+ range: rangeOf(analysis, word.span),
638
+ color
639
+ });
640
+ }
641
+ return out;
642
+ }
643
+ function getColorPresentations(color, span) {
644
+ const hex2 = (v) => to255(v).toString(16).padStart(2, "0");
645
+ const hex = color.alpha < 1 ? `#${hex2(color.red)}${hex2(color.green)}${hex2(color.blue)}${hex2(color.alpha)}` : `#${hex2(color.red)}${hex2(color.green)}${hex2(color.blue)}`;
646
+ const rgb = color.alpha < 1 ? `rgba(${to255(color.red)}, ${to255(color.green)}, ${to255(color.blue)}, ${Math.round(color.alpha * 100) / 100})` : `rgb(${to255(color.red)}, ${to255(color.green)}, ${to255(color.blue)})`;
647
+ return [{ label: hex }, { label: rgb }];
648
+ }
649
+ function parseColor(text) {
650
+ if (HEX_RE.test(text)) return parseHex(text);
651
+ const fn = FN_RE.exec(text);
652
+ if (fn) {
653
+ const args = fn[2].split(/[,\s/]+/).map((a) => a.trim()).filter(Boolean);
654
+ if (args.length < 3) return null;
655
+ if (fn[1].startsWith("rgb")) return parseRgbArgs(args);
656
+ return parseHslArgs(args);
657
+ }
658
+ return null;
659
+ }
660
+ function parseHex(text) {
661
+ const hex = text.slice(1);
662
+ const wide = hex.length >= 6;
663
+ const step = wide ? 2 : 1;
664
+ const channel = (i) => {
665
+ const part = hex.slice(i * step, i * step + step);
666
+ return parseInt(wide ? part : part + part, 16) / 255;
667
+ };
668
+ const hasAlpha = hex.length === 4 || hex.length === 8;
669
+ return {
670
+ red: channel(0),
671
+ green: channel(1),
672
+ blue: channel(2),
673
+ alpha: hasAlpha ? channel(3) : 1
674
+ };
675
+ }
676
+ function parseRgbArgs(args) {
677
+ const [r, g, b] = [
678
+ rgbChannel(args[0]),
679
+ rgbChannel(args[1]),
680
+ rgbChannel(args[2])
681
+ ];
682
+ if ([
683
+ r,
684
+ g,
685
+ b
686
+ ].some((v) => Number.isNaN(v))) return null;
687
+ const alpha = args[3] !== void 0 ? parseAlpha(args[3]) : 1;
688
+ return {
689
+ red: clamp01(r),
690
+ green: clamp01(g),
691
+ blue: clamp01(b),
692
+ alpha
693
+ };
694
+ }
695
+ function parseHslArgs(args) {
696
+ const h = parseFloat(args[0]);
697
+ const s = parseFloat(args[1]) / 100;
698
+ const l = parseFloat(args[2]) / 100;
699
+ if ([
700
+ h,
701
+ s,
702
+ l
703
+ ].some((v) => Number.isNaN(v))) return null;
704
+ const alpha = args[3] !== void 0 ? parseAlpha(args[3]) : 1;
705
+ const c = (1 - Math.abs(2 * l - 1)) * s;
706
+ const hp = (h % 360 + 360) % 360 / 60;
707
+ const x = c * (1 - Math.abs(hp % 2 - 1));
708
+ const [r1, g1, b1] = hp < 1 ? [
709
+ c,
710
+ x,
711
+ 0
712
+ ] : hp < 2 ? [
713
+ x,
714
+ c,
715
+ 0
716
+ ] : hp < 3 ? [
717
+ 0,
718
+ c,
719
+ x
720
+ ] : hp < 4 ? [
721
+ 0,
722
+ x,
723
+ c
724
+ ] : hp < 5 ? [
725
+ x,
726
+ 0,
727
+ c
728
+ ] : [
729
+ c,
730
+ 0,
731
+ x
732
+ ];
733
+ const m = l - c / 2;
734
+ return {
735
+ red: clamp01(r1 + m),
736
+ green: clamp01(g1 + m),
737
+ blue: clamp01(b1 + m),
738
+ alpha
739
+ };
740
+ }
741
+ function parseAlpha(raw) {
742
+ return clamp01(raw.endsWith("%") ? parseFloat(raw) / 100 : parseFloat(raw));
743
+ }
744
+ function clamp01(v) {
745
+ return Math.max(0, Math.min(1, v));
746
+ }
747
+ //#endregion
748
+ //#region src/completion.ts
749
+ const SECTION_KEYWORDS = [
750
+ "base",
751
+ "slots",
752
+ "variants",
753
+ "defaults",
754
+ "responsive",
755
+ "container",
756
+ "compound",
757
+ "tokens"
758
+ ];
759
+ const TOP_KEYWORDS = [
760
+ "theme",
761
+ "global",
762
+ "recipe",
763
+ "keyframes",
764
+ "style",
765
+ "component"
766
+ ];
767
+ const DEFAULT_GROUPS = [
768
+ "color",
769
+ "space",
770
+ "radius",
771
+ "font",
772
+ "breakpoint",
773
+ "container",
774
+ "duration",
775
+ "easing"
776
+ ];
777
+ function item(label, kind, detail) {
778
+ return {
779
+ label,
780
+ kind,
781
+ detail
782
+ };
783
+ }
784
+ function tokenKind(group, value) {
785
+ return group === "color" || /^#|rgb|hsl|oklch/.test(value) ? vscode_languageserver.CompletionItemKind.Color : vscode_languageserver.CompletionItemKind.Constant;
786
+ }
787
+ /** Returns the component whose span contains the offset, if any. */
788
+ function enclosingComponent(analysis, offset) {
789
+ for (const top of analysis.ast.items) if (top.kind === "component" && offset >= top.span.start && offset <= top.span.end) return top;
790
+ return null;
791
+ }
792
+ function localTokenGroups(component) {
793
+ const groups = /* @__PURE__ */ new Map();
794
+ if (!component) return groups;
795
+ for (const part of component.items) {
796
+ if (part.kind !== "tokens") continue;
797
+ for (const group of part.groups) {
798
+ const bucket = groups.get(group.name) ?? /* @__PURE__ */ new Map();
799
+ for (const entry of group.entries) bucket.set(entry.name, entry.value.text);
800
+ groups.set(group.name, bucket);
801
+ }
802
+ }
803
+ return groups;
804
+ }
805
+ function tokenValueOf(env, group, name) {
806
+ const entry = env.tokens[group]?.[name];
807
+ if (entry === void 0) return "";
808
+ if (typeof entry === "string") return entry;
809
+ return Object.entries(entry).map(([mode, value]) => `${mode}: ${value}`).join(", ");
810
+ }
811
+ function getCompletions(analysis, offset) {
812
+ const source = analysis.source;
813
+ const env = analysis.env;
814
+ const before = source.slice(0, offset);
815
+ const lineStart = before.lastIndexOf("\n") + 1;
816
+ const linePrefix = before.slice(lineStart);
817
+ const component = enclosingComponent(analysis, offset);
818
+ const locals = localTokenGroups(component);
819
+ const items = [];
820
+ const groupDot = linePrefix.match(/([A-Za-z_][A-Za-z0-9_-]*)\.$/);
821
+ if (groupDot) {
822
+ const group = groupDot[1];
823
+ const localBucket = locals.get(group);
824
+ if (localBucket) for (const [name, value] of localBucket) items.push({
825
+ label: name,
826
+ kind: tokenKind(group, value),
827
+ detail: `${value} — local to ${component.name}`
828
+ });
829
+ const bucket = env.tokens[group];
830
+ if (bucket) for (const name of Object.keys(bucket)) {
831
+ if (localBucket?.has(name)) continue;
832
+ const value = tokenValueOf(env, group, name);
833
+ const doc = env.tokenDocs[group]?.[name];
834
+ items.push({
835
+ label: name,
836
+ kind: tokenKind(group, value),
837
+ detail: value,
838
+ documentation: doc
839
+ });
840
+ }
841
+ if (group === "keyframes") for (const name of Object.keys(env.keyframes)) items.push(item(name, vscode_languageserver.CompletionItemKind.Event, "keyframes"));
842
+ if (items.length > 0) return items;
843
+ }
844
+ if (/\buse\s+$/.test(linePrefix)) {
845
+ for (const [name, recipe] of Object.entries(env.recipes)) items.push(item(name, vscode_languageserver.CompletionItemKind.Reference, `recipe — ${recipe.decls.length} decls`));
846
+ return items;
847
+ }
848
+ if (linePrefix.endsWith("@")) {
849
+ for (const mode of env.modes ?? []) items.push(item(mode, vscode_languageserver.CompletionItemKind.EnumMember, "theme mode"));
850
+ return items;
851
+ }
852
+ if (/^\s*$/.test(linePrefix) && before.trimEnd().endsWith("{")) {
853
+ if (/\b(theme|tokens)\s*\{\s*$/.test(before.slice(-40))) {
854
+ if (/\btheme\s*\{\s*$/.test(before.slice(-40))) items.push(item("modes:", vscode_languageserver.CompletionItemKind.Keyword, "light | dark;"));
855
+ const groups = new Set([...DEFAULT_GROUPS, ...Object.keys(env.tokens)]);
856
+ for (const group of groups) items.push(item(group, vscode_languageserver.CompletionItemKind.Module, "token group"));
857
+ }
858
+ if (/\bcomponent\s+[\w$]+\s*\{\s*$/.test(before)) for (const kw of SECTION_KEYWORDS) items.push(item(kw, vscode_languageserver.CompletionItemKind.Keyword));
859
+ if (/\b(defaults|compound)\s*\{\s*$/.test(before.slice(-24)) && component) for (const variant of variantsOf(component)) items.push(item(variant.name, vscode_languageserver.CompletionItemKind.Enum, variant.values.join(" | ")));
860
+ if (/\bresponsive\s*\{\s*$/.test(before.slice(-20))) for (const [bp, size] of Object.entries(env.breakpoints)) items.push(item(bp, vscode_languageserver.CompletionItemKind.Variable, `min-width: ${size}`));
861
+ if (/\bcontainer\s*\{\s*$/.test(before.slice(-20))) for (const [cq, size] of Object.entries(env.containers)) items.push(item(cq, vscode_languageserver.CompletionItemKind.Variable, `min-width: ${size}`));
862
+ if (items.length > 0) return items;
863
+ }
864
+ const settingMatch = linePrefix.match(/^\s*([A-Za-z_][\w$-]*)\s*:\s*$/);
865
+ if (settingMatch && component) {
866
+ const variant = variantsOf(component).find((v) => v.name === settingMatch[1]);
867
+ if (variant) {
868
+ for (const value of variant.values) items.push(item(value, vscode_languageserver.CompletionItemKind.EnumMember, `value of ${variant.name}`));
869
+ return items;
870
+ }
871
+ }
872
+ if (/:\s*[^;]*$/.test(linePrefix)) {
873
+ const groups = new Set([...Object.keys(env.tokens), ...locals.keys()]);
874
+ for (const group of groups) items.push(item(group, vscode_languageserver.CompletionItemKind.Module, "token group"));
875
+ if (Object.keys(env.keyframes).length > 0) items.push(item("keyframes", vscode_languageserver.CompletionItemKind.Module, "animations"));
876
+ return items;
877
+ }
878
+ for (const prop of allCssProperties()) items.push({
879
+ label: prop.name,
880
+ kind: vscode_languageserver.CompletionItemKind.Property,
881
+ detail: prop.syntax,
882
+ documentation: propertyDescription(prop) || void 0
883
+ });
884
+ if (items.length > 0 && component) return items;
885
+ for (const kw of [
886
+ ...TOP_KEYWORDS,
887
+ ...SECTION_KEYWORDS,
888
+ "use"
889
+ ]) items.push(item(kw, vscode_languageserver.CompletionItemKind.Keyword));
890
+ return items;
891
+ }
892
+ function variantsOf(component) {
893
+ const out = [];
894
+ for (const part of component.items) {
895
+ if (part.kind !== "variants") continue;
896
+ for (const variant of part.variants) out.push({
897
+ name: variant.name,
898
+ values: variant.values.map((v) => v.name)
899
+ });
900
+ }
901
+ return out;
902
+ }
903
+ //#endregion
904
+ //#region src/definition.ts
905
+ function getDefinition(analysis, offset, workspace) {
906
+ const target = nodeAtOffset(analysis.ast, offset);
907
+ if (!target) return null;
908
+ const theme = workspace.themeFor(analysis.file);
909
+ const isThemeDoc = theme !== null && theme.path === analysis.file;
910
+ const local = (span) => locationFor(analysis.file, analysis.index, span);
911
+ const inTheme = (span) => theme && !isThemeDoc ? locationFor(theme.path, theme.index, span) : null;
912
+ switch (target.kind) {
913
+ case "token-ref": {
914
+ if (target.word.group === "keyframes") {
915
+ const own = findKeyframes(analysis.ast, target.word.name);
916
+ if (own) return local(own);
917
+ const themed = theme && findKeyframes(theme.ast, target.word.name);
918
+ return themed ? inTheme(themed) : null;
919
+ }
920
+ if (target.component) {
921
+ const localToken = findLocalToken(target.component, target.word.group, target.word.name);
922
+ if (localToken) return local(localToken.span);
923
+ }
924
+ const own = findThemeEntry(analysis.ast, target.word.group, target.word.name);
925
+ if (own) return local(own);
926
+ const themed = theme && findThemeEntry(theme.ast, target.word.group, target.word.name);
927
+ return themed ? inTheme(themed) : null;
928
+ }
929
+ case "use-recipe": {
930
+ const own = findRecipe(analysis.ast, target.name);
931
+ if (own) return local(own);
932
+ const themed = theme && findRecipe(theme.ast, target.name);
933
+ return themed ? inTheme(themed) : null;
934
+ }
935
+ case "variant-setting": {
936
+ const variant = findVariantSpans(target.component, target.entry.variant);
937
+ if (!variant) return null;
938
+ if (target.part === "variant") return local(variant.nameSpan);
939
+ const value = variant.values.find((v) => v.name === target.entry.value);
940
+ return value ? local(value.nameSpan) : local(variant.nameSpan);
941
+ }
942
+ case "conditional-key": {
943
+ const group = target.context === "responsive" ? "breakpoint" : "container";
944
+ const own = findThemeEntry(analysis.ast, group, target.key);
945
+ if (own) return local(own);
946
+ const themed = theme && findThemeEntry(theme.ast, group, target.key);
947
+ return themed ? inTheme(themed) : null;
948
+ }
949
+ default: return null;
950
+ }
951
+ }
952
+ function locationFor(file, index, span) {
953
+ const range = index.spanToRange(span);
954
+ return {
955
+ uri: (0, node_url.pathToFileURL)(file).toString(),
956
+ range: {
957
+ start: {
958
+ line: range.start.line - 1,
959
+ character: range.start.col - 1
960
+ },
961
+ end: {
962
+ line: range.end.line - 1,
963
+ character: range.end.col - 1
964
+ }
965
+ }
966
+ };
967
+ }
968
+ function findThemeEntry(ast, group, name) {
969
+ for (const item of ast.items) {
970
+ if (item.kind !== "theme") continue;
971
+ for (const g of item.groups) {
972
+ if (g.name !== group) continue;
973
+ for (const entry of g.entries) if (entry.name === name) return entry.nameSpan;
974
+ }
975
+ }
976
+ return null;
977
+ }
978
+ function findRecipe(ast, name) {
979
+ for (const item of ast.items) if (item.kind === "recipe" && item.name === name) return item.nameSpan;
980
+ return null;
981
+ }
982
+ function findKeyframes(ast, name) {
983
+ for (const item of ast.items) if (item.kind === "keyframes" && item.name === name) return item.nameSpan;
984
+ return null;
985
+ }
986
+ function findVariantSpans(component, name) {
987
+ for (const item of component.items) {
988
+ if (item.kind !== "variants") continue;
989
+ const variant = item.variants.find((v) => v.name === name);
990
+ if (variant) return {
991
+ nameSpan: variant.nameSpan,
992
+ values: variant.values.map((v) => ({
993
+ name: v.name,
994
+ nameSpan: v.nameSpan
995
+ }))
996
+ };
997
+ }
998
+ return null;
999
+ }
1000
+ //#endregion
1001
+ //#region src/diagnostics.ts
1002
+ /** Maps compiler diagnostics to LSP diagnostics with full (start+end) ranges. */
1003
+ function toLspDiagnostics(analysis) {
1004
+ return analysis.diagnostics.map((d) => {
1005
+ const range = analysis.index.spanToRange(d.span);
1006
+ return {
1007
+ severity: d.severity === "error" ? 1 : 2,
1008
+ range: {
1009
+ start: {
1010
+ line: range.start.line - 1,
1011
+ character: range.start.col - 1
1012
+ },
1013
+ end: {
1014
+ line: range.end.line - 1,
1015
+ character: range.end.col - 1
1016
+ }
1017
+ },
1018
+ message: d.hint ? `${d.message} (${d.hint})` : d.message,
1019
+ source: "arvia",
1020
+ code: d.code
1021
+ };
1022
+ });
1023
+ }
1024
+ //#endregion
1025
+ //#region src/documents.ts
1026
+ function fileForUri(uri) {
1027
+ return uri.startsWith("file://") ? decodeURIComponent(uri.slice(7)) : uri;
1028
+ }
1029
+ /** Per-document analysis cache keyed by document version. */
1030
+ var DocumentStore = class {
1031
+ workspaceFor;
1032
+ cache = /* @__PURE__ */ new Map();
1033
+ constructor(workspaceFor) {
1034
+ this.workspaceFor = workspaceFor;
1035
+ }
1036
+ analysisFor(doc) {
1037
+ const cached = this.cache.get(doc.uri);
1038
+ if (cached && cached.version === doc.version) return cached.analysis;
1039
+ const file = fileForUri(doc.uri);
1040
+ const source = doc.getText();
1041
+ const analysis = {
1042
+ ...(0, _arviahq_compiler.analyze)(source, {
1043
+ filename: file,
1044
+ env: this.workspaceFor(doc.uri).envFor(file)
1045
+ }),
1046
+ index: new _arviahq_compiler.LineIndex(source),
1047
+ file,
1048
+ source
1049
+ };
1050
+ this.cache.set(doc.uri, {
1051
+ version: doc.version,
1052
+ analysis
1053
+ });
1054
+ return analysis;
1055
+ }
1056
+ invalidate(uri) {
1057
+ this.cache.delete(uri);
1058
+ }
1059
+ invalidateAll() {
1060
+ this.cache.clear();
1061
+ }
1062
+ };
1063
+ //#endregion
1064
+ //#region src/inlay-hints.ts
1065
+ const MAX_HINT_LENGTH = 28;
1066
+ /** `padding: space.4` → ghost text ` = 16px` after the ref. */
1067
+ function getInlayHints(analysis, range) {
1068
+ const startOffset = analysis.index.offsetAt({
1069
+ line: range.start.line + 1,
1070
+ col: range.start.character + 1
1071
+ });
1072
+ const endOffset = analysis.index.offsetAt({
1073
+ line: range.end.line + 1,
1074
+ col: range.end.character + 1
1075
+ });
1076
+ const hints = [];
1077
+ for (const { decl, component } of walkDeclarations(analysis.ast)) for (const word of decl.value.words) {
1078
+ if (word.kind !== "ref" || word.group === "keyframes") continue;
1079
+ if (word.span.end < startOffset || word.span.start > endOffset) continue;
1080
+ const resolved = resolveForHint(analysis, word.group, word.name, component);
1081
+ if (!resolved || resolved === word.text) continue;
1082
+ const position = analysis.index.positionAt(word.span.end);
1083
+ hints.push({
1084
+ position: {
1085
+ line: position.line - 1,
1086
+ character: position.col - 1
1087
+ },
1088
+ label: ` = ${truncate(resolved)}`,
1089
+ kind: vscode_languageserver.InlayHintKind.Type,
1090
+ paddingLeft: false
1091
+ });
1092
+ }
1093
+ return hints;
1094
+ }
1095
+ function resolveForHint(analysis, group, name, component) {
1096
+ if (component) {
1097
+ const local = findLocalToken(component, group, name);
1098
+ if (local) return local.value;
1099
+ }
1100
+ const entry = analysis.env.tokens[group]?.[name];
1101
+ if (entry === void 0) return null;
1102
+ if (typeof entry === "string") return entry;
1103
+ const first = analysis.env.modes?.[0];
1104
+ return (first && entry[first]) ?? Object.values(entry)[0] ?? null;
1105
+ }
1106
+ function truncate(value) {
1107
+ return value.length > MAX_HINT_LENGTH ? `${value.slice(0, MAX_HINT_LENGTH - 1)}…` : value;
1108
+ }
1109
+ //#endregion
1110
+ //#region src/rename.ts
1111
+ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
1112
+ const TOKEN_NAME_RE = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/;
1113
+ function prepareRename(analysis, offset) {
1114
+ const resolved = identityAt(analysis, offset);
1115
+ if (!resolved) return null;
1116
+ return {
1117
+ range: rangeOf(analysis, resolved.span),
1118
+ placeholder: resolved.placeholder
1119
+ };
1120
+ }
1121
+ function getRenameEdits(analysis, offset, newName, workspace, contentFor) {
1122
+ const resolved = identityAt(analysis, offset);
1123
+ if (!resolved) return null;
1124
+ const { identity } = resolved;
1125
+ if (!(identity.kind === "token" ? TOKEN_NAME_RE : NAME_RE).test(newName)) return null;
1126
+ const changes = {};
1127
+ const addEdits = (file, source, ast) => {
1128
+ const index = new _arviahq_compiler.LineIndex(source);
1129
+ const edits = editsInFile(ast, identity, newName);
1130
+ if (edits.length === 0) return;
1131
+ const uri = (0, node_url.pathToFileURL)(file).toString();
1132
+ changes[uri] = edits.map(({ span, text }) => ({
1133
+ range: lspRange(index, span),
1134
+ newText: text
1135
+ }));
1136
+ };
1137
+ addEdits(analysis.file, analysis.source, analysis.ast);
1138
+ if (crossFileKinds.has(identity.kind) && isSharedThemeMember(analysis, workspace)) {
1139
+ const themePath = node_path.default.resolve(analysis.file);
1140
+ for (const file of listArvFiles(workspace.root)) {
1141
+ const resolvedFile = node_path.default.resolve(file);
1142
+ if (resolvedFile === node_path.default.resolve(analysis.file)) continue;
1143
+ if (workspace.themePathFor(resolvedFile) !== themePath) continue;
1144
+ const source = contentFor(resolvedFile);
1145
+ if (source === null) continue;
1146
+ addEdits(resolvedFile, source, (0, _arviahq_compiler.parse)(source, resolvedFile).ast);
1147
+ }
1148
+ }
1149
+ return Object.keys(changes).length > 0 ? { changes } : null;
1150
+ }
1151
+ const crossFileKinds = new Set([
1152
+ "token",
1153
+ "recipe",
1154
+ "keyframes"
1155
+ ]);
1156
+ /** True when the current document is a theme other files can resolve to. */
1157
+ function isSharedThemeMember(analysis, workspace) {
1158
+ return workspace.themePathFor(analysis.file) === node_path.default.resolve(analysis.file);
1159
+ }
1160
+ function identityAt(analysis, offset) {
1161
+ const target = nodeAtOffset(analysis.ast, offset);
1162
+ if (!target) return null;
1163
+ switch (target.kind) {
1164
+ case "token-ref": {
1165
+ if (target.word.group === "keyframes") return {
1166
+ identity: {
1167
+ kind: "keyframes",
1168
+ name: target.word.name
1169
+ },
1170
+ span: target.word.span,
1171
+ placeholder: target.word.name
1172
+ };
1173
+ const local = target.component ? findLocalToken(target.component, target.word.group, target.word.name) : null;
1174
+ return {
1175
+ identity: {
1176
+ kind: "token",
1177
+ group: target.word.group,
1178
+ name: target.word.name,
1179
+ component: local ? target.component : null
1180
+ },
1181
+ span: target.word.span,
1182
+ placeholder: target.word.name
1183
+ };
1184
+ }
1185
+ case "token-entry": return {
1186
+ identity: {
1187
+ kind: "token",
1188
+ group: target.group.name,
1189
+ name: target.name,
1190
+ component: target.owner === "component" ? target.component : null
1191
+ },
1192
+ span: target.span,
1193
+ placeholder: target.name
1194
+ };
1195
+ case "use-recipe": return {
1196
+ identity: {
1197
+ kind: "recipe",
1198
+ name: target.name
1199
+ },
1200
+ span: target.span,
1201
+ placeholder: target.name
1202
+ };
1203
+ case "recipe-name": return {
1204
+ identity: {
1205
+ kind: "recipe",
1206
+ name: target.recipe.name
1207
+ },
1208
+ span: target.recipe.nameSpan,
1209
+ placeholder: target.recipe.name
1210
+ };
1211
+ case "keyframes-name": return {
1212
+ identity: {
1213
+ kind: "keyframes",
1214
+ name: target.keyframes.name
1215
+ },
1216
+ span: target.keyframes.nameSpan,
1217
+ placeholder: target.keyframes.name
1218
+ };
1219
+ case "variant-name": return {
1220
+ identity: {
1221
+ kind: "variant",
1222
+ component: target.component,
1223
+ name: target.variant.name
1224
+ },
1225
+ span: target.variant.nameSpan,
1226
+ placeholder: target.variant.name
1227
+ };
1228
+ case "variant-value-name": return {
1229
+ identity: {
1230
+ kind: "variant-value",
1231
+ component: target.component,
1232
+ variant: target.variant.name,
1233
+ name: target.value.name
1234
+ },
1235
+ span: target.value.nameSpan,
1236
+ placeholder: target.value.name
1237
+ };
1238
+ case "variant-setting":
1239
+ if (target.part === "variant") return {
1240
+ identity: {
1241
+ kind: "variant",
1242
+ component: target.component,
1243
+ name: target.entry.variant
1244
+ },
1245
+ span: target.entry.variantSpan,
1246
+ placeholder: target.entry.variant
1247
+ };
1248
+ return {
1249
+ identity: {
1250
+ kind: "variant-value",
1251
+ component: target.component,
1252
+ variant: target.entry.variant,
1253
+ name: target.entry.value
1254
+ },
1255
+ span: target.entry.valueSpan,
1256
+ placeholder: target.entry.value
1257
+ };
1258
+ case "slot-name": return {
1259
+ identity: {
1260
+ kind: "slot",
1261
+ component: target.component,
1262
+ name: target.name
1263
+ },
1264
+ span: target.span,
1265
+ placeholder: target.name
1266
+ };
1267
+ default: return null;
1268
+ }
1269
+ }
1270
+ function editsInFile(ast, identity, newName) {
1271
+ const edits = [];
1272
+ switch (identity.kind) {
1273
+ case "token": {
1274
+ const scoped = identity.component;
1275
+ for (const visit of walkTokenEntries(ast)) {
1276
+ if (visit.group.name !== identity.group || visit.entry.name !== identity.name) continue;
1277
+ if (scoped ? visit.component !== scoped : visit.owner !== "theme") continue;
1278
+ edits.push({
1279
+ span: visit.entry.nameSpan,
1280
+ text: newName
1281
+ });
1282
+ }
1283
+ for (const { decl, component } of walkDeclarations(ast)) for (const word of decl.value.words) {
1284
+ if (word.kind !== "ref" || word.group !== identity.group || word.name !== identity.name) continue;
1285
+ const shadowed = component ? findLocalToken(component, identity.group, identity.name) !== null : false;
1286
+ if (scoped ? component !== scoped : shadowed) continue;
1287
+ edits.push({
1288
+ span: word.span,
1289
+ text: `${identity.group}.${newName}`
1290
+ });
1291
+ }
1292
+ if (!scoped) {
1293
+ for (const visit of walkTokenEntries(ast)) for (const word of visit.entry.value.words) if (word.kind === "ref" && word.group === identity.group && word.name === identity.name) edits.push({
1294
+ span: word.span,
1295
+ text: `${identity.group}.${newName}`
1296
+ });
1297
+ }
1298
+ break;
1299
+ }
1300
+ case "recipe":
1301
+ for (const item of ast.items) if (item.kind === "recipe" && item.name === identity.name) edits.push({
1302
+ span: item.nameSpan,
1303
+ text: newName
1304
+ });
1305
+ for (const use of walkUses(ast)) if (use.recipe === identity.name) edits.push({
1306
+ span: use.recipeSpan,
1307
+ text: newName
1308
+ });
1309
+ break;
1310
+ case "keyframes":
1311
+ for (const item of ast.items) if (item.kind === "keyframes" && item.name === identity.name) edits.push({
1312
+ span: item.nameSpan,
1313
+ text: newName
1314
+ });
1315
+ for (const { value } of walkValues(ast)) for (const word of value.words) if (word.kind === "ref" && word.group === "keyframes" && word.name === identity.name) edits.push({
1316
+ span: word.span,
1317
+ text: `keyframes.${newName}`
1318
+ });
1319
+ break;
1320
+ case "variant":
1321
+ case "variant-value":
1322
+ case "slot": {
1323
+ const component = findComponent(ast, identity.component.name);
1324
+ if (component) edits.push(...componentEdits(component, identity, newName));
1325
+ break;
1326
+ }
1327
+ }
1328
+ return edits;
1329
+ }
1330
+ function componentEdits(component, identity, newName) {
1331
+ const edits = [];
1332
+ const settingEntries = (entries) => {
1333
+ for (const entry of entries) {
1334
+ if (identity.kind === "variant" && entry.variant === identity.name) edits.push({
1335
+ span: entry.variantSpan,
1336
+ text: newName
1337
+ });
1338
+ if (identity.kind === "variant-value" && entry.variant === identity.variant && entry.value === identity.name) edits.push({
1339
+ span: entry.valueSpan,
1340
+ text: newName
1341
+ });
1342
+ }
1343
+ };
1344
+ const slotName = (name, span) => {
1345
+ if (identity.kind === "slot" && name === identity.name) edits.push({
1346
+ span,
1347
+ text: newName
1348
+ });
1349
+ };
1350
+ for (const item of component.items) switch (item.kind) {
1351
+ case "slots":
1352
+ for (const slot of item.slots) slotName(slot.name, slot.nameSpan);
1353
+ break;
1354
+ case "base":
1355
+ for (const part of item.body.items) {
1356
+ if (part.kind === "slotblock") slotName(part.name, part.nameSpan);
1357
+ if (part.kind === "state") for (const slot of part.slots) slotName(slot.name, slot.nameSpan);
1358
+ }
1359
+ break;
1360
+ case "variants":
1361
+ for (const variant of item.variants) {
1362
+ if (identity.kind === "variant" && variant.name === identity.name) edits.push({
1363
+ span: variant.nameSpan,
1364
+ text: newName
1365
+ });
1366
+ for (const value of variant.values) {
1367
+ if (identity.kind === "variant-value" && variant.name === identity.variant && value.name === identity.name) edits.push({
1368
+ span: value.nameSpan,
1369
+ text: newName
1370
+ });
1371
+ for (const part of value.body.items) {
1372
+ if (part.kind === "slotblock") slotName(part.name, part.nameSpan);
1373
+ if (part.kind === "state") for (const slot of part.slots) slotName(slot.name, slot.nameSpan);
1374
+ }
1375
+ }
1376
+ }
1377
+ break;
1378
+ case "defaults":
1379
+ settingEntries(item.entries);
1380
+ break;
1381
+ case "responsive":
1382
+ case "container":
1383
+ for (const entry of item.entries) settingEntries(entry.variants);
1384
+ break;
1385
+ case "compound":
1386
+ settingEntries(item.matchers);
1387
+ for (const slot of item.slots) slotName(slot.name, slot.nameSpan);
1388
+ break;
1389
+ }
1390
+ return edits;
1391
+ }
1392
+ function findComponent(ast, name) {
1393
+ for (const item of ast.items) if (item.kind === "component" && item.name === name) return item;
1394
+ return null;
1395
+ }
1396
+ function lspRange(index, span) {
1397
+ const range = index.spanToRange(span);
1398
+ return {
1399
+ start: {
1400
+ line: range.start.line - 1,
1401
+ character: range.start.col - 1
1402
+ },
1403
+ end: {
1404
+ line: range.end.line - 1,
1405
+ character: range.end.col - 1
1406
+ }
1407
+ };
1408
+ }
1409
+ const SKIP_DIRS = new Set([
1410
+ "node_modules",
1411
+ "dist",
1412
+ ".git",
1413
+ "build",
1414
+ "coverage"
1415
+ ]);
1416
+ function listArvFiles(root) {
1417
+ const out = [];
1418
+ const walk = (dir) => {
1419
+ let entries;
1420
+ try {
1421
+ entries = node_fs.default.readdirSync(dir, { withFileTypes: true });
1422
+ } catch {
1423
+ return;
1424
+ }
1425
+ for (const entry of entries) if (entry.isDirectory()) {
1426
+ if (!SKIP_DIRS.has(entry.name)) walk(node_path.default.join(dir, entry.name));
1427
+ } else if (entry.name.endsWith(".arv")) out.push(node_path.default.join(dir, entry.name));
1428
+ };
1429
+ walk(root);
1430
+ return out;
1431
+ }
1432
+ function readFileOr(file) {
1433
+ try {
1434
+ return node_fs.default.readFileSync(file, "utf8");
1435
+ } catch {
1436
+ return null;
1437
+ }
1438
+ }
1439
+ //#endregion
1440
+ //#region src/symbols.ts
1441
+ function getDocumentSymbols(analysis) {
1442
+ const symbol = (name, kind, span, nameSpan, children, detail) => {
1443
+ const full = analysis.index.spanToRange(span);
1444
+ const sel = analysis.index.spanToRange(nameSpan);
1445
+ return {
1446
+ name,
1447
+ kind,
1448
+ detail,
1449
+ range: {
1450
+ start: {
1451
+ line: full.start.line - 1,
1452
+ character: full.start.col - 1
1453
+ },
1454
+ end: {
1455
+ line: full.end.line - 1,
1456
+ character: full.end.col - 1
1457
+ }
1458
+ },
1459
+ selectionRange: {
1460
+ start: {
1461
+ line: sel.start.line - 1,
1462
+ character: sel.start.col - 1
1463
+ },
1464
+ end: {
1465
+ line: sel.end.line - 1,
1466
+ character: sel.end.col - 1
1467
+ }
1468
+ },
1469
+ children
1470
+ };
1471
+ };
1472
+ const out = [];
1473
+ for (const item of analysis.ast.items) switch (item.kind) {
1474
+ case "theme": {
1475
+ const groups = item.groups.map((group) => symbol(group.name, vscode_languageserver.SymbolKind.Namespace, group.span, group.nameSpan, group.entries.map((entry) => symbol(entry.name, vscode_languageserver.SymbolKind.Constant, entry.span, entry.nameSpan, void 0, entry.value.text))));
1476
+ out.push(symbol("theme", vscode_languageserver.SymbolKind.Module, item.span, item.span, groups));
1477
+ break;
1478
+ }
1479
+ case "global":
1480
+ out.push(symbol("global", vscode_languageserver.SymbolKind.Module, item.span, item.span));
1481
+ break;
1482
+ case "recipe":
1483
+ out.push(symbol(item.name, vscode_languageserver.SymbolKind.Function, item.span, item.nameSpan, void 0, "recipe"));
1484
+ break;
1485
+ case "keyframes":
1486
+ out.push(symbol(item.name, vscode_languageserver.SymbolKind.Event, item.span, item.nameSpan, void 0, "keyframes"));
1487
+ break;
1488
+ case "styledecl":
1489
+ out.push(symbol(item.name, vscode_languageserver.SymbolKind.Constant, item.span, item.nameSpan, void 0, "style"));
1490
+ break;
1491
+ case "component": {
1492
+ const children = [];
1493
+ for (const part of item.items) {
1494
+ if (part.kind === "slots") for (const slot of part.slots) children.push(symbol(slot.name, vscode_languageserver.SymbolKind.Field, slot.span, slot.nameSpan, void 0, "slot"));
1495
+ if (part.kind === "variants") for (const variant of part.variants) children.push(symbol(variant.name, vscode_languageserver.SymbolKind.Enum, variant.span, variant.nameSpan, variant.values.map((value) => symbol(value.name, vscode_languageserver.SymbolKind.EnumMember, value.span, value.nameSpan)), "variant"));
1496
+ if (part.kind === "tokens") for (const group of part.groups) children.push(symbol(group.name, vscode_languageserver.SymbolKind.Namespace, group.span, group.nameSpan, group.entries.map((entry) => symbol(entry.name, vscode_languageserver.SymbolKind.Constant, entry.span, entry.nameSpan, void 0, entry.value.text)), "local tokens"));
1497
+ }
1498
+ out.push(symbol(item.name, vscode_languageserver.SymbolKind.Class, item.span, item.nameSpan, children, "component"));
1499
+ break;
1500
+ }
1501
+ }
1502
+ return out;
1503
+ }
1504
+ //#endregion
1505
+ //#region src/workspace.ts
1506
+ var WorkspaceState = class {
1507
+ root;
1508
+ /** themePath → loaded theme (null = path unreadable). */
1509
+ themes = /* @__PURE__ */ new Map();
1510
+ /** directory → resolved theme path for documents in it (null = none found). */
1511
+ themeByDir = /* @__PURE__ */ new Map();
1512
+ constructor(root) {
1513
+ this.root = root;
1514
+ }
1515
+ /**
1516
+ * Nearest theme for a document: starting at its directory and walking up to
1517
+ * the workspace root, check `theme.arv` then `src/theme.arv` at each level.
1518
+ * Supports monorepos with one theme per app/package.
1519
+ */
1520
+ themePathFor(file) {
1521
+ const resolved = node_path.default.resolve(file);
1522
+ if (node_path.default.basename(resolved) === "theme.arv") return resolved;
1523
+ let dir = node_path.default.dirname(resolved);
1524
+ const visited = [];
1525
+ let result = null;
1526
+ for (;;) {
1527
+ const cached = this.themeByDir.get(dir);
1528
+ if (cached !== void 0) {
1529
+ result = cached;
1530
+ break;
1531
+ }
1532
+ visited.push(dir);
1533
+ const direct = node_path.default.join(dir, "theme.arv");
1534
+ if (node_fs.default.existsSync(direct)) {
1535
+ result = direct;
1536
+ break;
1537
+ }
1538
+ const conventional = node_path.default.join(dir, "src", "theme.arv");
1539
+ if (node_fs.default.existsSync(conventional)) {
1540
+ result = conventional;
1541
+ break;
1542
+ }
1543
+ const parent = node_path.default.dirname(dir);
1544
+ if (dir === node_path.default.resolve(this.root) || parent === dir) {
1545
+ result = null;
1546
+ break;
1547
+ }
1548
+ dir = parent;
1549
+ }
1550
+ for (const d of visited) this.themeByDir.set(d, result);
1551
+ return result;
1552
+ }
1553
+ themeFor(file) {
1554
+ const themePath = this.themePathFor(file);
1555
+ if (!themePath) return null;
1556
+ let info = this.themes.get(themePath);
1557
+ if (info === void 0) {
1558
+ info = this.loadTheme(themePath);
1559
+ this.themes.set(themePath, info);
1560
+ }
1561
+ return info;
1562
+ }
1563
+ /** Shared env for a document — undefined for the theme file itself. */
1564
+ envFor(file) {
1565
+ const theme = this.themeFor(file);
1566
+ if (!theme || theme.path === node_path.default.resolve(file)) return void 0;
1567
+ return theme.env;
1568
+ }
1569
+ loadTheme(themePath) {
1570
+ let source;
1571
+ try {
1572
+ source = node_fs.default.readFileSync(themePath, "utf8");
1573
+ } catch {
1574
+ return null;
1575
+ }
1576
+ const result = (0, _arviahq_compiler.analyze)(source, { filename: themePath });
1577
+ return {
1578
+ path: themePath,
1579
+ env: result.diagnostics.some((d) => d.severity === "error") ? void 0 : result.env,
1580
+ ast: result.ast,
1581
+ source,
1582
+ index: new _arviahq_compiler.LineIndex(source)
1583
+ };
1584
+ }
1585
+ /** Drops caches for a changed/created/deleted .arv file. */
1586
+ invalidate(file) {
1587
+ this.themes.delete(node_path.default.resolve(file));
1588
+ this.themeByDir.clear();
1589
+ }
1590
+ invalidateAll() {
1591
+ this.themes.clear();
1592
+ this.themeByDir.clear();
1593
+ }
1594
+ };
1595
+ function workspaceRootFor(file) {
1596
+ let dir = node_path.default.dirname(file);
1597
+ for (;;) {
1598
+ if (node_fs.default.existsSync(node_path.default.join(dir, "package.json"))) return dir;
1599
+ const parent = node_path.default.dirname(dir);
1600
+ if (parent === dir) return node_path.default.dirname(file);
1601
+ dir = parent;
1602
+ }
1603
+ }
1604
+ //#endregion
1605
+ //#region src/server.ts
1606
+ const DIAGNOSTICS_DEBOUNCE_MS = 200;
1607
+ const connection = (0, vscode_languageserver_node_js.createConnection)();
1608
+ const documents = new vscode_languageserver_node_js.TextDocuments(vscode_languageserver_textdocument.TextDocument);
1609
+ const workspaces = /* @__PURE__ */ new Map();
1610
+ function pathToFileUri(file) {
1611
+ return (0, node_url.pathToFileURL)(file).toString();
1612
+ }
1613
+ function workspaceFor(uri) {
1614
+ const root = workspaceRootFor(fileForUri(uri));
1615
+ let ws = workspaces.get(root);
1616
+ if (!ws) {
1617
+ ws = new WorkspaceState(root);
1618
+ workspaces.set(root, ws);
1619
+ }
1620
+ return ws;
1621
+ }
1622
+ const store = new DocumentStore(workspaceFor);
1623
+ const diagnosticTimers = /* @__PURE__ */ new Map();
1624
+ function publishDiagnostics(doc) {
1625
+ const analysis = store.analysisFor(doc);
1626
+ connection.sendDiagnostics({
1627
+ uri: doc.uri,
1628
+ diagnostics: toLspDiagnostics(analysis)
1629
+ });
1630
+ }
1631
+ function scheduleDiagnostics(doc, immediate = false) {
1632
+ const pending = diagnosticTimers.get(doc.uri);
1633
+ if (pending) clearTimeout(pending);
1634
+ if (immediate) {
1635
+ publishDiagnostics(doc);
1636
+ return;
1637
+ }
1638
+ diagnosticTimers.set(doc.uri, setTimeout(() => {
1639
+ diagnosticTimers.delete(doc.uri);
1640
+ const current = documents.get(doc.uri);
1641
+ if (current) publishDiagnostics(current);
1642
+ }, DIAGNOSTICS_DEBOUNCE_MS));
1643
+ }
1644
+ connection.onInitialize((_params) => {
1645
+ return { capabilities: {
1646
+ textDocumentSync: vscode_languageserver_node_js.TextDocumentSyncKind.Incremental,
1647
+ completionProvider: { triggerCharacters: [
1648
+ ":",
1649
+ ".",
1650
+ "@",
1651
+ " "
1652
+ ] },
1653
+ hoverProvider: true,
1654
+ definitionProvider: true,
1655
+ documentSymbolProvider: true,
1656
+ colorProvider: true,
1657
+ inlayHintProvider: true,
1658
+ renameProvider: { prepareProvider: true }
1659
+ } };
1660
+ });
1661
+ documents.onDidOpen((event) => {
1662
+ scheduleDiagnostics(event.document, true);
1663
+ });
1664
+ documents.onDidChangeContent((event) => {
1665
+ scheduleDiagnostics(event.document);
1666
+ });
1667
+ documents.onDidSave((event) => {
1668
+ scheduleDiagnostics(event.document, true);
1669
+ });
1670
+ documents.onDidClose((event) => {
1671
+ const pending = diagnosticTimers.get(event.document.uri);
1672
+ if (pending) clearTimeout(pending);
1673
+ diagnosticTimers.delete(event.document.uri);
1674
+ store.invalidate(event.document.uri);
1675
+ connection.sendDiagnostics({
1676
+ uri: event.document.uri,
1677
+ diagnostics: []
1678
+ });
1679
+ });
1680
+ connection.onDidChangeWatchedFiles((params) => {
1681
+ for (const change of params.changes) {
1682
+ const file = fileForUri(change.uri);
1683
+ for (const ws of workspaces.values()) ws.invalidate(file);
1684
+ }
1685
+ store.invalidateAll();
1686
+ for (const doc of documents.all()) scheduleDiagnostics(doc);
1687
+ });
1688
+ connection.onCompletion((params) => {
1689
+ const doc = documents.get(params.textDocument.uri);
1690
+ if (!doc) return [];
1691
+ return getCompletions(store.analysisFor(doc), doc.offsetAt(params.position));
1692
+ });
1693
+ connection.onHover((params) => {
1694
+ const doc = documents.get(params.textDocument.uri);
1695
+ if (!doc) return null;
1696
+ const ws = workspaceFor(params.textDocument.uri);
1697
+ return getHover(store.analysisFor(doc), doc.offsetAt(params.position), ws);
1698
+ });
1699
+ connection.onDefinition((params) => {
1700
+ const doc = documents.get(params.textDocument.uri);
1701
+ if (!doc) return null;
1702
+ const ws = workspaceFor(params.textDocument.uri);
1703
+ return getDefinition(store.analysisFor(doc), doc.offsetAt(params.position), ws);
1704
+ });
1705
+ connection.onDocumentSymbol((params) => {
1706
+ const doc = documents.get(params.textDocument.uri);
1707
+ if (!doc) return [];
1708
+ return getDocumentSymbols(store.analysisFor(doc));
1709
+ });
1710
+ connection.onDocumentColor((params) => {
1711
+ const doc = documents.get(params.textDocument.uri);
1712
+ if (!doc) return [];
1713
+ return getDocumentColors(store.analysisFor(doc));
1714
+ });
1715
+ connection.onColorPresentation((params) => {
1716
+ const doc = documents.get(params.textDocument.uri);
1717
+ if (!doc) return [];
1718
+ return getColorPresentations(params.color, {
1719
+ start: doc.offsetAt(params.range.start),
1720
+ end: doc.offsetAt(params.range.end)
1721
+ });
1722
+ });
1723
+ connection.languages.inlayHint.on((params) => {
1724
+ const doc = documents.get(params.textDocument.uri);
1725
+ if (!doc) return [];
1726
+ return getInlayHints(store.analysisFor(doc), params.range);
1727
+ });
1728
+ connection.onPrepareRename((params) => {
1729
+ const doc = documents.get(params.textDocument.uri);
1730
+ if (!doc) return null;
1731
+ return prepareRename(store.analysisFor(doc), doc.offsetAt(params.position));
1732
+ });
1733
+ connection.onRenameRequest((params) => {
1734
+ const doc = documents.get(params.textDocument.uri);
1735
+ if (!doc) return null;
1736
+ const ws = workspaceFor(params.textDocument.uri);
1737
+ return getRenameEdits(store.analysisFor(doc), doc.offsetAt(params.position), params.newName, ws, (file) => {
1738
+ const open = documents.get(pathToFileUri(file));
1739
+ return open ? open.getText() : readFileOr(file);
1740
+ });
1741
+ });
1742
+ documents.listen(connection);
1743
+ connection.listen();
1744
+ //#endregion
1745
+
1746
+ //# sourceMappingURL=server.cjs.map