@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/src/server.ts
DELETED
|
@@ -1,937 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Zenith Language Server
|
|
3
|
-
*
|
|
4
|
-
* Provides full IntelliSense for Zenith .zen files.
|
|
5
|
-
*
|
|
6
|
-
* Architecture Principles:
|
|
7
|
-
* - Compiler is the source of truth
|
|
8
|
-
* - No runtime assumptions
|
|
9
|
-
* - Static analysis only
|
|
10
|
-
* - Graceful degradation for missing plugins
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
createConnection,
|
|
15
|
-
TextDocuments,
|
|
16
|
-
ProposedFeatures,
|
|
17
|
-
InitializeParams,
|
|
18
|
-
DidChangeConfigurationNotification,
|
|
19
|
-
CompletionItem,
|
|
20
|
-
CompletionItemKind,
|
|
21
|
-
TextDocumentPositionParams,
|
|
22
|
-
CodeActionParams,
|
|
23
|
-
CodeAction,
|
|
24
|
-
TextDocumentSyncKind,
|
|
25
|
-
InitializeResult,
|
|
26
|
-
Hover,
|
|
27
|
-
MarkupKind,
|
|
28
|
-
InsertTextFormat
|
|
29
|
-
} from 'vscode-languageserver/node';
|
|
30
|
-
|
|
31
|
-
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
32
|
-
import * as path from 'path';
|
|
33
|
-
|
|
34
|
-
import {
|
|
35
|
-
detectProjectRoot,
|
|
36
|
-
buildProjectGraph,
|
|
37
|
-
resolveComponent,
|
|
38
|
-
ProjectGraph
|
|
39
|
-
} from './project';
|
|
40
|
-
|
|
41
|
-
import {
|
|
42
|
-
DIRECTIVES,
|
|
43
|
-
isDirective,
|
|
44
|
-
getDirective,
|
|
45
|
-
getDirectiveNames,
|
|
46
|
-
canPlaceDirective,
|
|
47
|
-
parseForExpression
|
|
48
|
-
} from './metadata/directive-metadata';
|
|
49
|
-
|
|
50
|
-
import {
|
|
51
|
-
parseZenithImports,
|
|
52
|
-
hasRouterImport,
|
|
53
|
-
resolveModule,
|
|
54
|
-
resolveExport,
|
|
55
|
-
getAllModules,
|
|
56
|
-
getModuleExports
|
|
57
|
-
} from './imports';
|
|
58
|
-
|
|
59
|
-
import {
|
|
60
|
-
ROUTER_HOOKS,
|
|
61
|
-
ZENLINK_PROPS,
|
|
62
|
-
ROUTE_FIELDS,
|
|
63
|
-
getRouterHook,
|
|
64
|
-
isRouterHook,
|
|
65
|
-
getZenLinkPropNames
|
|
66
|
-
} from './router';
|
|
67
|
-
|
|
68
|
-
import { collectDiagnostics } from './diagnostics';
|
|
69
|
-
import { buildEventBindingCodeActions, buildDomLintCodeActions, buildWindowDocumentCodeActions } from './code-actions';
|
|
70
|
-
import { DEFAULT_SETTINGS, normalizeSettings, ZenithServerSettings } from './settings';
|
|
71
|
-
|
|
72
|
-
// Create connection and document manager
|
|
73
|
-
const connection = createConnection(ProposedFeatures.all);
|
|
74
|
-
const documents = new TextDocuments(TextDocument);
|
|
75
|
-
|
|
76
|
-
// Project graph cache
|
|
77
|
-
let projectGraphs: Map<string, ProjectGraph> = new Map();
|
|
78
|
-
let workspaceFolders: string[] = [];
|
|
79
|
-
let globalSettings: ZenithServerSettings = DEFAULT_SETTINGS;
|
|
80
|
-
|
|
81
|
-
// Lifecycle hooks and platform primitives with documentation
|
|
82
|
-
const LIFECYCLE_HOOKS = [
|
|
83
|
-
{ name: 'state', doc: 'Declare a reactive state variable', snippet: 'state ${1:name} = ${2:value}', kind: CompletionItemKind.Keyword },
|
|
84
|
-
{ name: 'zenMount', doc: 'Mount callback with ctx.cleanup for disposers', snippet: 'zenMount((ctx) => {\n\t$0\n})', kind: CompletionItemKind.Function },
|
|
85
|
-
{ name: 'zenOnMount', doc: 'Called when component is mounted to the DOM', snippet: 'zenOnMount(() => {\n\t$0\n})', kind: CompletionItemKind.Function },
|
|
86
|
-
{ name: 'zenOnDestroy', doc: 'Called when component is removed from the DOM', snippet: 'zenOnDestroy(() => {\n\t$0\n})', kind: CompletionItemKind.Function },
|
|
87
|
-
{ name: 'zenOnUpdate', doc: 'Called after any state update causes a re-render', snippet: 'zenOnUpdate(() => {\n\t$0\n})', kind: CompletionItemKind.Function },
|
|
88
|
-
{ name: 'zenEffect', doc: 'Reactive effect that re-runs when dependencies change', snippet: 'zenEffect(() => {\n\t$0\n})', kind: CompletionItemKind.Function },
|
|
89
|
-
{ name: 'useFetch', doc: 'Fetch data with caching and SSG support', snippet: 'useFetch("${1:url}")', kind: CompletionItemKind.Function }
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
const PLATFORM_PRIMITIVES = [
|
|
93
|
-
{ name: 'zenWindow', doc: 'SSR-safe window access (returns null when not in browser)', snippet: 'zenWindow()', kind: CompletionItemKind.Function },
|
|
94
|
-
{ name: 'zenDocument', doc: 'SSR-safe document access (returns null when not in browser)', snippet: 'zenDocument()', kind: CompletionItemKind.Function },
|
|
95
|
-
{ name: 'zenOn', doc: 'Event subscription with disposer; register via ctx.cleanup', snippet: 'zenOn(${1:target}, \'${2:event}\', ${3:handler})', kind: CompletionItemKind.Function },
|
|
96
|
-
{ name: 'zenResize', doc: 'Window resize handler; returns disposer for ctx.cleanup', snippet: 'zenResize(({ w, h }) => {\n\t$0\n})', kind: CompletionItemKind.Function },
|
|
97
|
-
{ name: 'collectRefs', doc: 'Collect multiple refs into a deterministic node list', snippet: 'collectRefs(${1:refA}, ${2:refB})', kind: CompletionItemKind.Function },
|
|
98
|
-
{ name: 'signal', doc: 'Create a signal for explicit get/set', snippet: 'signal(${1:0})', kind: CompletionItemKind.Function },
|
|
99
|
-
{ name: 'ref', doc: 'Create a ref for DOM node or value', snippet: 'ref<${1:HTMLElement}>()', kind: CompletionItemKind.Function }
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
// Common HTML elements
|
|
103
|
-
const HTML_ELEMENTS = [
|
|
104
|
-
{ tag: 'div', doc: 'Generic container element' },
|
|
105
|
-
{ tag: 'span', doc: 'Inline container element' },
|
|
106
|
-
{ tag: 'p', doc: 'Paragraph element' },
|
|
107
|
-
{ tag: 'a', doc: 'Anchor/link element', attrs: 'href="$1"' },
|
|
108
|
-
{ tag: 'button', doc: 'Button element', attrs: 'on:click={$1}' },
|
|
109
|
-
{ tag: 'input', doc: 'Input element', attrs: 'type="$1"', selfClosing: true },
|
|
110
|
-
{ tag: 'img', doc: 'Image element', attrs: 'src="$1" alt="$2"', selfClosing: true },
|
|
111
|
-
{ tag: 'h1', doc: 'Heading level 1' },
|
|
112
|
-
{ tag: 'h2', doc: 'Heading level 2' },
|
|
113
|
-
{ tag: 'h3', doc: 'Heading level 3' },
|
|
114
|
-
{ tag: 'h4', doc: 'Heading level 4' },
|
|
115
|
-
{ tag: 'h5', doc: 'Heading level 5' },
|
|
116
|
-
{ tag: 'h6', doc: 'Heading level 6' },
|
|
117
|
-
{ tag: 'ul', doc: 'Unordered list' },
|
|
118
|
-
{ tag: 'ol', doc: 'Ordered list' },
|
|
119
|
-
{ tag: 'li', doc: 'List item' },
|
|
120
|
-
{ tag: 'nav', doc: 'Navigation section' },
|
|
121
|
-
{ tag: 'header', doc: 'Header section' },
|
|
122
|
-
{ tag: 'footer', doc: 'Footer section' },
|
|
123
|
-
{ tag: 'main', doc: 'Main content' },
|
|
124
|
-
{ tag: 'section', doc: 'Generic section' },
|
|
125
|
-
{ tag: 'article', doc: 'Article content' },
|
|
126
|
-
{ tag: 'aside', doc: 'Sidebar content' },
|
|
127
|
-
{ tag: 'form', doc: 'Form element' },
|
|
128
|
-
{ tag: 'label', doc: 'Form label', attrs: 'for="$1"' },
|
|
129
|
-
{ tag: 'select', doc: 'Dropdown select' },
|
|
130
|
-
{ tag: 'option', doc: 'Select option', attrs: 'value="$1"' },
|
|
131
|
-
{ tag: 'textarea', doc: 'Multi-line text input' },
|
|
132
|
-
{ tag: 'table', doc: 'Table element' },
|
|
133
|
-
{ tag: 'thead', doc: 'Table header group' },
|
|
134
|
-
{ tag: 'tbody', doc: 'Table body group' },
|
|
135
|
-
{ tag: 'tr', doc: 'Table row' },
|
|
136
|
-
{ tag: 'th', doc: 'Table header cell' },
|
|
137
|
-
{ tag: 'td', doc: 'Table data cell' },
|
|
138
|
-
{ tag: 'br', doc: 'Line break', selfClosing: true },
|
|
139
|
-
{ tag: 'hr', doc: 'Horizontal rule', selfClosing: true },
|
|
140
|
-
{ tag: 'strong', doc: 'Strong emphasis (bold)' },
|
|
141
|
-
{ tag: 'em', doc: 'Emphasis (italic)' },
|
|
142
|
-
{ tag: 'code', doc: 'Inline code' },
|
|
143
|
-
{ tag: 'pre', doc: 'Preformatted text' },
|
|
144
|
-
{ tag: 'blockquote', doc: 'Block quotation' },
|
|
145
|
-
{ tag: 'slot', doc: 'Zenith slot for child content' }
|
|
146
|
-
];
|
|
147
|
-
|
|
148
|
-
// Common HTML attributes
|
|
149
|
-
const HTML_ATTRIBUTES = [
|
|
150
|
-
'id', 'class', 'style', 'title', 'href', 'src', 'alt', 'type', 'name', 'value',
|
|
151
|
-
'placeholder', 'disabled', 'checked', 'readonly', 'required', 'hidden'
|
|
152
|
-
];
|
|
153
|
-
|
|
154
|
-
// DOM events for on:event handlers
|
|
155
|
-
const DOM_EVENTS = [
|
|
156
|
-
'click', 'change', 'input', 'submit', 'keydown', 'keyup', 'keypress',
|
|
157
|
-
'focus', 'blur', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave'
|
|
158
|
-
];
|
|
159
|
-
|
|
160
|
-
// State analysis
|
|
161
|
-
function extractStates(script: string): Map<string, string> {
|
|
162
|
-
const states = new Map<string, string>();
|
|
163
|
-
const statePattern = /state\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*([^;\n]+)/g;
|
|
164
|
-
let match;
|
|
165
|
-
|
|
166
|
-
while ((match = statePattern.exec(script)) !== null) {
|
|
167
|
-
if (match[1] && match[2]) {
|
|
168
|
-
states.set(match[1], match[2].trim());
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return states;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Extract functions from script
|
|
176
|
-
function extractFunctions(script: string): Array<{ name: string, params: string, isAsync: boolean }> {
|
|
177
|
-
const functions: Array<{ name: string, params: string, isAsync: boolean }> = [];
|
|
178
|
-
const funcPattern = /(async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(([^)]*)\)/g;
|
|
179
|
-
let match;
|
|
180
|
-
|
|
181
|
-
while ((match = funcPattern.exec(script)) !== null) {
|
|
182
|
-
if (match[2]) {
|
|
183
|
-
functions.push({
|
|
184
|
-
name: match[2],
|
|
185
|
-
params: match[3] || '',
|
|
186
|
-
isAsync: !!match[1]
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Arrow functions assigned to const/let
|
|
192
|
-
const arrowPattern = /(?:const|let)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(async\s+)?\([^)]*\)\s*=>/g;
|
|
193
|
-
while ((match = arrowPattern.exec(script)) !== null) {
|
|
194
|
-
if (match[1]) {
|
|
195
|
-
functions.push({
|
|
196
|
-
name: match[1],
|
|
197
|
-
params: '',
|
|
198
|
-
isAsync: !!match[2]
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return functions;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Extract loop variables from zen:for directives
|
|
207
|
-
function extractLoopVariables(text: string): string[] {
|
|
208
|
-
const vars: string[] = [];
|
|
209
|
-
const loopPattern = /zen:for\s*=\s*["']([^"']+)["']/g;
|
|
210
|
-
let match;
|
|
211
|
-
|
|
212
|
-
while ((match = loopPattern.exec(text)) !== null) {
|
|
213
|
-
const parsed = parseForExpression(match[1]);
|
|
214
|
-
if (parsed) {
|
|
215
|
-
vars.push(parsed.itemVar);
|
|
216
|
-
if (parsed.indexVar) vars.push(parsed.indexVar);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return vars;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Get script content from document
|
|
224
|
-
function getScriptContent(text: string): string {
|
|
225
|
-
const match = text.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
|
|
226
|
-
return match ? match[1] : '';
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Check position context
|
|
230
|
-
function getPositionContext(text: string, offset: number): {
|
|
231
|
-
inScript: boolean;
|
|
232
|
-
inStyle: boolean;
|
|
233
|
-
inTag: boolean;
|
|
234
|
-
inExpression: boolean;
|
|
235
|
-
inTemplate: boolean;
|
|
236
|
-
inAttributeValue: boolean;
|
|
237
|
-
tagName: string | null;
|
|
238
|
-
currentWord: string;
|
|
239
|
-
afterAt: boolean;
|
|
240
|
-
afterColon: boolean;
|
|
241
|
-
} {
|
|
242
|
-
const before = text.substring(0, offset);
|
|
243
|
-
|
|
244
|
-
const scriptOpens = (before.match(/<script[^>]*>/gi) || []).length;
|
|
245
|
-
const scriptCloses = (before.match(/<\/script>/gi) || []).length;
|
|
246
|
-
const inScript = scriptOpens > scriptCloses;
|
|
247
|
-
|
|
248
|
-
const styleOpens = (before.match(/<style[^>]*>/gi) || []).length;
|
|
249
|
-
const styleCloses = (before.match(/<\/style>/gi) || []).length;
|
|
250
|
-
const inStyle = styleOpens > styleCloses;
|
|
251
|
-
|
|
252
|
-
const lastTagOpen = before.lastIndexOf('<');
|
|
253
|
-
const lastTagClose = before.lastIndexOf('>');
|
|
254
|
-
const inTag = lastTagOpen > lastTagClose;
|
|
255
|
-
|
|
256
|
-
const lastBraceOpen = before.lastIndexOf('{');
|
|
257
|
-
const lastBraceClose = before.lastIndexOf('}');
|
|
258
|
-
const inExpression = lastBraceOpen > lastBraceClose && !inScript && !inStyle;
|
|
259
|
-
|
|
260
|
-
const inTemplate = !inScript && !inStyle;
|
|
261
|
-
|
|
262
|
-
// Check if inside attribute value
|
|
263
|
-
const afterLastTag = before.substring(lastTagOpen);
|
|
264
|
-
const quoteMatch = afterLastTag.match(/=["'][^"']*$/);
|
|
265
|
-
const inAttributeValue = inTag && !!quoteMatch;
|
|
266
|
-
|
|
267
|
-
let tagName: string | null = null;
|
|
268
|
-
if (inTag) {
|
|
269
|
-
const tagMatch = before.substring(lastTagOpen).match(/<\/?([A-Za-z][A-Za-z0-9-]*)/);
|
|
270
|
-
if (tagMatch) {
|
|
271
|
-
tagName = tagMatch[1];
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Get current word being typed
|
|
276
|
-
const wordMatch = before.match(/[a-zA-Z_$:@][a-zA-Z0-9_$:-]*$/);
|
|
277
|
-
const currentWord = wordMatch ? wordMatch[0] : '';
|
|
278
|
-
|
|
279
|
-
// Check for @ or : prefix for event/binding completion
|
|
280
|
-
const afterAt = before.endsWith('@') || currentWord.startsWith('@');
|
|
281
|
-
const afterColon = before.endsWith(':') || (currentWord.startsWith(':') && !currentWord.startsWith(':'));
|
|
282
|
-
|
|
283
|
-
return { inScript, inStyle, inTag, inExpression, inTemplate, inAttributeValue, tagName, currentWord, afterAt, afterColon };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Get project graph for a document
|
|
287
|
-
function getProjectGraph(docUri: string): ProjectGraph | null {
|
|
288
|
-
const filePath = docUri.replace('file://', '');
|
|
289
|
-
const projectRoot = detectProjectRoot(path.dirname(filePath), workspaceFolders);
|
|
290
|
-
|
|
291
|
-
if (!projectRoot) {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (!projectGraphs.has(projectRoot)) {
|
|
296
|
-
projectGraphs.set(projectRoot, buildProjectGraph(projectRoot));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return projectGraphs.get(projectRoot) || null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Invalidate project graph on file changes
|
|
303
|
-
function invalidateProjectGraph(uri: string) {
|
|
304
|
-
const filePath = uri.replace('file://', '');
|
|
305
|
-
const projectRoot = detectProjectRoot(path.dirname(filePath), workspaceFolders);
|
|
306
|
-
if (projectRoot) {
|
|
307
|
-
projectGraphs.delete(projectRoot);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
connection.onInitialize((params: InitializeParams): InitializeResult => {
|
|
312
|
-
workspaceFolders = (params.workspaceFolders || [])
|
|
313
|
-
.map((folder) => folder.uri.replace('file://', ''));
|
|
314
|
-
if (workspaceFolders.length === 0 && params.rootUri) {
|
|
315
|
-
workspaceFolders = [params.rootUri.replace('file://', '')];
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return {
|
|
319
|
-
capabilities: {
|
|
320
|
-
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
321
|
-
completionProvider: {
|
|
322
|
-
resolveProvider: true,
|
|
323
|
-
triggerCharacters: ['{', '<', '"', "'", '=', '.', ' ', ':', '(', '@']
|
|
324
|
-
},
|
|
325
|
-
hoverProvider: true,
|
|
326
|
-
codeActionProvider: true
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
connection.onInitialized(() => {
|
|
332
|
-
connection.client.register(DidChangeConfigurationNotification.type);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
connection.onCompletion((params: TextDocumentPositionParams): CompletionItem[] => {
|
|
336
|
-
const document = documents.get(params.textDocument.uri);
|
|
337
|
-
if (!document) return [];
|
|
338
|
-
|
|
339
|
-
const text = document.getText();
|
|
340
|
-
const offset = document.offsetAt(params.position);
|
|
341
|
-
const ctx = getPositionContext(text, offset);
|
|
342
|
-
const completions: CompletionItem[] = [];
|
|
343
|
-
|
|
344
|
-
const graph = getProjectGraph(params.textDocument.uri);
|
|
345
|
-
const script = getScriptContent(text);
|
|
346
|
-
const states = extractStates(script);
|
|
347
|
-
const functions = extractFunctions(script);
|
|
348
|
-
const imports = parseZenithImports(script);
|
|
349
|
-
const routerEnabled = hasRouterImport(imports);
|
|
350
|
-
const loopVariables = extractLoopVariables(text);
|
|
351
|
-
|
|
352
|
-
// Get line content before cursor
|
|
353
|
-
const lineStart = text.lastIndexOf('\n', offset - 1) + 1;
|
|
354
|
-
const lineBefore = text.substring(lineStart, offset);
|
|
355
|
-
|
|
356
|
-
// === SCRIPT CONTEXT ===
|
|
357
|
-
if (ctx.inScript) {
|
|
358
|
-
// Lifecycle hooks and state
|
|
359
|
-
for (const hook of LIFECYCLE_HOOKS) {
|
|
360
|
-
if (!ctx.currentWord || hook.name.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
|
|
361
|
-
completions.push({
|
|
362
|
-
label: hook.name,
|
|
363
|
-
kind: hook.kind,
|
|
364
|
-
detail: hook.name === 'state' ? 'Zenith State' : 'Zenith Lifecycle',
|
|
365
|
-
documentation: { kind: MarkupKind.Markdown, value: hook.doc },
|
|
366
|
-
insertText: hook.snippet,
|
|
367
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
368
|
-
sortText: `0_${hook.name}`,
|
|
369
|
-
preselect: hook.name === 'state' && ctx.currentWord.startsWith('s')
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Platform primitives (zenWindow, zenDocument, zenOn, zenResize, collectRefs, signal, ref)
|
|
375
|
-
for (const prim of PLATFORM_PRIMITIVES) {
|
|
376
|
-
if (!ctx.currentWord || prim.name.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
|
|
377
|
-
completions.push({
|
|
378
|
-
label: prim.name,
|
|
379
|
-
kind: prim.kind,
|
|
380
|
-
detail: 'Zenith Platform',
|
|
381
|
-
documentation: { kind: MarkupKind.Markdown, value: prim.doc },
|
|
382
|
-
insertText: prim.snippet,
|
|
383
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
384
|
-
sortText: `0_${prim.name}`
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Soft suggestions: window -> zenWindow, document -> zenDocument (do not block normal JS)
|
|
390
|
-
const lc = ctx.currentWord.toLowerCase();
|
|
391
|
-
if (lc === 'window' || lc.startsWith('wind')) {
|
|
392
|
-
completions.push({
|
|
393
|
-
label: 'zenWindow',
|
|
394
|
-
kind: CompletionItemKind.Function,
|
|
395
|
-
detail: 'Zenith (SSR-safe)',
|
|
396
|
-
documentation: { kind: MarkupKind.Markdown, value: 'Use zenWindow() instead of window for SSR-safe access.' },
|
|
397
|
-
insertText: 'zenWindow()',
|
|
398
|
-
sortText: '0_zenWindow'
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
if (lc === 'document' || lc.startsWith('doc')) {
|
|
402
|
-
completions.push({
|
|
403
|
-
label: 'zenDocument',
|
|
404
|
-
kind: CompletionItemKind.Function,
|
|
405
|
-
detail: 'Zenith (SSR-safe)',
|
|
406
|
-
documentation: { kind: MarkupKind.Markdown, value: 'Use zenDocument() instead of document for SSR-safe access.' },
|
|
407
|
-
insertText: 'zenDocument()',
|
|
408
|
-
sortText: '0_zenDocument'
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Router hooks when router is imported
|
|
413
|
-
if (routerEnabled) {
|
|
414
|
-
for (const hook of Object.values(ROUTER_HOOKS)) {
|
|
415
|
-
if (!ctx.currentWord || hook.name.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
|
|
416
|
-
completions.push({
|
|
417
|
-
label: hook.name,
|
|
418
|
-
kind: CompletionItemKind.Function,
|
|
419
|
-
detail: hook.owner,
|
|
420
|
-
documentation: { kind: MarkupKind.Markdown, value: `${hook.description}\n\n**Returns:** \`${hook.returns}\`` },
|
|
421
|
-
insertText: `${hook.name}()`,
|
|
422
|
-
sortText: `0_${hook.name}`
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Declared functions
|
|
429
|
-
for (const func of functions) {
|
|
430
|
-
if (!ctx.currentWord || func.name.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
|
|
431
|
-
completions.push({
|
|
432
|
-
label: func.name,
|
|
433
|
-
kind: CompletionItemKind.Function,
|
|
434
|
-
detail: `${func.isAsync ? 'async ' : ''}function ${func.name}(${func.params})`,
|
|
435
|
-
insertText: `${func.name}($0)`,
|
|
436
|
-
insertTextFormat: InsertTextFormat.Snippet
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// State variables
|
|
442
|
-
for (const [name, value] of states) {
|
|
443
|
-
if (!ctx.currentWord || name.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
|
|
444
|
-
completions.push({
|
|
445
|
-
label: name,
|
|
446
|
-
kind: CompletionItemKind.Variable,
|
|
447
|
-
detail: `state ${name}`,
|
|
448
|
-
documentation: `Current value: ${value}`
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Import path completions
|
|
454
|
-
const isImportPath = /from\s+['"][^'"]*$/.test(lineBefore) || /import\s+['"][^'"]*$/.test(lineBefore);
|
|
455
|
-
if (isImportPath) {
|
|
456
|
-
for (const mod of getAllModules()) {
|
|
457
|
-
completions.push({
|
|
458
|
-
label: mod.module,
|
|
459
|
-
kind: CompletionItemKind.Module,
|
|
460
|
-
detail: mod.kind === 'plugin' ? 'Zenith Plugin' : 'Zenith Core',
|
|
461
|
-
documentation: mod.description,
|
|
462
|
-
insertText: mod.module
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// === EXPRESSION CONTEXT { } ===
|
|
469
|
-
if (ctx.inExpression) {
|
|
470
|
-
// State variables
|
|
471
|
-
for (const [name, value] of states) {
|
|
472
|
-
completions.push({
|
|
473
|
-
label: name,
|
|
474
|
-
kind: CompletionItemKind.Variable,
|
|
475
|
-
detail: `state ${name}`,
|
|
476
|
-
documentation: `Value: ${value}`,
|
|
477
|
-
sortText: `0_${name}`
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Functions
|
|
482
|
-
for (const func of functions) {
|
|
483
|
-
completions.push({
|
|
484
|
-
label: func.name,
|
|
485
|
-
kind: CompletionItemKind.Function,
|
|
486
|
-
detail: `${func.isAsync ? 'async ' : ''}function`,
|
|
487
|
-
insertText: `${func.name}()`,
|
|
488
|
-
sortText: `1_${func.name}`
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// Loop variables
|
|
493
|
-
for (const loopVar of loopVariables) {
|
|
494
|
-
completions.push({
|
|
495
|
-
label: loopVar,
|
|
496
|
-
kind: CompletionItemKind.Variable,
|
|
497
|
-
detail: 'loop variable',
|
|
498
|
-
sortText: `0_${loopVar}`
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Route fields when router is imported
|
|
503
|
-
if (routerEnabled) {
|
|
504
|
-
for (const field of ROUTE_FIELDS) {
|
|
505
|
-
completions.push({
|
|
506
|
-
label: `route.${field.name}`,
|
|
507
|
-
kind: CompletionItemKind.Property,
|
|
508
|
-
detail: field.type,
|
|
509
|
-
documentation: field.description,
|
|
510
|
-
sortText: `2_route_${field.name}`
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// === TEMPLATE CONTEXT (not in script/style) ===
|
|
517
|
-
if (ctx.inTemplate && !ctx.inExpression && !ctx.inAttributeValue) {
|
|
518
|
-
const isAfterOpenBracket = lineBefore.match(/<\s*$/);
|
|
519
|
-
const isTypingTag = ctx.currentWord.length > 0 && !ctx.inTag;
|
|
520
|
-
|
|
521
|
-
// Components and layouts (PascalCase)
|
|
522
|
-
if (graph && (isAfterOpenBracket || (isTypingTag && /^[A-Z]/.test(ctx.currentWord)))) {
|
|
523
|
-
for (const [name, info] of graph.layouts) {
|
|
524
|
-
if (!ctx.currentWord || name.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
|
|
525
|
-
const propStr = info.props.length > 0 ? ` ${info.props[0]}="$1"` : '';
|
|
526
|
-
completions.push({
|
|
527
|
-
label: name,
|
|
528
|
-
kind: CompletionItemKind.Class,
|
|
529
|
-
detail: `layout`,
|
|
530
|
-
documentation: { kind: MarkupKind.Markdown, value: `**Layout** from \`${path.basename(info.filePath)}\`\n\nProps: ${info.props.join(', ') || 'none'}` },
|
|
531
|
-
insertText: isAfterOpenBracket
|
|
532
|
-
? `${name}${propStr}>$0</${name}>`
|
|
533
|
-
: `<${name}${propStr}>$0</${name}>`,
|
|
534
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
535
|
-
sortText: `0_${name}`
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
for (const [name, info] of graph.components) {
|
|
541
|
-
if (!ctx.currentWord || name.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
|
|
542
|
-
completions.push({
|
|
543
|
-
label: name,
|
|
544
|
-
kind: CompletionItemKind.Class,
|
|
545
|
-
detail: `component`,
|
|
546
|
-
documentation: { kind: MarkupKind.Markdown, value: `**Component** from \`${path.basename(info.filePath)}\`\n\nProps: ${info.props.join(', ') || 'none'}` },
|
|
547
|
-
insertText: isAfterOpenBracket
|
|
548
|
-
? `${name} $0/>`
|
|
549
|
-
: `<${name} $0/>`,
|
|
550
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
551
|
-
sortText: `0_${name}`
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// ZenLink when router is imported
|
|
558
|
-
if (routerEnabled && (isAfterOpenBracket || (isTypingTag && ctx.currentWord.toLowerCase().startsWith('z')))) {
|
|
559
|
-
completions.push({
|
|
560
|
-
label: 'ZenLink',
|
|
561
|
-
kind: CompletionItemKind.Class,
|
|
562
|
-
detail: 'router component',
|
|
563
|
-
documentation: { kind: MarkupKind.Markdown, value: '**Router Component** (zenith/router)\n\nDeclarative navigation component for routes.\n\n**Props:** to, preload, replace, class, activeClass' },
|
|
564
|
-
insertText: isAfterOpenBracket ? 'ZenLink to="$1">$0</ZenLink>' : '<ZenLink to="$1">$0</ZenLink>',
|
|
565
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
566
|
-
sortText: '0_ZenLink'
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// HTML elements (lowercase)
|
|
571
|
-
if (isAfterOpenBracket || (isTypingTag && /^[a-z]/.test(ctx.currentWord))) {
|
|
572
|
-
for (const el of HTML_ELEMENTS) {
|
|
573
|
-
if (!ctx.currentWord || el.tag.startsWith(ctx.currentWord.toLowerCase())) {
|
|
574
|
-
let snippet: string;
|
|
575
|
-
if (el.selfClosing) {
|
|
576
|
-
snippet = el.attrs ? `${el.tag} ${el.attrs} />` : `${el.tag} />`;
|
|
577
|
-
} else {
|
|
578
|
-
snippet = el.attrs ? `${el.tag} ${el.attrs}>$0</${el.tag}>` : `${el.tag}>$0</${el.tag}>`;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
completions.push({
|
|
582
|
-
label: el.tag,
|
|
583
|
-
kind: CompletionItemKind.Property,
|
|
584
|
-
detail: 'HTML',
|
|
585
|
-
documentation: el.doc,
|
|
586
|
-
insertText: isAfterOpenBracket ? snippet : `<${snippet}>`,
|
|
587
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
588
|
-
sortText: `1_${el.tag}`
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// === INSIDE TAG (attributes) ===
|
|
596
|
-
if (ctx.inTag && ctx.tagName && !ctx.inAttributeValue) {
|
|
597
|
-
// Directives (zen:if, zen:for, etc.)
|
|
598
|
-
const elementType = ctx.tagName === 'slot' ? 'slot' : (/^[A-Z]/.test(ctx.tagName) ? 'component' : 'element');
|
|
599
|
-
|
|
600
|
-
for (const directiveName of getDirectiveNames()) {
|
|
601
|
-
if (canPlaceDirective(directiveName, elementType as 'element' | 'component' | 'slot')) {
|
|
602
|
-
if (!ctx.currentWord || directiveName.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
|
|
603
|
-
const directive = getDirective(directiveName);
|
|
604
|
-
if (directive) {
|
|
605
|
-
completions.push({
|
|
606
|
-
label: directive.name,
|
|
607
|
-
kind: CompletionItemKind.Keyword,
|
|
608
|
-
detail: directive.category,
|
|
609
|
-
documentation: { kind: MarkupKind.Markdown, value: `${directive.description}\n\n**Syntax:** \`${directive.syntax}\`` },
|
|
610
|
-
insertText: `${directive.name}="$1"`,
|
|
611
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
612
|
-
sortText: `0_${directive.name}`
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// on:event completions
|
|
620
|
-
if (!ctx.currentWord || ctx.currentWord.startsWith('on:') || ctx.currentWord === 'on') {
|
|
621
|
-
for (const event of DOM_EVENTS) {
|
|
622
|
-
completions.push({
|
|
623
|
-
label: `on:${event}`,
|
|
624
|
-
kind: CompletionItemKind.Event,
|
|
625
|
-
detail: 'event binding',
|
|
626
|
-
documentation: `Bind to ${event} event`,
|
|
627
|
-
insertText: `on:${event}={$1}`,
|
|
628
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
629
|
-
sortText: `1_on:${event}`
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// :prop reactive bindings
|
|
635
|
-
if (ctx.afterColon || ctx.currentWord.startsWith(':')) {
|
|
636
|
-
for (const attr of HTML_ATTRIBUTES) {
|
|
637
|
-
completions.push({
|
|
638
|
-
label: `:${attr}`,
|
|
639
|
-
kind: CompletionItemKind.Property,
|
|
640
|
-
detail: 'reactive binding',
|
|
641
|
-
documentation: `Reactive binding for ${attr}`,
|
|
642
|
-
insertText: `:${attr}="$1"`,
|
|
643
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
644
|
-
sortText: `1_:${attr}`
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Component props
|
|
650
|
-
if (/^[A-Z]/.test(ctx.tagName) && graph) {
|
|
651
|
-
const component = resolveComponent(graph, ctx.tagName);
|
|
652
|
-
if (component) {
|
|
653
|
-
for (const prop of component.props) {
|
|
654
|
-
completions.push({
|
|
655
|
-
label: prop,
|
|
656
|
-
kind: CompletionItemKind.Property,
|
|
657
|
-
detail: `prop of <${ctx.tagName}>`,
|
|
658
|
-
insertText: `${prop}={$1}`,
|
|
659
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
660
|
-
sortText: `0_${prop}`
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// ZenLink props
|
|
667
|
-
if (routerEnabled && ctx.tagName === 'ZenLink') {
|
|
668
|
-
for (const prop of ZENLINK_PROPS) {
|
|
669
|
-
if (!ctx.currentWord || prop.name.toLowerCase().startsWith(ctx.currentWord.toLowerCase())) {
|
|
670
|
-
completions.push({
|
|
671
|
-
label: prop.name,
|
|
672
|
-
kind: CompletionItemKind.Property,
|
|
673
|
-
detail: prop.required ? `${prop.type} (required)` : prop.type,
|
|
674
|
-
documentation: prop.description,
|
|
675
|
-
insertText: prop.name === 'to' ? `${prop.name}="$1"` : `${prop.name}`,
|
|
676
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
677
|
-
sortText: prop.required ? `0_${prop.name}` : `1_${prop.name}`
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// HTML attributes
|
|
684
|
-
for (const attr of HTML_ATTRIBUTES) {
|
|
685
|
-
if (!ctx.currentWord || attr.startsWith(ctx.currentWord.toLowerCase())) {
|
|
686
|
-
completions.push({
|
|
687
|
-
label: attr,
|
|
688
|
-
kind: CompletionItemKind.Property,
|
|
689
|
-
detail: 'HTML attribute',
|
|
690
|
-
insertText: `${attr}="$1"`,
|
|
691
|
-
insertTextFormat: InsertTextFormat.Snippet,
|
|
692
|
-
sortText: `3_${attr}`
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// === INSIDE ATTRIBUTE VALUE ===
|
|
699
|
-
if (ctx.inAttributeValue) {
|
|
700
|
-
// Event handler: offer functions
|
|
701
|
-
const eventMatch = lineBefore.match(/on:[a-zA-Z][a-zA-Z0-9_-]*=["'{][^"'{}]*$/);
|
|
702
|
-
if (eventMatch) {
|
|
703
|
-
for (const func of functions) {
|
|
704
|
-
completions.push({
|
|
705
|
-
label: func.name,
|
|
706
|
-
kind: CompletionItemKind.Function,
|
|
707
|
-
detail: 'function',
|
|
708
|
-
insertText: func.name
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
return completions;
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
connection.onCompletionResolve((item: CompletionItem): CompletionItem => {
|
|
718
|
-
return item;
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
connection.onCodeAction((params: CodeActionParams): CodeAction[] => {
|
|
722
|
-
const document = documents.get(params.textDocument.uri);
|
|
723
|
-
if (!document) {
|
|
724
|
-
return [];
|
|
725
|
-
}
|
|
726
|
-
const eventActions = buildEventBindingCodeActions(document, params.context.diagnostics);
|
|
727
|
-
const domLintActions = buildDomLintCodeActions(document, params.context.diagnostics);
|
|
728
|
-
const windowDocActions = buildWindowDocumentCodeActions(document, params.range);
|
|
729
|
-
return [...eventActions, ...domLintActions, ...windowDocActions];
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
connection.onHover((params: TextDocumentPositionParams): Hover | null => {
|
|
733
|
-
const document = documents.get(params.textDocument.uri);
|
|
734
|
-
if (!document) return null;
|
|
735
|
-
|
|
736
|
-
const text = document.getText();
|
|
737
|
-
const offset = document.offsetAt(params.position);
|
|
738
|
-
|
|
739
|
-
// Get word at position (including : and @ prefixes)
|
|
740
|
-
const before = text.substring(0, offset);
|
|
741
|
-
const after = text.substring(offset);
|
|
742
|
-
const wordBefore = before.match(/[a-zA-Z0-9_$:@-]*$/)?.[0] || '';
|
|
743
|
-
const wordAfter = after.match(/^[a-zA-Z0-9_$:-]*/)?.[0] || '';
|
|
744
|
-
const word = wordBefore + wordAfter;
|
|
745
|
-
|
|
746
|
-
if (!word) return null;
|
|
747
|
-
|
|
748
|
-
// Check directives (zen:if, zen:for, etc.)
|
|
749
|
-
if (isDirective(word)) {
|
|
750
|
-
const directive = getDirective(word);
|
|
751
|
-
if (directive) {
|
|
752
|
-
let notes = '';
|
|
753
|
-
if (directive.name === 'zen:for') {
|
|
754
|
-
notes = '- No runtime loop\n- Compiled into static DOM instructions\n- Creates scope: `item`, `index`';
|
|
755
|
-
} else {
|
|
756
|
-
notes = '- Compile-time directive\n- No runtime assumptions\n- Processed at build time';
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
return {
|
|
760
|
-
contents: {
|
|
761
|
-
kind: MarkupKind.Markdown,
|
|
762
|
-
value: `### ${directive.name}\n\n${directive.description}\n\n**Syntax:** \`${directive.syntax}\`\n\n**Notes:**\n${notes}\n\n**Example:**\n\`\`\`html\n${directive.example}\n\`\`\``
|
|
763
|
-
}
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Check router hooks
|
|
769
|
-
if (isRouterHook(word)) {
|
|
770
|
-
const hook = getRouterHook(word);
|
|
771
|
-
if (hook) {
|
|
772
|
-
return {
|
|
773
|
-
contents: {
|
|
774
|
-
kind: MarkupKind.Markdown,
|
|
775
|
-
value: `### ${hook.name}()\n\n**${hook.owner}**\n\n${hook.description}\n\n**Restrictions:** ${hook.restrictions}\n\n**Returns:** \`${hook.returns}\`\n\n**Signature:**\n\`\`\`typescript\n${hook.signature}\n\`\`\``
|
|
776
|
-
}
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Check lifecycle hooks
|
|
782
|
-
const hook = LIFECYCLE_HOOKS.find(h => h.name === word);
|
|
783
|
-
if (hook) {
|
|
784
|
-
return {
|
|
785
|
-
contents: {
|
|
786
|
-
kind: MarkupKind.Markdown,
|
|
787
|
-
value: `### ${hook.name}\n\n${hook.doc}\n\n\`\`\`typescript\n${hook.snippet.replace(/\$\d/g, '').replace('$0', '// ...')}\n\`\`\``
|
|
788
|
-
}
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Check ZenLink
|
|
793
|
-
if (word === 'ZenLink') {
|
|
794
|
-
const script = getScriptContent(text);
|
|
795
|
-
const imports = parseZenithImports(script);
|
|
796
|
-
if (hasRouterImport(imports)) {
|
|
797
|
-
return {
|
|
798
|
-
contents: {
|
|
799
|
-
kind: MarkupKind.Markdown,
|
|
800
|
-
value: '### `<ZenLink>`\n\n**Router Component** (zenith/router)\n\nDeclarative navigation component for routes.\n\n**Props:**\n- `to` (string, required) - Route path\n- `preload` (boolean) - Prefetch on hover\n- `replace` (boolean) - Replace history entry\n- `class` (string) - CSS class\n- `activeClass` (string) - Class when active'
|
|
801
|
-
}
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Check states
|
|
807
|
-
const script = getScriptContent(text);
|
|
808
|
-
const states = extractStates(script);
|
|
809
|
-
if (states.has(word)) {
|
|
810
|
-
return {
|
|
811
|
-
contents: {
|
|
812
|
-
kind: MarkupKind.Markdown,
|
|
813
|
-
value: `### state \`${word}\`\n\n**Type:** inferred\n\n**Initial value:** \`${states.get(word)}\``
|
|
814
|
-
}
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Check functions
|
|
819
|
-
const functions = extractFunctions(script);
|
|
820
|
-
const func = functions.find(f => f.name === word);
|
|
821
|
-
if (func) {
|
|
822
|
-
return {
|
|
823
|
-
contents: {
|
|
824
|
-
kind: MarkupKind.Markdown,
|
|
825
|
-
value: `### ${func.isAsync ? 'async ' : ''}function \`${func.name}\`\n\n\`\`\`typescript\n${func.isAsync ? 'async ' : ''}function ${func.name}(${func.params})\n\`\`\``
|
|
826
|
-
}
|
|
827
|
-
};
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// Check imports
|
|
831
|
-
const imports = parseZenithImports(script);
|
|
832
|
-
for (const imp of imports) {
|
|
833
|
-
if (imp.specifiers.includes(word)) {
|
|
834
|
-
const exportMeta = resolveExport(imp.module, word);
|
|
835
|
-
if (exportMeta) {
|
|
836
|
-
const resolved = resolveModule(imp.module);
|
|
837
|
-
const owner = resolved.kind === 'plugin' ? 'Plugin' : resolved.kind === 'core' ? 'Core' : 'External';
|
|
838
|
-
return {
|
|
839
|
-
contents: {
|
|
840
|
-
kind: MarkupKind.Markdown,
|
|
841
|
-
value: `### ${word}\n\n**${owner}** (${imp.module})\n\n${exportMeta.description}\n\n**Signature:**\n\`\`\`typescript\n${exportMeta.signature || word}\n\`\`\``
|
|
842
|
-
}
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// Check components
|
|
849
|
-
const graph = getProjectGraph(params.textDocument.uri);
|
|
850
|
-
if (graph) {
|
|
851
|
-
const component = resolveComponent(graph, word);
|
|
852
|
-
if (component) {
|
|
853
|
-
return {
|
|
854
|
-
contents: {
|
|
855
|
-
kind: MarkupKind.Markdown,
|
|
856
|
-
value: `### ${component.type} \`<${component.name}>\`\n\n**File:** \`${component.filePath}\`\n\n**Props:** ${component.props.join(', ') || 'none'}`
|
|
857
|
-
}
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// Check HTML elements
|
|
863
|
-
const htmlEl = HTML_ELEMENTS.find(e => e.tag === word);
|
|
864
|
-
if (htmlEl) {
|
|
865
|
-
return {
|
|
866
|
-
contents: {
|
|
867
|
-
kind: MarkupKind.Markdown,
|
|
868
|
-
value: `### HTML \`<${htmlEl.tag}>\`\n\n${htmlEl.doc}`
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return null;
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
// Debounce + cancellation for diagnostics (prevents editor lag from rapid typing)
|
|
877
|
-
const DEBOUNCE_MS = 150;
|
|
878
|
-
const validationTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
|
879
|
-
const validationIds = new Map<string, number>();
|
|
880
|
-
|
|
881
|
-
documents.onDidChangeContent(change => {
|
|
882
|
-
const uri = change.document.uri;
|
|
883
|
-
const existing = validationTimeouts.get(uri);
|
|
884
|
-
if (existing) clearTimeout(existing);
|
|
885
|
-
validationTimeouts.set(
|
|
886
|
-
uri,
|
|
887
|
-
setTimeout(() => {
|
|
888
|
-
validationTimeouts.delete(uri);
|
|
889
|
-
validateDocument(change.document);
|
|
890
|
-
}, DEBOUNCE_MS)
|
|
891
|
-
);
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
documents.onDidSave(event => {
|
|
895
|
-
validateDocument(event.document);
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
documents.onDidOpen(event => {
|
|
899
|
-
validateDocument(event.document);
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
async function validateDocument(document: TextDocument) {
|
|
903
|
-
const uri = document.uri;
|
|
904
|
-
const id = (validationIds.get(uri) ?? 0) + 1;
|
|
905
|
-
validationIds.set(uri, id);
|
|
906
|
-
|
|
907
|
-
const graph = getProjectGraph(uri);
|
|
908
|
-
const filePath = uri.replace('file://', '');
|
|
909
|
-
const projectRoot = detectProjectRoot(path.dirname(filePath), workspaceFolders);
|
|
910
|
-
const diagnostics = await collectDiagnostics(document, graph, globalSettings, projectRoot);
|
|
911
|
-
|
|
912
|
-
if (validationIds.get(uri) !== id) return;
|
|
913
|
-
connection.sendDiagnostics({ uri, diagnostics });
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
connection.onDidChangeConfiguration((change) => {
|
|
917
|
-
const config = (change.settings?.zenith ?? change.settings) as unknown;
|
|
918
|
-
globalSettings = normalizeSettings(config);
|
|
919
|
-
|
|
920
|
-
for (const doc of documents.all()) {
|
|
921
|
-
validateDocument(doc);
|
|
922
|
-
}
|
|
923
|
-
});
|
|
924
|
-
|
|
925
|
-
// Watch for file changes
|
|
926
|
-
connection.onDidChangeWatchedFiles(params => {
|
|
927
|
-
for (const change of params.changes) {
|
|
928
|
-
invalidateProjectGraph(change.uri);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
for (const doc of documents.all()) {
|
|
932
|
-
validateDocument(doc);
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
documents.listen(connection);
|
|
937
|
-
connection.listen();
|