@nudojs/lsp 0.2.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 +34 -0
- package/README.md +35 -0
- package/agent-skill/SKILL.md +82 -0
- package/dist/server.d.ts +8 -1
- package/dist/server.js +44431 -92
- package/package.json +15 -3
- 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__/definition.test.ts +32 -0
- package/src/__tests__/lsp-integration.test.ts +921 -0
- package/src/__tests__/nudo-dep-evict.test.ts +103 -0
- package/src/agent-tools.ts +486 -0
- package/src/semantic-tokens.ts +9 -0
- package/src/server.ts +517 -71
- package/src/symbols.ts +109 -0
- package/src/validation.ts +348 -0
package/package.json
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nudojs/lsp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Language Server Protocol implementation for the Nudo type inference engine",
|
|
4
5
|
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"nudo",
|
|
9
|
+
"type-inference",
|
|
10
|
+
"javascript",
|
|
11
|
+
"lsp",
|
|
12
|
+
"language-server"
|
|
13
|
+
],
|
|
5
14
|
"main": "src/server.ts",
|
|
6
15
|
"repository": {
|
|
7
16
|
"type": "git",
|
|
8
17
|
"url": "git+https://github.com/nudojs/nudo.git",
|
|
9
18
|
"directory": "packages/lsp"
|
|
10
19
|
},
|
|
20
|
+
"homepage": "https://github.com/nudojs/nudo/tree/main/packages/lsp",
|
|
21
|
+
"bugs": "https://github.com/nudojs/nudo/issues",
|
|
11
22
|
"publishConfig": {
|
|
12
23
|
"access": "public",
|
|
13
24
|
"provenance": true,
|
|
@@ -16,8 +27,9 @@
|
|
|
16
27
|
"dependencies": {
|
|
17
28
|
"vscode-languageserver": "^9.0.0",
|
|
18
29
|
"vscode-languageserver-textdocument": "^1.0.0",
|
|
19
|
-
"@nudojs/service": "0.
|
|
20
|
-
"@nudojs/core": "0.
|
|
30
|
+
"@nudojs/service": "0.3.0",
|
|
31
|
+
"@nudojs/core": "0.3.0",
|
|
32
|
+
"@nudojs/parser": "0.3.0"
|
|
21
33
|
},
|
|
22
34
|
"scripts": {
|
|
23
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
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { hoverTool } from "../agent-tools.ts";
|
|
3
|
+
|
|
4
|
+
describe("nudo.hover agent tool", () => {
|
|
5
|
+
it("returns lossless abs at position", () => {
|
|
6
|
+
const src = `const x = 1 + 2;\n`;
|
|
7
|
+
// col of `2` is 14 (0-based)
|
|
8
|
+
const r = hoverTool(
|
|
9
|
+
{ file: "/t/h.js", line: 1, column: 14, source: src },
|
|
10
|
+
{ readFile: () => src },
|
|
11
|
+
);
|
|
12
|
+
const payload = JSON.parse(r.content[0].text);
|
|
13
|
+
expect(payload.abs).toBeDefined();
|
|
14
|
+
expect(payload.abs).toContain("2");
|
|
15
|
+
expect(payload).toHaveProperty("ext");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("can include inlays", () => {
|
|
19
|
+
const src = `function needsPositive(x) {\n if (x > 0) return x;\n return 0;\n}\n`;
|
|
20
|
+
const r = hoverTool(
|
|
21
|
+
{ file: "/t/i.js", line: 1, column: 9, source: src, includeInlays: true },
|
|
22
|
+
{ readFile: () => src },
|
|
23
|
+
);
|
|
24
|
+
const payload = JSON.parse(r.content[0].text);
|
|
25
|
+
expect(Array.isArray(payload.inlays)).toBe(true);
|
|
26
|
+
expect(payload.inlays.length).toBeGreaterThan(0);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { inferTool } from "../agent-tools.ts";
|
|
3
|
+
|
|
4
|
+
describe("nudo.infer agent tool", () => {
|
|
5
|
+
const src = `function scale(x) { return x + 1; }\n`;
|
|
6
|
+
|
|
7
|
+
it("returns InferJson v1 with abs intension", () => {
|
|
8
|
+
const r = inferTool(
|
|
9
|
+
{ file: "/t/scale.js", source: src, format: "json" },
|
|
10
|
+
{ readFile: () => src },
|
|
11
|
+
);
|
|
12
|
+
const json = JSON.parse(r.content[0].text);
|
|
13
|
+
expect(json.version).toBe(1);
|
|
14
|
+
expect(json.functions[0].name).toBe("scale");
|
|
15
|
+
expect(json.functions[0].cases[0].intension.abs).toContain("#");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("text format lists abs lines", () => {
|
|
19
|
+
const r = inferTool({ file: "/t/scale.js", source: src }, { readFile: () => src });
|
|
20
|
+
const text = r.content[0].text;
|
|
21
|
+
expect(text).toContain("scale");
|
|
22
|
+
expect(text).toContain("abs:");
|
|
23
|
+
expect(text).toContain('"version": 1');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("filters by function name", () => {
|
|
27
|
+
const multi = `
|
|
28
|
+
function a(x) { return x; }
|
|
29
|
+
function b(x) { return x + 1; }
|
|
30
|
+
`;
|
|
31
|
+
const r = inferTool(
|
|
32
|
+
{ file: "/t/m.js", source: multi, format: "json", functions: ["b"] },
|
|
33
|
+
{ readFile: () => multi },
|
|
34
|
+
);
|
|
35
|
+
const json = JSON.parse(r.content[0].text);
|
|
36
|
+
expect(json.functions.map((f: { name: string }) => f.name)).toEqual(["b"]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { clearValidationState, checkToLspDiagnostics, validateText } from "../validation.ts";
|
|
3
|
+
|
|
4
|
+
const STD = `
|
|
5
|
+
export const positive = number().gt(0);
|
|
6
|
+
`;
|
|
7
|
+
|
|
8
|
+
const src = `
|
|
9
|
+
/// @nudo:import { positive } from "./std.nudo.js"
|
|
10
|
+
/**
|
|
11
|
+
* @nudo:refine x positive
|
|
12
|
+
*/
|
|
13
|
+
function needsPositive(x) {
|
|
14
|
+
if (x > 0) return x;
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
needsPositive(-1);
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const loadModule = (spec: string) =>
|
|
21
|
+
spec.includes("std.nudo") ? STD : undefined;
|
|
22
|
+
|
|
23
|
+
describe("Abs check as LSP diagnostics", () => {
|
|
24
|
+
it("maps constraint violations to nudo-check diagnostics", () => {
|
|
25
|
+
const diags = checkToLspDiagnostics("/t/check.js", src, loadModule);
|
|
26
|
+
expect(diags.length).toBeGreaterThan(0);
|
|
27
|
+
const d = diags[0]!;
|
|
28
|
+
expect(d.source).toBe("nudo-check");
|
|
29
|
+
expect(d.code).toBe("nudo:constraint-violated");
|
|
30
|
+
expect(d.message).toContain("actual");
|
|
31
|
+
expect(d.message).toContain("expected");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("validateText publishes check diags first", async () => {
|
|
35
|
+
clearValidationState();
|
|
36
|
+
let sent: { source?: string; code?: string }[] = [];
|
|
37
|
+
await validateText("/t/check2.js", "file:///t/check2.js", src, 1, {
|
|
38
|
+
sendDiagnostics: (p) => {
|
|
39
|
+
sent = p.diagnostics.map((d) => ({ source: d.source, code: d.code as string }));
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
// validateText 用磁盘 loadModule;虚拟路径下无 std.nudo.js → 无 check diags 也可
|
|
43
|
+
// 至少不崩溃
|
|
44
|
+
expect(Array.isArray(sent)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parse } from "@nudojs/parser";
|
|
3
|
+
import { buildSymbolTable, findDefinition, findReferences } from "../symbols.ts";
|
|
4
|
+
|
|
5
|
+
describe("symbol table", () => {
|
|
6
|
+
it("finds function definition", () => {
|
|
7
|
+
const source = `function add(a, b) { return a + b; }`;
|
|
8
|
+
const ast = parse(source);
|
|
9
|
+
const table = buildSymbolTable(ast, "file:///test.js");
|
|
10
|
+
const def = findDefinition(table, "add");
|
|
11
|
+
expect(def).not.toBeNull();
|
|
12
|
+
expect(def!.kind).toBe("function");
|
|
13
|
+
expect(def!.loc.start.line).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("finds variable definition", () => {
|
|
17
|
+
const source = `const x = 42;`;
|
|
18
|
+
const ast = parse(source);
|
|
19
|
+
const table = buildSymbolTable(ast, "file:///test.js");
|
|
20
|
+
const def = findDefinition(table, "x");
|
|
21
|
+
expect(def).not.toBeNull();
|
|
22
|
+
expect(def!.kind).toBe("variable");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("collects references", () => {
|
|
26
|
+
const source = `const x = 42; const y = x + 1;`;
|
|
27
|
+
const ast = parse(source);
|
|
28
|
+
const table = buildSymbolTable(ast, "file:///test.js");
|
|
29
|
+
const refs = table.references.filter((r) => r.name === "x");
|
|
30
|
+
expect(refs.length).toBeGreaterThan(0);
|
|
31
|
+
});
|
|
32
|
+
});
|