@zenithbuild/language-server 0.6.0 → 0.6.17
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/README.md +8 -39
- package/bin/zenith-language-server.js +2 -0
- package/dist/server.mjs +597 -0
- package/package.json +37 -26
- package/.github/workflows/release.yml +0 -261
- package/.releaserc.json +0 -73
- package/CHANGELOG.md +0 -59
- package/RELEASE_NOTES.md +0 -33
- package/RELEASE_NOTES_v0.6.0.md +0 -39
- package/scripts/release.ts +0 -571
- package/src/code-actions.ts +0 -219
- package/src/contracts.ts +0 -100
- package/src/diagnostics.ts +0 -603
- package/src/imports.ts +0 -207
- package/src/metadata/core-imports.ts +0 -163
- package/src/metadata/directive-metadata.ts +0 -109
- package/src/metadata/plugin-imports.ts +0 -116
- package/src/project.ts +0 -283
- package/src/router.ts +0 -180
- package/src/server.ts +0 -937
- package/src/settings.ts +0 -18
- package/src/types/zenith-compiler.d.ts +0 -3
- package/test/contracts.spec.ts +0 -37
- package/test/diagnostics.spec.ts +0 -120
- package/test/fixtures/content-plugin.zen +0 -77
- package/test/fixtures/core-only.zen +0 -59
- package/test/fixtures/no-plugins.zen +0 -115
- package/test/fixtures/router-enabled.zen +0 -76
- package/test/project-root.spec.ts +0 -44
- package/tsconfig.json +0 -25
- package/tsconfig.test.json +0 -25
package/README.md
CHANGED
|
@@ -1,46 +1,15 @@
|
|
|
1
|
-
# @zenithbuild/language-server
|
|
1
|
+
# @zenithbuild/language-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Canonical LSP server for Zenith.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
This package provides the "brains" for Zenith editor support. It implements the Language Server Protocol to provide features like autocomplete, hover information, diagnostics, and code actions across any supporting editor (VS Code, Vim, etc.).
|
|
8
|
-
|
|
9
|
-
## Features
|
|
10
|
-
|
|
11
|
-
- **Diagnostics**: Real-time error reporting and linting for `.zen` files.
|
|
12
|
-
- **Completion**: Context-aware suggestions for Zenith-specific syntax and standard HTML.
|
|
13
|
-
- **Hover Information**: Detailed documentation on hover for core components and hooks.
|
|
14
|
-
- **Document Symbols**: Outline and navigation support for complex components.
|
|
15
|
-
- **Contract Enforcement**:
|
|
16
|
-
- `on:click={handler}` event syntax diagnostics + quick fixes for `onclick` / `@click`.
|
|
17
|
-
- Component script policy (`zenith.componentScripts`: `forbid` | `allow`).
|
|
18
|
-
- CSS import contract diagnostics for local precompiled CSS only.
|
|
19
|
-
- **Project Root Resolution**:
|
|
20
|
-
- nearest `zenith.config.*`
|
|
21
|
-
- nearest `package.json` with `@zenithbuild/cli`
|
|
22
|
-
- workspace-aware fallback heuristics
|
|
23
|
-
|
|
24
|
-
## Settings
|
|
25
|
-
|
|
26
|
-
- `zenith.componentScripts`
|
|
27
|
-
- `forbid` (default): components may not contain `<script>`.
|
|
28
|
-
- `allow`: disables the component-script contract diagnostic.
|
|
29
|
-
|
|
30
|
-
## Architecture
|
|
31
|
-
|
|
32
|
-
The server is built with `vscode-languageserver` and is designed to be decoupled from the VS Code extension, allowing it to be reused in other IDEs or environments.
|
|
33
|
-
|
|
34
|
-
## Development
|
|
5
|
+
Global install:
|
|
35
6
|
|
|
36
7
|
```bash
|
|
37
|
-
|
|
38
|
-
bun run build
|
|
39
|
-
|
|
40
|
-
# Run in watch mode
|
|
41
|
-
bun run dev
|
|
8
|
+
npm i -g @zenithbuild/language-server
|
|
42
9
|
```
|
|
43
10
|
|
|
44
|
-
|
|
11
|
+
Run over stdio:
|
|
45
12
|
|
|
46
|
-
|
|
13
|
+
```bash
|
|
14
|
+
zenith-language-server
|
|
15
|
+
```
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
// src/main.ts
|
|
2
|
+
import {
|
|
3
|
+
createConnection,
|
|
4
|
+
DidChangeConfigurationNotification,
|
|
5
|
+
ProposedFeatures,
|
|
6
|
+
TextDocumentSyncKind
|
|
7
|
+
} from "vscode-languageserver/node";
|
|
8
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
9
|
+
import { TextDocuments } from "vscode-languageserver";
|
|
10
|
+
|
|
11
|
+
// src/completions.ts
|
|
12
|
+
import {
|
|
13
|
+
CompletionItemKind,
|
|
14
|
+
InsertTextFormat
|
|
15
|
+
} from "vscode-languageserver/node";
|
|
16
|
+
|
|
17
|
+
// src/docs.ts
|
|
18
|
+
var DOCS_BASE_URL = "https://github.com/zenithbuild/framework/blob/master/";
|
|
19
|
+
var SYMBOL_DOCS = {
|
|
20
|
+
zenEffect: {
|
|
21
|
+
label: "zenEffect",
|
|
22
|
+
summary: "Reactive side effect that re-runs when its dependencies change.",
|
|
23
|
+
example: "zenEffect(() => {\n count.get()\n})",
|
|
24
|
+
docPath: "docs/documentation/reactivity/effects-vs-mount.md"
|
|
25
|
+
},
|
|
26
|
+
zenMount: {
|
|
27
|
+
label: "zenMount",
|
|
28
|
+
summary: "Mount-time lifecycle boundary for DOM effects and cleanup registration.",
|
|
29
|
+
example: "zenMount((ctx) => {\n ctx.cleanup(offResize)\n})",
|
|
30
|
+
docPath: "docs/documentation/reactivity/effects-vs-mount.md"
|
|
31
|
+
},
|
|
32
|
+
state: {
|
|
33
|
+
label: "state",
|
|
34
|
+
summary: "Reactive binding for values that directly drive DOM expressions.",
|
|
35
|
+
example: "state open = false\nfunction toggle() { open = !open }",
|
|
36
|
+
docPath: "docs/documentation/reactivity/reactivity-model.md"
|
|
37
|
+
},
|
|
38
|
+
signal: {
|
|
39
|
+
label: "signal",
|
|
40
|
+
summary: "Stable reactive container with explicit get() and set() operations.",
|
|
41
|
+
example: "const count = signal(0)\ncount.set(count.get() + 1)",
|
|
42
|
+
docPath: "docs/documentation/reactivity/reactivity-model.md"
|
|
43
|
+
},
|
|
44
|
+
ref: {
|
|
45
|
+
label: "ref",
|
|
46
|
+
summary: "Typed DOM handle for measurements, focus, animation, and mount-time access.",
|
|
47
|
+
example: "const shell = ref<HTMLDivElement>()",
|
|
48
|
+
docPath: "docs/documentation/reactivity/reactivity-model.md"
|
|
49
|
+
},
|
|
50
|
+
zenWindow: {
|
|
51
|
+
label: "zenWindow",
|
|
52
|
+
summary: "SSR-safe window accessor that returns null when the browser environment is absent.",
|
|
53
|
+
example: "const win = zenWindow()\nif (!win) return",
|
|
54
|
+
docPath: "docs/documentation/reactivity/dom-and-environment.md"
|
|
55
|
+
},
|
|
56
|
+
zenDocument: {
|
|
57
|
+
label: "zenDocument",
|
|
58
|
+
summary: "SSR-safe document accessor for global DOM wiring inside mount-time logic.",
|
|
59
|
+
example: "const doc = zenDocument()\nif (!doc) return",
|
|
60
|
+
docPath: "docs/documentation/reactivity/dom-and-environment.md"
|
|
61
|
+
},
|
|
62
|
+
zenOn: {
|
|
63
|
+
label: "zenOn",
|
|
64
|
+
summary: "Canonical event subscription primitive that returns a disposer.",
|
|
65
|
+
example: "const off = zenOn(doc, 'keydown', handleKey)\nctx.cleanup(off)",
|
|
66
|
+
docPath: "docs/documentation/reactivity/dom-and-environment.md"
|
|
67
|
+
},
|
|
68
|
+
zenResize: {
|
|
69
|
+
label: "zenResize",
|
|
70
|
+
summary: "Canonical window resize primitive for reactive viewport updates.",
|
|
71
|
+
example: "const off = zenResize(({ w, h }) => viewport.set({ w, h }))",
|
|
72
|
+
docPath: "docs/documentation/reactivity/dom-and-environment.md"
|
|
73
|
+
},
|
|
74
|
+
collectRefs: {
|
|
75
|
+
label: "collectRefs",
|
|
76
|
+
summary: "Deterministic multi-node collection helper that replaces selector scans.",
|
|
77
|
+
example: "const nodes = collectRefs(linkRefA, linkRefB, linkRefC)",
|
|
78
|
+
docPath: "docs/documentation/reactivity/dom-and-environment.md"
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
var canonicalScriptSymbols = [
|
|
82
|
+
"zenMount",
|
|
83
|
+
"zenEffect",
|
|
84
|
+
"state",
|
|
85
|
+
"signal",
|
|
86
|
+
"ref",
|
|
87
|
+
"zenWindow",
|
|
88
|
+
"zenDocument",
|
|
89
|
+
"zenOn",
|
|
90
|
+
"zenResize",
|
|
91
|
+
"collectRefs"
|
|
92
|
+
];
|
|
93
|
+
var canonicalEventAttributes = [
|
|
94
|
+
"on:click",
|
|
95
|
+
"on:dblclick",
|
|
96
|
+
"on:keydown",
|
|
97
|
+
"on:keyup",
|
|
98
|
+
"on:esc",
|
|
99
|
+
"on:submit",
|
|
100
|
+
"on:input",
|
|
101
|
+
"on:change",
|
|
102
|
+
"on:focus",
|
|
103
|
+
"on:blur",
|
|
104
|
+
"on:pointerdown",
|
|
105
|
+
"on:pointerup",
|
|
106
|
+
"on:pointermove",
|
|
107
|
+
"on:pointerenter",
|
|
108
|
+
"on:pointerleave",
|
|
109
|
+
"on:hoverin",
|
|
110
|
+
"on:hoverout",
|
|
111
|
+
"on:dragstart",
|
|
112
|
+
"on:dragover",
|
|
113
|
+
"on:drop",
|
|
114
|
+
"on:scroll",
|
|
115
|
+
"on:contextmenu"
|
|
116
|
+
];
|
|
117
|
+
function getSymbolDoc(symbol) {
|
|
118
|
+
return SYMBOL_DOCS[symbol];
|
|
119
|
+
}
|
|
120
|
+
function getDocUrl(docPath) {
|
|
121
|
+
return `${DOCS_BASE_URL}${docPath}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/text.ts
|
|
125
|
+
var wordPattern = /[A-Za-z0-9_:$.-]/;
|
|
126
|
+
function offsetAt(text, position) {
|
|
127
|
+
const lines = text.split("\n");
|
|
128
|
+
const lineIndex = Math.max(0, Math.min(position.line, lines.length - 1));
|
|
129
|
+
let offset = 0;
|
|
130
|
+
for (let index = 0; index < lineIndex; index += 1) {
|
|
131
|
+
offset += lines[index].length + 1;
|
|
132
|
+
}
|
|
133
|
+
return offset + Math.max(0, Math.min(position.character, lines[lineIndex].length));
|
|
134
|
+
}
|
|
135
|
+
function getWordRange(text, position) {
|
|
136
|
+
const offset = offsetAt(text, position);
|
|
137
|
+
let start = offset;
|
|
138
|
+
let end = offset;
|
|
139
|
+
while (start > 0 && wordPattern.test(text[start - 1] ?? "")) {
|
|
140
|
+
start -= 1;
|
|
141
|
+
}
|
|
142
|
+
while (end < text.length && wordPattern.test(text[end] ?? "")) {
|
|
143
|
+
end += 1;
|
|
144
|
+
}
|
|
145
|
+
if (start === end) {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
start: positionAt(text, start),
|
|
150
|
+
end: positionAt(text, end)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function getWord(text, position) {
|
|
154
|
+
const range = getWordRange(text, position);
|
|
155
|
+
if (!range) {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
const start = offsetAt(text, range.start);
|
|
159
|
+
const end = offsetAt(text, range.end);
|
|
160
|
+
return text.slice(start, end);
|
|
161
|
+
}
|
|
162
|
+
function getCompletionPrefix(text, position) {
|
|
163
|
+
const offset = offsetAt(text, position);
|
|
164
|
+
let start = offset;
|
|
165
|
+
while (start > 0 && wordPattern.test(text[start - 1] ?? "")) {
|
|
166
|
+
start -= 1;
|
|
167
|
+
}
|
|
168
|
+
return text.slice(start, offset);
|
|
169
|
+
}
|
|
170
|
+
function getCompletionContext(text, position) {
|
|
171
|
+
const offset = offsetAt(text, position);
|
|
172
|
+
if (isInsideScript(text, offset)) {
|
|
173
|
+
return "script";
|
|
174
|
+
}
|
|
175
|
+
if (isInsideBraces(text, offset)) {
|
|
176
|
+
return "expression";
|
|
177
|
+
}
|
|
178
|
+
if (isInsideTag(text, offset)) {
|
|
179
|
+
return "attribute";
|
|
180
|
+
}
|
|
181
|
+
return "markup";
|
|
182
|
+
}
|
|
183
|
+
function isInsideScript(text, offset) {
|
|
184
|
+
const lower = text.slice(0, offset).toLowerCase();
|
|
185
|
+
const lastOpen = lower.lastIndexOf("<script");
|
|
186
|
+
const lastClose = lower.lastIndexOf("</script");
|
|
187
|
+
if (lastOpen === -1 || lastOpen < lastClose) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const openEnd = lower.indexOf(">", lastOpen);
|
|
191
|
+
return openEnd !== -1 && openEnd < offset;
|
|
192
|
+
}
|
|
193
|
+
function isInsideTag(text, offset) {
|
|
194
|
+
const before = text.slice(0, offset);
|
|
195
|
+
const lastOpen = before.lastIndexOf("<");
|
|
196
|
+
const lastClose = before.lastIndexOf(">");
|
|
197
|
+
return lastOpen > lastClose;
|
|
198
|
+
}
|
|
199
|
+
function isInsideBraces(text, offset) {
|
|
200
|
+
let depth = 0;
|
|
201
|
+
let inSingle = false;
|
|
202
|
+
let inDouble = false;
|
|
203
|
+
let inTemplate = false;
|
|
204
|
+
let escaped = false;
|
|
205
|
+
for (let index = 0; index < offset; index += 1) {
|
|
206
|
+
const char = text[index] ?? "";
|
|
207
|
+
if (escaped) {
|
|
208
|
+
escaped = false;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (char === "\\") {
|
|
212
|
+
escaped = true;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (inSingle) {
|
|
216
|
+
if (char === "'") {
|
|
217
|
+
inSingle = false;
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (inDouble) {
|
|
222
|
+
if (char === '"') {
|
|
223
|
+
inDouble = false;
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (inTemplate) {
|
|
228
|
+
if (char === "`") {
|
|
229
|
+
inTemplate = false;
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (char === "'") {
|
|
234
|
+
inSingle = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (char === '"') {
|
|
238
|
+
inDouble = true;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (char === "`") {
|
|
242
|
+
inTemplate = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (char === "{") {
|
|
246
|
+
depth += 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (char === "}") {
|
|
250
|
+
depth = Math.max(0, depth - 1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return depth > 0;
|
|
254
|
+
}
|
|
255
|
+
function positionAt(text, offset) {
|
|
256
|
+
const safeOffset = Math.max(0, Math.min(offset, text.length));
|
|
257
|
+
const slice = text.slice(0, safeOffset);
|
|
258
|
+
const lines = slice.split("\n");
|
|
259
|
+
const line = lines.length - 1;
|
|
260
|
+
const character = lines.at(-1)?.length ?? 0;
|
|
261
|
+
return { line, character };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/completions.ts
|
|
265
|
+
var blockedFrameworkTokens = ["react", "vue", "svelte", "rfce"];
|
|
266
|
+
function createScriptCompletion(label) {
|
|
267
|
+
return {
|
|
268
|
+
label,
|
|
269
|
+
kind: CompletionItemKind.Function,
|
|
270
|
+
detail: "Zenith canonical primitive",
|
|
271
|
+
insertText: label
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function createEventCompletion(label) {
|
|
275
|
+
return {
|
|
276
|
+
label,
|
|
277
|
+
kind: CompletionItemKind.Property,
|
|
278
|
+
detail: "Canonical Zenith DOM event binding",
|
|
279
|
+
insertTextFormat: InsertTextFormat.Snippet,
|
|
280
|
+
insertText: `${label}={$1:handler}`
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function createPropHandlerCompletion() {
|
|
284
|
+
return {
|
|
285
|
+
label: "onClick={handler}",
|
|
286
|
+
kind: CompletionItemKind.Snippet,
|
|
287
|
+
detail: "Pass handler props through components, then bind them back to on:* in component markup.",
|
|
288
|
+
insertTextFormat: InsertTextFormat.Snippet,
|
|
289
|
+
insertText: "onClick={$1:handler}"
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function getCompletionItems(text, position) {
|
|
293
|
+
const context = getCompletionContext(text, position);
|
|
294
|
+
const prefix = getCompletionPrefix(text, position).toLowerCase();
|
|
295
|
+
if (context === "script" || context === "expression") {
|
|
296
|
+
return filterFrameworkNoise(canonicalScriptSymbols.map((symbol) => createScriptCompletion(symbol)), prefix);
|
|
297
|
+
}
|
|
298
|
+
if (context === "attribute") {
|
|
299
|
+
const items = canonicalEventAttributes.map((eventName) => createEventCompletion(eventName));
|
|
300
|
+
items.push(createPropHandlerCompletion());
|
|
301
|
+
return filterFrameworkNoise(items, prefix);
|
|
302
|
+
}
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
function filterFrameworkNoise(items, typedPrefix) {
|
|
306
|
+
if (blockedFrameworkTokens.some((token) => typedPrefix.includes(token))) {
|
|
307
|
+
return items;
|
|
308
|
+
}
|
|
309
|
+
return items.filter((item) => {
|
|
310
|
+
const haystack = [
|
|
311
|
+
item.label,
|
|
312
|
+
item.detail ?? "",
|
|
313
|
+
typeof item.insertText === "string" ? item.insertText : ""
|
|
314
|
+
].join(" ").toLowerCase();
|
|
315
|
+
return blockedFrameworkTokens.every((token) => !haystack.includes(token));
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/diagnostics.ts
|
|
320
|
+
import { fileURLToPath } from "node:url";
|
|
321
|
+
import { compile } from "@zenithbuild/compiler";
|
|
322
|
+
import { DiagnosticSeverity } from "vscode-languageserver/node";
|
|
323
|
+
async function collectDiagnosticsFromSource(source, filePath, strictDomLints) {
|
|
324
|
+
try {
|
|
325
|
+
const result = compile({ source, filePath });
|
|
326
|
+
if (result.schemaVersion !== 1) {
|
|
327
|
+
return [compilerContractDiagnostic(`Unsupported compiler schemaVersion: ${String(result.schemaVersion)}`)];
|
|
328
|
+
}
|
|
329
|
+
const warnings = Array.isArray(result.warnings) ? result.warnings : [];
|
|
330
|
+
return warnings.map((warning) => ({
|
|
331
|
+
source: "zenith",
|
|
332
|
+
code: warning.code,
|
|
333
|
+
message: warning.message,
|
|
334
|
+
severity: resolveSeverity(warning, strictDomLints),
|
|
335
|
+
range: toRange(warning.range)
|
|
336
|
+
}));
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return [compilerContractDiagnostic(String(error))];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function resolveDocumentPath(uri) {
|
|
342
|
+
if (uri.startsWith("file://")) {
|
|
343
|
+
return fileURLToPath(uri);
|
|
344
|
+
}
|
|
345
|
+
return uri.replace(/^[a-z]+:\/\//i, "/virtual/");
|
|
346
|
+
}
|
|
347
|
+
function resolveSeverity(warning, strictDomLints) {
|
|
348
|
+
if (strictDomLints && warning.code.startsWith("ZEN-DOM-")) {
|
|
349
|
+
return DiagnosticSeverity.Error;
|
|
350
|
+
}
|
|
351
|
+
if (warning.severity === "error") {
|
|
352
|
+
return DiagnosticSeverity.Error;
|
|
353
|
+
}
|
|
354
|
+
if (warning.severity === "hint") {
|
|
355
|
+
return DiagnosticSeverity.Hint;
|
|
356
|
+
}
|
|
357
|
+
if (warning.severity === "info") {
|
|
358
|
+
return DiagnosticSeverity.Information;
|
|
359
|
+
}
|
|
360
|
+
return DiagnosticSeverity.Warning;
|
|
361
|
+
}
|
|
362
|
+
function toRange(range) {
|
|
363
|
+
if (!range) {
|
|
364
|
+
return {
|
|
365
|
+
start: { line: 0, character: 0 },
|
|
366
|
+
end: { line: 0, character: 1 }
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
start: {
|
|
371
|
+
line: Math.max(0, range.start.line - 1),
|
|
372
|
+
character: Math.max(0, range.start.column - 1)
|
|
373
|
+
},
|
|
374
|
+
end: {
|
|
375
|
+
line: Math.max(0, range.end.line - 1),
|
|
376
|
+
character: Math.max(0, range.end.column - 1)
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function compilerContractDiagnostic(message) {
|
|
381
|
+
return {
|
|
382
|
+
source: "zenith",
|
|
383
|
+
code: "ZENITH-COMPILER",
|
|
384
|
+
message,
|
|
385
|
+
severity: DiagnosticSeverity.Error,
|
|
386
|
+
range: {
|
|
387
|
+
start: { line: 0, character: 0 },
|
|
388
|
+
end: { line: 0, character: 1 }
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/hover.ts
|
|
394
|
+
import { MarkupKind } from "vscode-languageserver/node";
|
|
395
|
+
function getHover(text, position) {
|
|
396
|
+
const symbol = getWord(text, position);
|
|
397
|
+
const docs = getSymbolDoc(symbol);
|
|
398
|
+
const range = getWordRange(text, position);
|
|
399
|
+
if (!docs || !range) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const docsUrl = getDocUrl(docs.docPath);
|
|
403
|
+
const markdown = [
|
|
404
|
+
`**${docs.label}**`,
|
|
405
|
+
"",
|
|
406
|
+
docs.summary,
|
|
407
|
+
"",
|
|
408
|
+
"```ts",
|
|
409
|
+
docs.example,
|
|
410
|
+
"```",
|
|
411
|
+
"",
|
|
412
|
+
`Docs: [${docs.docPath}](${docsUrl})`
|
|
413
|
+
].join("\n");
|
|
414
|
+
return {
|
|
415
|
+
range,
|
|
416
|
+
contents: {
|
|
417
|
+
kind: MarkupKind.Markdown,
|
|
418
|
+
value: markdown
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/settings.ts
|
|
424
|
+
var defaultSettings = {
|
|
425
|
+
strictDomLints: false,
|
|
426
|
+
enableFrameworkSnippets: false
|
|
427
|
+
};
|
|
428
|
+
var SettingsStore = class {
|
|
429
|
+
constructor(connection, supportsWorkspaceConfiguration) {
|
|
430
|
+
this.connection = connection;
|
|
431
|
+
this.supportsWorkspaceConfiguration = supportsWorkspaceConfiguration;
|
|
432
|
+
}
|
|
433
|
+
#cache = /* @__PURE__ */ new Map();
|
|
434
|
+
clear(uri) {
|
|
435
|
+
if (uri) {
|
|
436
|
+
this.#cache.delete(uri);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
this.#cache.clear();
|
|
440
|
+
}
|
|
441
|
+
async get(uri) {
|
|
442
|
+
if (!this.supportsWorkspaceConfiguration) {
|
|
443
|
+
return defaultSettings;
|
|
444
|
+
}
|
|
445
|
+
const cached = this.#cache.get(uri);
|
|
446
|
+
if (cached) {
|
|
447
|
+
return cached;
|
|
448
|
+
}
|
|
449
|
+
const pending = this.load(uri);
|
|
450
|
+
this.#cache.set(uri, pending);
|
|
451
|
+
return pending;
|
|
452
|
+
}
|
|
453
|
+
async load(uri) {
|
|
454
|
+
const config = await this.connection.workspace.getConfiguration({
|
|
455
|
+
scopeUri: uri,
|
|
456
|
+
section: "zenith"
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
strictDomLints: config?.strictDomLints === true,
|
|
460
|
+
enableFrameworkSnippets: config?.enableFrameworkSnippets === true
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// src/validation.ts
|
|
466
|
+
function createValidationScheduler(validate, delayMs = 150) {
|
|
467
|
+
const states = /* @__PURE__ */ new Map();
|
|
468
|
+
function nextValidationId(uri) {
|
|
469
|
+
const state = states.get(uri) ?? { timer: void 0, validationId: 0 };
|
|
470
|
+
state.validationId += 1;
|
|
471
|
+
states.set(uri, state);
|
|
472
|
+
return state.validationId;
|
|
473
|
+
}
|
|
474
|
+
function cancelTimer(uri) {
|
|
475
|
+
const state = states.get(uri);
|
|
476
|
+
if (!state?.timer) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
clearTimeout(state.timer);
|
|
480
|
+
state.timer = void 0;
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
schedule(uri) {
|
|
484
|
+
const validationId = nextValidationId(uri);
|
|
485
|
+
cancelTimer(uri);
|
|
486
|
+
const state = states.get(uri);
|
|
487
|
+
state.timer = setTimeout(() => {
|
|
488
|
+
state.timer = void 0;
|
|
489
|
+
void validate(uri, validationId);
|
|
490
|
+
}, delayMs);
|
|
491
|
+
},
|
|
492
|
+
async flush(uri) {
|
|
493
|
+
const validationId = nextValidationId(uri);
|
|
494
|
+
cancelTimer(uri);
|
|
495
|
+
await validate(uri, validationId);
|
|
496
|
+
},
|
|
497
|
+
clear(uri) {
|
|
498
|
+
cancelTimer(uri);
|
|
499
|
+
states.delete(uri);
|
|
500
|
+
},
|
|
501
|
+
dispose() {
|
|
502
|
+
for (const uri of states.keys()) {
|
|
503
|
+
cancelTimer(uri);
|
|
504
|
+
}
|
|
505
|
+
states.clear();
|
|
506
|
+
},
|
|
507
|
+
isLatest(uri, validationId) {
|
|
508
|
+
return (states.get(uri)?.validationId ?? 0) === validationId;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/main.ts
|
|
514
|
+
function startLanguageServer() {
|
|
515
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
516
|
+
const documents = new TextDocuments(TextDocument);
|
|
517
|
+
let supportsWorkspaceConfiguration = false;
|
|
518
|
+
let settings = new SettingsStore(connection, supportsWorkspaceConfiguration);
|
|
519
|
+
const scheduler = createValidationScheduler(async (uri, validationId) => {
|
|
520
|
+
const document = documents.get(uri);
|
|
521
|
+
if (!document) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const filePath = resolveDocumentPath(document.uri);
|
|
525
|
+
const workspaceSettings = await settings.get(uri);
|
|
526
|
+
const diagnostics = await collectDiagnosticsFromSource(
|
|
527
|
+
document.getText(),
|
|
528
|
+
filePath,
|
|
529
|
+
workspaceSettings.strictDomLints
|
|
530
|
+
);
|
|
531
|
+
if (!scheduler.isLatest(uri, validationId)) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
connection.sendDiagnostics({ uri, diagnostics });
|
|
535
|
+
}, 150);
|
|
536
|
+
connection.onInitialize((params) => {
|
|
537
|
+
supportsWorkspaceConfiguration = params.capabilities.workspace?.configuration === true;
|
|
538
|
+
settings = new SettingsStore(connection, supportsWorkspaceConfiguration);
|
|
539
|
+
return {
|
|
540
|
+
capabilities: {
|
|
541
|
+
textDocumentSync: {
|
|
542
|
+
openClose: true,
|
|
543
|
+
change: TextDocumentSyncKind.Incremental,
|
|
544
|
+
save: { includeText: false }
|
|
545
|
+
},
|
|
546
|
+
completionProvider: {
|
|
547
|
+
triggerCharacters: [":", "<", "{"]
|
|
548
|
+
},
|
|
549
|
+
hoverProvider: true
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
connection.onInitialized(() => {
|
|
554
|
+
if (supportsWorkspaceConfiguration) {
|
|
555
|
+
void connection.client.register(DidChangeConfigurationNotification.type, void 0);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
connection.onDidChangeConfiguration(async () => {
|
|
559
|
+
settings.clear();
|
|
560
|
+
for (const document of documents.all()) {
|
|
561
|
+
await scheduler.flush(document.uri);
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
connection.onCompletion((params) => {
|
|
565
|
+
const document = documents.get(params.textDocument.uri);
|
|
566
|
+
if (!document) {
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
return getCompletionItems(document.getText(), params.position);
|
|
570
|
+
});
|
|
571
|
+
connection.onHover((params) => {
|
|
572
|
+
const document = documents.get(params.textDocument.uri);
|
|
573
|
+
if (!document) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
return getHover(document.getText(), params.position);
|
|
577
|
+
});
|
|
578
|
+
documents.onDidOpen((event) => {
|
|
579
|
+
void scheduler.flush(event.document.uri);
|
|
580
|
+
});
|
|
581
|
+
documents.onDidChangeContent((event) => {
|
|
582
|
+
scheduler.schedule(event.document.uri);
|
|
583
|
+
});
|
|
584
|
+
documents.onDidSave((event) => {
|
|
585
|
+
void scheduler.flush(event.document.uri);
|
|
586
|
+
});
|
|
587
|
+
documents.onDidClose((event) => {
|
|
588
|
+
scheduler.clear(event.document.uri);
|
|
589
|
+
settings.clear(event.document.uri);
|
|
590
|
+
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
|
|
591
|
+
});
|
|
592
|
+
documents.listen(connection);
|
|
593
|
+
connection.listen();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/server.ts
|
|
597
|
+
startLanguageServer();
|