@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.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/bin/arvia-language-server.cjs +2 -0
- package/dist/server.cjs +1746 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +1 -0
- package/package.json +49 -0
package/dist/server.cjs
ADDED
|
@@ -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
|