@lokascript/language-server 3.3.0 → 4.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.
Files changed (3) hide show
  1. package/README.md +6 -5
  2. package/dist/server.js +144 -240
  3. package/package.json +11 -6
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): `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)
18
+ - **auto** (default): `lokascript` mode when the semantic package exposes its API; `hyperscript` mode in builds that replace it with a shim
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
@@ -117,8 +117,8 @@ require('lspconfig').lokascript.setup{}
117
117
 
118
118
  The server reads its settings three ways, and every field is optional (a partial object is merged over the defaults):
119
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);
120
+ 1. `initializationOptions` on `initialize` (the VS Code extension passes `{ 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`; the VS Code extension does);
122
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
123
 
124
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.
@@ -227,9 +227,10 @@ For users who prefer the `hyperscript` namespace:
227
227
  ## Dependencies
228
228
 
229
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.
230
+ - `@hyperfixi/engine` is an **optional** peer: the engine that runs hyperscript. With it the server surfaces its parse errors (at the position the engine stopped) and gives the AST nodes their source positions for hover, symbols and completions.
231
+ - `@hyperfixi/core` is an **optional** peer, for two of its entries: `/ast-utils` (complexity diagnostics, hover, symbols and completions on the interchange) and `/lsp-metadata` (keyword and hover docs). The AST nodes come from `@lokascript/semantic`, which names each command's roles itself. Without either, diagnostics degrade to the pattern-based quote/bracket checks and hover uses built-in fallback docs. (Until 4.0 the parse errors and nodes came from `@hyperfixi/core`'s parser.)
231
232
 
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.
233
+ A bundler that wants an English-only, dependency-free server replaces the semantic, framework, engine and core imports with throwing or empty shims at bundle time; the server's capability probes are written for that case.
233
234
 
234
235
  ## Development
235
236
 
package/dist/server.js CHANGED
@@ -24,13 +24,25 @@ var defaultSettings = {
24
24
 
25
25
  // src/command-tiers.ts
26
26
  var HYPERSCRIPT_COMMANDS = [
27
- // Core commands
27
+ // The engine's commands.
28
28
  "add",
29
+ "answer",
29
30
  "append",
31
+ "ask",
32
+ "beep!",
33
+ "blur",
34
+ "break",
35
+ "breakpoint",
30
36
  "call",
37
+ "clear",
38
+ "close",
39
+ "continue",
40
+ "decrement",
31
41
  "default",
42
+ "empty",
32
43
  "exit",
33
44
  "fetch",
45
+ "focus",
34
46
  "for",
35
47
  "get",
36
48
  "go",
@@ -38,16 +50,27 @@ var HYPERSCRIPT_COMMANDS = [
38
50
  "hide",
39
51
  "if",
40
52
  "increment",
41
- "decrement",
42
53
  "js",
43
54
  "log",
55
+ "make",
56
+ "measure",
57
+ "morph",
58
+ "open",
59
+ "pick",
44
60
  "put",
45
61
  "remove",
62
+ "render",
46
63
  "repeat",
64
+ "reset",
47
65
  "return",
66
+ "scroll",
67
+ "select",
48
68
  "send",
49
69
  "set",
70
+ "settle",
50
71
  "show",
72
+ "start",
73
+ "swap",
51
74
  "take",
52
75
  "tell",
53
76
  "throw",
@@ -55,203 +78,63 @@ var HYPERSCRIPT_COMMANDS = [
55
78
  "transition",
56
79
  "trigger",
57
80
  "wait",
81
+ // Continues a `repeat`; completed like a command.
58
82
  "while",
59
- // Upstream commands this list omitted until Arc A step 4.1. Each was probed
60
- // on hyperscript.org@0.9.93 in the form shown and produced the command node
61
- // named beside it; none of them is a LokaScript invention.
62
- "blur",
63
- // `blur #a` → BlurCommand
64
- "break",
65
- // `repeat 3 times break end` → controlflow.js BreakCommand
66
- "breakpoint",
67
- // `breakpoint` → BreakpointCommand (core debug.js)
68
- "clear",
69
- // `clear #a` → EmptyCommand — upstream spells the
70
- "empty",
71
- // `empty #a` pair as ONE command with two keywords
72
- "close",
73
- // `close #a` → CloseCommand
74
- "continue",
75
- // `repeat 3 times continue end` → controlflow.js ContinueCommand
76
- "focus",
77
- // `focus #a` → FocusCommand
78
- "open",
79
- // `open #a` → OpenCommand
80
- "pick",
81
- // `pick items 1 to 3 from "hello"` → PickCommand
82
- "render",
83
- // `render #tpl` → RenderCommand
84
- "reset",
85
- // `reset #a` → ResetCommand
86
- "scroll",
87
- // `scroll to #a` → ScrollCommand
88
- "select",
89
- // `select #a` → SelectCommand
90
- "start",
91
- // `start view transition end` → ViewTransitionCommand
92
- "swap",
93
- // `swap $a with $b` → SwapCommand. The htmx-style
94
- // `swap <content> into <target>` form is an EXTENSION of this same
95
- // command, not a different one — `commands/dom/swap.ts` implements the
96
- // upstream value-swap variant explicitly, under a `variant` discriminator.
97
- // Never written by a user: `pseudo-command` is the internal registry name for
98
- // method-call-as-command (`foo() on #a`), and `-` is not an identifier
99
- // character, so no source text can contain the token. Listed so the
100
- // partition is total. The mechanism is upstream — the probe above yields
101
- // PseudoCommand from `parsetree/commands/pseudoCommand.js`, which the core
102
- // `_hyperscript.js` bundle registers.
103
- "pseudo-command",
104
- // Moved OUT of LOKASCRIPT_ONLY_COMMANDS by the same 4.1 oracle run. Each was
105
- // asserted to be a LokaScript extension and each parses on stock
106
- // hyperscript.org@0.9.93, so the LSP was raising a spurious *error*
107
- // diagnostic on portable code — the same defect as the omissions above, with
108
- // the sign flipped.
109
- "make",
110
- // `make a <div/>` → MakeCommand (basic.js)
111
- "measure",
112
- // `measure #a` → MeasureCommand (dom.js)
113
- "morph",
114
- // `morph #a to "<p>x</p>"` → MorphCommand (dom.js)
115
- "settle",
116
- // `settle` → SettleCommand (animations.js)
117
- // `install` is upstream at the position it is actually written — `_="install
118
- // Foo"` parses as InstallFeature. It is a FEATURE upstream and a registered
119
- // command here, so `on click install Foo` works only in LokaScript. The
120
- // lists are keyed by name and cannot express "upstream in feature position
121
- // only"; classified by the canonical usage, which is portable.
122
- "install",
123
- // Definitions
83
+ // The engine's features (`set` and `js` are features too, listed above).
124
84
  "behavior",
85
+ "bind",
125
86
  "def",
126
87
  "init",
88
+ "install",
89
+ "live",
127
90
  "on",
91
+ "when",
92
+ // Upstream extensions (ext/eventsource.js, ext/socket.js, ext/worker.js); not in the engine.
128
93
  "eventsource",
129
94
  "socket",
130
95
  "worker"
131
96
  ];
