@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.
@@ -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
+ });
@@ -11,6 +11,8 @@ import {
11
11
  generateDts,
12
12
  typeValueToZodSchema,
13
13
  generateGuardFunction,
14
+ buildSemanticTokens,
15
+ SEMANTIC_TOKEN_TYPES,
14
16
  } from "@nudojs/service";
15
17
  import { T } from "@nudojs/core";
16
18
  import { parse } from "@nudojs/parser";
@@ -79,7 +81,6 @@ function first(arr) {
79
81
  }
80
82
 
81
83
  /**
82
- * @nudo:returns (T.string)
83
84
  * @nudo:case "num" (42)
84
85
  */
85
86
  function alwaysString(x) {
@@ -109,13 +110,6 @@ describe("LSP Integration - Full Pipeline", () => {
109
110
  expect(hint.label).toBeTruthy();
110
111
  }
111
112
  });
112
-
113
- it("generates diagnostics for assertion failures", () => {
114
- const result = analyzeFile(filePath, testCode);
115
- const assertionDiags = result.diagnostics.filter(d => d.code === "nudo-assertion-failed");
116
- expect(assertionDiags.length).toBeGreaterThan(0);
117
- expect(assertionDiags[0].message).toContain("inferred");
118
- });
119
113
  });
120
114
 
