@lokascript/language-server 3.0.0 → 3.1.1
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 +20 -18
- package/dist/server.js +233 -140
- package/package.json +9 -11
- package/dist/dist-2GK6K2QJ.js +0 -7895
- package/dist/server.d.ts +0 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ The language server supports four operating modes:
|
|
|
15
15
|
|
|
16
16
|
### Mode Selection
|
|
17
17
|
|
|
18
|
-
- **auto** (default):
|
|
18
|
+
- **auto** (default): `lokascript` mode when the semantic package exposes its API; `hyperscript` mode in builds that replace it with a shim (the standalone `hyperscript-vscode` extension does this)
|
|
19
19
|
- **hyperscript**: Enforces \_hyperscript-compatible syntax, English keywords only
|
|
20
20
|
- **hyperscript-i18n**: Enforces \_hyperscript-compatible syntax with multilingual keyword support. Use this if you have original \_hyperscript with `@lokascript/hyperscript-adapter` for writing in non-English languages
|
|
21
21
|
- **lokascript**: Enables all features including LokaScript extensions and multilingual support
|
|
@@ -50,9 +50,11 @@ This allows LokaScript users to maintain \_hyperscript compatibility by using `h
|
|
|
50
50
|
|
|
51
51
|
### Multilingual Support
|
|
52
52
|
|
|
53
|
-
Works with hyperscript written in any of
|
|
53
|
+
Works with hyperscript written in any of the 24 supported languages:
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
ar (Arabic), bn (Bengali), de (German), en (English), es (Spanish), fr (French), he (Hebrew), hi (Hindi), id (Indonesian), it (Italian), ja (Japanese), ko (Korean), ms (Malay), pl (Polish), pt (Portuguese), qu (Quechua), ru (Russian), sw (Swahili), th (Thai), tl (Tagalog), tr (Turkish), uk (Ukrainian), vi (Vietnamese), zh (Chinese)
|
|
56
|
+
|
|
57
|
+
In `lokascript` and `hyperscript-i18n` modes the server parses each region with the semantic front-end in the configured `language`, falling back to auto-detection across the other languages when that fails. When the front-end accepts a region in a non-English language, the English core parser's errors for that region are suppressed (the core parser is English-only, and its "unexpected token" on valid Spanish is noise, not a diagnostic).
|
|
56
58
|
|
|
57
59
|
### HTML Support
|
|
58
60
|
|
|
@@ -113,7 +115,13 @@ require('lspconfig').lokascript.setup{}
|
|
|
113
115
|
|
|
114
116
|
## Configuration
|
|
115
117
|
|
|
116
|
-
The server
|
|
118
|
+
The server reads its settings three ways, and every field is optional (a partial object is merged over the defaults):
|
|
119
|
+
|
|
120
|
+
1. `initializationOptions` on `initialize` (both VS Code extensions pass `{ language }` here);
|
|
121
|
+
2. a `workspace/didChangeConfiguration` push carrying a `lokascript` or `hyperscript` section (vscode-languageclient only sends one when the client sets `synchronize.configurationSection`; both bundled extensions do);
|
|
122
|
+
3. a `workspace/configuration` pull for the `lokascript` and `hyperscript` sections, made at startup and whenever a push arrives with no payload, for clients that advertise the capability.
|
|
123
|
+
|
|
124
|
+
When both namespaces are present the `lokascript` one wins, unless the server was launched with `HYPERSCRIPT_LS_DEFAULT_MODE` set (the standalone hyperscript product), in which case `hyperscript` wins.
|
|
117
125
|
|
|
118
126
|
```json
|
|
119
127
|
{
|
|
@@ -218,13 +226,10 @@ For users who prefer the `hyperscript` namespace:
|
|
|
218
226
|
|
|
219
227
|
## Dependencies
|
|
220
228
|
|
|
221
|
-
|
|
229
|
+
- `@lokascript/semantic` and `@lokascript/framework` are **required** (regular dependencies): the semantic package registers the 24 languages at startup, and the framework renders the LSE bracket notation shown in hover. They used to be declared as optional peers while being imported statically, so a server installed without them crashed at startup instead of falling back.
|
|
230
|
+
- `@hyperfixi/core` is an **optional** peer. With it the server surfaces real parse errors, complexity diagnostics and schema-inferred roles in hover; without it, diagnostics degrade to the pattern-based quote/bracket checks and hover uses built-in fallback docs.
|
|
222
231
|
|
|
223
|
-
|
|
224
|
-
- `@lokascript/ast-toolkit` - Enables AST-based analysis and complexity metrics
|
|
225
|
-
- `@lokascript/core` - Enables full hyperscript parsing and AST-based formatting
|
|
226
|
-
|
|
227
|
-
Without these dependencies, the server falls back to pattern-based analysis (English only).
|
|
232
|
+
A bundler that wants an English-only, dependency-free server (the standalone `hyperscript-vscode` extension) replaces the semantic, framework and core imports with throwing or empty shims at bundle time; the server's capability probes are written for that case.
|
|
228
233
|
|
|
229
234
|
## Development
|
|
230
235
|
|
|
@@ -247,7 +252,7 @@ npm test -- --coverage
|
|
|
247
252
|
|
|
248
253
|
## Architecture
|
|
249
254
|
|
|
250
|
-
The language server is
|
|
255
|
+
The language server entry point is `server.ts` (LSP wiring and the request handlers), with the testable logic in sibling modules — `extraction.ts` (HTML region extraction and position mapping), `simple-diagnostics.ts`, `command-tiers.ts` (the _hyperscript-compatibility allowlists), `symbol-table.ts` (definition/references/rename), `document-symbols.ts`, `completion-context.ts`, `formatting.ts` and `localized-descriptions.ts`. Together they:
|
|
251
256
|
|
|
252
257
|
1. **Extracts** hyperscript regions from HTML documents
|
|
253
258
|
2. **Analyzes** code using semantic parsing (multilingual) or pattern-based fallback
|
|
@@ -299,13 +304,10 @@ npm test -- --run src/server.test.ts
|
|
|
299
304
|
|
|
300
305
|
Test categories:
|
|
301
306
|
|
|
302
|
-
- HTML document detection
|
|
303
|
-
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
- Find References
|
|
307
|
-
- Code Formatting
|
|
308
|
-
- LSP integration tests
|
|
307
|
+
- Unit tests against the shipped modules: HTML document detection, region extraction, position mapping, the symbol table (definition, references, rename), document symbols, completion context, formatting, diagnostics, and the compatibility allowlists
|
|
308
|
+
- `lsp-integration.test.ts` spawns the built `dist/server.js` over stdio and speaks JSON-RPC: diagnostics publishing (including the non-English cases), completions, hover, symbols, definition, references, rename, formatting, code actions, configuration changes in both push and pull form, and the `lokascript/translateWithVerification` request
|
|
309
|
+
|
|
310
|
+
`npm test` rebuilds this package first (`pretest`), and refreshes any stale sibling `dist/` the integration suite resolves through the workspace.
|
|
309
311
|
|
|
310
312
|
## License
|
|
311
313
|
|
package/dist/server.js
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
CompletionItemKind,
|
|
10
10
|
MarkupKind,
|
|
11
11
|
DiagnosticSeverity as DiagnosticSeverity2,
|
|
12
|
-
SymbolKind,
|
|
13
12
|
CodeActionKind,
|
|
14
13
|
DidChangeConfigurationNotification,
|
|
15
14
|
TextEdit
|
|
@@ -161,10 +160,11 @@ var LOKASCRIPT_ONLY_COMMANDS = [
|
|
|
161
160
|
var ALL_COMMANDS = [...HYPERSCRIPT_COMMANDS, ...LOKASCRIPT_ONLY_COMMANDS];
|
|
162
161
|
var LOKASCRIPT_SYNTAX_PATTERNS = {
|
|
163
162
|
/**
|
|
164
|
-
* Possessive dot notation
|
|
165
|
-
*
|
|
163
|
+
* Possessive dot notation on `its`: its.value
|
|
164
|
+
* Upstream resolves `my.x` and `your.x` (they are plain symbols), so only
|
|
165
|
+
* `its.` is non-portable; write `its value` for _hyperscript compatibility.
|
|
166
166
|
*/
|
|
167
|
-
dotNotation: /\
|
|
167
|
+
dotNotation: /\bits\.\w+/,
|
|
168
168
|
/**
|
|
169
169
|
* Optional chaining: my?.value
|
|
170
170
|
*/
|
|
@@ -172,17 +172,49 @@ var LOKASCRIPT_SYNTAX_PATTERNS = {
|
|
|
172
172
|
/**
|
|
173
173
|
* Extended 'as' conversions
|
|
174
174
|
*/
|
|
175
|
-
extendedAsConversion: /\bas\s+(
|
|
175
|
+
extendedAsConversion: /\bas\s+(Math|Values)\b/i,
|
|
176
176
|
/**
|
|
177
177
|
* Debounce/throttle modifiers with duration: .debounce(300), .throttle(1s)
|
|
178
178
|
*/
|
|
179
179
|
temporalModifiers: /\.(debounce|throttle)\s*\(\s*\d+/i
|
|
180
180
|
};
|
|
181
|
+
function blankStringsAndComments(code) {
|
|
182
|
+
let out = "";
|
|
183
|
+
let i = 0;
|
|
184
|
+
while (i < code.length) {
|
|
185
|
+
const ch = code[i];
|
|
186
|
+
if (ch === "-" && code[i + 1] === "-") {
|
|
187
|
+
const eol = code.indexOf("\n", i);
|
|
188
|
+
const end = eol === -1 ? code.length : eol;
|
|
189
|
+
out += " ".repeat(end - i);
|
|
190
|
+
i = end;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const prev = i === 0 ? " " : code[i - 1];
|
|
194
|
+
const opensString = ch === '"' || ch === "`" || ch === "'" && /[\s(,\[=]/.test(prev);
|
|
195
|
+
if (opensString) {
|
|
196
|
+
let j = i + 1;
|
|
197
|
+
while (j < code.length && code[j] !== ch) {
|
|
198
|
+
if (code[j] === "\\") j++;
|
|
199
|
+
if (code[j] === "\n" && ch !== "`") break;
|
|
200
|
+
j++;
|
|
201
|
+
}
|
|
202
|
+
const end = Math.min(j + 1, code.length);
|
|
203
|
+
out += " ".repeat(end - i);
|
|
204
|
+
i = end;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
out += ch;
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
181
212
|
function detectLokascriptFeatures(code) {
|
|
182
213
|
const detected = [];
|
|
214
|
+
const scannable = blankStringsAndComments(code);
|
|
183
215
|
for (const cmd of LOKASCRIPT_ONLY_COMMANDS) {
|
|
184
|
-
const pattern = new RegExp(
|
|
185
|
-
if (pattern.test(
|
|
216
|
+
const pattern = cmd === "unless" ? /(?<=^[ \t]*|\bthen\s+|\bend\s+|\belse\s+)unless(?![\w-])/im : new RegExp(`(?<![\\w.:$@^#-])${cmd}(?![\\w-])(?!\\s*\\()`, "i");
|
|
217
|
+
if (pattern.test(scannable)) {
|
|
186
218
|
detected.push({
|
|
187
219
|
feature: "command",
|
|
188
220
|
description: `'${cmd}' command is a LokaScript extension`,
|
|
@@ -226,6 +258,7 @@ function detectLokascriptFeatures(code) {
|
|
|
226
258
|
// src/extraction.ts
|
|
227
259
|
function isHtmlDocument(uri, content) {
|
|
228
260
|
if (uri.endsWith(".html") || uri.endsWith(".htm")) return true;
|
|
261
|
+
if (/\.(hs|_hs|hyperscript)$/i.test(uri)) return false;
|
|
229
262
|
if (content.trim().startsWith("<!DOCTYPE") || content.trim().startsWith("<html")) return true;
|
|
230
263
|
if (/<\w+[^>]*>/.test(content) && !content.trim().startsWith("on ")) return true;
|
|
231
264
|
return false;
|
|
@@ -376,6 +409,9 @@ function getWordAtPosition(line, character) {
|
|
|
376
409
|
if (start === end) return null;
|
|
377
410
|
return { text: line.slice(start, end), start, end };
|
|
378
411
|
}
|
|
412
|
+
function escapeRegExp(str) {
|
|
413
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
414
|
+
}
|
|
379
415
|
function findNextNonEmptyLine(lines, startIndex) {
|
|
380
416
|
for (let i = startIndex; i < lines.length; i++) {
|
|
381
417
|
const trimmed = lines[i].trim();
|
|
@@ -425,7 +461,7 @@ import { DiagnosticSeverity } from "vscode-languageserver/node.js";
|
|
|
425
461
|
function isPossessive(code, i) {
|
|
426
462
|
if (i === 0) return false;
|
|
427
463
|
const prev = code[i - 1];
|
|
428
|
-
if (!/[\w\-#.]/.test(prev)) return false;
|
|
464
|
+
if (!/[\w\-#.)\]>]/.test(prev)) return false;
|
|
429
465
|
if (i + 1 >= code.length || code[i + 1] !== "s") return false;
|
|
430
466
|
if (i + 2 >= code.length) return true;
|
|
431
467
|
const after = code[i + 2];
|
|
@@ -459,6 +495,13 @@ function runSimpleDiagnostics(code, _language = "en") {
|
|
|
459
495
|
col++;
|
|
460
496
|
continue;
|
|
461
497
|
}
|
|
498
|
+
if (ch === "-" && code[i + 1] === "-") {
|
|
499
|
+
const eol = code.indexOf("\n", i);
|
|
500
|
+
if (eol === -1) break;
|
|
501
|
+
col += eol - i;
|
|
502
|
+
i = eol - 1;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
462
505
|
switch (ch) {
|
|
463
506
|
case "'":
|
|
464
507
|
if (isPossessive(code, i)) {
|
|
@@ -632,14 +675,15 @@ function buildSymbolTable(regions, getKeywordVariants2) {
|
|
|
632
675
|
const defaultKws = variants("default");
|
|
633
676
|
const installKws = variants("install");
|
|
634
677
|
const callKws = variants("call");
|
|
635
|
-
const
|
|
636
|
-
const
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
const
|
|
678
|
+
const kw = (words) => `(?<![\\p{L}\\p{N}_])(${words.map(escapeRegExp).join("|")})(?![\\p{L}\\p{N}_])`;
|
|
679
|
+
const behaviorDefPat = new RegExp(`${kw(behaviorKws)}\\s+(\\w+)`, "giu");
|
|
680
|
+
const funcDefPat = new RegExp(`${kw(defKws)}\\s+(\\w+)`, "giu");
|
|
681
|
+
const varDefPat = new RegExp(`${kw([...setKws, ...defaultKws])}\\s+([:$]\\w+)`, "giu");
|
|
682
|
+
const installPat = new RegExp(`${kw(installKws)}\\s+(\\w+)`, "giu");
|
|
683
|
+
const callPat = new RegExp(`${kw(callKws)}\\s+(\\w+)`, "giu");
|
|
684
|
+
const varRefPat = /(?<![\p{L}\p{N}_)\]\-])[:$]\w+/gu;
|
|
641
685
|
const funcCallPat = /\b(\w+)\s*\(/g;
|
|
642
|
-
const caretWritePat = new RegExp(
|
|
686
|
+
const caretWritePat = new RegExp(`${kw(setKws)}\\s+\\^([A-Za-z_]\\w*)`, "giu");
|
|
643
687
|
const caretRefPat = /\^([A-Za-z_]\w*)/g;
|
|
644
688
|
const attrsRefPat = /\battrs\.([A-Za-z_]\w*)/g;
|
|
645
689
|
function getOrCreate(name, kind, scope) {
|
|
@@ -662,6 +706,11 @@ function buildSymbolTable(regions, getKeywordVariants2) {
|
|
|
662
706
|
character: toDocChar(region, localLine, localChar),
|
|
663
707
|
length
|
|
664
708
|
};
|
|
709
|
+
if (locationIndex.some(
|
|
710
|
+
(l) => l.name === entry.name && l.line === loc.line && l.start === loc.character
|
|
711
|
+
)) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
665
714
|
if (kind === "def") {
|
|
666
715
|
entry.definitions.push(loc);
|
|
667
716
|
} else {
|
|
@@ -881,6 +930,82 @@ function getSymbolTable(uri, version, regions, getKeywordVariants2) {
|
|
|
881
930
|
return table;
|
|
882
931
|
}
|
|
883
932
|
|
|
933
|
+
// src/completion-context.ts
|
|
934
|
+
function inferContext(beforeCursor) {
|
|
935
|
+
if (/\^\w*$/.test(beforeCursor)) return "caret";
|
|
936
|
+
if (/\battrs\.\w*$/.test(beforeCursor)) return "attrs-property";
|
|
937
|
+
const trimmed = beforeCursor.trim();
|
|
938
|
+
if (/\bon\s*$/.test(trimmed)) return "event";
|
|
939
|
+
if (/\bthen\s*$/.test(trimmed)) return "command";
|
|
940
|
+
if (/^(on\s+\w+\s*)$/.test(trimmed)) return "command";
|
|
941
|
+
if (/(to|from|into|on)\s*$/.test(trimmed)) return "selector";
|
|
942
|
+
if (/\bif\s*$/.test(trimmed)) return "expression";
|
|
943
|
+
if (/\bset\s+:\w+\s+to\s*$/.test(trimmed)) return "expression";
|
|
944
|
+
return "default";
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/document-symbols.ts
|
|
948
|
+
import { SymbolKind } from "vscode-languageserver/node.js";
|
|
949
|
+
var COMMAND_POSITION = "(?<=^[ \\t]*|(?<![\\p{L}\\p{N}_])end\\s+)";
|
|
950
|
+
var NOT_WORD_AFTER = "(?![\\p{L}\\p{N}_])";
|
|
951
|
+
var NAME = "([\\p{L}\\p{N}_]+)";
|
|
952
|
+
function alternation(words) {
|
|
953
|
+
return `(${words.map(escapeRegExp).join("|")})`;
|
|
954
|
+
}
|
|
955
|
+
function symbol(name, detail, kind, line, start, length) {
|
|
956
|
+
const range = {
|
|
957
|
+
start: { line, character: start },
|
|
958
|
+
end: { line, character: start + length }
|
|
959
|
+
};
|
|
960
|
+
return { name, detail, kind, range, selectionRange: range };
|
|
961
|
+
}
|
|
962
|
+
function extractDocumentSymbols(code, getVariants = (eng) => [eng]) {
|
|
963
|
+
const symbols = [];
|
|
964
|
+
const lines = code.split("\n");
|
|
965
|
+
const onPattern = new RegExp(
|
|
966
|
+
`${COMMAND_POSITION}${alternation(getVariants("on"))}\\s+(${NAME.slice(1, -1)}(?::[\\p{L}\\p{N}_]+)?(?:\\[.*?\\])?)`,
|
|
967
|
+
"giu"
|
|
968
|
+
);
|
|
969
|
+
const behaviorPattern = new RegExp(
|
|
970
|
+
`${COMMAND_POSITION}${alternation(getVariants("behavior"))}\\s+${NAME}`,
|
|
971
|
+
"giu"
|
|
972
|
+
);
|
|
973
|
+
const defPattern = new RegExp(
|
|
974
|
+
`${COMMAND_POSITION}${alternation(getVariants("def"))}\\s+${NAME}`,
|
|
975
|
+
"giu"
|
|
976
|
+
);
|
|
977
|
+
const initPattern = new RegExp(
|
|
978
|
+
`${COMMAND_POSITION}${alternation(getVariants("init"))}${NOT_WORD_AFTER}`,
|
|
979
|
+
"giu"
|
|
980
|
+
);
|
|
981
|
+
for (let i = 0; i < lines.length; i++) {
|
|
982
|
+
const line = lines[i];
|
|
983
|
+
for (const m of line.matchAll(onPattern)) {
|
|
984
|
+
const at = m.index ?? 0;
|
|
985
|
+
symbols.push(
|
|
986
|
+
symbol(`${m[1]} ${m[2]}`, "Event Handler", SymbolKind.Event, i, at, m[0].length)
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
for (const m of line.matchAll(behaviorPattern)) {
|
|
990
|
+
const at = m.index ?? 0;
|
|
991
|
+
symbols.push(
|
|
992
|
+
symbol(`${m[1]} ${m[2]}`, "Behavior Definition", SymbolKind.Class, i, at, m[0].length)
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
for (const m of line.matchAll(defPattern)) {
|
|
996
|
+
const at = m.index ?? 0;
|
|
997
|
+
symbols.push(
|
|
998
|
+
symbol(`${m[1]} ${m[2]}`, "Function Definition", SymbolKind.Function, i, at, m[0].length)
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
for (const m of line.matchAll(initPattern)) {
|
|
1002
|
+
const at = m.index ?? 0;
|
|
1003
|
+
symbols.push(symbol(m[1], "Initialization", SymbolKind.Constructor, i, at, m[1].length));
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return symbols;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
884
1009
|
// src/localized-descriptions.ts
|
|
885
1010
|
var DESCRIPTIONS = {
|
|
886
1011
|
en: {
|
|
@@ -1241,7 +1366,7 @@ try {
|
|
|
1241
1366
|
({ schemaRoleInferrer: inferRoles } = await import("@hyperfixi/core/multilingual"));
|
|
1242
1367
|
} catch {
|
|
1243
1368
|
console.error(
|
|
1244
|
-
"[lokascript-ls] @
|
|
1369
|
+
"[lokascript-ls] @hyperfixi/core/multilingual not available \u2014 hover shows roles for `set` and `go` only. Install a core build that ships the multilingual entry."
|
|
1245
1370
|
);
|
|
1246
1371
|
}
|
|
1247
1372
|
fromCoreASTFn = (node) => core.fromCoreAST(
|
|
@@ -1262,7 +1387,7 @@ try {
|
|
|
1262
1387
|
}
|
|
1263
1388
|
var frameworkIR = null;
|
|
1264
1389
|
try {
|
|
1265
|
-
const fw = await import("
|
|
1390
|
+
const fw = await import("@lokascript/framework");
|
|
1266
1391
|
frameworkIR = { fromInterchangeNode: fw.fromInterchangeNode, renderExplicit: fw.renderExplicit };
|
|
1267
1392
|
console.error(
|
|
1268
1393
|
"[lokascript-ls] @lokascript/framework loaded \u2014 LSE bracket notation in hover enabled"
|
|
@@ -1564,8 +1689,60 @@ function getSemanticAnalyzer() {
|
|
|
1564
1689
|
}
|
|
1565
1690
|
return cachedAnalyzer;
|
|
1566
1691
|
}
|
|
1567
|
-
var
|
|
1568
|
-
|
|
1692
|
+
var VALID_MODES = /* @__PURE__ */ new Set([
|
|
1693
|
+
"hyperscript",
|
|
1694
|
+
"hyperscript-i18n",
|
|
1695
|
+
"lokascript",
|
|
1696
|
+
"auto"
|
|
1697
|
+
]);
|
|
1698
|
+
function baseSettings() {
|
|
1699
|
+
return envDefaultMode ? { ...defaultSettings, mode: envDefaultMode } : { ...defaultSettings };
|
|
1700
|
+
}
|
|
1701
|
+
function isRecord(value) {
|
|
1702
|
+
return typeof value === "object" && value !== null;
|
|
1703
|
+
}
|
|
1704
|
+
function mergeSettings(base, partial) {
|
|
1705
|
+
const out = { ...base };
|
|
1706
|
+
if (!isRecord(partial)) return out;
|
|
1707
|
+
const p = partial;
|
|
1708
|
+
if (typeof p.mode === "string" && VALID_MODES.has(p.mode)) out.mode = p.mode;
|
|
1709
|
+
if (typeof p.language === "string" && p.language.trim()) out.language = p.language.trim();
|
|
1710
|
+
if (typeof p.maxDiagnostics === "number" && p.maxDiagnostics > 0) {
|
|
1711
|
+
out.maxDiagnostics = Math.floor(p.maxDiagnostics);
|
|
1712
|
+
}
|
|
1713
|
+
return out;
|
|
1714
|
+
}
|
|
1715
|
+
var globalSettings = baseSettings();
|
|
1716
|
+
var hasConfigurationCapability = false;
|
|
1717
|
+
var configSections = envDefaultMode ? ["hyperscript", "lokascript"] : ["lokascript", "hyperscript"];
|
|
1718
|
+
function applySettings(next) {
|
|
1719
|
+
globalSettings = next;
|
|
1720
|
+
const previousMode = resolvedMode;
|
|
1721
|
+
resolvedMode = resolveMode(globalSettings);
|
|
1722
|
+
if (previousMode !== resolvedMode) {
|
|
1723
|
+
connection.console.log(`Mode changed to '${resolvedMode}' (${getBranding(resolvedMode)})`);
|
|
1724
|
+
}
|
|
1725
|
+
translationCache = null;
|
|
1726
|
+
reverseKeywordCache = null;
|
|
1727
|
+
documents.all().forEach(validateDocument);
|
|
1728
|
+
}
|
|
1729
|
+
async function pullConfiguration() {
|
|
1730
|
+
if (!hasConfigurationCapability) return;
|
|
1731
|
+
try {
|
|
1732
|
+
const items = await connection.workspace.getConfiguration(
|
|
1733
|
+
configSections.map((section) => ({ section }))
|
|
1734
|
+
);
|
|
1735
|
+
const [primary, fallback] = Array.isArray(items) ? items : [];
|
|
1736
|
+
let merged = mergeSettings(baseSettings(), fallback);
|
|
1737
|
+
merged = mergeSettings(merged, primary);
|
|
1738
|
+
applySettings(merged);
|
|
1739
|
+
} catch (e) {
|
|
1740
|
+
connection.console.log(`[lokascript-ls] workspace/configuration failed: ${e}`);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
connection.onInitialize((params) => {
|
|
1744
|
+
hasConfigurationCapability = !!params.capabilities.workspace?.configuration;
|
|
1745
|
+
globalSettings = mergeSettings(baseSettings(), params.initializationOptions);
|
|
1569
1746
|
resolvedMode = resolveMode(globalSettings);
|
|
1570
1747
|
const brand = getBranding(resolvedMode);
|
|
1571
1748
|
connection.console.log(`${brand} Language Server initializing (mode: ${resolvedMode})...`);
|
|
@@ -1592,19 +1769,20 @@ connection.onInitialized(() => {
|
|
|
1592
1769
|
connection.client.register(DidChangeConfigurationNotification.type, void 0);
|
|
1593
1770
|
const brand = getBranding(resolvedMode);
|
|
1594
1771
|
connection.console.log(`${brand} Language Server initialized`);
|
|
1772
|
+
void pullConfiguration();
|
|
1595
1773
|
});
|
|
1596
1774
|
connection.onDidChangeConfiguration((change) => {
|
|
1597
|
-
const
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
const brand = getBranding(resolvedMode);
|
|
1603
|
-
connection.console.log(`Mode changed to '${resolvedMode}' (${brand})`);
|
|
1775
|
+
const settings = change.settings;
|
|
1776
|
+
const pushed = isRecord(settings) ? settings[configSections[0]] ?? settings[configSections[1]] : void 0;
|
|
1777
|
+
if (pushed !== void 0) {
|
|
1778
|
+
applySettings(mergeSettings(baseSettings(), pushed));
|
|
1779
|
+
return;
|
|
1604
1780
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1781
|
+
if (hasConfigurationCapability) {
|
|
1782
|
+
void pullConfiguration();
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
applySettings(baseSettings());
|
|
1608
1786
|
});
|
|
1609
1787
|
documents.onDidChangeContent((change) => {
|
|
1610
1788
|
validateDocument(change.document);
|
|
@@ -1660,6 +1838,7 @@ async function getDiagnostics(code, language) {
|
|
|
1660
1838
|
}
|
|
1661
1839
|
}
|
|
1662
1840
|
const analyzer = isMultilingualEnabled(resolvedMode) ? getSemanticAnalyzer() : null;
|
|
1841
|
+
let semanticParsedForeign = false;
|
|
1663
1842
|
if (analyzer) {
|
|
1664
1843
|
try {
|
|
1665
1844
|
let result = analyzer.analyze(code, language);
|
|
@@ -1706,6 +1885,7 @@ async function getDiagnostics(code, language) {
|
|
|
1706
1885
|
message: `Code could not be fully parsed (${(result.confidence * 100).toFixed(0)}% confidence)`
|
|
1707
1886
|
});
|
|
1708
1887
|
}
|
|
1888
|
+
semanticParsedForeign = result.confidence >= 0.5 && usedLanguage !== "en";
|
|
1709
1889
|
if (result.confidence >= 0.5 && result.command) {
|
|
1710
1890
|
const cmd = result.command.name;
|
|
1711
1891
|
const roles = result.command.roles;
|
|
@@ -1735,7 +1915,7 @@ async function getDiagnostics(code, language) {
|
|
|
1735
1915
|
if (parseFunction && fromCoreASTFn) {
|
|
1736
1916
|
try {
|
|
1737
1917
|
const parseResult = parseFunction(code);
|
|
1738
|
-
if (parseResult.errors?.length) {
|
|
1918
|
+
if (parseResult.errors?.length && !semanticParsedForeign) {
|
|
1739
1919
|
for (const err of parseResult.errors) {
|
|
1740
1920
|
diagnostics.push({
|
|
1741
1921
|
range: {
|
|
@@ -1766,16 +1946,18 @@ async function getDiagnostics(code, language) {
|
|
|
1766
1946
|
}
|
|
1767
1947
|
}
|
|
1768
1948
|
} catch (parseError) {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1949
|
+
if (!semanticParsedForeign) {
|
|
1950
|
+
diagnostics.push({
|
|
1951
|
+
range: {
|
|
1952
|
+
start: { line: 0, character: 0 },
|
|
1953
|
+
end: { line: 0, character: Math.min(code.length, 80) }
|
|
1954
|
+
},
|
|
1955
|
+
severity: DiagnosticSeverity2.Error,
|
|
1956
|
+
code: "parse-error",
|
|
1957
|
+
source: brand,
|
|
1958
|
+
message: parseError.message || "Failed to parse"
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1779
1961
|
}
|
|
1780
1962
|
}
|
|
1781
1963
|
if (diagnostics.length === 0) {
|
|
@@ -1907,18 +2089,6 @@ function getAttrsCompletions() {
|
|
|
1907
2089
|
}
|
|
1908
2090
|
];
|
|
1909
2091
|
}
|
|
1910
|
-
function inferContext(beforeCursor) {
|
|
1911
|
-
if (/\^\w*$/.test(beforeCursor)) return "caret";
|
|
1912
|
-
if (/\battrs\.\w*$/.test(beforeCursor)) return "attrs-property";
|
|
1913
|
-
const trimmed = beforeCursor.trim();
|
|
1914
|
-
if (/\bon\s*$/.test(trimmed)) return "event";
|
|
1915
|
-
if (/\bthen\s*$/.test(trimmed)) return "command";
|
|
1916
|
-
if (/^(on\s+\w+\s*)$/.test(trimmed)) return "command";
|
|
1917
|
-
if (/(to|from|into|on)\s*$/.test(trimmed)) return "selector";
|
|
1918
|
-
if (/\bif\s*$/.test(trimmed)) return "expression";
|
|
1919
|
-
if (/\bset\s+:\w+\s+to\s*$/.test(trimmed)) return "expression";
|
|
1920
|
-
return "default";
|
|
1921
|
-
}
|
|
1922
2092
|
function getContextualCompletions(context, language) {
|
|
1923
2093
|
const completions = [];
|
|
1924
2094
|
const effectiveLanguage = isMultilingualEnabled(resolvedMode) ? language : "en";
|
|
@@ -2551,27 +2721,27 @@ connection.onDocumentSymbol((params) => {
|
|
|
2551
2721
|
const allSymbols = [];
|
|
2552
2722
|
for (const region of regions) {
|
|
2553
2723
|
const symbols = extractSymbols(region.code, language);
|
|
2554
|
-
for (const
|
|
2724
|
+
for (const symbol2 of symbols) {
|
|
2555
2725
|
allSymbols.push({
|
|
2556
|
-
...
|
|
2726
|
+
...symbol2,
|
|
2557
2727
|
range: {
|
|
2558
2728
|
start: {
|
|
2559
|
-
line: region.startLine +
|
|
2560
|
-
character:
|
|
2729
|
+
line: region.startLine + symbol2.range.start.line,
|
|
2730
|
+
character: symbol2.range.start.line === 0 ? region.startChar + symbol2.range.start.character : symbol2.range.start.character
|
|
2561
2731
|
},
|
|
2562
2732
|
end: {
|
|
2563
|
-
line: region.startLine +
|
|
2564
|
-
character:
|
|
2733
|
+
line: region.startLine + symbol2.range.end.line,
|
|
2734
|
+
character: symbol2.range.end.line === 0 ? region.startChar + symbol2.range.end.character : symbol2.range.end.character
|
|
2565
2735
|
}
|
|
2566
2736
|
},
|
|
2567
2737
|
selectionRange: {
|
|
2568
2738
|
start: {
|
|
2569
|
-
line: region.startLine +
|
|
2570
|
-
character:
|
|
2739
|
+
line: region.startLine + symbol2.selectionRange.start.line,
|
|
2740
|
+
character: symbol2.selectionRange.start.line === 0 ? region.startChar + symbol2.selectionRange.start.character : symbol2.selectionRange.start.character
|
|
2571
2741
|
},
|
|
2572
2742
|
end: {
|
|
2573
|
-
line: region.startLine +
|
|
2574
|
-
character:
|
|
2743
|
+
line: region.startLine + symbol2.selectionRange.end.line,
|
|
2744
|
+
character: symbol2.selectionRange.end.line === 0 ? region.startChar + symbol2.selectionRange.end.character : symbol2.selectionRange.end.character
|
|
2575
2745
|
}
|
|
2576
2746
|
}
|
|
2577
2747
|
});
|
|
@@ -2583,8 +2753,6 @@ connection.onDocumentSymbol((params) => {
|
|
|
2583
2753
|
}
|
|
2584
2754
|
});
|
|
2585
2755
|
function extractSymbols(code, language) {
|
|
2586
|
-
const symbols = [];
|
|
2587
|
-
const lines = code.split("\n");
|
|
2588
2756
|
const getVariants = (eng) => {
|
|
2589
2757
|
const variants = [eng];
|
|
2590
2758
|
if (!semanticPackage || language === "en") return variants;
|
|
@@ -2602,82 +2770,7 @@ function extractSymbols(code, language) {
|
|
|
2602
2770
|
}
|
|
2603
2771
|
return variants;
|
|
2604
2772
|
};
|
|
2605
|
-
|
|
2606
|
-
const behaviorVariants = getVariants("behavior");
|
|
2607
|
-
const defVariants = getVariants("def");
|
|
2608
|
-
const initVariants = getVariants("init");
|
|
2609
|
-
const onPattern = new RegExp(`\\b(${onVariants.join("|")})\\s+(\\w+(?:\\[.*?\\])?)`, "gi");
|
|
2610
|
-
const behaviorPattern = new RegExp(`\\b(${behaviorVariants.join("|")})\\s+(\\w+)`, "gi");
|
|
2611
|
-
const defPattern = new RegExp(`\\b(${defVariants.join("|")})\\s+(\\w+)`, "gi");
|
|
2612
|
-
const initPattern = new RegExp(`\\b(${initVariants.join("|")})\\b`, "gi");
|
|
2613
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2614
|
-
const line = lines[i];
|
|
2615
|
-
for (const match of line.matchAll(onPattern)) {
|
|
2616
|
-
const matchIndex = match.index ?? 0;
|
|
2617
|
-
symbols.push({
|
|
2618
|
-
name: `${match[1]} ${match[2]}`,
|
|
2619
|
-
detail: "Event Handler",
|
|
2620
|
-
kind: SymbolKind.Event,
|
|
2621
|
-
range: {
|
|
2622
|
-
start: { line: i, character: matchIndex },
|
|
2623
|
-
end: { line: i, character: matchIndex + match[0].length }
|
|
2624
|
-
},
|
|
2625
|
-
selectionRange: {
|
|
2626
|
-
start: { line: i, character: matchIndex },
|
|
2627
|
-
end: { line: i, character: matchIndex + match[0].length }
|
|
2628
|
-
}
|
|
2629
|
-
});
|
|
2630
|
-
}
|
|
2631
|
-
for (const match of line.matchAll(behaviorPattern)) {
|
|
2632
|
-
const matchIndex = match.index ?? 0;
|
|
2633
|
-
symbols.push({
|
|
2634
|
-
name: `${match[1]} ${match[2]}`,
|
|
2635
|
-
detail: "Behavior Definition",
|
|
2636
|
-
kind: SymbolKind.Class,
|
|
2637
|
-
range: {
|
|
2638
|
-
start: { line: i, character: matchIndex },
|
|
2639
|
-
end: { line: i, character: matchIndex + match[0].length }
|
|
2640
|
-
},
|
|
2641
|
-
selectionRange: {
|
|
2642
|
-
start: { line: i, character: matchIndex },
|
|
2643
|
-
end: { line: i, character: matchIndex + match[0].length }
|
|
2644
|
-
}
|
|
2645
|
-
});
|
|
2646
|
-
}
|
|
2647
|
-
for (const match of line.matchAll(defPattern)) {
|
|
2648
|
-
const matchIndex = match.index ?? 0;
|
|
2649
|
-
symbols.push({
|
|
2650
|
-
name: `${match[1]} ${match[2]}`,
|
|
2651
|
-
detail: "Function Definition",
|
|
2652
|
-
kind: SymbolKind.Function,
|
|
2653
|
-
range: {
|
|
2654
|
-
start: { line: i, character: matchIndex },
|
|
2655
|
-
end: { line: i, character: matchIndex + match[0].length }
|
|
2656
|
-
},
|
|
2657
|
-
selectionRange: {
|
|
2658
|
-
start: { line: i, character: matchIndex },
|
|
2659
|
-
end: { line: i, character: matchIndex + match[0].length }
|
|
2660
|
-
}
|
|
2661
|
-
});
|
|
2662
|
-
}
|
|
2663
|
-
for (const match of line.matchAll(initPattern)) {
|
|
2664
|
-
const matchIndex = match.index ?? 0;
|
|
2665
|
-
symbols.push({
|
|
2666
|
-
name: match[1],
|
|
2667
|
-
detail: "Initialization",
|
|
2668
|
-
kind: SymbolKind.Constructor,
|
|
2669
|
-
range: {
|
|
2670
|
-
start: { line: i, character: matchIndex },
|
|
2671
|
-
end: { line: i, character: matchIndex + match[1].length }
|
|
2672
|
-
},
|
|
2673
|
-
selectionRange: {
|
|
2674
|
-
start: { line: i, character: matchIndex },
|
|
2675
|
-
end: { line: i, character: matchIndex + match[1].length }
|
|
2676
|
-
}
|
|
2677
|
-
});
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
return symbols;
|
|
2773
|
+
return extractDocumentSymbols(code, getVariants);
|
|
2681
2774
|
}
|
|
2682
2775
|
connection.onCodeAction((params) => {
|
|
2683
2776
|
const diagnostics = params.context.diagnostics;
|