@nudojs/lsp 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @nudojs/lsp
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5786fa5: Promote the B-path (transpile + in-process Abs runtime) to the primary evaluation path for capable sources.
8
+
9
+ - Module graph now supports relative imports, bare-package harvest, default/namespace imports, re-exports, `export *`, require, cycle/depth/missing guards, and env modules (`path` / `node:path` etc.).
10
+ - Diagnostics, `@nudo:env` / mock injection, `call@` / `entry@` provenance, method-missing, unknown-recv, generators, classes, async/await, optional chaining, and destructuring run through B.
11
+ - When B hosts a file, `evaluateProgram` is skipped so TypeValue no longer double-reports.
12
+ - CLI `check` / `types` / `test` accept directories; check uses an Abs-only gate.
13
+ - LSP hover / `getTypeAtPosition` on capable files read the Abs node table; `*.nudo.js` edits evict L0 and recheck open parents.
14
+ - Public core surface now re-exports the algebra API (`Abs`, check, generalize, exec, bridge, `parseSource`, `stripTypes`).
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [5786fa5]
19
+ - Updated dependencies [5786fa5]
20
+ - Updated dependencies [5786fa5]
21
+ - @nudojs/core@0.3.0
22
+ - @nudojs/service@0.3.0
23
+ - @nudojs/parser@0.3.0
24
+
3
25
  ## 0.3.0
4
26
 
5
27
  ### Minor Changes
@@ -33,41 +33,40 @@ All commands are available as `workspace/executeCommand` (dot form) and as custo
33
33
 
34
34
  | Command (request alias) | Arguments (JSON) | Returns |
35
35
  |---|---|---|
36
- | `nudo.whatIf` (`nudo/whatIf`) | `{ "file": "src/app.js", "bindings": [{ "name": "x", "type": "string" }], "target": "y" }` | Text: the inferred type of `target` **under the assumed bindings** — e.g. `Type of "y": string \| number` |
36
+ | `nudo.whatIf` (`nudo/whatIf`) | `{ "file": "src/config.js", "bindings": [{ "name": "raw", "type": "string" }], "target": "size" }` | Text: the inferred type of `target` **under the assumed bindings** — e.g. `Type of "size": number`; bindings match top-level declarations only |
37
37
  | `nudo.trace` (`nudo/trace`) | `{ "file": "src/app.js", "functionName": "parse" }` | Text: one line per case, e.g. `Input: (T.string) => Output: number` |
38
38
  | `nudo.suggestCase` (`nudo/suggestCase`) | `{ "file": "src/app.js", "functionName": "parse" }` | Text: paste-ready `@nudo:case` directives when every case is call-site synthesized, e.g. `Function "parse" has 2 synthesized case(s); suggested directives:`; otherwise the current case count, e.g. `Function "parse" already has 3 case(s)`, or a suggested `@nudo:case` directive |
39
39
  | `nudo.selectCase` (`nudo/selectCase`) | `{ "file": "src/app.js", "functionName": "parse", "caseIndex": 1 }` | `{ "success": true }` — switches the active case (affects hover/diagnostics until changed back) |
40
40
  | `nudo.getActiveCases` (`nudo/getActiveCases`) | `{ "file": "src/app.js" }` | `{ "parse": 1, "greet": 0 }` — active case index per function |
41
41
 
42
- Diagnostics (failed `@nudo:returns` assertions, unreachable code, …) are available as LSP diagnostics — push (`textDocument/publishDiagnostics`) and pull (`textDocument/diagnostic`).
42
+ Diagnostics (failed `@nudo:refine` assertions, unreachable code, …) are available as LSP diagnostics — push (`textDocument/publishDiagnostics`) and pull (`textDocument/diagnostic`).
43
43
 
44
44
  ## What-if workflow
45
45
 
46
- The signature move: hypothesize a type for one binding, observe what another binding becomes — without editing any source.
46
+ The signature move: hypothesize a type for a top-level binding, observe what another binding becomes — without editing any source.
47
47
 
48
48
  ```js
49
- // src/app.js
50
- function normalize(x) {
51
- const trimmed = x.trim();
52
- const y = Number(trimmed);
53
- return y;
54
- }
49
+ // src/config.js
50
+ const raw = readInput();
51
+ const size = raw.length;
55
52
  ```