132
- var LOKASCRIPT_ONLY_COMMANDS = [
133
- // Upstream _hyperscript has no `prepend`; it offers only
134
- // `put <content> at the start of <target>`.
135
- "prepend",
136
- // `prepend "x" to #a` → Unexpected Token : prepend
137
- "process",
138
- // `process "<p>x</p>"` → Unexpected Token : process
139
- // Added by Arc A step 4.1 — the omissions that produced NO diagnostic.
140
- "copy",
141
- // `copy "x"` → Unexpected Token : copy
142
- "push",
143
- // `push url "/x"` → Unexpected Token : push
144
- "replace",
145
- // `replace url "/x"` → Unexpected Token : replace
146
- // Upstream spells this command `beep!`, with the bang as part of the token:
147
- // `beep! me` parses (BeepCommand), `beep me` does not. hyperfixi registers it
148
- // as `beep` and accepts both spellings, so the portable spelling is `beep!`
149
- // and the bare one is the extension. Classified by the registered name,
150
- // which is what these lists key on.
151
- "beep",
152
- // `beep me` → Unexpected Token : beep
153
- // Upstream has `unless` only as a TRAILING statement modifier
154
- // (`log "x" unless true` → UnlessStatementModifier, kernel.js
155
- // parseIndirectStatement). The registered `unless` COMMAND — the leading
156
- // block form, an alias of `if` — is the LokaScript addition.
157
- "unless"
158
- // `unless true log "x" end` → Unexpected Token : unless
97
+ var LOKASCRIPT_ONLY_COMMANDS = [];
98
+ var ALL_COMMANDS = [
99
+ ...HYPERSCRIPT_COMMANDS,
100
+ ...LOKASCRIPT_ONLY_COMMANDS
159
101
  ];
160
- var ALL_COMMANDS = [...HYPERSCRIPT_COMMANDS, ...LOKASCRIPT_ONLY_COMMANDS];
161
- var LOKASCRIPT_SYNTAX_PATTERNS = {
162
- /**
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
- */
167
- dotNotation: /\bits\.\w+/,
168
- /**
169
- * Optional chaining: my?.value
170
- */
171
- optionalChaining: /\b(my|your|its)\?\.\w+/,
172
- /**
173
- * Extended 'as' conversions
174
- */
175
- extendedAsConversion: /\bas\s+(Math|Values)\b/i,
176
- /**
177
- * Debounce/throttle modifiers with duration: .debounce(300), .throttle(1s)
178
- */
179
- temporalModifiers: /\.(debounce|throttle)\s*\(\s*\d+/i
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++;
102
+ var ENGINE_ADDITIONS = {
103
+ newExpression: {
104
+ pattern: "new-expression",
105
+ description: "'new X()' is an @hyperfixi/engine addition; upstream reads `new` as a variable (use `make a X`)"
106
+ },
107
+ toggleElementCommand: {
108
+ pattern: "toggle-element",
109
+ description: "'toggle <element>' (open or close a dialog, details or popover) is an @hyperfixi/engine addition"
209
110
  }
210
- return out;
211
- }
212
- function detectLokascriptFeatures(code) {
111
+ };
112
+ function detectLokascriptFeatures(engineTree) {
213
113
  const detected = [];
214
- const scannable = blankStringsAndComments(code);
215
- for (const cmd of LOKASCRIPT_ONLY_COMMANDS) {
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)) {
114
+ const walk = (node) => {
115
+ if (Array.isArray(node)) {
116
+ for (const item of node) walk(item);
117
+ return;
118
+ }
119
+ if (typeof node !== "object" || node === null) return;
120
+ const type = Reflect.get(node, "type");
121
+ const addition = typeof type === "string" ? ENGINE_ADDITIONS[type] : void 0;
122
+ if (addition) {
123
+ const start = Reflect.get(node, "start");
124
+ const end = Reflect.get(node, "end");
218
125
  detected.push({
219
- feature: "command",
220
- description: `'${cmd}' command is a LokaScript extension`,
221
- pattern: cmd
126
+ feature: "syntax",
127
+ description: addition.description,
128
+ pattern: addition.pattern,
129
+ ...typeof start === "number" && { start },
130
+ ...typeof end === "number" && { end }
222
131
  });
223
132
  }
224
- }
225
- if (LOKASCRIPT_SYNTAX_PATTERNS.dotNotation.test(code)) {
226
- detected.push({
227
- feature: "syntax",
228
- description: "Dot notation (my.property) is a LokaScript extension. Use 'my property' for _hyperscript compatibility",
229
- pattern: "dot-notation"
230
- });
231
- }
232
- if (LOKASCRIPT_SYNTAX_PATTERNS.optionalChaining.test(code)) {
233
- detected.push({
234
- feature: "syntax",
235
- description: "Optional chaining (my?.property) is a LokaScript extension",
236
- pattern: "optional-chaining"
237
- });
238
- }
239
- const asMatch = code.match(LOKASCRIPT_SYNTAX_PATTERNS.extendedAsConversion);
240
- if (asMatch) {
241
- detected.push({
242
- feature: "conversion",
243
- description: `'as ${asMatch[1]}' is a LokaScript extension`,
244
- pattern: `as-${asMatch[1].toLowerCase()}`
245
- });
246
- }
247
- const temporalMatch = code.match(LOKASCRIPT_SYNTAX_PATTERNS.temporalModifiers);
248
- if (temporalMatch) {
249
- detected.push({
250
- feature: "modifier",
251
- description: `'.${temporalMatch[1]}()' modifier is a LokaScript extension`,
252
- pattern: temporalMatch[1].toLowerCase()
253
- });
254
- }
133
+ for (const value of Object.values(node)) {
134
+ if (typeof value === "object" && value !== null) walk(value);
135
+ }
136
+ };
137
+ walk(engineTree);
255
138
  return detected;
256
139
  }
257
140
 
@@ -1355,28 +1238,21 @@ function getOtherLanguages() {
1355
1238
  return semanticPackage.getRegisteredLanguages().filter((l) => l !== "en");
1356
1239
  }
1357
1240
  var interchangeLSP = null;
1358
- var fromCoreASTFn = null;
1359
- var parseFunction = null;
1241
+ var engineParse = null;
1360
1242
  var lspMetadata = null;
1361
1243
  try {
1362
- const core = await import("@hyperfixi/core");
1363
- parseFunction = core.parse;
1364
- let inferRoles = null;
1365
- try {
1366
- ({ schemaRoleInferrer: inferRoles } = await import("@hyperfixi/core/multilingual"));
1367
- } catch {
1368
- console.error(
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."
1370
- );
1371
- }
1372
- fromCoreASTFn = (node) => core.fromCoreAST(
1373
- node,
1374
- inferRoles ? { inferRoles } : void 0
1375
- );
1244
+ const engine = await import("@hyperfixi/engine");
1245
+ engine.register(...engine.everything);
1246
+ engineParse = engine.parse;
1247
+ } catch {
1248
+ console.error("[lokascript-ls] @hyperfixi/engine not available \u2014 parse errors disabled");
1249
+ }
1250
+ try {
1376
1251
  interchangeLSP = await import("@hyperfixi/core/ast-utils");
1377
- console.error("[lokascript-ls] @hyperfixi/core loaded \u2014 AST parsing + interchange LSP enabled");
1378
1252
  } catch {
1379
- console.error("[lokascript-ls] @hyperfixi/core not available \u2014 diagnostics and hover degraded");
1253
+ console.error(
1254
+ "[lokascript-ls] @hyperfixi/core/ast-utils not available \u2014 AST diagnostics disabled"
1255
+ );
1380
1256
  }
1381
1257
  try {
1382
1258
  lspMetadata = await import("@hyperfixi/core/lsp-metadata");
@@ -1398,8 +1274,46 @@ try {
1398
1274
  );
1399
1275
  }
1400
1276
  console.error(
1401
- `[lokascript-ls] capabilities: core=${!!parseFunction}, interchange=${!!interchangeLSP}, metadata=${!!lspMetadata}, framework=${!!frameworkIR}`
1277
+ `[lokascript-ls] capabilities: engine=${!!engineParse}, interchange=${!!interchangeLSP}, metadata=${!!lspMetadata}, framework=${!!frameworkIR}`
1402
1278
  );
1279
+ function parseToInterchange(code, language) {
1280
+ const errors = [];
1281
+ let engineTree = null;
1282
+ if (engineParse && (language === "en" || language.startsWith("en-"))) {
1283
+ try {
1284
+ engineTree = engineParse(code);
1285
+ } catch (e) {
1286
+ const message = e instanceof Error ? e.message : String(e);
1287
+ const token = typeof e === "object" && e !== null ? Reflect.get(e, "token") : void 0;
1288
+ const start = typeof token === "object" && token !== null ? Reflect.get(token, "start") : void 0;
1289
+ const before = typeof start === "number" ? code.slice(0, start) : null;
1290
+ errors.push({
1291
+ message: message.split("\n")[0] ?? message,
1292
+ ...before !== null ? {
1293
+ line: before.split("\n").length,
1294
+ column: before.length - (before.lastIndexOf("\n") + 1)
1295
+ } : {}
1296
+ });
1297
+ }
1298
+ }
1299
+ let nodes = [];
1300
+ try {
1301
+ const node = semanticPackage?.parseSemantic?.(code, language)?.node;
1302
+ const ast = node ? semanticPackage.buildAST(node)?.ast : null;
1303
+ if (ast) {
1304
+ const converted = semanticPackage.fromSemanticAST(ast);
1305
+ nodes = (Array.isArray(converted) ? converted : [converted]).filter(
1306
+ (n) => n?.type !== "error"
1307
+ );
1308
+ if (engineTree && interchangeLSP?.withEnginePositions) {
1309
+ nodes = interchangeLSP.withEnginePositions(nodes, code, engineTree);
1310
+ }
1311
+ }
1312
+ } catch {
1313
+ nodes = [];
1314
+ }
1315
+ return { nodes, errors };
1316
+ }
1403
1317
  var FALLBACK_KEYWORDS = [
1404
1318
  "toggle",
1405
1319
  "add",
@@ -1446,8 +1360,7 @@ var FALLBACK_KEYWORDS = [
1446
1360
  "not",
1447
1361
  "is",
1448
1362
  "exists",
1449
- "empty",
1450
- "has"
1363
+ "empty"
1451
1364
  ];
1452
1365
  var FALLBACK_HOVER_DOCS = {
1453
1366
  toggle: {
@@ -1556,11 +1469,6 @@ var FALLBACK_HOVER_DOCS = {
1556
1469
  description: "Appends content to an element.",
1557
1470
  example: 'append "<li>item</li>" to #list'
1558
1471
  },
1559
- prepend: {
1560
- title: "prepend",
1561
- description: "Prepends content to an element.",
1562
- example: 'prepend "<li>first</li>" to #list'
1563
- },
1564
1472
  take: {
1565
1473
  title: "take",
1566
1474
  description: "Takes a class from sibling elements (exclusive toggle).",
@@ -1613,13 +1521,8 @@ var FALLBACK_HOVER_DOCS = {
1613
1521
  },
1614
1522
  tell: {
1615
1523
  title: "tell",
1616
- description: "Sets the default target for subsequent commands.",
1617
- example: "tell #modal then add .visible"
1618
- },
1619
- copy: {
1620
- title: "copy",
1621
- description: "Copies text to the clipboard.",
1622
- example: 'copy "Hello" to the clipboard'
1524
+ description: "Runs commands with another element as `you` (and the implicit target).",
1525
+ example: "tell #modal add .visible end"
1623
1526
  }
1624
1527
  };
1625
1528
  var FALLBACK_EVENT_NAMES = [
@@ -1854,13 +1757,17 @@ function nameCollisionDiagnostics(code, language, source) {
1854
1757
  async function getDiagnostics(code, language) {
1855
1758
  const diagnostics = [];
1856
1759
  const brand = getBranding(resolvedMode);
1857
- if (isHyperscriptCompatMode(resolvedMode)) {
1858
- const lokascriptFeatures = detectLokascriptFeatures(code);
1859
- for (const feature of lokascriptFeatures) {
1760
+ if (isHyperscriptCompatMode(resolvedMode) && engineParse) {
1761
+ let tree = null;
1762
+ try {
1763
+ tree = engineParse(code);
1764
+ } catch {
1765
+ }
1766
+ for (const feature of detectLokascriptFeatures(tree)) {
1860
1767
  diagnostics.push({
1861
1768
  range: {
1862
- start: { line: 0, character: 0 },
1863
- end: { line: 0, character: code.split("\n")[0]?.length || code.length }
1769
+ start: offsetToPosition(code, feature.start ?? 0),
1770
+ end: offsetToPosition(code, feature.end ?? code.split("\n")[0]?.length ?? 0)
1864
1771
  },
1865
1772
  severity: DiagnosticSeverity2.Error,
1866
1773
  code: "lokascript-only",
@@ -1871,6 +1778,7 @@ async function getDiagnostics(code, language) {
1871
1778
  }
1872
1779
  const analyzer = isMultilingualEnabled(resolvedMode) ? getSemanticAnalyzer() : null;
1873
1780
  let semanticParsedForeign = false;
1781
+ let detectedLanguage = "en";
1874
1782
  if (analyzer) {
1875
1783
  try {
1876
1784
  let result = analyzer.analyze(code, language);
@@ -1918,6 +1826,7 @@ async function getDiagnostics(code, language) {
1918
1826
  });
1919
1827
  }
1920
1828
  semanticParsedForeign = result.confidence >= 0.5 && usedLanguage !== "en";
1829
+ if (semanticParsedForeign) detectedLanguage = usedLanguage;
1921
1830
  if (semanticParsedForeign) {
1922
1831
  diagnostics.push(...nameCollisionDiagnostics(code, usedLanguage, brand));
1923
1832
  }
@@ -1947,11 +1856,11 @@ async function getDiagnostics(code, language) {
1947
1856
  connection.console.log(`[${brand}-ls] Semantic parsing failed, trying fallback: ${e}`);
1948
1857
  }
1949
1858
  }
1950
- if (parseFunction && fromCoreASTFn) {
1859
+ if (engineParse || interchangeLSP) {
1951
1860
  try {
1952
- const parseResult = parseFunction(code);
1953
- if (parseResult.errors?.length && !semanticParsedForeign) {
1954
- for (const err of parseResult.errors) {
1861
+ const parsed = parseToInterchange(code, detectedLanguage);
1862
+ if (parsed.errors.length && !semanticParsedForeign) {
1863
+ for (const err of parsed.errors) {
1955
1864
  diagnostics.push({
1956
1865
  range: {
1957
1866
  start: {
@@ -1970,14 +1879,10 @@ async function getDiagnostics(code, language) {
1970
1879
  });
1971
1880
  }
1972
1881
  }
1973
- if (parseResult.node && interchangeLSP) {
1974
- const interchange = fromCoreASTFn(parseResult.node);
1975
- if (interchange && interchange.type !== "literal") {
1976
- const nodes = Array.isArray(interchange) ? interchange : [interchange];
1977
- const astDiagnostics = interchangeLSP.interchangeToLSPDiagnostics(nodes, {
1978
- source: brand
1979
- });
1980
- diagnostics.push(...astDiagnostics);
1882
+ if (parsed.nodes.length && interchangeLSP) {
1883
+ const nodes = parsed.nodes.filter((n) => n.type !== "literal");
1884
+ if (nodes.length) {
1885
+ diagnostics.push(...interchangeLSP.interchangeToLSPDiagnostics(nodes, { source: brand }));
1981
1886
  }
1982
1887
  }
1983
1888
  } catch (parseError) {
@@ -2418,19 +2323,18 @@ connection.onHover((params) => {
2418
2323
  }
2419
2324
  });
2420
2325
  function tryGetLSE(code) {
2421
- if (!interchangeLSP || !parseFunction || !fromCoreASTFn || !frameworkIR) return null;
2326
+ if (!interchangeLSP || !frameworkIR) return null;
2422
2327
  try {
2423
- const parseResult = parseFunction(code);
2424
- if (!parseResult.node) return null;
2425
- const interchange = fromCoreASTFn(parseResult.node);
2426
- if (!interchange) return null;
2427
- const semanticNode = frameworkIR.fromInterchangeNode(interchange);
2328
+ const [node] = parseToInterchange(code, globalSettings.language || "en").nodes;
2329
+ if (!node) return null;
2330
+ const semanticNode = frameworkIR.fromInterchangeNode(node);
2428
2331
  return frameworkIR.renderExplicit(semanticNode);
2429
2332
  } catch {
2430
2333
  return null;
2431
2334
  }
2432
2335
  }
2433
2336
  function expandSigilPrefix(line, word) {
2337
+ if (line[word.end] === "!") return { text: word.text + "!", start: word.start };
2434
2338
  if (word.start === 0) return null;
2435
2339
  const prev = line[word.start - 1];
2436
2340
  if (prev !== "^" && prev !== "#") return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokascript/language-server",
3
- "version": "3.3.0",
3
+ "version": "4.0.0",
4
4
  "description": "Language Server Protocol implementation for LokaScript/hyperscript with 24 language support",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -14,7 +14,7 @@
14
14
  "test": "vitest",
15
15
  "typecheck": "tsc --noEmit",
16
16
  "test:check": "npm run pretest --silent && VITEST_QUIET=1 bash ../../scripts/vitest-run.sh --reporter=dot",
17
- "pretest": "bash ../../scripts/ensure-fresh.sh ../intent ../framework ../semantic ../core && npm run build --silent"
17
+ "pretest": "bash ../../scripts/ensure-fresh.sh ../intent ../framework ../semantic ../core ../engine && npm run build --silent"
18
18
  },
19
19
  "keywords": [
20
20
  "lsp",
@@ -28,21 +28,26 @@
28
28
  "author": "LokaScript Contributors",
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "@lokascript/framework": "^3.3.0",
32
- "@lokascript/semantic": "^3.3.0",
31
+ "@lokascript/framework": "^4.0.0",
32
+ "@lokascript/semantic": "^4.0.0",
33
33
  "vscode-languageserver": "^9.0.1",
34
34
  "vscode-languageserver-textdocument": "^1.0.15"
35
35
  },
36
36
  "peerDependencies": {
37
- "@hyperfixi/core": "^3.3.0"
37
+ "@hyperfixi/core": "^4.0.0",
38
+ "@hyperfixi/engine": "^4.0.0"
38
39
  },
39
40
  "peerDependenciesMeta": {
40
41
  "@hyperfixi/core": {
41
42
  "optional": true
43
+ },
44
+ "@hyperfixi/engine": {
45
+ "optional": true
42
46
  }
43
47
  },
44
48
  "devDependencies": {
45
- "@hyperfixi/core": "^3.3.0",
49
+ "@hyperfixi/core": "^4.0.0",
50
+ "@hyperfixi/engine": "^4.0.0",
46
51
  "@types/node": "^26.6.3",
47
52
  "@vitest/coverage-v8": "^5.0.0",
48
53
  "tsup": "^8.0.0",