@zenithbuild/language 0.6.1 → 0.7.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/out/server.mjs ADDED
@@ -0,0 +1,931 @@
1
+ // src/main.ts
2
+ import {
3
+ createConnection,
4
+ DidChangeConfigurationNotification,
5
+ ProposedFeatures,
6
+ TextDocumentSyncKind
7
+ } from "vscode-languageserver/node.js";
8
+ import { TextDocument } from "vscode-languageserver-textdocument";
9
+ import { TextDocuments } from "vscode-languageserver";
10
+
11
+ // src/code-actions.ts
12
+ import {
13
+ CodeActionKind
14
+ } from "vscode-languageserver/node.js";
15
+ var DOM_QUERY_SUPPRESS = "// zen-allow:dom-query explain interop reason";
16
+ var DOM_LISTENER_TODO = "// TODO(zenith): replace addEventListener with zenOn(target, eventName, handler)";
17
+ var DOM_LISTENER_CLEANUP = "// TODO(zenith): register the disposer with ctx.cleanup(...) inside zenMount";
18
+ var DOM_WRAPPER_TODO = "// TODO(zenith): replace this guard with zenWindow() / zenDocument()";
19
+ function getCodeActions(text, uri, diagnostics) {
20
+ const actions = [];
21
+ for (const diagnostic of diagnostics) {
22
+ const code = typeof diagnostic.code === "string" ? diagnostic.code : "";
23
+ if (!code) {
24
+ continue;
25
+ }
26
+ if (code === "ZEN-DOM-QUERY") {
27
+ const action = createDomQuerySuppressAction(text, uri, diagnostic);
28
+ if (action) {
29
+ actions.push(action);
30
+ }
31
+ continue;
32
+ }
33
+ if (code === "ZEN-DOM-LISTENER") {
34
+ const action = createCommentInsertionAction(
35
+ text,
36
+ uri,
37
+ diagnostic,
38
+ [DOM_LISTENER_TODO, DOM_LISTENER_CLEANUP],
39
+ "Zenith: Add zenOn migration note"
40
+ );
41
+ if (action) {
42
+ actions.push(action);
43
+ }
44
+ continue;
45
+ }
46
+ if (code === "ZEN-DOM-WRAPPER") {
47
+ const replacementAction = createGlobalThisReplacementAction(text, uri, diagnostic);
48
+ if (replacementAction) {
49
+ actions.push(replacementAction);
50
+ }
51
+ const noteAction = createCommentInsertionAction(
52
+ text,
53
+ uri,
54
+ diagnostic,
55
+ [DOM_WRAPPER_TODO],
56
+ "Zenith: Add zenWindow/zenDocument migration note"
57
+ );
58
+ if (noteAction) {
59
+ actions.push(noteAction);
60
+ }
61
+ }
62
+ }
63
+ return actions;
64
+ }
65
+ function createDomQuerySuppressAction(text, uri, diagnostic) {
66
+ const lineIndex = diagnostic.range.start.line;
67
+ if (lineIndex > 0) {
68
+ const previousLine = getLine(text, lineIndex - 1);
69
+ if (previousLine?.includes("zen-allow:dom-query")) {
70
+ return null;
71
+ }
72
+ }
73
+ return createCommentInsertionAction(
74
+ text,
75
+ uri,
76
+ diagnostic,
77
+ [DOM_QUERY_SUPPRESS],
78
+ "Zenith: Suppress DOM query with zen-allow comment"
79
+ );
80
+ }
81
+ function createGlobalThisReplacementAction(text, uri, diagnostic) {
82
+ const lineIndex = diagnostic.range.start.line;
83
+ const line = getLine(text, lineIndex);
84
+ if (!line) {
85
+ return null;
86
+ }
87
+ if (line.includes("globalThis.window")) {
88
+ return createReplacementAction(
89
+ uri,
90
+ diagnostic,
91
+ {
92
+ start: { line: lineIndex, character: line.indexOf("globalThis.window") },
93
+ end: { line: lineIndex, character: line.indexOf("globalThis.window") + "globalThis.window".length }
94
+ },
95
+ "zenWindow()",
96
+ "Zenith: Replace globalThis.window with zenWindow()"
97
+ );
98
+ }
99
+ if (line.includes("globalThis.document")) {
100
+ return createReplacementAction(
101
+ uri,
102
+ diagnostic,
103
+ {
104
+ start: { line: lineIndex, character: line.indexOf("globalThis.document") },
105
+ end: { line: lineIndex, character: line.indexOf("globalThis.document") + "globalThis.document".length }
106
+ },
107
+ "zenDocument()",
108
+ "Zenith: Replace globalThis.document with zenDocument()"
109
+ );
110
+ }
111
+ return null;
112
+ }
113
+ function createCommentInsertionAction(text, uri, diagnostic, commentLines, title) {
114
+ const lineIndex = diagnostic.range.start.line;
115
+ const line = getLine(text, lineIndex);
116
+ if (line === void 0) {
117
+ return null;
118
+ }
119
+ const indent = line.match(/^\s*/)?.[0] ?? "";
120
+ const previousLine = lineIndex > 0 ? getLine(text, lineIndex - 1) : void 0;
121
+ if (previousLine && commentLines.every((commentLine) => previousLine.includes(commentLine.trim()))) {
122
+ return null;
123
+ }
124
+ const newText = commentLines.map((commentLine) => `${indent}${commentLine}`).join("\n") + "\n";
125
+ return createReplacementAction(
126
+ uri,
127
+ diagnostic,
128
+ {
129
+ start: { line: lineIndex, character: 0 },
130
+ end: { line: lineIndex, character: 0 }
131
+ },
132
+ newText,
133
+ title
134
+ );
135
+ }
136
+ function createReplacementAction(uri, diagnostic, range, newText, title) {
137
+ const edits = [{ range, newText }];
138
+ const documentEdit = {
139
+ textDocument: {
140
+ uri,
141
+ version: null
142
+ },
143
+ edits
144
+ };
145
+ return {
146
+ title,
147
+ kind: CodeActionKind.QuickFix,
148
+ diagnostics: [diagnostic],
149
+ edit: {
150
+ documentChanges: [documentEdit]
151
+ }
152
+ };
153
+ }
154
+ function getLine(text, lineIndex) {
155
+ const lines = text.split("\n");
156
+ return lines[lineIndex];
157
+ }
158
+
159
+ // src/completions.ts
160
+ import {
161
+ CompletionItemKind,
162
+ InsertTextFormat,
163
+ MarkupKind
164
+ } from "vscode-languageserver/node.js";
165
+
166
+ // src/docs.ts
167
+ var DOCS_BASE_URL = "https://github.com/zenithbuild/framework/blob/master/";
168
+ function createEventDoc(label, summary, example) {
169
+ return {
170
+ label,
171
+ summary,
172
+ example,
173
+ docPath: "docs/documentation/syntax/events.md"
174
+ };
175
+ }
176
+ var SYMBOL_DOCS = {
177
+ zenEffect: {
178
+ label: "zenEffect",
179
+ summary: "Reactive side effect that re-runs when its dependencies change.",
180
+ example: "zenEffect(() => {\n count.get()\n})",
181
+ docPath: "docs/documentation/reactivity/effects-vs-mount.md"
182
+ },
183
+ zenMount: {
184
+ label: "zenMount",
185
+ summary: "Mount-time lifecycle boundary for DOM effects and cleanup registration.",
186
+ example: "zenMount((ctx) => {\n ctx.cleanup(offResize)\n})",
187
+ docPath: "docs/documentation/reactivity/effects-vs-mount.md"
188
+ },
189
+ state: {
190
+ label: "state",
191
+ summary: "Reactive binding for values that directly drive DOM expressions.",
192
+ example: "state open = false\nfunction toggle() { open = !open }",
193
+ docPath: "docs/documentation/reactivity/reactivity-model.md"
194
+ },
195
+ signal: {
196
+ label: "signal",
197
+ summary: "Stable reactive container with explicit get() and set() operations.",
198
+ example: "const count = signal(0)\ncount.set(count.get() + 1)",
199
+ docPath: "docs/documentation/reactivity/reactivity-model.md"
200
+ },
201
+ ref: {
202
+ label: "ref",
203
+ summary: "Typed DOM handle for measurements, focus, animation, and mount-time access.",
204
+ example: "const shell = ref<HTMLDivElement>()",
205
+ docPath: "docs/documentation/reactivity/reactivity-model.md"
206
+ },
207
+ zenWindow: {
208
+ label: "zenWindow",
209
+ summary: "SSR-safe window accessor that returns null when the browser environment is absent.",
210
+ example: "const win = zenWindow()\nif (!win) return",
211
+ docPath: "docs/documentation/reactivity/dom-and-environment.md"
212
+ },
213
+ zenDocument: {
214
+ label: "zenDocument",
215
+ summary: "SSR-safe document accessor for global DOM wiring inside mount-time logic.",
216
+ example: "const doc = zenDocument()\nif (!doc) return",
217
+ docPath: "docs/documentation/reactivity/dom-and-environment.md"
218
+ },
219
+ zenOn: {
220
+ label: "zenOn",
221
+ summary: "Canonical event subscription primitive that returns a disposer.",
222
+ example: "const off = zenOn(doc, 'keydown', handleKey)\nctx.cleanup(off)",
223
+ docPath: "docs/documentation/reactivity/dom-and-environment.md"
224
+ },
225
+ zenResize: {
226
+ label: "zenResize",
227
+ summary: "Canonical window resize primitive for reactive viewport updates.",
228
+ example: "const off = zenResize(({ w, h }) => viewport.set({ w, h }))",
229
+ docPath: "docs/documentation/reactivity/dom-and-environment.md"
230
+ },
231
+ collectRefs: {
232
+ label: "collectRefs",
233
+ summary: "Deterministic multi-node collection helper that replaces selector scans.",
234
+ example: "const nodes = collectRefs(linkRefA, linkRefB, linkRefC)",
235
+ docPath: "docs/documentation/reactivity/dom-and-environment.md"
236
+ },
237
+ "on:esc": {
238
+ ...createEventDoc(
239
+ "on:esc",
240
+ "Escape-filtered keydown alias that routes through Zenith\u2019s document-level esc dispatch.",
241
+ "<button on:esc={closeMenu}>Close</button>"
242
+ )
243
+ },
244
+ "on:hoverin": {
245
+ ...createEventDoc(
246
+ "on:hoverin",
247
+ "Hover sugar alias for pointerenter when hover logic needs real event wiring.",
248
+ "<div on:hoverin={handleEnter}></div>"
249
+ )
250
+ },
251
+ "on:hoverout": {
252
+ ...createEventDoc(
253
+ "on:hoverout",
254
+ "Hover sugar alias for pointerleave when hover logic needs real event wiring.",
255
+ "<div on:hoverout={handleLeave}></div>"
256
+ )
257
+ },
258
+ "on:click": createEventDoc(
259
+ "on:click",
260
+ "Canonical mouse click binding in Zenith\u2019s universal on:* event model.",
261
+ "<button on:click={handleClick}>Press</button>"
262
+ ),
263
+ "on:doubleclick": createEventDoc(
264
+ "on:doubleclick",
265
+ "Canonical alias that normalizes doubleclick bindings to the emitted dblclick event.",
266
+ "<button on:doubleclick={handleDoubleClick}>Press</button>"
267
+ ),
268
+ "on:dblclick": createEventDoc(
269
+ "on:dblclick",
270
+ "Canonical double-click binding using the normalized dblclick event name.",
271
+ "<button on:dblclick={handleDoubleClick}>Press</button>"
272
+ ),
273
+ "on:keydown": createEventDoc(
274
+ "on:keydown",
275
+ "Canonical keyboard keydown binding in Zenith\u2019s universal on:* event model.",
276
+ "<div on:keydown={handleKeydown}></div>"
277
+ ),
278
+ "on:keyup": createEventDoc(
279
+ "on:keyup",
280
+ "Canonical keyboard keyup binding in Zenith\u2019s universal on:* event model.",
281
+ "<div on:keyup={handleKeyup}></div>"
282
+ ),
283
+ "on:submit": createEventDoc(
284
+ "on:submit",
285
+ "Canonical form submit binding in Zenith\u2019s universal on:* event model.",
286
+ "<form on:submit={handleSubmit}></form>"
287
+ ),
288
+ "on:input": createEventDoc(
289
+ "on:input",
290
+ "Canonical input binding for immediate form-value updates.",
291
+ "<input on:input={handleInput} />"
292
+ ),
293
+ "on:change": createEventDoc(
294
+ "on:change",
295
+ "Canonical change binding for committed form-value updates.",
296
+ "<input on:change={handleChange} />"
297
+ ),
298
+ "on:focus": createEventDoc(
299
+ "on:focus",
300
+ "Canonical focus binding for element focus transitions.",
301
+ "<input on:focus={handleFocus} />"
302
+ ),
303
+ "on:blur": createEventDoc(
304
+ "on:blur",
305
+ "Canonical blur binding for element focus exit transitions.",
306
+ "<input on:blur={handleBlur} />"
307
+ ),
308
+ "on:pointerdown": createEventDoc(
309
+ "on:pointerdown",
310
+ "Canonical pointerdown binding from the recommended pointer event set.",
311
+ "<div on:pointerdown={handlePointerDown}></div>"
312
+ ),
313
+ "on:pointerup": createEventDoc(
314
+ "on:pointerup",
315
+ "Canonical pointerup binding from the recommended pointer event set.",
316
+ "<div on:pointerup={handlePointerUp}></div>"
317
+ ),
318
+ "on:pointermove": createEventDoc(
319
+ "on:pointermove",
320
+ "Canonical pointermove binding from the recommended pointer event set.",
321
+ "<svg on:pointermove={handlePointerMove}></svg>"
322
+ ),
323
+ "on:pointerenter": createEventDoc(
324
+ "on:pointerenter",
325
+ "Direct pointerenter binding remains fully supported alongside on:hoverin.",
326
+ "<div on:pointerenter={handleEnter}></div>"
327
+ ),
328
+ "on:pointerleave": createEventDoc(
329
+ "on:pointerleave",
330
+ "Direct pointerleave binding remains fully supported alongside on:hoverout.",
331
+ "<div on:pointerleave={handleLeave}></div>"
332
+ ),
333
+ "on:dragstart": createEventDoc(
334
+ "on:dragstart",
335
+ "Canonical dragstart binding from Zenith\u2019s recommended drag event set.",
336
+ '<div draggable="true" on:dragstart={handleDragStart}></div>'
337
+ ),
338
+ "on:dragover": createEventDoc(
339
+ "on:dragover",
340
+ "Canonical dragover binding from Zenith\u2019s recommended drag event set.",
341
+ "<div on:dragover={handleDragOver}></div>"
342
+ ),
343
+ "on:drop": createEventDoc(
344
+ "on:drop",
345
+ "Canonical drop binding from Zenith\u2019s recommended drag event set.",
346
+ "<div on:drop={handleDrop}></div>"
347
+ ),
348
+ "on:scroll": createEventDoc(
349
+ "on:scroll",
350
+ "Canonical scroll binding from Zenith\u2019s recommended event set.",
351
+ "<div on:scroll={handleScroll}></div>"
352
+ ),
353
+ "on:contextmenu": createEventDoc(
354
+ "on:contextmenu",
355
+ "Canonical contextmenu binding from Zenith\u2019s recommended mouse event set.",
356
+ "<div on:contextmenu={handleContextMenu}></div>"
357
+ )
358
+ };
359
+ var canonicalScriptSymbols = [
360
+ "zenMount",
361
+ "zenEffect",
362
+ "state",
363
+ "signal",
364
+ "ref",
365
+ "zenWindow",
366
+ "zenDocument",
367
+ "zenOn",
368
+ "zenResize",
369
+ "collectRefs"
370
+ ];
371
+ var canonicalEventAttributes = [
372
+ "on:click",
373
+ "on:doubleclick",
374
+ "on:dblclick",
375
+ "on:keydown",
376
+ "on:keyup",
377
+ "on:esc",
378
+ "on:submit",
379
+ "on:input",
380
+ "on:change",
381
+ "on:focus",
382
+ "on:blur",
383
+ "on:pointerdown",
384
+ "on:pointerup",
385
+ "on:pointermove",
386
+ "on:pointerenter",
387
+ "on:pointerleave",
388
+ "on:hoverin",
389
+ "on:hoverout",
390
+ "on:dragstart",
391
+ "on:dragover",
392
+ "on:drop",
393
+ "on:scroll",
394
+ "on:contextmenu"
395
+ ];
396
+ function getSymbolDoc(symbol) {
397
+ return SYMBOL_DOCS[symbol];
398
+ }
399
+ function getDocUrl(docPath) {
400
+ return `${DOCS_BASE_URL}${docPath}`;
401
+ }
402
+
403
+ // src/text.ts
404
+ var wordPattern = /[A-Za-z0-9_:$.-]/;
405
+ function offsetAt(text, position) {
406
+ const lines = text.split("\n");
407
+ const lineIndex = Math.max(0, Math.min(position.line, lines.length - 1));
408
+ let offset = 0;
409
+ for (let index = 0; index < lineIndex; index += 1) {
410
+ offset += lines[index].length + 1;
411
+ }
412
+ return offset + Math.max(0, Math.min(position.character, lines[lineIndex].length));
413
+ }
414
+ function getWordRange(text, position) {
415
+ const offset = offsetAt(text, position);
416
+ let start = offset;
417
+ let end = offset;
418
+ while (start > 0 && wordPattern.test(text[start - 1] ?? "")) {
419
+ start -= 1;
420
+ }
421
+ while (end < text.length && wordPattern.test(text[end] ?? "")) {
422
+ end += 1;
423
+ }
424
+ if (start === end) {
425
+ return void 0;
426
+ }
427
+ return {
428
+ start: positionAt(text, start),
429
+ end: positionAt(text, end)
430
+ };
431
+ }
432
+ function getWord(text, position) {
433
+ const range = getWordRange(text, position);
434
+ if (!range) {
435
+ return "";
436
+ }
437
+ const start = offsetAt(text, range.start);
438
+ const end = offsetAt(text, range.end);
439
+ return text.slice(start, end);
440
+ }
441
+ function getCompletionPrefix(text, position) {
442
+ const offset = offsetAt(text, position);
443
+ let start = offset;
444
+ while (start > 0 && wordPattern.test(text[start - 1] ?? "")) {
445
+ start -= 1;
446
+ }
447
+ return text.slice(start, offset);
448
+ }
449
+ function getCompletionContext(text, position) {
450
+ const offset = offsetAt(text, position);
451
+ if (isInsideScript(text, offset)) {
452
+ return "script";
453
+ }
454
+ if (isInsideBraces(text, offset)) {
455
+ return "expression";
456
+ }
457
+ if (isInsideTag(text, offset)) {
458
+ return "attribute";
459
+ }
460
+ return "markup";
461
+ }
462
+ function isInsideScript(text, offset) {
463
+ const lower = text.slice(0, offset).toLowerCase();
464
+ const lastOpen = lower.lastIndexOf("<script");
465
+ const lastClose = lower.lastIndexOf("</script");
466
+ if (lastOpen === -1 || lastOpen < lastClose) {
467
+ return false;
468
+ }
469
+ const openEnd = lower.indexOf(">", lastOpen);
470
+ return openEnd !== -1 && openEnd < offset;
471
+ }
472
+ function isInsideTag(text, offset) {
473
+ const before = text.slice(0, offset);
474
+ const lastOpen = before.lastIndexOf("<");
475
+ const lastClose = before.lastIndexOf(">");
476
+ return lastOpen > lastClose;
477
+ }
478
+ function isInsideBraces(text, offset) {
479
+ let depth = 0;
480
+ let inSingle = false;
481
+ let inDouble = false;
482
+ let inTemplate = false;
483
+ let escaped = false;
484
+ for (let index = 0; index < offset; index += 1) {
485
+ const char = text[index] ?? "";
486
+ if (escaped) {
487
+ escaped = false;
488
+ continue;
489
+ }
490
+ if (char === "\\") {
491
+ escaped = true;
492
+ continue;
493
+ }
494
+ if (inSingle) {
495
+ if (char === "'") {
496
+ inSingle = false;
497
+ }
498
+ continue;
499
+ }
500
+ if (inDouble) {
501
+ if (char === '"') {
502
+ inDouble = false;
503
+ }
504
+ continue;
505
+ }
506
+ if (inTemplate) {
507
+ if (char === "`") {
508
+ inTemplate = false;
509
+ }
510
+ continue;
511
+ }
512
+ if (char === "'") {
513
+ inSingle = true;
514
+ continue;
515
+ }
516
+ if (char === '"') {
517
+ inDouble = true;
518
+ continue;
519
+ }
520
+ if (char === "`") {
521
+ inTemplate = true;
522
+ continue;
523
+ }
524
+ if (char === "{") {
525
+ depth += 1;
526
+ continue;
527
+ }
528
+ if (char === "}") {
529
+ depth = Math.max(0, depth - 1);
530
+ }
531
+ }
532
+ return depth > 0;
533
+ }
534
+ function positionAt(text, offset) {
535
+ const safeOffset = Math.max(0, Math.min(offset, text.length));
536
+ const slice = text.slice(0, safeOffset);
537
+ const lines = slice.split("\n");
538
+ const line = lines.length - 1;
539
+ const character = lines.at(-1)?.length ?? 0;
540
+ return { line, character };
541
+ }
542
+
543
+ // src/completions.ts
544
+ var blockedFrameworkTokens = ["react", "vue", "svelte", "rfce"];
545
+ function createScriptCompletion(label) {
546
+ const docs = getSymbolDoc(label);
547
+ return {
548
+ label,
549
+ kind: CompletionItemKind.Function,
550
+ detail: docs?.summary ?? "Zenith canonical primitive",
551
+ ...docs ? {
552
+ documentation: {
553
+ kind: MarkupKind.Markdown,
554
+ value: `Docs: [${docs.docPath}](${getDocUrl(docs.docPath)})`
555
+ }
556
+ } : {},
557
+ insertText: label
558
+ };
559
+ }
560
+ function createEventCompletion(label) {
561
+ const docs = getSymbolDoc(label);
562
+ return {
563
+ label,
564
+ kind: CompletionItemKind.Property,
565
+ detail: docs?.summary ?? "Canonical Zenith DOM event binding",
566
+ ...docs ? {
567
+ documentation: {
568
+ kind: MarkupKind.Markdown,
569
+ value: `Docs: [${docs.docPath}](${getDocUrl(docs.docPath)})`
570
+ }
571
+ } : {},
572
+ insertTextFormat: InsertTextFormat.Snippet,
573
+ insertText: `${label}={\${1:handler}}`
574
+ };
575
+ }
576
+ function createPropHandlerCompletion() {
577
+ return {
578
+ label: "onClick={handler}",
579
+ kind: CompletionItemKind.Snippet,
580
+ detail: "Pass handler props through components, then bind them back to on:* in component markup.",
581
+ insertTextFormat: InsertTextFormat.Snippet,
582
+ insertText: "onClick={${1:handler}}"
583
+ };
584
+ }
585
+ function getCompletionItems(text, position) {
586
+ const context = getCompletionContext(text, position);
587
+ const prefix = getCompletionPrefix(text, position).toLowerCase();
588
+ if (context === "script" || context === "expression") {
589
+ return filterFrameworkNoise(canonicalScriptSymbols.map((symbol) => createScriptCompletion(symbol)), prefix);
590
+ }
591
+ if (context === "attribute") {
592
+ const items = canonicalEventAttributes.map((eventName) => createEventCompletion(eventName));
593
+ items.push(createPropHandlerCompletion());
594
+ return filterFrameworkNoise(items, prefix);
595
+ }
596
+ return [];
597
+ }
598
+ function filterFrameworkNoise(items, typedPrefix) {
599
+ if (blockedFrameworkTokens.some((token) => typedPrefix.includes(token))) {
600
+ return items;
601
+ }
602
+ return items.filter((item) => {
603
+ const haystack = [
604
+ item.label,
605
+ typeof item.insertText === "string" ? item.insertText : ""
606
+ ].join(" ").toLowerCase();
607
+ return blockedFrameworkTokens.every((token) => !haystack.includes(token));
608
+ });
609
+ }
610
+
611
+ // src/diagnostics.ts
612
+ import { fileURLToPath } from "node:url";
613
+ import { compile } from "@zenithbuild/compiler";
614
+ import {
615
+ DiagnosticSeverity,
616
+ DiagnosticTag
617
+ } from "vscode-languageserver/node.js";
618
+ async function collectDiagnosticsFromSource(source, filePath, strictDomLints) {
619
+ try {
620
+ const result = compile({ source, filePath });
621
+ if (result.schemaVersion !== 1) {
622
+ return [compilerContractDiagnostic(`Unsupported compiler schemaVersion: ${String(result.schemaVersion)}`)];
623
+ }
624
+ return mapCompilerEnvelopeToDiagnostics(result, strictDomLints);
625
+ } catch (error) {
626
+ return [compilerContractDiagnostic(String(error))];
627
+ }
628
+ }
629
+ function mapCompilerEnvelopeToDiagnostics(result, strictDomLints) {
630
+ const diagnostics = Array.isArray(result.diagnostics) ? result.diagnostics : [];
631
+ if (diagnostics.length > 0) {
632
+ return diagnostics.map((diagnostic) => {
633
+ const tags = mapTags(diagnostic.tags);
634
+ return {
635
+ source: diagnostic.source ?? "zenith",
636
+ code: diagnostic.code,
637
+ message: diagnostic.message,
638
+ severity: resolveSeverity(diagnostic, strictDomLints),
639
+ range: toRange(diagnostic.range),
640
+ ...tags ? { tags } : {}
641
+ };
642
+ });
643
+ }
644
+ const warnings = Array.isArray(result.warnings) ? result.warnings : [];
645
+ return warnings.map((warning) => ({
646
+ source: "zenith",
647
+ code: warning.code,
648
+ message: warning.message,
649
+ severity: resolveSeverity(warning, strictDomLints),
650
+ range: toRange(warning.range)
651
+ }));
652
+ }
653
+ function resolveDocumentPath(uri) {
654
+ if (uri.startsWith("file://")) {
655
+ return fileURLToPath(uri);
656
+ }
657
+ return uri.replace(/^[a-z]+:\/\//i, "/virtual/");
658
+ }
659
+ function resolveSeverity(diagnostic, strictDomLints) {
660
+ if (strictDomLints && diagnostic.code.startsWith("ZEN-DOM-")) {
661
+ return DiagnosticSeverity.Error;
662
+ }
663
+ if (diagnostic.severity === "error") {
664
+ return DiagnosticSeverity.Error;
665
+ }
666
+ if (diagnostic.severity === "hint") {
667
+ return DiagnosticSeverity.Hint;
668
+ }
669
+ if (diagnostic.severity === "information") {
670
+ return DiagnosticSeverity.Information;
671
+ }
672
+ return DiagnosticSeverity.Warning;
673
+ }
674
+ function mapTags(tags) {
675
+ if (!Array.isArray(tags) || tags.length === 0) {
676
+ return void 0;
677
+ }
678
+ return tags.flatMap((tag) => {
679
+ if (tag === "deprecated") {
680
+ return [DiagnosticTag.Deprecated];
681
+ }
682
+ if (tag === "unnecessary") {
683
+ return [DiagnosticTag.Unnecessary];
684
+ }
685
+ return [];
686
+ });
687
+ }
688
+ function toRange(range) {
689
+ if (!range) {
690
+ return {
691
+ start: { line: 0, character: 0 },
692
+ end: { line: 0, character: 1 }
693
+ };
694
+ }
695
+ return {
696
+ start: {
697
+ line: Math.max(0, range.start.line - 1),
698
+ character: Math.max(0, range.start.column - 1)
699
+ },
700
+ end: {
701
+ line: Math.max(0, range.end.line - 1),
702
+ character: Math.max(0, range.end.column - 1)
703
+ }
704
+ };
705
+ }
706
+ function compilerContractDiagnostic(message) {
707
+ return {
708
+ source: "zenith",
709
+ code: "ZENITH-COMPILER",
710
+ message,
711
+ severity: DiagnosticSeverity.Error,
712
+ range: {
713
+ start: { line: 0, character: 0 },
714
+ end: { line: 0, character: 1 }
715
+ }
716
+ };
717
+ }
718
+
719
+ // src/hover.ts
720
+ import { MarkupKind as MarkupKind2 } from "vscode-languageserver/node.js";
721
+ function getHover(text, position) {
722
+ const symbol = getWord(text, position);
723
+ const docs = getSymbolDoc(symbol);
724
+ const range = getWordRange(text, position);
725
+ if (!docs || !range) {
726
+ return null;
727
+ }
728
+ const docsUrl = getDocUrl(docs.docPath);
729
+ const markdown = [
730
+ `**${docs.label}**`,
731
+ "",
732
+ docs.summary,
733
+ "",
734
+ "```ts",
735
+ docs.example,
736
+ "```",
737
+ "",
738
+ `Docs: [${docs.docPath}](${docsUrl})`
739
+ ].join("\n");
740
+ return {
741
+ range,
742
+ contents: {
743
+ kind: MarkupKind2.Markdown,
744
+ value: markdown
745
+ }
746
+ };
747
+ }
748
+
749
+ // src/settings.ts
750
+ var defaultSettings = {
751
+ strictDomLints: false,
752
+ enableFrameworkSnippets: false
753
+ };
754
+ var SettingsStore = class {
755
+ constructor(connection, supportsWorkspaceConfiguration) {
756
+ this.connection = connection;
757
+ this.supportsWorkspaceConfiguration = supportsWorkspaceConfiguration;
758
+ }
759
+ #cache = /* @__PURE__ */ new Map();
760
+ clear(uri) {
761
+ if (uri) {
762
+ this.#cache.delete(uri);
763
+ return;
764
+ }
765
+ this.#cache.clear();
766
+ }
767
+ async get(uri) {
768
+ if (!this.supportsWorkspaceConfiguration) {
769
+ return defaultSettings;
770
+ }
771
+ const cached = this.#cache.get(uri);
772
+ if (cached) {
773
+ return cached;
774
+ }
775
+ const pending = this.load(uri);
776
+ this.#cache.set(uri, pending);
777
+ return pending;
778
+ }
779
+ async load(uri) {
780
+ const config = await this.connection.workspace.getConfiguration({
781
+ scopeUri: uri,
782
+ section: "zenith"
783
+ });
784
+ return {
785
+ strictDomLints: config?.strictDomLints === true,
786
+ enableFrameworkSnippets: config?.enableFrameworkSnippets === true
787
+ };
788
+ }
789
+ };
790
+
791
+ // src/validation.ts
792
+ function createValidationScheduler(validate, delayMs = 150) {
793
+ const states = /* @__PURE__ */ new Map();
794
+ function nextValidationId(uri) {
795
+ const state = states.get(uri) ?? { timer: void 0, validationId: 0 };
796
+ state.validationId += 1;
797
+ states.set(uri, state);
798
+ return state.validationId;
799
+ }
800
+ function cancelTimer(uri) {
801
+ const state = states.get(uri);
802
+ if (!state?.timer) {
803
+ return;
804
+ }
805
+ clearTimeout(state.timer);
806
+ state.timer = void 0;
807
+ }
808
+ return {
809
+ schedule(uri) {
810
+ const validationId = nextValidationId(uri);
811
+ cancelTimer(uri);
812
+ const state = states.get(uri);
813
+ state.timer = setTimeout(() => {
814
+ state.timer = void 0;
815
+ void validate(uri, validationId);
816
+ }, delayMs);
817
+ },
818
+ async flush(uri) {
819
+ const validationId = nextValidationId(uri);
820
+ cancelTimer(uri);
821
+ await validate(uri, validationId);
822
+ },
823
+ clear(uri) {
824
+ cancelTimer(uri);
825
+ states.delete(uri);
826
+ },
827
+ dispose() {
828
+ for (const uri of states.keys()) {
829
+ cancelTimer(uri);
830
+ }
831
+ states.clear();
832
+ },
833
+ isLatest(uri, validationId) {
834
+ return (states.get(uri)?.validationId ?? 0) === validationId;
835
+ }
836
+ };
837
+ }
838
+
839
+ // src/main.ts
840
+ function startLanguageServer() {
841
+ const connection = createConnection(ProposedFeatures.all);
842
+ const documents = new TextDocuments(TextDocument);
843
+ let supportsWorkspaceConfiguration = false;
844
+ let settings = new SettingsStore(connection, supportsWorkspaceConfiguration);
845
+ const scheduler = createValidationScheduler(async (uri, validationId) => {
846
+ const document = documents.get(uri);
847
+ if (!document) {
848
+ return;
849
+ }
850
+ const filePath = resolveDocumentPath(document.uri);
851
+ const workspaceSettings = await settings.get(uri);
852
+ const diagnostics = await collectDiagnosticsFromSource(
853
+ document.getText(),
854
+ filePath,
855
+ workspaceSettings.strictDomLints
856
+ );
857
+ if (!scheduler.isLatest(uri, validationId)) {
858
+ return;
859
+ }
860
+ connection.sendDiagnostics({ uri, diagnostics });
861
+ }, 150);
862
+ connection.onInitialize((params) => {
863
+ supportsWorkspaceConfiguration = params.capabilities.workspace?.configuration === true;
864
+ settings = new SettingsStore(connection, supportsWorkspaceConfiguration);
865
+ return {
866
+ capabilities: {
867
+ textDocumentSync: {
868
+ openClose: true,
869
+ change: TextDocumentSyncKind.Incremental,
870
+ save: { includeText: false }
871
+ },
872
+ completionProvider: {
873
+ triggerCharacters: [":", "<", "{"]
874
+ },
875
+ hoverProvider: true,
876
+ codeActionProvider: true
877
+ }
878
+ };
879
+ });
880
+ connection.onInitialized(() => {
881
+ if (supportsWorkspaceConfiguration) {
882
+ void connection.client.register(DidChangeConfigurationNotification.type, void 0);
883
+ }
884
+ });
885
+ connection.onDidChangeConfiguration(async () => {
886
+ settings.clear();
887
+ for (const document of documents.all()) {
888
+ await scheduler.flush(document.uri);
889
+ }
890
+ });
891
+ connection.onCompletion((params) => {
892
+ const document = documents.get(params.textDocument.uri);
893
+ if (!document) {
894
+ return [];
895
+ }
896
+ return getCompletionItems(document.getText(), params.position);
897
+ });
898
+ connection.onHover((params) => {
899
+ const document = documents.get(params.textDocument.uri);
900
+ if (!document) {
901
+ return null;
902
+ }
903
+ return getHover(document.getText(), params.position);
904
+ });
905
+ connection.onCodeAction((params) => {
906
+ const document = documents.get(params.textDocument.uri);
907
+ if (!document) {
908
+ return [];
909
+ }
910
+ return getCodeActions(document.getText(), params.textDocument.uri, params.context.diagnostics);
911
+ });
912
+ documents.onDidOpen((event) => {
913
+ void scheduler.flush(event.document.uri);
914
+ });
915
+ documents.onDidChangeContent((event) => {
916
+ scheduler.schedule(event.document.uri);
917
+ });
918
+ documents.onDidSave((event) => {
919
+ void scheduler.flush(event.document.uri);
920
+ });
921
+ documents.onDidClose((event) => {
922
+ scheduler.clear(event.document.uri);
923
+ settings.clear(event.document.uri);
924
+ connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
925
+ });
926
+ documents.listen(connection);
927
+ connection.listen();
928
+ }
929
+
930
+ // src/server.ts
931
+ startLanguageServer();