121
115
  describe("getTypeAtPosition", () => {
@@ -691,8 +685,9 @@ describe("LSP Integration - Agent Tools (whatIf / suggestCase / trace)", () => {
691
685
 
692
686
  // without assumptions x=1 folds to a literal and y = 2
693
687
  expect(base.content[0].text).toContain('Type of "y": 2');
694
- // with x:string the + operator widens y to string — the assumption really flowed
695
- expect(assumed.content[0].text).toContain('Type of "y": string');
688
+ // with x:string the + operator keeps template structure — the assumption really flowed
689
+ expect(assumed.content[0].text).toContain('Type of "y":');
690
+ expect(assumed.content[0].text).toContain("string");
696
691
  expect(assumed.content[0].text).not.toBe(base.content[0].text);
697
692
  expect(assumed.content[0].text).toContain("Bindings applied: x: string");
698
693
  });
@@ -747,7 +742,8 @@ describe("LSP Integration - Agent Tools (whatIf / suggestCase / trace)", () => {
747
742
  const viaUri = whatIf(
748
743
  { file: `file://${plainPath}`, bindings: [{ name: "x", type: "string" }], target: "y" },
749
744
  );
750
- expect(viaUri.content[0].text).toContain('Type of "y": string');
745
+ expect(viaUri.content[0].text).toContain('Type of "y":');
746
+ expect(viaUri.content[0].text).toContain("string");
751
747
 
752
748
  // normalizeFilePath decodes and resolves both forms identically
753
749
  expect(normalizeFilePath(`file://${plainPath}`)).toBe(normalizeFilePath(plainPath));
@@ -820,3 +816,106 @@ describe("LSP Integration - Agent Tools (whatIf / suggestCase / trace)", () => {
820
816
  expect(zero.content[0].text).toContain("Suggested: /** @nudo:case */");
821
817
  });
822
818
  });
819
+
820
+ // 来源:IDE 深度批次——service 层 semantic tokens 提取(server.ts 只接线,
821
+ // 此处直接测 buildSemanticTokens,不 import server.ts)
822
+ describe("LSP Integration - Semantic Tokens", () => {
823
+ const tokenSrc = [
824
+ "const answer = 42;", // L1: answer → variable
825
+ "const double = (n) => n * 2;", // L2: double → function(推断), n → parameter
826
+ "function add(a, b) { return a + b; }", // L3: add → function, a/b → parameter
827
+ 'const config = { host: "x", run() {} };', // L4: config → variable, host → property, run → method
828
+ "answer + double(1) + add(1, config.host);", // L5: 纯使用位,不上色
829
+ ].join("\n");
830
+
831
+ type Decoded = { line: number; char: number; length: number; typeIndex: number; modifiers: number };
832
+
833
+ const decode = (data: number[]): Decoded[] => {
834
+ expect(data.length % 5).toBe(0);
835
+ const out: Decoded[] = [];
836
+ let line = 0;
837
+ let char = 0;
838
+ for (let i = 0; i < data.length; i += 5) {
839
+ const [deltaLine, deltaChar, length, typeIndex, modifiers] = data.slice(i, i + 5);
840
+ line += deltaLine;
841
+ char = deltaLine === 0 ? char + deltaChar : deltaChar;
842
+ out.push({ line, char, length, typeIndex, modifiers });
843
+ }
844
+ return out;
845
+ };
846
+
847
+ it("emits LSP five-tuples for top-level const/function/params/object keys", () => {
848
+ const data = buildSemanticTokens("/test/sem-tok.js", tokenSrc);
849
+
850
+ // LSP 相对编码契约:首个 token deltaLine=0;数据是五元组扁平数组
851
+ expect(data[0]).toBe(0);
852
+
853
+ const tokens = decode(data);
854
+
855
+ // 单调不减(按 line、再按 char 排序)
856
+ for (let i = 1; i < tokens.length; i++) {
857
+ const prev = tokens[i - 1];
858
+ const cur = tokens[i];
859
+ expect(cur.line).toBeGreaterThan(prev.line - 1);
860
+ if (cur.line === prev.line) expect(cur.char).toBeGreaterThan(prev.char);
861
+ }
862
+
863
+ const find = (line: number, char: number) => tokens.find((t) => t.line === line && t.char === char);
864
+
865
+ // 顶层 const:数值绑定 → variable;函数绑定(箭头)→ function
866
+ const answer = find(0, 6);
867
+ expect(answer).toMatchObject({
868
+ length: "answer".length,
869
+ typeIndex: SEMANTIC_TOKEN_TYPES.indexOf("variable"),
870
+ modifiers: 1, // declaration
871
+ });
872
+ const double = find(1, 6);
873
+ expect(double).toMatchObject({
874
+ length: "double".length,
875
+ typeIndex: SEMANTIC_TOKEN_TYPES.indexOf("function"),
876
+ });
877
+
878
+ // 函数声明名 + 参数
879
+ expect(find(2, 9)).toMatchObject({ length: 3, typeIndex: SEMANTIC_TOKEN_TYPES.indexOf("function") });
880
+ expect(find(2, 13)).toMatchObject({ length: 1, typeIndex: SEMANTIC_TOKEN_TYPES.indexOf("parameter") });
881
+ expect(find(2, 16)).toMatchObject({ length: 1, typeIndex: SEMANTIC_TOKEN_TYPES.indexOf("parameter") });
882
+ // 箭头函数参数
883
+ expect(find(1, 16)).toMatchObject({ length: 1, typeIndex: SEMANTIC_TOKEN_TYPES.indexOf("parameter") });
884
+
885
+ // 对象键:数据属性 → property;方法/函数值键 → method
886
+ expect(find(3, 17)).toMatchObject({ length: 4, typeIndex: SEMANTIC_TOKEN_TYPES.indexOf("property") });
887
+ expect(find(3, 28)).toMatchObject({ length: 3, typeIndex: SEMANTIC_TOKEN_TYPES.indexOf("method") });
888
+
889
+ // length 与标识符长度一致(全量断言,逐 token)
890
+ const idents: [number, number, string][] = [
891
+ [0, 6, "answer"], [1, 6, "double"], [1, 16, "n"],
892
+ [2, 9, "add"], [2, 13, "a"], [2, 16, "b"],
893
+ [3, 6, "config"], [3, 17, "host"], [3, 28, "run"],
894
+ ];
895
+ for (const [line, char, name] of idents) {
896
+ expect(find(line, char)?.length).toBe(name.length);
897
+ }
898
+
899
+ // 纯使用位不上色:L5 的 answer/double/config.host 引用无 token
900
+ expect(tokens.filter((t) => t.line === 4)).toEqual([]);
901
+ // 声明位恰 9 个 token,无多余(AST 其他标识符均未上色)
902
+ expect(tokens).toHaveLength(9);
903
+ });
904
+
905
+ it("colors a top-level const arrow binding as function via inference", () => {
906
+ // 推断驱动的证据:同样叫 double,绑定非函数时必须是 variable
907
+ const data = buildSemanticTokens("/test/sem-tok-var.js", "const double = 42;\ndouble;\n");
908
+ const tokens = decode(data);
909
+ expect(tokens).toHaveLength(1);
910
+ expect(tokens[0]).toMatchObject({
911
+ line: 0,
912
+ char: 6,
913
+ length: 6,
914
+ typeIndex: SEMANTIC_TOKEN_TYPES.indexOf("variable"),
915
+ });
916
+ });
917
+
918
+ it("returns empty tokens for unparseable source", () => {
919
+ expect(buildSemanticTokens("/test/broken.js", "const (")).toEqual([]);
920
+ });
921
+ });
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import {
6
+ clearValidationState,
7
+ handleNudoDepFileChanged,
8
+ nudoDepParents,
9
+ registerNudoImportDeps,
10
+ validateText,
11
+ type ValidateTextDeps,
12
+ } from "../validation.ts";
13
+ import { generalizeFromAst, getGeneralizeMemoSize, resetGeneralizeMemo } from "@nudojs/core";
14
+
15
+ describe("*.nudo.js watched-file → targeted eviction + parent revalidate", () => {
16
+ beforeEach(() => {
17
+ clearValidationState();
18
+ });
19
+
20
+ it("registerNudoImportDeps records parent → dep edges", () => {
21
+ const dir = mkdtempSync(join(tmpdir(), "nudo-dep-idx-"));
22
+ const parent = join(dir, "a.js");
23
+ const dep = join(dir, "shapes.nudo.js");
24
+ registerNudoImportDeps(
25
+ parent,
26
+ `/// @nudo:import { positive } from "./shapes.nudo.js"\nfunction f(x){return x;}\n`,
27
+ );
28
+ expect(nudoDepParents.get(resolve(dep).replace(/\\/g, "/"))).toContain(
29
+ resolve(parent).replace(/\\/g, "/"),
30
+ );
31
+ });
32
+
33
+ it("handleNudoDepFileChanged revalidates open parent and evicts memo deps", async () => {
34
+ const dir = mkdtempSync(join(tmpdir(), "nudo-dep-chg-"));
35
+ const parent = join(dir, "a.js");
36
+ const dep = join(dir, "shapes.nudo.js");
37
+ writeFileSync(dep, "export const positive = number().gt(0);\n");
38
+ const parentSrc = `/// @nudo:import { positive } from "./shapes.nudo.js"
39
+ /**
40
+ * @nudo:refine x positive
41
+ */
42
+ function needsPositive(x) {
43
+ return x;
44
+ }
45
+ needsPositive(-1);
46
+ `;
47
+ writeFileSync(parent, parentSrc);
48
+ // 模拟已验证会话:登记 @nudo:import 边
49
+ registerNudoImportDeps(parent, parentSrc);
50
+
51
+ // 建立 generalize 缓存(含 dep 路径索引)
52
+ const loadModule = (spec: string, fromFile: string) => {
53
+ if (spec.includes("shapes")) return "export const positive = number().gt(0);\n";
54
+ return undefined;
55
+ };
56
+ const g1 = generalizeFromAst("needsPositive", parentSrc, {
57
+ refine: { loadModule, fromFile: parent },
58
+ });
59
+ expect(g1).toBeDefined();
60
+ expect(getGeneralizeMemoSize()).toBeGreaterThan(0);
61
+
62
+ let revalidated = 0;
63
+ const sent = new Map<string, unknown[]>();
64
+ const openDocs = new Map([
65
+ [
66
+ resolve(parent).replace(/\\/g, "/"),
67
+ {
68
+ uri: `file://${parent}`,
69
+ version: 2,
70
+ getText: () => parentSrc,
71
+ },
72
+ ],
73
+ ]);
74
+ const deps: ValidateTextDeps = {
75
+ sendDiagnostics: (p) => {
76
+ sent.set(p.uri, p.diagnostics);
77
+ revalidated++;
78
+ },
79
+ getOpenDocumentByPath: (p) => openDocs.get(p.replace(/\\/g, "/")),
80
+ };
81
+
82
+ await handleNudoDepFileChanged(dep, deps);
83
+
84
+ // 定向逐出后,同参 generalize 得新实例
85
+ const g2 = generalizeFromAst("needsPositive", parentSrc, {
86
+ refine: { loadModule, fromFile: parent },
87
+ });
88
+ expect(g2).toBeDefined();
89
+ expect(g2).not.toBe(g1);
90
+ // 打开中的父文件被重检
91
+ expect(revalidated).toBeGreaterThan(0);
92
+ expect(sent.has(`file://${parent}`)).toBe(true);
93
+ });
94
+
95
+ it("does nothing when no parent depends on the path", async () => {
96
+ resetGeneralizeMemo();
97
+ const before = getGeneralizeMemoSize();
98
+ await handleNudoDepFileChanged("/nope/x.nudo.js", {
99
+ sendDiagnostics: () => {},
100
+ });
101
+ expect(getGeneralizeMemoSize()).toBe(before);
102
+ });
103
+ });
@@ -14,10 +14,17 @@
14
14
  */
15
15
  import { readFileSync } from "node:fs";
16
16
  import { resolve } from "node:path";
17
- import { analyzeFile, buildCaseDirective } from "@nudojs/service";
17
+ import {
18
+ analyzeFile,
19
+ buildCaseDirective,
20
+ getHoverAtPosition,
21
+ collectAbsInlays,
22
+ serializeInferJson,
23
+ } from "@nudojs/service";
18
24
  import { parse } from "@nudojs/parser";
19
- import { T, typeValueToString } from "@nudojs/core";
20
- import type { TypeValue } from "@nudojs/core";
25
+ import { T, typeValueToString, checkSource, serializeCheckJson, pTrue } from "@nudojs/core";
26
+ import type { TypeValue, CheckJson } from "@nudojs/core";
27
+ import { lspLoadModule } from "./validation.ts";
21
28
 
22
29
  export type TypeBinding = { name: string; type: string };
23
30
 
@@ -207,6 +214,160 @@ function analysisError(err: unknown): AgentToolResult {
207
214
  return textResult(`Error: ${(err as Error).message}`);
208
215
  }
209
216
 
217
+ export type CheckToolParams = {
218
+ file: string;
219
+ source?: string;
220
+ /** "json" → CheckJson only;缺省人类可读摘要 + JSON */
221
+ format?: "text" | "json";
222
+ /** 解析 @nudo:import 的相对 .nudo.js(测试可注入) */
223
+ loadModule?: (spec: string, fromFile: string) => string | undefined;
224
+ };
225
+
226
+ /**
227
+ * Agent 门禁工具:返回 CheckJson v1 契约(与 CLI --json 同构)。
228
+ */
229
+ export function checkTool(
230
+ params: CheckToolParams,
231
+ deps: AgentToolDeps = {},
232
+ ): AgentToolResult {
233
+ try {
234
+ const filePath = normalizeFilePath(params.file);
235
+ const source = params.source ?? readSource(filePath, deps);
236
+ const report = checkSource(filePath, source, pTrue, {
237
+ loadModule: params.loadModule ?? lspLoadModule,
238
+ fromFile: filePath,
239
+ });
240
+ const json = serializeCheckJson(report);
241
+ if (params.format === "json") {
242
+ return textResult(JSON.stringify(json, null, 2));
243
+ }
244
+ const lines: string[] = [
245
+ json.ok ? "nudo check OK" : "nudo check FAILED",
246
+ `${json.summary.errors} error · ${json.summary.warnings} warning · ${json.summary.functions} fn`,
247
+ ];
248
+ for (const i of json.issues) {
249
+ if (i.severity === "info") continue;
250
+ const loc = i.line != null ? ` L${i.line}` : "";
251
+ lines.push(`[${i.severity}]${loc} ${i.message}`);
252
+ if (i.actual) lines.push(` actual: ${i.actual}`);
253
+ if (i.expected) lines.push(` expected: ${i.expected}`);
254
+ }
255
+ lines.push("");
256
+ lines.push(JSON.stringify(json, null, 2));
257
+ return textResult(lines.join("\n"));
258
+ } catch (err) {
259
+ return analysisError(err);
260
+ }
261
+ }
262
+
263
+ export type HoverToolParams = {
264
+ file: string;
265
+ /** 1-based 行 */
266
+ line: number;
267
+ /** 0-based 列 */
268
+ column: number;
269
+ source?: string;
270
+ /** true 时同时返回该文件全部 Abs inlay */
271
+ includeInlays?: boolean;
272
+ /** 可选:*.nudo.js 加载(契约进 inlay) */
273
+ loadModule?: (spec: string, fromFile: string) => string | undefined;
274
+ };
275
+
276
+ /**
277
+ * Agent hover:无损 Abs(不经 TypeValue bridge)。
278
+ * 与 LSP hover 同一信息源。
279
+ */
280
+ export function hoverTool(
281
+ params: HoverToolParams,
282
+ deps: AgentToolDeps = {},
283
+ ): AgentToolResult {
284
+ try {
285
+ const filePath = normalizeFilePath(params.file);
286
+ const source = params.source ?? readSource(filePath, deps);
287
+ const hover = getHoverAtPosition(filePath, source, params.line, params.column);
288
+ const payload: Record<string, unknown> = {
289
+ file: filePath,
290
+ line: params.line,
291
+ column: params.column,
292
+ abs: hover?.abs ?? null,
293
+ absMultiline: hover?.absMultiline ?? null,
294
+ intension: hover?.intension ?? null,
295
+ /** 有损外延,仅对照 */
296
+ ext: hover?.typeText ?? null,
297
+ };
298
+ if (params.includeInlays) {
299
+ payload.inlays = collectAbsInlays(source, {
300
+ loadModule: params.loadModule ?? lspLoadModule,
301
+ fromFile: filePath,
302
+ });
303
+ }
304
+ return textResult(JSON.stringify(payload, null, 2));
305
+ } catch (err) {
306
+ return analysisError(err);
307
+ }
308
+ }
309
+
310
+ export type InferToolParams = {
311
+ file: string;
312
+ source?: string;
313
+ /** "json" → InferJson only;缺省摘要 + JSON */
314
+ format?: "text" | "json";
315
+ /** 只返回这些函数名(可选过滤) */
316
+ functions?: string[];
317
+ };
318
+
319
+ /**
320
+ * Agent infer:InferJson v1 契约(与 CLI infer --json 同构)。
321
+ * intension 携带无损 Abs;args/result 为 TypeValue 投影。
322
+ */
323
+ export function inferTool(
324
+ params: InferToolParams,
325
+ deps: AgentToolDeps = {},
326
+ ): AgentToolResult {
327
+ try {
328
+ const filePath = normalizeFilePath(params.file);
329
+ const source = params.source ?? readSource(filePath, deps);
330
+ const result = analyzeFile(filePath, source);
331
+ let json = serializeInferJson(result, filePath);
332
+ if (params.functions && params.functions.length > 0) {
333
+ const keep = new Set(params.functions);
334
+ const filtered = json.functions.filter((f) => keep.has(f.name));
335
+ let cases = 0;
336
+ for (const f of filtered) {
337
+ cases += (f as { cases?: unknown[] }).cases?.length ?? 0;
338
+ }
339
+ const summary = {
340
+ ...json.summary,
341
+ functions: filtered.length,
342
+ cases,
343
+ };
344
+ json = { ...json, functions: filtered, summary } as typeof json;
345
+ }
346
+ if (params.format === "json") {
347
+ return textResult(JSON.stringify(json, null, 2));
348
+ }
349
+ const lines: string[] = [
350
+ `nudo infer ${json.file}`,
351
+ `${json.summary.functions} fn · ${json.summary.cases} case · ${json.summary.diagnostics} diag`,
352
+ ];
353
+ for (const f of json.functions) {
354
+ lines.push("");
355
+ lines.push(`${f.name}${f.entryOnly ? " [entry-only]" : ""}`);
356
+ for (const c of f.cases) {
357
+ const args = c.args.join(", ");
358
+ lines.push(` ${c.name}: (${args}) => ${c.result}`);
359
+ if (c.intension?.abs) lines.push(` abs: ${c.intension.abs}`);
360
+ else if (c.intension?.display) lines.push(` intension: ${c.intension.display}`);
361
+ }
362
+ }
363
+ lines.push("");
364
+ lines.push(JSON.stringify(json, null, 2));
365
+ return textResult(lines.join("\n"));
366
+ } catch (err) {
367
+ return analysisError(err);
368
+ }
369
+ }
370
+
210
371
  export type WhatIfParams = {
211
372
  file: string;
212
373
  bindings?: TypeBinding[];
@@ -1,45 +1,9 @@
1
- export const TOKEN_TYPES = [
2
- "function",
3
- "variable",
4
- "parameter",
5
- "property",
6
- "type",
7
- "keyword",
8
- "string",
9
- "number",
10
- "comment",
11
- "decorator",
12
- ] as const;
13
-
14
- export const TOKEN_MODIFIERS = [
15
- "declaration",
16
- "readonly",
17
- "deprecated",
18
- "unreachable",
19
- ] as const;
20
-
21
- export type SemanticToken = {
22
- line: number;
23
- char: number;
24
- length: number;
25
- typeIndex: number;
26
- modifierBitmask: number;
27
- };
28
-
29
- export function encodeSemanticTokens(tokens: SemanticToken[]): number[] {
30
- const result: number[] = [];
31
- let prevLine = 0;
32
- let prevChar = 0;
33
-
34
- for (const token of tokens) {
35
- const deltaLine = token.line - prevLine;
36
- const deltaChar = deltaLine === 0 ? token.char - prevChar : token.char;
37
-
38
- result.push(deltaLine, deltaChar, token.length, token.typeIndex, token.modifierBitmask);
39
-
40
- prevLine = token.line;
41
- prevChar = token.char;
42
- }
43
-
44
- return result;
45
- }
1
+ // Semantic tokens 的图例与编码单一来源在 @nudojs/service(提取器
2
+ // buildSemanticTokens 与图例同处一模块,tokenType 索引不可能与图例漂移)。
3
+ // 此处仅按 server.ts 原引用名(TOKEN_TYPES/TOKEN_MODIFIERS)再导出。
4
+ export {
5
+ SEMANTIC_TOKEN_TYPES as TOKEN_TYPES,
6
+ SEMANTIC_TOKEN_MODIFIERS as TOKEN_MODIFIERS,
7
+ encodeSemanticTokens,
8
+ type SemanticToken,
9
+ } from "@nudojs/service";