56
53
 
57
- Assume `x` is a string, ask what `y` is:
54
+ Assume `raw` is a string, ask what `size` is:
58
55
 
59
56
  ```json
60
57
  {
61
58
  "command": "nudo.whatIf",
62
59
  "arguments": [{
63
- "file": "src/app.js",
64
- "bindings": [{ "name": "x", "type": "string" }],
65
- "target": "y"
60
+ "file": "src/config.js",
61
+ "bindings": [{ "name": "raw", "type": "string" }],
62
+ "target": "size"
66
63
  }]
67
64
  }
68
65
  ```
69
66
 
70
- → `Type of "y": number`. Then flip the hypothesis (`"type": "string | null"`) and re-ask to see how the code's guard branches change the result. Use this to preview refactors, validate an API's return type before calling it, or check that a fix actually narrows a type.
67
+ → `Type of "size": number`. Then flip the hypothesis (`"type": "string | null"`) and re-ask: the same `size` now reports `unknown`, because the null arm can propagate through `.length`. Use this to preview refactors, validate an API's return type before calling it, or compare how alternative type assumptions ripple through downstream code.
68
+
69
+ Bindings only match **top-level declarations** (`const`/`let`/`var`/`function`) in the file. A function parameter name does not resolve — you get `Type of "...": unknown` plus `Bindings not applied (no top-level declaration found): x` in the reply.
71
70
 
72
71
  ## Type expression syntax
73
72
 
package/dist/server.js CHANGED
@@ -43396,11 +43396,14 @@ import {
43396
43396
  InlayHintKind
43397
43397
  } from "vscode-languageserver/node";
43398
43398
  import { TextDocument } from "vscode-languageserver-textdocument";
43399
- import { typeValueToString as typeValueToString2 } from "@nudojs/core";
43400
43399
  import {
43401
43400
  getTypeAtPosition,
43401
+ getHoverAtPosition as getHoverAtPosition2,
43402
43402
  getCompletionsAtPosition,
43403
- getCasesForFile
43403
+ getCasesForFile,
43404
+ buildSemanticTokens,
43405
+ isNudoTargetPath,
43406
+ collectAbsInlays as collectAbsInlays2
43404
43407
  } from "@nudojs/service";
43405
43408
  import { parse as parse2 } from "@nudojs/parser";
43406
43409
 
@@ -43491,32 +43494,33 @@ function findIdentifierAtPosition(ast, line, column) {
43491
43494
  }
43492
43495
 
43493
43496
  // src/semantic-tokens.ts
43494
- var TOKEN_TYPES = [
43495
- "function",
43496
- "variable",
43497
- "parameter",
43498
- "property",
43499
- "type",
43500
- "keyword",
43501
- "string",
43502
- "number",
43503
- "comment",
43504
- "decorator"
43505
- ];
43506
- var TOKEN_MODIFIERS = [
43507
- "declaration",
43508
- "readonly",
43509
- "deprecated",
43510
- "unreachable"
43511
- ];
43497
+ import {
43498
+ SEMANTIC_TOKEN_TYPES,
43499
+ SEMANTIC_TOKEN_MODIFIERS,
43500
+ encodeSemanticTokens
43501
+ } from "@nudojs/service";
43512
43502
 
43513
43503
  // src/validation.ts
43504
+ import { dirname, resolve as resolvePath } from "path";
43514
43505
  import {
43515
43506
  analyzeFile,
43516
43507
  analyzeFileAsync,
43517
43508
  buildModuleGraph,
43518
- computeDirtySet
43509
+ computeDirtySet,
43510
+ defaultLoadModule,
43511
+ clearAnalysisSessionCaches,
43512
+ evictAbsModuleCacheFiles,
43513
+ evictBPathCacheForFiles,
43514
+ evictAnalysisFileCacheForFiles,
43515
+ evictFnAnalysisCacheForFiles
43519
43516
  } from "@nudojs/service";
