@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 +22 -0
- package/agent-skill/SKILL.md +13 -14
- package/dist/server.js +309 -50
- package/package.json +4 -4
- package/src/__tests__/agent-check.test.ts +47 -0
- package/src/__tests__/agent-hover.test.ts +28 -0
- package/src/__tests__/agent-infer.test.ts +38 -0
- package/src/__tests__/check-diagnostics.test.ts +46 -0
- package/src/__tests__/lsp-integration.test.ts +110 -11
- package/src/__tests__/nudo-dep-evict.test.ts +103 -0
- package/src/agent-tools.ts +164 -3
- package/src/semantic-tokens.ts +9 -45
- package/src/server.ts +99 -23
- package/src/validation.ts +154 -3
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
|
package/agent-skill/SKILL.md
CHANGED
|
@@ -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/
|
|
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:
|
|
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
|
|
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/
|
|
50
|
-
|
|
51
|
-
|
|
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 `
|
|
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/
|
|
64
|
-
"bindings": [{ "name": "
|
|
65
|
-
"target": "
|
|
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 "
|
|
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
|
-
|
|
43495
|
-
|
|
43496
|
-
|
|
43497
|
-
|
|
43498
|
-
|
|
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
|
-
|
|
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|
|
|
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
|
-
|
|
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 {
|
|
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: [...
|
|
43861
|
-
tokenModifiers: [...
|
|
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
|
-
|
|
43913
|
-
if (
|
|
43914
|
-
|
|
43915
|
-
|
|
43916
|
-
|
|
43917
|
-
|
|
43918
|
-
|
|
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
|
|
43963
|
-
if (!
|
|
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:
|
|
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
|
-
|
|
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 (!
|
|
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
|
+
"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.
|
|
31
|
-
"@nudojs/core": "0.
|
|
32
|
-
"@nudojs/parser": "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
|
+
});
|