@lokascript/language-server 2.0.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/dist/server.js ADDED
@@ -0,0 +1,1578 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import {
5
+ createConnection,
6
+ TextDocuments,
7
+ ProposedFeatures,
8
+ TextDocumentSyncKind,
9
+ CompletionItemKind,
10
+ MarkupKind,
11
+ DiagnosticSeverity,
12
+ SymbolKind,
13
+ CodeActionKind,
14
+ DidChangeConfigurationNotification,
15
+ TextEdit
16
+ } from "vscode-languageserver/node.js";
17
+ import { TextDocument } from "vscode-languageserver-textdocument";
18
+
19
+ // src/types.ts
20
+ var defaultSettings = {
21
+ mode: "auto",
22
+ language: "en",
23
+ maxDiagnostics: 100
24
+ };
25
+
26
+ // src/command-tiers.ts
27
+ var HYPERSCRIPT_COMMANDS = [
28
+ // Core commands
29
+ "add",
30
+ "append",
31
+ "call",
32
+ "default",
33
+ "exit",
34
+ "fetch",
35
+ "for",
36
+ "get",
37
+ "go",
38
+ "halt",
39
+ "hide",
40
+ "if",
41
+ "increment",
42
+ "decrement",
43
+ "js",
44
+ "log",
45
+ "put",
46
+ "remove",
47
+ "repeat",
48
+ "return",
49
+ "send",
50
+ "set",
51
+ "show",
52
+ "take",
53
+ "tell",
54
+ "throw",
55
+ "toggle",
56
+ "transition",
57
+ "trigger",
58
+ "wait",
59
+ "while",
60
+ // Definitions
61
+ "behavior",
62
+ "def",
63
+ "init",
64
+ "on",
65
+ "eventsource",
66
+ "socket",
67
+ "worker"
68
+ ];
69
+ var LOKASCRIPT_ONLY_COMMANDS = [
70
+ "make",
71
+ "settle",
72
+ "measure",
73
+ "transfer",
74
+ "morph",
75
+ "persist",
76
+ "install",
77
+ "process-partials"
78
+ ];
79
+ var ALL_COMMANDS = [...HYPERSCRIPT_COMMANDS, ...LOKASCRIPT_ONLY_COMMANDS];
80
+ var LOKASCRIPT_SYNTAX_PATTERNS = {
81
+ /**
82
+ * Possessive dot notation: my.textContent, its.value, your.classList
83
+ * Original _hyperscript uses space: my textContent
84
+ */
85
+ dotNotation: /\b(my|your|its)\.\w+/,
86
+ /**
87
+ * Optional chaining: my?.value
88
+ */
89
+ optionalChaining: /\b(my|your|its)\?\.\w+/,
90
+ /**
91
+ * Extended 'as' conversions
92
+ */
93
+ extendedAsConversion: /\bas\s+(Int|Integer|Float|JSON|FormData|Date|URLSearchParams|Set|Map)\b/i,
94
+ /**
95
+ * Debounce/throttle modifiers with duration: .debounce(300), .throttle(1s)
96
+ */
97
+ temporalModifiers: /\.(debounce|throttle)\s*\(\s*\d+/i
98
+ };
99
+ function detectLokascriptFeatures(code) {
100
+ const detected = [];
101
+ for (const cmd of LOKASCRIPT_ONLY_COMMANDS) {
102
+ const pattern = new RegExp(`\\b${cmd}\\b`, "i");
103
+ if (pattern.test(code)) {
104
+ detected.push({
105
+ feature: "command",
106
+ description: `'${cmd}' command is a LokaScript extension`,
107
+ pattern: cmd
108
+ });
109
+ }
110
+ }
111
+ if (LOKASCRIPT_SYNTAX_PATTERNS.dotNotation.test(code)) {
112
+ detected.push({
113
+ feature: "syntax",
114
+ description: "Dot notation (my.property) is a LokaScript extension. Use 'my property' for _hyperscript compatibility",
115
+ pattern: "dot-notation"
116
+ });
117
+ }
118
+ if (LOKASCRIPT_SYNTAX_PATTERNS.optionalChaining.test(code)) {
119
+ detected.push({
120
+ feature: "syntax",
121
+ description: "Optional chaining (my?.property) is a LokaScript extension",
122
+ pattern: "optional-chaining"
123
+ });
124
+ }
125
+ const asMatch = code.match(LOKASCRIPT_SYNTAX_PATTERNS.extendedAsConversion);
126
+ if (asMatch) {
127
+ detected.push({
128
+ feature: "conversion",
129
+ description: `'as ${asMatch[1]}' is a LokaScript extension`,
130
+ pattern: `as-${asMatch[1].toLowerCase()}`
131
+ });
132
+ }
133
+ const temporalMatch = code.match(LOKASCRIPT_SYNTAX_PATTERNS.temporalModifiers);
134
+ if (temporalMatch) {
135
+ detected.push({
136
+ feature: "modifier",
137
+ description: `'.${temporalMatch[1]}()' modifier is a LokaScript extension`,
138
+ pattern: temporalMatch[1].toLowerCase()
139
+ });
140
+ }
141
+ return detected;
142
+ }
143
+ function getCommandsForMode(mode) {
144
+ return mode === "hyperscript" ? HYPERSCRIPT_COMMANDS : ALL_COMMANDS;
145
+ }
146
+
147
+ // src/extraction.ts
148
+ function isHtmlDocument(uri, content) {
149
+ if (uri.endsWith(".html") || uri.endsWith(".htm")) return true;
150
+ if (content.trim().startsWith("<!DOCTYPE") || content.trim().startsWith("<html")) return true;
151
+ if (/<\w+[^>]*>/.test(content) && !content.trim().startsWith("on ")) return true;
152
+ return false;
153
+ }
154
+ function extractHyperscriptRegions(content) {
155
+ const regions = [];
156
+ const fullContent = content;
157
+ const doubleQuoteRegex = /_="([^"\\]*(?:\\.[^"\\]*)*)"/g;
158
+ let match;
159
+ while ((match = doubleQuoteRegex.exec(fullContent)) !== null) {
160
+ const code = match[1].replace(/\\"/g, '"');
161
+ const startOffset = match.index + 3;
162
+ const endOffset = match.index + match[0].length - 1;
163
+ const startPos = offsetToPosition(content, startOffset);
164
+ const endPos = offsetToPosition(content, endOffset);
165
+ regions.push({
166
+ code,
167
+ startLine: startPos.line,
168
+ startChar: startPos.character,
169
+ endLine: endPos.line,
170
+ endChar: endPos.character,
171
+ type: "attribute"
172
+ });
173
+ }
174
+ const singleQuoteRegex = /_='([^'\\]*(?:\\.[^'\\]*)*)'/g;
175
+ while ((match = singleQuoteRegex.exec(fullContent)) !== null) {
176
+ const code = match[1].replace(/\\'/g, "'");
177
+ const startOffset = match.index + 3;
178
+ const endOffset = match.index + match[0].length - 1;
179
+ const startPos = offsetToPosition(content, startOffset);
180
+ const endPos = offsetToPosition(content, endOffset);
181
+ regions.push({
182
+ code,
183
+ startLine: startPos.line,
184
+ startChar: startPos.character,
185
+ endLine: endPos.line,
186
+ endChar: endPos.character,
187
+ type: "attribute"
188
+ });
189
+ }
190
+ const scriptRegex = /<script\s+type=["']text\/hyperscript["'][^>]*>([\s\S]*?)<\/script>/gi;
191
+ while ((match = scriptRegex.exec(fullContent)) !== null) {
192
+ const code = match[1];
193
+ const startOffset = match.index + match[0].indexOf(">") + 1;
194
+ const endOffset = match.index + match[0].lastIndexOf("</script>");
195
+ const startPos = offsetToPosition(content, startOffset);
196
+ const endPos = offsetToPosition(content, endOffset);
197
+ regions.push({
198
+ code,
199
+ startLine: startPos.line,
200
+ startChar: startPos.character,
201
+ endLine: endPos.line,
202
+ endChar: endPos.character,
203
+ type: "script"
204
+ });
205
+ }
206
+ return regions;
207
+ }
208
+ function offsetToPosition(content, offset) {
209
+ let line = 0;
210
+ let character = 0;
211
+ const maxOffset = Math.min(offset, content.length);
212
+ for (let i = 0; i < maxOffset; i++) {
213
+ if (content[i] === "\r" && content[i + 1] === "\n") {
214
+ line++;
215
+ character = 0;
216
+ i++;
217
+ } else if (content[i] === "\n") {
218
+ line++;
219
+ character = 0;
220
+ } else if (content[i] === "\r") {
221
+ line++;
222
+ character = 0;
223
+ } else {
224
+ character++;
225
+ }
226
+ }
227
+ return { line, character };
228
+ }
229
+ function findRegionAtPosition(regions, line, character) {
230
+ for (const region of regions) {
231
+ const afterStart = line > region.startLine || line === region.startLine && character >= region.startChar;
232
+ const beforeEnd = line < region.endLine || line === region.endLine && character <= region.endChar;
233
+ if (afterStart && beforeEnd) {
234
+ const localLine = line - region.startLine;
235
+ let localChar;
236
+ if (line === region.startLine) {
237
+ localChar = character - region.startChar;
238
+ } else {
239
+ localChar = character;
240
+ }
241
+ return { region, localLine, localChar };
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+ function findLineInRegion(code, offset) {
247
+ let line = 0;
248
+ for (let i = 0; i < offset && i < code.length; i++) {
249
+ if (code[i] === "\n") line++;
250
+ }
251
+ return line;
252
+ }
253
+ function findCharInLine(code, offset) {
254
+ let lastNewline = -1;
255
+ for (let i = 0; i < offset && i < code.length; i++) {
256
+ if (code[i] === "\n") lastNewline = i;
257
+ }
258
+ return offset - lastNewline - 1;
259
+ }
260
+
261
+ // src/utils.ts
262
+ function getWordAtPosition(line, character) {
263
+ let start = character;
264
+ let end = character;
265
+ const isWordChar = (char) => {
266
+ if (!char) return false;
267
+ return /[\p{L}\p{M}\p{N}_.-]/u.test(char);
268
+ };
269
+ while (start > 0 && isWordChar(line[start - 1])) {
270
+ start--;
271
+ }
272
+ while (end < line.length && isWordChar(line[end])) {
273
+ end++;
274
+ }
275
+ if (start === end) return null;
276
+ return { text: line.slice(start, end), start, end };
277
+ }
278
+ function escapeRegExp(str) {
279
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
280
+ }
281
+ function findNextNonEmptyLine(lines, startIndex) {
282
+ for (let i = startIndex; i < lines.length; i++) {
283
+ const trimmed = lines[i].trim();
284
+ if (trimmed) return trimmed;
285
+ }
286
+ return null;
287
+ }
288
+
289
+ // src/formatting.ts
290
+ function formatHyperscript(code, indentStr = " ") {
291
+ const lines = code.split("\n");
292
+ const formattedLines = [];
293
+ let indentLevel = 0;
294
+ const blockKeywords = /^(behavior|def|if|repeat|for|while)\b/i;
295
+ const dedentKeywords = /^(end|else)\b/i;
296
+ const onKeyword = /^on\b/i;
297
+ for (let i = 0; i < lines.length; i++) {
298
+ const line = lines[i];
299
+ const trimmed = line.trim();
300
+ if (!trimmed) {
301
+ formattedLines.push("");
302
+ continue;
303
+ }
304
+ if (dedentKeywords.test(trimmed)) {
305
+ indentLevel = Math.max(0, indentLevel - 1);
306
+ }
307
+ const indent = indentStr.repeat(indentLevel);
308
+ formattedLines.push(indent + trimmed);
309
+ if (blockKeywords.test(trimmed) && !/\bend\s*$/i.test(trimmed)) {
310
+ indentLevel++;
311
+ }
312
+ if (onKeyword.test(trimmed)) {
313
+ const nextLine = findNextNonEmptyLine(lines, i + 1);
314
+ if (nextLine && !/^(end|on)\b/i.test(nextLine)) {
315
+ indentLevel++;
316
+ }
317
+ }
318
+ if (/^else\b/i.test(trimmed)) {
319
+ indentLevel++;
320
+ }
321
+ }
322
+ return formattedLines.join("\n");
323
+ }
324
+
325
+ // src/server.ts
326
+ import * as semanticImport from "@lokascript/semantic";
327
+ var semanticPackage = semanticImport;
328
+ function getOtherLanguages() {
329
+ if (!semanticPackage?.getRegisteredLanguages) return [];
330
+ return semanticPackage.getRegisteredLanguages().filter((l) => l !== "en");
331
+ }
332
+ var interchangeLSP = null;
333
+ var fromCoreASTFn = null;
334
+ var parseFunction = null;
335
+ var lspMetadata = null;
336
+ try {
337
+ const core = await import("@hyperfixi/core");
338
+ parseFunction = core.parse;
339
+ fromCoreASTFn = core.fromCoreAST;
340
+ interchangeLSP = await import("@hyperfixi/core/ast-utils");
341
+ } catch {
342
+ console.error("[lokascript-ls] @hyperfixi/core not available");
343
+ }
344
+ try {
345
+ lspMetadata = await import("@hyperfixi/core/lsp-metadata");
346
+ } catch {
347
+ console.error(
348
+ "[lokascript-ls] @hyperfixi/core/lsp-metadata not available - using fallback keywords"
349
+ );
350
+ }
351
+ var FALLBACK_KEYWORDS = [
352
+ "toggle",
353
+ "add",
354
+ "remove",
355
+ "show",
356
+ "hide",
357
+ "put",
358
+ "set",
359
+ "get",
360
+ "fetch",
361
+ "wait",
362
+ "send",
363
+ "trigger",
364
+ "call",
365
+ "return",
366
+ "throw",
367
+ "halt",
368
+ "exit",
369
+ "go",
370
+ "log",
371
+ "on",
372
+ "me",
373
+ "you",
374
+ "it",
375
+ "result",
376
+ "if",
377
+ "else",
378
+ "then",
379
+ "end",
380
+ "repeat",
381
+ "for",
382
+ "while",
383
+ "behavior",
384
+ "def",
385
+ "init",
386
+ "first",
387
+ "last",
388
+ "next",
389
+ "previous",
390
+ "closest",
391
+ "parent",
392
+ "and",
393
+ "or",
394
+ "not",
395
+ "is",
396
+ "exists",
397
+ "empty",
398
+ "has"
399
+ ];
400
+ var FALLBACK_HOVER_DOCS = {
401
+ toggle: {
402
+ title: "toggle",
403
+ description: "Toggles a class, attribute, or visibility state.",
404
+ example: "toggle .active on me"
405
+ },
406
+ add: {
407
+ title: "add",
408
+ description: "Adds a class or attribute to an element.",
409
+ example: "add .highlight to me"
410
+ },
411
+ remove: {
412
+ title: "remove",
413
+ description: "Removes a class, attribute, or element.",
414
+ example: "remove .highlight from me"
415
+ },
416
+ show: { title: "show", description: "Makes an element visible.", example: "show #modal" },
417
+ hide: { title: "hide", description: "Hides an element.", example: "hide #modal" },
418
+ put: {
419
+ title: "put",
420
+ description: "Sets the content of an element.",
421
+ example: 'put "Hello" into #message'
422
+ },
423
+ set: { title: "set", description: "Sets a variable or property.", example: "set :count to 0" },
424
+ get: {
425
+ title: "get",
426
+ description: "Gets a value from an element or variable.",
427
+ example: "get the value of #input"
428
+ },
429
+ fetch: {
430
+ title: "fetch",
431
+ description: "Makes an HTTP request.",
432
+ example: "fetch /api/data as json"
433
+ },
434
+ wait: { title: "wait", description: "Pauses execution for a duration.", example: "wait 1s" },
435
+ send: {
436
+ title: "send",
437
+ description: "Dispatches a custom event.",
438
+ example: "send myEvent to #target"
439
+ },
440
+ trigger: {
441
+ title: "trigger",
442
+ description: "Triggers an event on an element.",
443
+ example: "trigger click on #btn"
444
+ },
445
+ call: {
446
+ title: "call",
447
+ description: "Calls a function or method.",
448
+ example: "call myFunction()"
449
+ },
450
+ log: { title: "log", description: "Logs a value to the console.", example: 'log "Hello"' },
451
+ go: { title: "go", description: "Navigates to a URL.", example: "go to url /home" },
452
+ on: {
453
+ title: "on",
454
+ description: "Defines an event handler.",
455
+ example: "on click toggle .active"
456
+ },
457
+ if: {
458
+ title: "if",
459
+ description: "Conditional execution.",
460
+ example: "if .active toggle .active"
461
+ },
462
+ repeat: {
463
+ title: "repeat",
464
+ description: "Loops a specified number of times.",
465
+ example: 'repeat 5 times log "hello"'
466
+ },
467
+ behavior: {
468
+ title: "behavior",
469
+ description: "Defines a reusable behavior.",
470
+ example: "behavior Clickable\\n on click toggle .active\\nend"
471
+ },
472
+ me: {
473
+ title: "me",
474
+ description: "References the current element.",
475
+ example: "toggle .active on me"
476
+ },
477
+ you: {
478
+ title: "you",
479
+ description: "References the element that triggered the event.",
480
+ example: "add .clicked to you"
481
+ },
482
+ it: {
483
+ title: "it",
484
+ description: "References the result of the last expression.",
485
+ example: "fetch /api/data then put it into #result"
486
+ },
487
+ result: {
488
+ title: "result",
489
+ description: "Alias for it - the result of the last expression.",
490
+ example: "call myFunction() then log result"
491
+ }
492
+ };
493
+ var FALLBACK_EVENT_NAMES = [
494
+ "click",
495
+ "dblclick",
496
+ "mouseenter",
497
+ "mouseleave",
498
+ "keydown",
499
+ "keyup",
500
+ "input",
501
+ "change",
502
+ "submit",
503
+ "load",
504
+ "focus",
505
+ "blur"
506
+ ];
507
+ function resolveMode(settings) {
508
+ if (settings.mode === "hyperscript") return "hyperscript";
509
+ if (settings.mode === "hyperscript-i18n") return "hyperscript-i18n";
510
+ if (settings.mode === "lokascript") return "lokascript";
511
+ return semanticPackage ? "lokascript" : "hyperscript";
512
+ }
513
+ function getBranding(mode) {
514
+ return mode === "lokascript" ? "lokascript" : "hyperscript";
515
+ }
516
+ function isMultilingualEnabled(mode) {
517
+ return mode === "lokascript" || mode === "hyperscript-i18n";
518
+ }
519
+ function isHyperscriptCompatMode(mode) {
520
+ return mode === "hyperscript" || mode === "hyperscript-i18n";
521
+ }
522
+ var resolvedMode = "hyperscript";
523
+ var envDefaultMode = process.env.HYPERSCRIPT_LS_DEFAULT_MODE;
524
+ var connection = createConnection(ProposedFeatures.all);
525
+ var documents = new TextDocuments(TextDocument);
526
+ var cachedAnalyzer = null;
527
+ function getSemanticAnalyzer() {
528
+ if (!semanticPackage) return null;
529
+ if (!cachedAnalyzer) {
530
+ cachedAnalyzer = semanticPackage.createSemanticAnalyzer();
531
+ }
532
+ return cachedAnalyzer;
533
+ }
534
+ var globalSettings = envDefaultMode ? { ...defaultSettings, mode: envDefaultMode } : defaultSettings;
535
+ connection.onInitialize((params) => {
536
+ resolvedMode = resolveMode(globalSettings);
537
+ const brand = getBranding(resolvedMode);
538
+ connection.console.log(`${brand} Language Server initializing (mode: ${resolvedMode})...`);
539
+ return {
540
+ capabilities: {
541
+ textDocumentSync: TextDocumentSyncKind.Incremental,
542
+ completionProvider: {
543
+ triggerCharacters: [".", "#", "@", " ", ":"],
544
+ resolveProvider: false
545
+ },
546
+ hoverProvider: true,
547
+ documentSymbolProvider: true,
548
+ codeActionProvider: {
549
+ codeActionKinds: [CodeActionKind.QuickFix]
550
+ },
551
+ definitionProvider: true,
552
+ referencesProvider: true,
553
+ documentFormattingProvider: true
554
+ }
555
+ };
556
+ });
557
+ connection.onInitialized(() => {
558
+ connection.client.register(DidChangeConfigurationNotification.type, void 0);
559
+ const brand = getBranding(resolvedMode);
560
+ connection.console.log(`${brand} Language Server initialized`);
561
+ });
562
+ connection.onDidChangeConfiguration((change) => {
563
+ const envDefaults = envDefaultMode ? { ...defaultSettings, mode: envDefaultMode } : defaultSettings;
564
+ globalSettings = change.settings?.lokascript || change.settings?.hyperscript || envDefaults;
565
+ const previousMode = resolvedMode;
566
+ resolvedMode = resolveMode(globalSettings);
567
+ if (previousMode !== resolvedMode) {
568
+ const brand = getBranding(resolvedMode);
569
+ connection.console.log(`Mode changed to '${resolvedMode}' (${brand})`);
570
+ }
571
+ translationCache = null;
572
+ reverseKeywordCache = null;
573
+ documents.all().forEach(validateDocument);
574
+ });
575
+ documents.onDidChangeContent((change) => {
576
+ validateDocument(change.document);
577
+ });
578
+ async function validateDocument(document) {
579
+ const text = document.getText();
580
+ const uri = document.uri;
581
+ let allDiagnostics = [];
582
+ if (isHtmlDocument(uri, text)) {
583
+ const regions = extractHyperscriptRegions(text);
584
+ for (const region of regions) {
585
+ const diagnostics = await getDiagnostics(region.code, globalSettings.language);
586
+ for (const diag of diagnostics) {
587
+ allDiagnostics.push({
588
+ ...diag,
589
+ range: {
590
+ start: {
591
+ line: region.startLine + diag.range.start.line,
592
+ character: diag.range.start.line === 0 ? region.startChar + diag.range.start.character : diag.range.start.character
593
+ },
594
+ end: {
595
+ line: region.startLine + diag.range.end.line,
596
+ character: diag.range.end.line === 0 ? region.startChar + diag.range.end.character : diag.range.end.character
597
+ }
598
+ }
599
+ });
600
+ }
601
+ }
602
+ } else {
603
+ allDiagnostics = await getDiagnostics(text, globalSettings.language);
604
+ }
605
+ connection.sendDiagnostics({ uri, diagnostics: allDiagnostics });
606
+ }
607
+ async function getDiagnostics(code, language) {
608
+ const diagnostics = [];
609
+ const brand = getBranding(resolvedMode);
610
+ if (isHyperscriptCompatMode(resolvedMode)) {
611
+ const lokascriptFeatures = detectLokascriptFeatures(code);
612
+ for (const feature of lokascriptFeatures) {
613
+ diagnostics.push({
614
+ range: {
615
+ start: { line: 0, character: 0 },
616
+ end: { line: 0, character: code.split("\n")[0]?.length || code.length }
617
+ },
618
+ severity: DiagnosticSeverity.Error,
619
+ code: "lokascript-only",
620
+ source: brand,
621
+ message: `${feature.description} (not compatible with _hyperscript)`
622
+ });
623
+ }
624
+ }
625
+ const analyzer = isMultilingualEnabled(resolvedMode) ? getSemanticAnalyzer() : null;
626
+ if (analyzer) {
627
+ try {
628
+ let result = analyzer.analyze(code, language);
629
+ let usedLanguage = language;
630
+ if (result.confidence < 0.5) {
631
+ const languagesToTry = getOtherLanguages();
632
+ for (const lang of languagesToTry) {
633
+ if (lang === usedLanguage) continue;
634
+ try {
635
+ const tryResult = analyzer.analyze(code, lang);
636
+ if (tryResult.confidence > result.confidence) {
637
+ result = tryResult;
638
+ usedLanguage = lang;
639
+ if (result.confidence >= 0.7) break;
640
+ }
641
+ } catch (e) {
642
+ connection.console.log(`[${brand}-ls] Language detection failed for '${lang}': ${e}`);
643
+ }
644
+ }
645
+ }
646
+ if (result.errors && result.errors.length > 0) {
647
+ for (const err of result.errors) {
648
+ diagnostics.push({
649
+ range: {
650
+ start: { line: 0, character: 0 },
651
+ end: { line: 0, character: code.split("\n")[0]?.length || code.length }
652
+ },
653
+ severity: DiagnosticSeverity.Error,
654
+ code: "semantic-error",
655
+ source: brand,
656
+ message: err
657
+ });
658
+ }
659
+ }
660
+ if (result.confidence > 0 && result.confidence < 0.3) {
661
+ diagnostics.push({
662
+ range: {
663
+ start: { line: 0, character: 0 },
664
+ end: { line: 0, character: code.length }
665
+ },
666
+ severity: DiagnosticSeverity.Warning,
667
+ code: "low-confidence",
668
+ source: brand,
669
+ message: `Code could not be fully parsed (${(result.confidence * 100).toFixed(0)}% confidence)`
670
+ });
671
+ }
672
+ if (result.confidence >= 0.5 && result.command) {
673
+ const cmd = result.command.name;
674
+ const roles = result.command.roles;
675
+ if (cmd === "toggle" && !roles.has("patient")) {
676
+ diagnostics.push({
677
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: code.length } },
678
+ severity: DiagnosticSeverity.Warning,
679
+ code: "missing-role",
680
+ source: brand,
681
+ message: "toggle command missing target (add .class, @attr, or selector)"
682
+ });
683
+ }
684
+ if (cmd === "put" && !roles.has("destination")) {
685
+ diagnostics.push({
686
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: code.length } },
687
+ severity: DiagnosticSeverity.Warning,
688
+ code: "missing-role",
689
+ source: brand,
690
+ message: 'put command missing destination (add "into #element")'
691
+ });
692
+ }
693
+ }
694
+ } catch (e) {
695
+ connection.console.log(`[${brand}-ls] Semantic parsing failed, trying fallback: ${e}`);
696
+ }
697
+ }
698
+ if (diagnostics.length === 0 && interchangeLSP && parseFunction && fromCoreASTFn) {
699
+ try {
700
+ const ast = parseFunction(code);
701
+ if (ast) {
702
+ const interchange = fromCoreASTFn(ast);
703
+ const nodes = interchange.type === "event" ? [interchange] : [interchange];
704
+ const astDiagnostics = interchangeLSP.interchangeToLSPDiagnostics(nodes, { source: brand });
705
+ diagnostics.push(...astDiagnostics);
706
+ }
707
+ } catch (parseError) {
708
+ diagnostics.push({
709
+ range: {
710
+ start: { line: 0, character: 0 },
711
+ end: { line: 0, character: code.length }
712
+ },
713
+ severity: DiagnosticSeverity.Error,
714
+ code: "parse-error",
715
+ source: brand,
716
+ message: parseError.message || "Failed to parse"
717
+ });
718
+ }
719
+ }
720
+ if (diagnostics.length === 0) {
721
+ const simpleDiagnostics = runSimpleDiagnostics(code, language);
722
+ diagnostics.push(...simpleDiagnostics.map((d) => ({ ...d, source: brand })));
723
+ }
724
+ return diagnostics.slice(0, globalSettings.maxDiagnostics);
725
+ }
726
+ function runSimpleDiagnostics(code, _language) {
727
+ const diagnostics = [];
728
+ const lines = code.split("\n");
729
+ for (let i = 0; i < lines.length; i++) {
730
+ const line = lines[i];
731
+ const singleQuotes = (line.match(/'/g) || []).length;
732
+ const doubleQuotes = (line.match(/"/g) || []).length;
733
+ if (singleQuotes % 2 !== 0) {
734
+ diagnostics.push({
735
+ range: { start: { line: i, character: 0 }, end: { line: i, character: line.length } },
736
+ severity: DiagnosticSeverity.Error,
737
+ code: "unmatched-quote",
738
+ source: "lokascript",
739
+ message: "Unmatched single quote"
740
+ });
741
+ }
742
+ if (doubleQuotes % 2 !== 0) {
743
+ diagnostics.push({
744
+ range: { start: { line: i, character: 0 }, end: { line: i, character: line.length } },
745
+ severity: DiagnosticSeverity.Error,
746
+ code: "unmatched-quote",
747
+ source: "lokascript",
748
+ message: "Unmatched double quote"
749
+ });
750
+ }
751
+ }
752
+ const openParens = (code.match(/\(/g) || []).length;
753
+ const closeParens = (code.match(/\)/g) || []).length;
754
+ if (openParens !== closeParens) {
755
+ diagnostics.push({
756
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
757
+ severity: DiagnosticSeverity.Error,
758
+ code: "unbalanced-parens",
759
+ source: "lokascript",
760
+ message: `Unbalanced parentheses: ${openParens} open, ${closeParens} close`
761
+ });
762
+ }
763
+ return diagnostics;
764
+ }
765
+ connection.onCompletion((params) => {
766
+ const document = documents.get(params.textDocument.uri);
767
+ if (!document) return [];
768
+ const text = document.getText();
769
+ const uri = document.uri;
770
+ const position = params.position;
771
+ const language = globalSettings.language;
772
+ if (isHtmlDocument(uri, text)) {
773
+ const regions = extractHyperscriptRegions(text);
774
+ const found = findRegionAtPosition(regions, position.line, position.character);
775
+ if (!found) {
776
+ return [];
777
+ }
778
+ const regionLines = found.region.code.split("\n");
779
+ let beforeCursor = "";
780
+ for (let i = 0; i <= found.localLine && i < regionLines.length; i++) {
781
+ if (i === found.localLine) {
782
+ beforeCursor += regionLines[i].slice(0, found.localChar);
783
+ } else {
784
+ beforeCursor += regionLines[i] + "\n";
785
+ }
786
+ }
787
+ const context = inferContext(beforeCursor);
788
+ return getContextualCompletions(context, language);
789
+ } else {
790
+ const offset = document.offsetAt(position);
791
+ const beforeCursor = text.slice(0, offset);
792
+ const context = inferContext(beforeCursor);
793
+ return getContextualCompletions(context, language);
794
+ }
795
+ });
796
+ function inferContext(beforeCursor) {
797
+ const trimmed = beforeCursor.trim();
798
+ if (/\bon\s*$/.test(trimmed)) return "event";
799
+ if (/\bthen\s*$/.test(trimmed)) return "command";
800
+ if (/^(on\s+\w+\s*)$/.test(trimmed)) return "command";
801
+ if (/(to|from|into|on)\s*$/.test(trimmed)) return "selector";
802
+ if (/\bif\s*$/.test(trimmed)) return "expression";
803
+ if (/\bset\s+:\w+\s+to\s*$/.test(trimmed)) return "expression";
804
+ return "default";
805
+ }
806
+ function getContextualCompletions(context, language) {
807
+ const completions = [];
808
+ const effectiveLanguage = isMultilingualEnabled(resolvedMode) ? language : "en";
809
+ const getKeyword = (eng) => {
810
+ if (!isMultilingualEnabled(resolvedMode) || !semanticPackage || effectiveLanguage === "en") {
811
+ return eng;
812
+ }
813
+ try {
814
+ const profile = semanticPackage.getProfile(effectiveLanguage);
815
+ if (profile?.keywords?.[eng]?.primary) {
816
+ return profile.keywords[eng].primary;
817
+ }
818
+ } catch (e) {
819
+ const brand = getBranding(resolvedMode);
820
+ connection.console.log(`[${brand}-ls] Keyword lookup failed for '${eng}': ${e}`);
821
+ }
822
+ return eng;
823
+ };
824
+ const commandMode = isHyperscriptCompatMode(resolvedMode) ? "hyperscript" : "lokascript";
825
+ const availableCommands = getCommandsForMode(commandMode);
826
+ const isCommandAvailable = (cmd) => availableCommands.includes(cmd);
827
+ switch (context) {
828
+ case "event":
829
+ const eventNames = lspMetadata?.EVENT_NAMES ?? FALLBACK_EVENT_NAMES;
830
+ for (const eventName of eventNames) {
831
+ completions.push({
832
+ label: eventName,
833
+ kind: CompletionItemKind.Event,
834
+ detail: `${eventName} event`
835
+ });
836
+ }
837
+ break;
838
+ case "command":
839
+ completions.push(
840
+ {
841
+ label: getKeyword("toggle"),
842
+ kind: CompletionItemKind.Method,
843
+ detail: "Toggle class/visibility",
844
+ insertText: `${getKeyword("toggle")} .\${1:class}`
845
+ },
846
+ {
847
+ label: getKeyword("add"),
848
+ kind: CompletionItemKind.Method,
849
+ detail: "Add class/attribute",
850
+ insertText: `${getKeyword("add")} .\${1:class} to \${2:me}`
851
+ },
852
+ {
853
+ label: getKeyword("remove"),
854
+ kind: CompletionItemKind.Method,
855
+ detail: "Remove class/element",
856
+ insertText: `${getKeyword("remove")} .\${1:class} from \${2:me}`
857
+ },
858
+ { label: getKeyword("show"), kind: CompletionItemKind.Method, detail: "Show element" },
859
+ { label: getKeyword("hide"), kind: CompletionItemKind.Method, detail: "Hide element" },
860
+ {
861
+ label: getKeyword("put"),
862
+ kind: CompletionItemKind.Method,
863
+ detail: "Set content",
864
+ insertText: `${getKeyword("put")} \${1:value} into \${2:target}`
865
+ },
866
+ {
867
+ label: getKeyword("set"),
868
+ kind: CompletionItemKind.Method,
869
+ detail: "Set variable",
870
+ insertText: `${getKeyword("set")} \${1:variable} to \${2:value}`
871
+ },
872
+ {
873
+ label: getKeyword("fetch"),
874
+ kind: CompletionItemKind.Method,
875
+ detail: "HTTP request",
876
+ insertText: `${getKeyword("fetch")} \${1:/api/endpoint}`
877
+ },
878
+ {
879
+ label: getKeyword("wait"),
880
+ kind: CompletionItemKind.Method,
881
+ detail: "Pause execution",
882
+ insertText: `${getKeyword("wait")} \${1:1s}`
883
+ },
884
+ {
885
+ label: getKeyword("send"),
886
+ kind: CompletionItemKind.Method,
887
+ detail: "Dispatch event",
888
+ insertText: `${getKeyword("send")} \${1:eventName} to \${2:target}`
889
+ },
890
+ { label: getKeyword("trigger"), kind: CompletionItemKind.Method, detail: "Trigger event" },
891
+ { label: getKeyword("log"), kind: CompletionItemKind.Method, detail: "Console log" },
892
+ { label: getKeyword("if"), kind: CompletionItemKind.Keyword, detail: "Conditional" },
893
+ { label: getKeyword("repeat"), kind: CompletionItemKind.Keyword, detail: "Loop" }
894
+ );
895
+ if (!isHyperscriptCompatMode(resolvedMode)) {
896
+ completions.push(
897
+ {
898
+ label: getKeyword("make"),
899
+ kind: CompletionItemKind.Method,
900
+ detail: "Create element or instance",
901
+ insertText: `${getKeyword("make")} a \${1:div}`
902
+ },
903
+ {
904
+ label: getKeyword("settle"),
905
+ kind: CompletionItemKind.Method,
906
+ detail: "Wait for CSS transitions"
907
+ },
908
+ {
909
+ label: getKeyword("measure"),
910
+ kind: CompletionItemKind.Method,
911
+ detail: "Get element dimensions"
912
+ },
913
+ {
914
+ label: getKeyword("morph"),
915
+ kind: CompletionItemKind.Method,
916
+ detail: "DOM morph with state preservation",
917
+ insertText: `${getKeyword("morph")} \${1:target} to \${2:content}`
918
+ },
919
+ {
920
+ label: getKeyword("persist"),
921
+ kind: CompletionItemKind.Method,
922
+ detail: "Save to browser storage",
923
+ insertText: `${getKeyword("persist")} \${1:value} as \${2:key}`
924
+ },
925
+ {
926
+ label: getKeyword("install"),
927
+ kind: CompletionItemKind.Method,
928
+ detail: "Install behavior on element",
929
+ insertText: `${getKeyword("install")} \${1:Behavior}`
930
+ }
931
+ );
932
+ }
933
+ break;
934
+ case "expression":
935
+ completions.push(
936
+ { label: "me", kind: CompletionItemKind.Variable, detail: "Current element" },
937
+ { label: "you", kind: CompletionItemKind.Variable, detail: "Event target" },
938
+ { label: "it", kind: CompletionItemKind.Variable, detail: "Last result" },
939
+ { label: "result", kind: CompletionItemKind.Variable, detail: "Last result (alias)" },
940
+ { label: "first", kind: CompletionItemKind.Keyword, detail: "First in collection" },
941
+ { label: "last", kind: CompletionItemKind.Keyword, detail: "Last in collection" },
942
+ { label: "next", kind: CompletionItemKind.Keyword, detail: "Next sibling" },
943
+ { label: "previous", kind: CompletionItemKind.Keyword, detail: "Previous sibling" },
944
+ { label: "closest", kind: CompletionItemKind.Keyword, detail: "Closest ancestor" },
945
+ { label: "parent", kind: CompletionItemKind.Keyword, detail: "Parent element" }
946
+ );
947
+ break;
948
+ case "selector":
949
+ completions.push(
950
+ {
951
+ label: "#",
952
+ kind: CompletionItemKind.Snippet,
953
+ detail: "ID selector",
954
+ insertText: "#${1:id}"
955
+ },
956
+ {
957
+ label: ".",
958
+ kind: CompletionItemKind.Snippet,
959
+ detail: "Class selector",
960
+ insertText: ".${1:class}"
961
+ },
962
+ {
963
+ label: "<",
964
+ kind: CompletionItemKind.Snippet,
965
+ detail: "Tag selector",
966
+ insertText: "<${1:tag}/>"
967
+ }
968
+ );
969
+ break;
970
+ default:
971
+ completions.push(
972
+ { label: getKeyword("on"), kind: CompletionItemKind.Keyword, detail: "Event handler" },
973
+ { label: getKeyword("init"), kind: CompletionItemKind.Keyword, detail: "Initialization" },
974
+ {
975
+ label: getKeyword("behavior"),
976
+ kind: CompletionItemKind.Class,
977
+ detail: "Define behavior"
978
+ },
979
+ { label: getKeyword("def"), kind: CompletionItemKind.Function, detail: "Define function" }
980
+ );
981
+ }
982
+ return completions;
983
+ }
984
+ connection.onHover((params) => {
985
+ try {
986
+ const document = documents.get(params.textDocument.uri);
987
+ if (!document) return null;
988
+ const text = document.getText();
989
+ const uri = document.uri;
990
+ const position = params.position;
991
+ if (isHtmlDocument(uri, text)) {
992
+ const regions = extractHyperscriptRegions(text);
993
+ const found = findRegionAtPosition(regions, position.line, position.character);
994
+ if (!found) {
995
+ return null;
996
+ }
997
+ const regionLines = found.region.code.split("\n");
998
+ const currentLine2 = regionLines[found.localLine];
999
+ if (!currentLine2) return null;
1000
+ const word2 = getWordAtPosition(currentLine2, found.localChar);
1001
+ if (!word2) return null;
1002
+ const doc2 = getHoverDocumentation(word2.text, globalSettings.language);
1003
+ if (!doc2) return null;
1004
+ const startChar = found.localLine === 0 ? found.region.startChar + word2.start : word2.start;
1005
+ const endChar = found.localLine === 0 ? found.region.startChar + word2.end : word2.end;
1006
+ return {
1007
+ contents: { kind: MarkupKind.Markdown, value: doc2 },
1008
+ range: {
1009
+ start: { line: position.line, character: startChar },
1010
+ end: { line: position.line, character: endChar }
1011
+ }
1012
+ };
1013
+ }
1014
+ const lines = text.split("\n");
1015
+ const currentLine = lines[position.line];
1016
+ if (!currentLine) return null;
1017
+ const word = getWordAtPosition(currentLine, position.character);
1018
+ if (!word) return null;
1019
+ const doc = getHoverDocumentation(word.text, globalSettings.language);
1020
+ if (!doc) return null;
1021
+ return {
1022
+ contents: { kind: MarkupKind.Markdown, value: doc },
1023
+ range: {
1024
+ start: { line: position.line, character: word.start },
1025
+ end: { line: position.line, character: word.end }
1026
+ }
1027
+ };
1028
+ } catch (error) {
1029
+ connection.console.error(`Hover error: ${error}`);
1030
+ return null;
1031
+ }
1032
+ });
1033
+ function getHoverDocumentation(word, language) {
1034
+ const wordLower = word.toLowerCase();
1035
+ const canonicalKey = isMultilingualEnabled(resolvedMode) ? resolveCanonicalKeyword(wordLower, language) : wordLower;
1036
+ const docs = lspMetadata?.HOVER_DOCS ?? FALLBACK_HOVER_DOCS;
1037
+ const doc = docs[canonicalKey];
1038
+ if (!doc) return null;
1039
+ let content = `**${doc.title}**`;
1040
+ if (isMultilingualEnabled(resolvedMode) && wordLower !== canonicalKey) {
1041
+ content = `**${word}** (${canonicalKey})`;
1042
+ }
1043
+ content += `
1044
+
1045
+ ${doc.description}`;
1046
+ content += `
1047
+
1048
+ \`\`\`hyperscript
1049
+ ${doc.example}
1050
+ \`\`\``;
1051
+ if (isMultilingualEnabled(resolvedMode)) {
1052
+ const translations = getKeywordTranslations(canonicalKey);
1053
+ if (translations) {
1054
+ content += `
1055
+
1056
+ **Translations:** ${translations}`;
1057
+ }
1058
+ }
1059
+ return content;
1060
+ }
1061
+ var translationCache = null;
1062
+ function buildTranslationCache() {
1063
+ const cache = /* @__PURE__ */ new Map();
1064
+ if (!semanticPackage) return cache;
1065
+ const langMap = {
1066
+ es: "Spanish",
1067
+ ja: "Japanese",
1068
+ ko: "Korean",
1069
+ de: "German",
1070
+ fr: "French"
1071
+ };
1072
+ const canonicalKeywords = [
1073
+ "toggle",
1074
+ "add",
1075
+ "remove",
1076
+ "show",
1077
+ "hide",
1078
+ "put",
1079
+ "set",
1080
+ "get",
1081
+ "fetch",
1082
+ "wait",
1083
+ "send",
1084
+ "trigger",
1085
+ "call",
1086
+ "log",
1087
+ "go",
1088
+ "on",
1089
+ "if",
1090
+ "repeat",
1091
+ "behavior",
1092
+ "me",
1093
+ "you",
1094
+ "it",
1095
+ "result"
1096
+ ];
1097
+ for (const canonicalKey of canonicalKeywords) {
1098
+ const translations = [];
1099
+ for (const [lang, langName] of Object.entries(langMap)) {
1100
+ try {
1101
+ const profile = semanticPackage.getProfile(lang);
1102
+ const kw = profile?.keywords?.[canonicalKey];
1103
+ if (kw) {
1104
+ const trans = kw;
1105
+ if (trans.primary && trans.primary !== canonicalKey) {
1106
+ translations.push(`${langName}: \`${trans.primary}\``);
1107
+ }
1108
+ }
1109
+ } catch (e) {
1110
+ connection.console.log(`[lokascript-ls] Translation cache: profile '${lang}' not found`);
1111
+ }
1112
+ }
1113
+ if (translations.length > 0) {
1114
+ cache.set(canonicalKey, translations.join(" \xB7 "));
1115
+ }
1116
+ }
1117
+ return cache;
1118
+ }
1119
+ function getKeywordTranslations(canonicalKey) {
1120
+ if (!translationCache) {
1121
+ translationCache = buildTranslationCache();
1122
+ }
1123
+ return translationCache.get(canonicalKey) || null;
1124
+ }
1125
+ var reverseKeywordCache = null;
1126
+ function buildReverseKeywordCache() {
1127
+ const cache = /* @__PURE__ */ new Map();
1128
+ if (!semanticPackage) return cache;
1129
+ const languagesToTry = getOtherLanguages();
1130
+ for (const lang of languagesToTry) {
1131
+ try {
1132
+ const profile = semanticPackage.getProfile(lang);
1133
+ if (profile?.keywords) {
1134
+ for (const [canonicalKey, translation] of Object.entries(profile.keywords)) {
1135
+ const trans = translation;
1136
+ if (trans.primary) {
1137
+ cache.set(trans.primary.toLowerCase(), canonicalKey);
1138
+ }
1139
+ if (trans.alternatives) {
1140
+ for (const alt of trans.alternatives) {
1141
+ cache.set(alt.toLowerCase(), canonicalKey);
1142
+ }
1143
+ }
1144
+ }
1145
+ }
1146
+ if (profile?.references) {
1147
+ for (const [engRef, translatedRef] of Object.entries(profile.references)) {
1148
+ if (typeof translatedRef === "string") {
1149
+ cache.set(translatedRef.toLowerCase(), engRef);
1150
+ }
1151
+ }
1152
+ }
1153
+ } catch (e) {
1154
+ connection.console.log(`[lokascript-ls] Reverse keyword cache: profile '${lang}' not found`);
1155
+ }
1156
+ }
1157
+ return cache;
1158
+ }
1159
+ function resolveCanonicalKeyword(word, _language) {
1160
+ const canonicalKeywords = lspMetadata?.ALL_KEYWORDS ?? FALLBACK_KEYWORDS;
1161
+ if (canonicalKeywords.includes(word)) return word;
1162
+ if (!reverseKeywordCache) {
1163
+ reverseKeywordCache = buildReverseKeywordCache();
1164
+ }
1165
+ return reverseKeywordCache.get(word) || word;
1166
+ }
1167
+ connection.onDefinition((params) => {
1168
+ try {
1169
+ const document = documents.get(params.textDocument.uri);
1170
+ if (!document) return null;
1171
+ const text = document.getText();
1172
+ const uri = document.uri;
1173
+ const position = params.position;
1174
+ let targetWord = null;
1175
+ if (isHtmlDocument(uri, text)) {
1176
+ const regions = extractHyperscriptRegions(text);
1177
+ const found = findRegionAtPosition(regions, position.line, position.character);
1178
+ if (!found) {
1179
+ return null;
1180
+ }
1181
+ const regionLines = found.region.code.split("\n");
1182
+ const currentLine = regionLines[found.localLine];
1183
+ if (!currentLine) return null;
1184
+ const word = getWordAtPosition(currentLine, found.localChar);
1185
+ if (!word) return null;
1186
+ targetWord = word.text;
1187
+ } else {
1188
+ const lines = text.split("\n");
1189
+ const currentLine = lines[position.line];
1190
+ if (!currentLine) return null;
1191
+ const word = getWordAtPosition(currentLine, position.character);
1192
+ if (!word) return null;
1193
+ targetWord = word.text;
1194
+ }
1195
+ if (!targetWord) return null;
1196
+ const locations = [];
1197
+ const behaviorVariants = getKeywordVariants("behavior");
1198
+ const defVariants = getKeywordVariants("def");
1199
+ const behaviorPattern = new RegExp(
1200
+ `\\b(${behaviorVariants.join("|")})\\s+(${escapeRegExp(targetWord)})\\b`,
1201
+ "gi"
1202
+ );
1203
+ const defPattern = new RegExp(
1204
+ `\\b(${defVariants.join("|")})\\s+(${escapeRegExp(targetWord)})\\b`,
1205
+ "gi"
1206
+ );
1207
+ if (isHtmlDocument(uri, text)) {
1208
+ const regions = extractHyperscriptRegions(text);
1209
+ for (const region of regions) {
1210
+ for (const match of region.code.matchAll(behaviorPattern)) {
1211
+ const matchLine = findLineInRegion(region.code, match.index ?? 0);
1212
+ const matchChar = findCharInLine(region.code, match.index ?? 0);
1213
+ locations.push({
1214
+ uri: params.textDocument.uri,
1215
+ range: {
1216
+ start: {
1217
+ line: region.startLine + matchLine,
1218
+ character: matchLine === 0 ? region.startChar + matchChar : matchChar
1219
+ },
1220
+ end: {
1221
+ line: region.startLine + matchLine,
1222
+ character: (matchLine === 0 ? region.startChar + matchChar : matchChar) + match[0].length
1223
+ }
1224
+ }
1225
+ });
1226
+ }
1227
+ for (const match of region.code.matchAll(defPattern)) {
1228
+ const matchLine = findLineInRegion(region.code, match.index ?? 0);
1229
+ const matchChar = findCharInLine(region.code, match.index ?? 0);
1230
+ locations.push({
1231
+ uri: params.textDocument.uri,
1232
+ range: {
1233
+ start: {
1234
+ line: region.startLine + matchLine,
1235
+ character: matchLine === 0 ? region.startChar + matchChar : matchChar
1236
+ },
1237
+ end: {
1238
+ line: region.startLine + matchLine,
1239
+ character: (matchLine === 0 ? region.startChar + matchChar : matchChar) + match[0].length
1240
+ }
1241
+ }
1242
+ });
1243
+ }
1244
+ }
1245
+ } else {
1246
+ for (const match of text.matchAll(behaviorPattern)) {
1247
+ const pos = offsetToPosition(text, match.index ?? 0);
1248
+ locations.push({
1249
+ uri: params.textDocument.uri,
1250
+ range: {
1251
+ start: pos,
1252
+ end: { line: pos.line, character: pos.character + match[0].length }
1253
+ }
1254
+ });
1255
+ }
1256
+ for (const match of text.matchAll(defPattern)) {
1257
+ const pos = offsetToPosition(text, match.index ?? 0);
1258
+ locations.push({
1259
+ uri: params.textDocument.uri,
1260
+ range: {
1261
+ start: pos,
1262
+ end: { line: pos.line, character: pos.character + match[0].length }
1263
+ }
1264
+ });
1265
+ }
1266
+ }
1267
+ if (locations.length === 0) return null;
1268
+ if (locations.length === 1) return locations[0];
1269
+ return locations;
1270
+ } catch (error) {
1271
+ connection.console.error(`Go to Definition error: ${error}`);
1272
+ return null;
1273
+ }
1274
+ });
1275
+ function getKeywordVariants(eng) {
1276
+ const variants = [eng];
1277
+ if (!semanticPackage) return variants;
1278
+ for (const lang of getOtherLanguages()) {
1279
+ try {
1280
+ const profile = semanticPackage.getProfile(lang);
1281
+ if (profile?.keywords?.[eng]) {
1282
+ const trans = profile.keywords[eng];
1283
+ if (trans.primary && !variants.includes(trans.primary)) {
1284
+ variants.push(trans.primary);
1285
+ }
1286
+ if (trans.alternatives) {
1287
+ for (const alt of trans.alternatives) {
1288
+ if (!variants.includes(alt)) {
1289
+ variants.push(alt);
1290
+ }
1291
+ }
1292
+ }
1293
+ }
1294
+ } catch {
1295
+ }
1296
+ }
1297
+ return variants;
1298
+ }
1299
+ connection.onReferences((params) => {
1300
+ try {
1301
+ const document = documents.get(params.textDocument.uri);
1302
+ if (!document) return null;
1303
+ const text = document.getText();
1304
+ const uri = document.uri;
1305
+ const position = params.position;
1306
+ let targetWord = null;
1307
+ if (isHtmlDocument(uri, text)) {
1308
+ const regions = extractHyperscriptRegions(text);
1309
+ const found = findRegionAtPosition(regions, position.line, position.character);
1310
+ if (!found) return null;
1311
+ const regionLines = found.region.code.split("\n");
1312
+ const currentLine = regionLines[found.localLine];
1313
+ if (!currentLine) return null;
1314
+ const word = getWordAtPosition(currentLine, found.localChar);
1315
+ if (!word) return null;
1316
+ targetWord = word.text;
1317
+ } else {
1318
+ const lines = text.split("\n");
1319
+ const currentLine = lines[position.line];
1320
+ if (!currentLine) return null;
1321
+ const word = getWordAtPosition(currentLine, position.character);
1322
+ if (!word) return null;
1323
+ targetWord = word.text;
1324
+ }
1325
+ if (!targetWord) return null;
1326
+ const locations = [];
1327
+ const wordPattern = new RegExp(`\\b${escapeRegExp(targetWord)}\\b`, "gi");
1328
+ if (isHtmlDocument(uri, text)) {
1329
+ const regions = extractHyperscriptRegions(text);
1330
+ for (const region of regions) {
1331
+ for (const match of region.code.matchAll(wordPattern)) {
1332
+ const matchLine = findLineInRegion(region.code, match.index ?? 0);
1333
+ const matchChar = findCharInLine(region.code, match.index ?? 0);
1334
+ locations.push({
1335
+ uri: params.textDocument.uri,
1336
+ range: {
1337
+ start: {
1338
+ line: region.startLine + matchLine,
1339
+ character: matchLine === 0 ? region.startChar + matchChar : matchChar
1340
+ },
1341
+ end: {
1342
+ line: region.startLine + matchLine,
1343
+ character: (matchLine === 0 ? region.startChar + matchChar : matchChar) + match[0].length
1344
+ }
1345
+ }
1346
+ });
1347
+ }
1348
+ }
1349
+ } else {
1350
+ for (const match of text.matchAll(wordPattern)) {
1351
+ const pos = offsetToPosition(text, match.index ?? 0);
1352
+ locations.push({
1353
+ uri: params.textDocument.uri,
1354
+ range: {
1355
+ start: pos,
1356
+ end: { line: pos.line, character: pos.character + match[0].length }
1357
+ }
1358
+ });
1359
+ }
1360
+ }
1361
+ return locations.length > 0 ? locations : null;
1362
+ } catch (error) {
1363
+ connection.console.error(`Find References error: ${error}`);
1364
+ return null;
1365
+ }
1366
+ });
1367
+ connection.onDocumentFormatting((params) => {
1368
+ try {
1369
+ const document = documents.get(params.textDocument.uri);
1370
+ if (!document) return null;
1371
+ const text = document.getText();
1372
+ const uri = document.uri;
1373
+ const options = params.options;
1374
+ const indentStr = options.insertSpaces ? " ".repeat(options.tabSize) : " ";
1375
+ if (isHtmlDocument(uri, text)) {
1376
+ connection.console.log("[lokascript-ls] Formatting not supported for HTML documents");
1377
+ return null;
1378
+ }
1379
+ const formatted = formatHyperscript(text, indentStr);
1380
+ if (formatted !== text) {
1381
+ return [
1382
+ TextEdit.replace(
1383
+ {
1384
+ start: { line: 0, character: 0 },
1385
+ end: { line: document.lineCount, character: 0 }
1386
+ },
1387
+ formatted
1388
+ )
1389
+ ];
1390
+ }
1391
+ return null;
1392
+ } catch (error) {
1393
+ connection.console.error(`Document Formatting error: ${error}`);
1394
+ return null;
1395
+ }
1396
+ });
1397
+ connection.onDocumentSymbol((params) => {
1398
+ const document = documents.get(params.textDocument.uri);
1399
+ if (!document) return [];
1400
+ const text = document.getText();
1401
+ const uri = document.uri;
1402
+ const language = globalSettings.language;
1403
+ if (isHtmlDocument(uri, text)) {
1404
+ const regions = extractHyperscriptRegions(text);
1405
+ const allSymbols = [];
1406
+ for (const region of regions) {
1407
+ const symbols = extractSymbols(region.code, language);
1408
+ for (const symbol of symbols) {
1409
+ allSymbols.push({
1410
+ ...symbol,
1411
+ range: {
1412
+ start: {
1413
+ line: region.startLine + symbol.range.start.line,
1414
+ character: symbol.range.start.line === 0 ? region.startChar + symbol.range.start.character : symbol.range.start.character
1415
+ },
1416
+ end: {
1417
+ line: region.startLine + symbol.range.end.line,
1418
+ character: symbol.range.end.line === 0 ? region.startChar + symbol.range.end.character : symbol.range.end.character
1419
+ }
1420
+ },
1421
+ selectionRange: {
1422
+ start: {
1423
+ line: region.startLine + symbol.selectionRange.start.line,
1424
+ character: symbol.selectionRange.start.line === 0 ? region.startChar + symbol.selectionRange.start.character : symbol.selectionRange.start.character
1425
+ },
1426
+ end: {
1427
+ line: region.startLine + symbol.selectionRange.end.line,
1428
+ character: symbol.selectionRange.end.line === 0 ? region.startChar + symbol.selectionRange.end.character : symbol.selectionRange.end.character
1429
+ }
1430
+ }
1431
+ });
1432
+ }
1433
+ }
1434
+ return allSymbols;
1435
+ } else {
1436
+ return extractSymbols(text, language);
1437
+ }
1438
+ });
1439
+ function extractSymbols(code, language) {
1440
+ const symbols = [];
1441
+ const lines = code.split("\n");
1442
+ const getVariants = (eng) => {
1443
+ const variants = [eng];
1444
+ if (!semanticPackage || language === "en") return variants;
1445
+ try {
1446
+ const profile = semanticPackage.getProfile(language);
1447
+ if (profile?.keywords?.[eng]) {
1448
+ const trans = profile.keywords[eng];
1449
+ if (trans.primary) variants.push(trans.primary);
1450
+ if (trans.alternatives) variants.push(...trans.alternatives);
1451
+ }
1452
+ } catch (e) {
1453
+ connection.console.log(
1454
+ `[lokascript-ls] Symbol extraction: variant lookup failed for '${eng}'`
1455
+ );
1456
+ }
1457
+ return variants;
1458
+ };
1459
+ const onVariants = getVariants("on");
1460
+ const behaviorVariants = getVariants("behavior");
1461
+ const defVariants = getVariants("def");
1462
+ const initVariants = getVariants("init");
1463
+ const onPattern = new RegExp(`\\b(${onVariants.join("|")})\\s+(\\w+(?:\\[.*?\\])?)`, "gi");
1464
+ const behaviorPattern = new RegExp(`\\b(${behaviorVariants.join("|")})\\s+(\\w+)`, "gi");
1465
+ const defPattern = new RegExp(`\\b(${defVariants.join("|")})\\s+(\\w+)`, "gi");
1466
+ const initPattern = new RegExp(`\\b(${initVariants.join("|")})\\b`, "gi");
1467
+ for (let i = 0; i < lines.length; i++) {
1468
+ const line = lines[i];
1469
+ for (const match of line.matchAll(onPattern)) {
1470
+ const matchIndex = match.index ?? 0;
1471
+ symbols.push({
1472
+ name: `${match[1]} ${match[2]}`,
1473
+ detail: "Event Handler",
1474
+ kind: SymbolKind.Event,
1475
+ range: {
1476
+ start: { line: i, character: matchIndex },
1477
+ end: { line: i, character: matchIndex + match[0].length }
1478
+ },
1479
+ selectionRange: {
1480
+ start: { line: i, character: matchIndex },
1481
+ end: { line: i, character: matchIndex + match[0].length }
1482
+ }
1483
+ });
1484
+ }
1485
+ for (const match of line.matchAll(behaviorPattern)) {
1486
+ const matchIndex = match.index ?? 0;
1487
+ symbols.push({
1488
+ name: `${match[1]} ${match[2]}`,
1489
+ detail: "Behavior Definition",
1490
+ kind: SymbolKind.Class,
1491
+ range: {
1492
+ start: { line: i, character: matchIndex },
1493
+ end: { line: i, character: matchIndex + match[0].length }
1494
+ },
1495
+ selectionRange: {
1496
+ start: { line: i, character: matchIndex },
1497
+ end: { line: i, character: matchIndex + match[0].length }
1498
+ }
1499
+ });
1500
+ }
1501
+ for (const match of line.matchAll(defPattern)) {
1502
+ const matchIndex = match.index ?? 0;
1503
+ symbols.push({
1504
+ name: `${match[1]} ${match[2]}`,
1505
+ detail: "Function Definition",
1506
+ kind: SymbolKind.Function,
1507
+ range: {
1508
+ start: { line: i, character: matchIndex },
1509
+ end: { line: i, character: matchIndex + match[0].length }
1510
+ },
1511
+ selectionRange: {
1512
+ start: { line: i, character: matchIndex },
1513
+ end: { line: i, character: matchIndex + match[0].length }
1514
+ }
1515
+ });
1516
+ }
1517
+ for (const match of line.matchAll(initPattern)) {
1518
+ const matchIndex = match.index ?? 0;
1519
+ symbols.push({
1520
+ name: match[1],
1521
+ detail: "Initialization",
1522
+ kind: SymbolKind.Constructor,
1523
+ range: {
1524
+ start: { line: i, character: matchIndex },
1525
+ end: { line: i, character: matchIndex + match[1].length }
1526
+ },
1527
+ selectionRange: {
1528
+ start: { line: i, character: matchIndex },
1529
+ end: { line: i, character: matchIndex + match[1].length }
1530
+ }
1531
+ });
1532
+ }
1533
+ }
1534
+ return symbols;
1535
+ }
1536
+ connection.onCodeAction((params) => {
1537
+ const diagnostics = params.context.diagnostics;
1538
+ const actions = [];
1539
+ for (const diagnostic of diagnostics) {
1540
+ if (diagnostic.code === "missing-role" && diagnostic.message.includes("toggle")) {
1541
+ actions.push({
1542
+ title: "Add class target to toggle",
1543
+ kind: CodeActionKind.QuickFix,
1544
+ diagnostics: [diagnostic],
1545
+ edit: {
1546
+ changes: {
1547
+ [params.textDocument.uri]: [
1548
+ {
1549
+ range: diagnostic.range,
1550
+ newText: "toggle .${1:classname}"
1551
+ }
1552
+ ]
1553
+ }
1554
+ }
1555
+ });
1556
+ }
1557
+ if (diagnostic.code === "missing-role" && diagnostic.message.includes("put")) {
1558
+ actions.push({
1559
+ title: "Add destination to put command",
1560
+ kind: CodeActionKind.QuickFix,
1561
+ diagnostics: [diagnostic],
1562
+ edit: {
1563
+ changes: {
1564
+ [params.textDocument.uri]: [
1565
+ {
1566
+ range: diagnostic.range,
1567
+ newText: "put ${1:value} into ${2:#element}"
1568
+ }
1569
+ ]
1570
+ }
1571
+ }
1572
+ });
1573
+ }
1574
+ }
1575
+ return actions;
1576
+ });
1577
+ documents.listen(connection);
1578
+ connection.listen();