43517
+ import {
43518
+ checkSource,
43519
+ pTrue,
43520
+ evictGeneralizeMemoForPaths,
43521
+ evictCheckSourceMemoForPaths,
43522
+ extractNudoImports
43523
+ } from "@nudojs/core";
43520
43524
  import {
43521
43525
  DiagnosticSeverity,
43522
43526
  DiagnosticTag
@@ -43524,15 +43528,65 @@ import {
43524
43528
  var analysisCache = /* @__PURE__ */ new Map();
43525
43529
  var knownFiles = /* @__PURE__ */ new Set();
43526
43530
  var moduleGraphCache = /* @__PURE__ */ new Map();
43531
+ var nudoDepParents = /* @__PURE__ */ new Map();
43532
+ function normPath(p) {
43533
+ return p.replace(/\\/g, "/");
43534
+ }
43535
+ function registerNudoImportDeps(filePath, source) {
43536
+ const parent = normPath(resolvePath(filePath));
43537
+ for (const set of nudoDepParents.values()) {
43538
+ set.delete(parent);
43539
+ }
43540
+ const imports = extractNudoImports(source);
43541
+ for (const imp of imports) {
43542
+ if (!imp.spec.startsWith(".") && !imp.spec.startsWith("/")) continue;
43543
+ const dep = normPath(resolvePath(dirname(filePath), imp.spec));
43544
+ let set = nudoDepParents.get(dep);
43545
+ if (!set) {
43546
+ set = /* @__PURE__ */ new Set();
43547
+ nudoDepParents.set(dep, set);
43548
+ }
43549
+ set.add(parent);
43550
+ }
43551
+ }
43552
+ async function handleNudoDepFileChanged(nudoPath, deps) {
43553
+ const p = normPath(resolvePath(nudoPath));
43554
+ evictGeneralizeMemoForPaths([p]);
43555
+ evictCheckSourceMemoForPaths([p]);
43556
+ const parents = nudoDepParents.get(p);
43557
+ if (!parents || parents.size === 0) return;
43558
+ const parentList = [...parents];
43559
+ evictBPathCacheForFiles(parentList);
43560
+ evictAnalysisFileCacheForFiles(parentList);
43561
+ evictFnAnalysisCacheForFiles(parentList);
43562
+ for (const parent of parentList) {
43563
+ const doc = deps.getOpenDocumentByPath?.(parent);
43564
+ if (!doc) continue;
43565
+ await validateText(
43566
+ parent,
43567
+ doc.uri,
43568
+ doc.getText(),
43569
+ doc.version,
43570
+ deps,
43571
+ false
43572
+ );
43573
+ }
43574
+ }
43527
43575
  function forgetValidatedFile(filePath) {
43576
+ const parent = normPath(resolvePath(filePath));
43528
43577
  knownFiles.delete(filePath);
43529
43578
  analysisCache.delete(filePath);
43579
+ for (const set of nudoDepParents.values()) {
43580
+ set.delete(parent);
43581
+ }
43530
43582
  }
43531
43583
  function evictModuleGraphCacheEntries(uris) {
43532
- for (const uri of uris) moduleGraphCache.delete(uriToFilePath(uri));
43584
+ const paths = uris.map(uriToFilePath);
43585
+ for (const p of paths) moduleGraphCache.delete(p);
43586
+ evictAbsModuleCacheFiles(paths);
43533
43587
  }
43534
43588
  function hasNudoDirectives(source) {
43535
- return /@nudo:(case|mock|pure|skip|sample|returns|env|mock-module|as|replace)\b/.test(source);
43589
+ return /@nudo:(case|mock|pure|skip|sample|refine|import|env|mock-module|as|replace)\b/.test(source);
43536
43590
  }
43537
43591
  function uriToFilePath(uri) {
43538
43592
  return uri.startsWith("file://") ? decodeURIComponent(uri.slice(7)) : uri;
@@ -43578,6 +43632,43 @@ function toLspDiagnostic(d, uri) {
43578
43632
  }
43579
43633
  return diag;
43580
43634
  }
43635
+ function lspLoadModule(spec, fromFile) {
43636
+ return defaultLoadModule(spec, fromFile);
43637
+ }
43638
+ function checkToLspDiagnostics(filePath, source, loadModule) {
43639
+ try {
43640
+ const report = checkSource(filePath, source, pTrue, {
43641
+ loadModule: loadModule ?? lspLoadModule,
43642
+ fromFile: filePath
43643
+ });
43644
+ return report.issues.filter((i) => i.severity === "error" || i.severity === "warning").map((i) => {
43645
+ const line = (i.line ?? 1) - 1;
43646
+ const col = i.column ?? 0;
43647
+ const parts = [i.message];
43648
+ if (i.actual) parts.push(`actual: ${i.actual}`);
43649
+ if (i.expected) parts.push(`expected: ${i.expected}`);
43650
+ if (i.suggestion) parts.push(i.suggestion);
43651
+ return {
43652
+ severity: i.severity === "error" ? DiagnosticSeverity.Error : i.severity === "warning" ? DiagnosticSeverity.Warning : DiagnosticSeverity.Information,
43653
+ range: {
43654
+ start: { line, character: col },
43655
+ end: { line, character: col + 1 }
43656
+ },
43657
+ message: parts.join(" \xB7 "),
43658
+ source: "nudo-check",
43659
+ code: i.code,
43660
+ data: {
43661
+ actual: i.actual,
43662
+ expected: i.expected,
43663
+ fn: i.fn,
43664
+ suggestions: i.suggestion ? [i.suggestion] : []
43665
+ }
43666
+ };
43667
+ });
43668
+ } catch {
43669
+ return [];
43670
+ }
43671
+ }
43581
43672
  async function validateText(filePath, uri, text, version, deps, propagate = false) {
43582
43673
  if (deps.isNudoUri && !deps.isNudoUri(uri)) {
43583
43674
  deps.sendDiagnostics({ uri, diagnostics: [] });
@@ -43600,7 +43691,10 @@ async function validateText(filePath, uri, text, version, deps, propagate = fals
43600
43691
  }
43601
43692
  analysisCache.set(filePath, { version, result });
43602
43693
  knownFiles.add(filePath);
43603
- deps.sendDiagnostics({ uri, diagnostics: result.diagnostics.map((d) => toLspDiagnostic(d, uri)) });
43694
+ registerNudoImportDeps(filePath, text);
43695
+ const checkDiags = checkToLspDiagnostics(filePath, text);
43696
+ const evalDiags = result.diagnostics.map((d) => toLspDiagnostic(d, uri));
43697
+ deps.sendDiagnostics({ uri, diagnostics: [...checkDiags, ...evalDiags] });
43604
43698
  if (!propagate || !deps.getOpenDocumentByPath) return;
43605
43699
  let dependents;
43606
43700
  try {
@@ -43612,6 +43706,9 @@ async function validateText(filePath, uri, text, version, deps, propagate = fals
43612
43706
  if (dirtyPath === filePath) continue;
43613
43707
  const doc = deps.getOpenDocumentByPath(dirtyPath);
43614
43708
  if (!doc) continue;
43709
+ evictBPathCacheForFiles([dirtyPath]);
43710
+ evictAnalysisFileCacheForFiles([dirtyPath]);
43711
+ evictFnAnalysisCacheForFiles([dirtyPath]);
43615
43712
  await validateText(dirtyPath, doc.uri, doc.getText(), doc.version, deps, false);
43616
43713
  }
43617
43714
  }
@@ -43619,9 +43716,15 @@ async function validateText(filePath, uri, text, version, deps, propagate = fals
43619
43716
  // src/agent-tools.ts
43620
43717
  import { readFileSync } from "fs";
43621
43718
  import { resolve } from "path";
43622
- import { analyzeFile as analyzeFile2, buildCaseDirective } from "@nudojs/service";
43719
+ import {
43720
+ analyzeFile as analyzeFile2,
43721
+ buildCaseDirective,
43722
+ getHoverAtPosition,
43723
+ collectAbsInlays,
43724
+ serializeInferJson
43725
+ } from "@nudojs/service";
43623
43726
  import { parse } from "@nudojs/parser";
43624
- import { T, typeValueToString } from "@nudojs/core";
43727
+ import { T, typeValueToString, checkSource as checkSource2, serializeCheckJson, pTrue as pTrue2 } from "@nudojs/core";
43625
43728
  function textResult(text) {
43626
43729
  return { content: [{ type: "text", text }] };
43627
43730
  }
@@ -43736,6 +43839,106 @@ function readSource(filePath, deps = {}) {
43736
43839
  function analysisError(err) {
43737
43840
  return textResult(`Error: ${err.message}`);
43738
43841
  }
43842
+ function checkTool(params, deps = {}) {
43843
+ try {
43844
+ const filePath = normalizeFilePath(params.file);
43845
+ const source = params.source ?? readSource(filePath, deps);
43846
+ const report = checkSource2(filePath, source, pTrue2, {
43847
+ loadModule: params.loadModule ?? lspLoadModule,
43848
+ fromFile: filePath
43849
+ });
43850
+ const json = serializeCheckJson(report);
43851
+ if (params.format === "json") {
43852
+ return textResult(JSON.stringify(json, null, 2));
43853
+ }
43854
+ const lines = [
43855
+ json.ok ? "nudo check OK" : "nudo check FAILED",
43856
+ `${json.summary.errors} error \xB7 ${json.summary.warnings} warning \xB7 ${json.summary.functions} fn`
43857
+ ];
43858
+ for (const i of json.issues) {
43859
+ if (i.severity === "info") continue;
43860
+ const loc = i.line != null ? ` L${i.line}` : "";
43861
+ lines.push(`[${i.severity}]${loc} ${i.message}`);
43862
+ if (i.actual) lines.push(` actual: ${i.actual}`);
43863
+ if (i.expected) lines.push(` expected: ${i.expected}`);
43864
+ }
43865
+ lines.push("");
43866
+ lines.push(JSON.stringify(json, null, 2));
43867
+ return textResult(lines.join("\n"));
43868
+ } catch (err) {
43869
+ return analysisError(err);
43870
+ }
43871
+ }
43872
+ function hoverTool(params, deps = {}) {
43873
+ try {
43874
+ const filePath = normalizeFilePath(params.file);
43875
+ const source = params.source ?? readSource(filePath, deps);
43876
+ const hover = getHoverAtPosition(filePath, source, params.line, params.column);
43877
+ const payload = {
43878
+ file: filePath,
43879
+ line: params.line,
43880
+ column: params.column,
43881
+ abs: hover?.abs ?? null,
43882
+ absMultiline: hover?.absMultiline ?? null,
43883
+ intension: hover?.intension ?? null,
43884
+ /** 有损外延,仅对照 */
43885
+ ext: hover?.typeText ?? null
43886
+ };
43887
+ if (params.includeInlays) {
43888
+ payload.inlays = collectAbsInlays(source, {
43889
+ loadModule: params.loadModule ?? lspLoadModule,
43890
+ fromFile: filePath
43891
+ });
43892
+ }
43893
+ return textResult(JSON.stringify(payload, null, 2));
43894
+ } catch (err) {
43895
+ return analysisError(err);
43896
+ }
43897
+ }
43898
+ function inferTool(params, deps = {}) {
43899
+ try {
43900
+ const filePath = normalizeFilePath(params.file);
43901
+ const source = params.source ?? readSource(filePath, deps);
43902
+ const result = analyzeFile2(filePath, source);
43903
+ let json = serializeInferJson(result, filePath);
43904
+ if (params.functions && params.functions.length > 0) {
43905
+ const keep = new Set(params.functions);
43906
+ const filtered = json.functions.filter((f) => keep.has(f.name));
43907
+ let cases = 0;
43908
+ for (const f of filtered) {
43909
+ cases += f.cases?.length ?? 0;
43910
+ }
43911
+ const summary = {
43912
+ ...json.summary,
43913
+ functions: filtered.length,
43914
+ cases
43915
+ };
43916
+ json = { ...json, functions: filtered, summary };
43917
+ }
43918
+ if (params.format === "json") {
43919
+ return textResult(JSON.stringify(json, null, 2));
43920
+ }
43921
+ const lines = [
43922
+ `nudo infer ${json.file}`,
43923
+ `${json.summary.functions} fn \xB7 ${json.summary.cases} case \xB7 ${json.summary.diagnostics} diag`
43924
+ ];
43925
+ for (const f of json.functions) {
43926
+ lines.push("");
43927
+ lines.push(`${f.name}${f.entryOnly ? " [entry-only]" : ""}`);
43928
+ for (const c of f.cases) {
43929
+ const args = c.args.join(", ");
43930
+ lines.push(` ${c.name}: (${args}) => ${c.result}`);
43931
+ if (c.intension?.abs) lines.push(` abs: ${c.intension.abs}`);
43932
+ else if (c.intension?.display) lines.push(` intension: ${c.intension.display}`);
43933
+ }
43934
+ }
43935
+ lines.push("");
43936
+ lines.push(JSON.stringify(json, null, 2));
43937
+ return textResult(lines.join("\n"));
43938
+ } catch (err) {
43939
+ return analysisError(err);
43940
+ }
43941
+ }
43739
43942
  function whatIf(params, deps = {}) {
43740
43943
  try {
43741
43944
  const filePath = normalizeFilePath(params.file);
@@ -43820,6 +44023,9 @@ var NUDO_COMMANDS = [
43820
44023
  "nudo.whatIf",
43821
44024
  "nudo.suggestCase",
43822
44025
  "nudo.trace",
44026
+ "nudo.check",
44027
+ "nudo.hover",
44028
+ "nudo.infer",
43823
44029
  "nudo.selectCase",
43824
44030
  "nudo.getActiveCases"
43825
44031
  ];
@@ -43857,8 +44063,8 @@ connection.onInitialize((_params) => ({
43857
44063
  semanticTokensProvider: {
43858
44064
  full: true,
43859
44065
  legend: {
43860
- tokenTypes: [...TOKEN_TYPES],
43861
- tokenModifiers: [...TOKEN_MODIFIERS]
44066
+ tokenTypes: [...SEMANTIC_TOKEN_TYPES],
44067
+ tokenModifiers: [...SEMANTIC_TOKEN_MODIFIERS]
43862
44068
  }
43863
44069
  },
43864
44070
  executeCommandProvider: {
@@ -43906,16 +44112,27 @@ function registerWatchedFilesListener(listener) {
43906
44112
  if (idx >= 0) watchedFilesListeners.splice(idx, 1);
43907
44113
  };
43908
44114
  }
44115
+ function isNudoDepPath(filePath) {
44116
+ return /\.nudo\.(js|mjs|ts)$/.test(filePath);
44117
+ }
43909
44118
  function handleWatchedFilesChanges(changes, isOpen) {
43910
44119
  const gone = [];
44120
+ const nudoTouched = [];
43911
44121
  for (const change of changes) {
43912
- if (change.type !== FileChangeType.Deleted) continue;
43913
- if (isOpen(change.uri)) continue;
43914
- gone.push(change.uri);
43915
- forgetValidatedFile(uriToFilePath(change.uri));
43916
- activeCases.delete(change.uri);
43917
- nudoFileCache.delete(change.uri);
43918
- connection.sendDiagnostics({ uri: change.uri, diagnostics: [] });
44122
+ const filePath = uriToFilePath(change.uri);
44123
+ if (change.type === FileChangeType.Deleted) {
44124
+ if (isOpen(change.uri)) continue;
44125
+ gone.push(change.uri);
44126
+ forgetValidatedFile(filePath);
44127
+ activeCases.delete(change.uri);
44128
+ nudoFileCache.delete(change.uri);
44129
+ connection.sendDiagnostics({ uri: change.uri, diagnostics: [] });
44130
+ if (isNudoDepPath(filePath)) nudoTouched.push(filePath);
44131
+ continue;
44132
+ }
44133
+ if (isNudoDepPath(filePath)) {
44134
+ nudoTouched.push(filePath);
44135
+ }
43919
44136
  }
43920
44137
  if (gone.length > 0) {
43921
44138
  for (const listener of [...watchedFilesListeners]) {
@@ -43925,6 +44142,13 @@ function handleWatchedFilesChanges(changes, isOpen) {
43925
44142
  }
43926
44143
  }
43927
44144
  }
44145
+ if (nudoTouched.length > 0) {
44146
+ const deps = validationDeps();
44147
+ for (const p of nudoTouched) {
44148
+ void handleNudoDepFileChanged(p, deps).catch(() => {
44149
+ });
44150
+ }
44151
+ }
43928
44152
  return gone;
43929
44153
  }
43930
44154
  connection.onDidChangeWatchedFiles(
@@ -43959,14 +44183,27 @@ connection.onHover((params) => {
43959
44183
  const column = params.position.character;
43960
44184
  const cases = getActiveCasesForUri(params.textDocument.uri);
43961
44185
  try {
43962
- const tv = getTypeAtPosition(filePath, source, line, column, cases);
43963
- if (!tv) return null;
44186
+ const hover = getHoverAtPosition2(filePath, source, line, column, cases);
44187
+ if (!hover) return null;
44188
+ const lines = [];
44189
+ if (hover.absMultiline) {
44190
+ lines.push("```nudo", hover.absMultiline, "```");
44191
+ } else if (hover.abs) {
44192
+ lines.push("```nudo", hover.abs, "```");
44193
+ }
44194
+ if (hover.intension && hover.intension !== hover.abs) {
44195
+ lines.push("```nudo", hover.intension, "```");
44196
+ }
44197
+ if (hover.typeText && hover.typeText !== hover.intension && hover.typeText !== hover.abs) {
44198
+ lines.push("```nudo", `ext: ${hover.typeText}`, "```");
44199
+ }
44200
+ if (lines.length === 0) {
44201
+ lines.push("```nudo", hover.typeText, "```");
44202
+ }
43964
44203
  return {
43965
44204
  contents: {
43966
44205
  kind: MarkupKind.Markdown,
43967
- value: `\`\`\`nudo
43968
- ${typeValueToString2(tv)}
43969
- \`\`\``
44206
+ value: lines.join("\n")
43970
44207
  }
43971
44208
  };
43972
44209
  } catch {
@@ -44048,6 +44285,22 @@ connection.languages.inlayHint.on((params) => {
44048
44285
  paddingLeft: true
44049
44286
  });
44050
44287
  }
44288
+ try {
44289
+ for (const abs of collectAbsInlays2(source, {
44290
+ loadModule: lspLoadModule,
44291
+ fromFile: filePath
44292
+ })) {
44293
+ const lineIdx = abs.line - 1;
44294
+ if (lineIdx < 0 || lineIdx >= lines.length) continue;
44295
+ hints.push({
44296
+ position: { line: lineIdx, character: abs.character },
44297
+ label: abs.label,
44298
+ kind: abs.kind === "parameter" ? InlayHintKind.Parameter : InlayHintKind.Type,
44299
+ paddingLeft: true
44300
+ });
44301
+ }
44302
+ } catch {
44303
+ }
44051
44304
  return hints;
44052
44305
  } catch {
44053
44306
  return [];
@@ -44153,14 +44406,6 @@ connection.onCodeAction((params) => {
44153
44406
  }
44154
44407
  });
44155
44408
  }
44156
- if (diag.code === "nudo-assertion-failed") {
44157
- actions.push({
44158
- title: "Update @nudo:returns to match inferred type",
44159
- kind: "quickfix",
44160
- diagnostics: [diag],
44161
- isPreferred: false
44162
- });
44163
- }
44164
44409
  }
44165
44410
  return actions;
44166
44411
  });
@@ -44239,7 +44484,12 @@ connection.languages.semanticTokens.on((params) => {
44239
44484
  const document2 = documents.get(params.textDocument.uri);
44240
44485
  if (!document2) return { data: [] };
44241
44486
  if (!isNudoFile(params.textDocument.uri)) return { data: [] };
44242
- return { data: [] };
44487
+ try {
44488
+ const filePath = uriToFilePath(document2.uri);
44489
+ return { data: buildSemanticTokens(filePath, document2.getText()) };
44490
+ } catch {
44491
+ return { data: [] };
44492
+ }
44243
44493
  });
44244
44494
  function uriForFileOrUri(params) {
44245
44495
  if (params.uri) return params.uri;
@@ -44281,6 +44531,12 @@ function dispatchNudoCommand(command, arg) {
44281
44531
  return suggestCase(arg, agentToolDeps);
44282
44532
  case "nudo.trace":
44283
44533
  return trace(arg, agentToolDeps);
44534
+ case "nudo.check":
44535
+ return checkTool(arg, agentToolDeps);
44536
+ case "nudo.hover":
44537
+ return hoverTool(arg, agentToolDeps);
44538
+ case "nudo.infer":
44539
+ return inferTool(arg, agentToolDeps);
44284
44540
  case "nudo.selectCase":
44285
44541
  return handleSelectCase(arg);
44286
44542
  case "nudo.getActiveCases":
@@ -44300,6 +44556,9 @@ function dispatchAgentRequest(command, params) {
44300
44556
  connection.onRequest("nudo/whatIf", (params) => dispatchAgentRequest("nudo.whatIf", params));
44301
44557
  connection.onRequest("nudo/suggestCase", (params) => dispatchAgentRequest("nudo.suggestCase", params));
44302
44558
  connection.onRequest("nudo/trace", (params) => dispatchAgentRequest("nudo.trace", params));
44559
+ connection.onRequest("nudo/check", (params) => dispatchAgentRequest("nudo.check", params));
44560
+ connection.onRequest("nudo/hover", (params) => dispatchAgentRequest("nudo.hover", params));
44561
+ connection.onRequest("nudo/infer", (params) => dispatchAgentRequest("nudo.infer", params));
44303
44562
  connection.languages.diagnostics.on((params) => {
44304
44563
  const document2 = documents.get(params.textDocument.uri);
44305
44564
  if (!document2) return { kind: "full", items: [] };
@@ -44317,7 +44576,7 @@ connection.languages.diagnostics.on((params) => {
44317
44576
  });
44318
44577
  var nudoFileCache = /* @__PURE__ */ new Map();
44319
44578
  function isNudoFile(uri) {
44320
- if (!uri.endsWith(".js") && !uri.endsWith(".ts") && !uri.endsWith(".mjs")) return false;
44579
+ if (!isNudoTargetPath(uriToFilePath(uri))) return false;
44321
44580
  const cached = nudoFileCache.get(uri);
44322
44581
  if (cached !== void 0) return cached;
44323
44582
  const doc = documents.get(uri);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nudojs/lsp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Language Server Protocol implementation for the Nudo type inference engine",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,9 +27,9 @@
27
27
  "dependencies": {
28
28
  "vscode-languageserver": "^9.0.0",
29
29
  "vscode-languageserver-textdocument": "^1.0.0",
30
- "@nudojs/service": "0.2.1",
31
- "@nudojs/core": "0.2.0",
32
- "@nudojs/parser": "0.2.0"
30
+ "@nudojs/service": "0.3.0",
31
+ "@nudojs/core": "0.3.0",
32
+ "@nudojs/parser": "0.3.0"
33
33
  },
34
34
  "scripts": {
35
35
  "build": "tsup src/server.ts --format esm --dts"
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { checkTool } from "../agent-tools.ts";
3
+
4
+ const STD = `
5
+ export const positive = number().gt(0);
6
+ export const delay = number().gt(0);
7
+ `;
8
+
9
+ const src = `
10
+ /// @nudo:import { positive } from "./std.nudo.js"
11
+ /**
12
+ * @nudo:refine x positive
13
+ */
14
+ function needsPositive(x) {
15
+ if (x > 0) return x;
16
+ return 0;
17
+ }
18
+ needsPositive(-1);
19
+ `;
20
+
21
+ const loadModule = (spec: string) =>
22
+ spec.includes("std.nudo") ? STD : undefined;
23
+
24
+ describe("nudo.check agent tool", () => {
25
+ it("returns CheckJson v1 in json format", () => {
26
+ const r = checkTool(
27
+ { file: "/t/a.js", source: src, format: "json", loadModule },
28
+ { readFile: () => src },
29
+ );
30
+ const parsed = JSON.parse(r.content[0].text);
31
+ expect(parsed.version).toBe(1);
32
+ expect(parsed.ok).toBe(false);
33
+ expect(parsed.issues.some((i: { code: string }) => i.code === "nudo:constraint-violated")).toBe(true);
34
+ });
35
+
36
+ it("text format includes actual/expected", () => {
37
+ const r = checkTool(
38
+ { file: "/t/a.js", source: src, loadModule },
39
+ { readFile: () => src },
40
+ );
41
+ const text = r.content[0].text;
42
+ expect(text).toContain("FAILED");
43
+ expect(text).toContain("actual");
44
+ expect(text).toContain("expected");
45
+ expect(text).toContain('"version": 1');
46
+ });
47
+ });