@akanjs/language 3.1.2-rc.3 → 3.1.2-rc.4

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/README.ko.md CHANGED
@@ -40,3 +40,44 @@ index.renameAt(fileName, offset, "inFeed", readText);
40
40
  키에서 만들어진 이름 전부와 모듈 dictionary의 키와 함께 바꾸는 편집을 돌려줍니다.
41
41
 
42
42
  ES module과 CommonJS로 배포하며, 런타임 의존성은 없습니다.
43
+
44
+ ## 언어 서버
45
+
46
+ `akan-language-server --stdio`는 같은 색인을 LSP로 냅니다. Akan 확장이 있는 VS Code와 Cursor 말고 다른 에디터를
47
+ 위한 것입니다. TypeScript 서버 옆에 띄우면 에디터가 두 서버의 답을 합쳐 줍니다. 정의, hover, 참조를 답합니다.
48
+ 에디터의 루트나 그 위에서 가장 가까운, `apps/*`나 `libs/*`에 `akan.config.ts`가 있는 폴더를 색인합니다. 에디터가
49
+ watched-file 등록을 지원하면 디스크에서 바뀐 파일도 다시 읽습니다.
50
+
51
+ 설치 절처럼 워크스페이스에 추가해서 bin이 `node_modules/.bin`에 들어가게 하세요. 그러면 `bunx --no-install`이 그
52
+ 사본을 실행하고, 레지스트리에서 받아 오지 않습니다.
53
+
54
+ Neovim 0.11 이상:
55
+
56
+ ```lua
57
+ vim.lsp.config("akan", {
58
+ cmd = function(dispatchers, config)
59
+ return vim.lsp.rpc.start({ "bunx", "--no-install", "akan-language-server", "--stdio" }, dispatchers, {
60
+ cwd = config.root_dir,
61
+ })
62
+ end,
63
+ filetypes = { "typescript", "typescriptreact" },
64
+ root_markers = { "bun.lock" },
65
+ })
66
+ vim.lsp.enable("akan")
67
+ ```
68
+
69
+ Helix의 `languages.toml`:
70
+
71
+ ```toml
72
+ [language-server.akan]
73
+ command = "bunx"
74
+ args = ["--no-install", "akan-language-server", "--stdio"]
75
+
76
+ [[language]]
77
+ name = "typescript"
78
+ language-servers = ["typescript-language-server", "akan"]
79
+
80
+ [[language]]
81
+ name = "tsx"
82
+ language-servers = ["typescript-language-server", "akan"]
83
+ ```
package/README.md CHANGED
@@ -40,3 +40,45 @@ name resolves within its own app and every lib, never another app. `renameAt` re
40
40
  query key together with every name derived from it and its key in the module dictionary.
41
41
 
42
42
  The package ships as an ES module and as CommonJS, with no runtime dependency.
43
+
44
+ ## Language server
45
+
46
+ `akan-language-server --stdio` serves the same index over LSP. It is for editors other than VS Code and Cursor,
47
+ which have the Akan extension. Run it next to TypeScript's server, and the editor merges the two servers' answers. It
48
+ answers definition, hover and references. It indexes the nearest folder at or above the editor's root whose `apps/*`
49
+ or `libs/*` holds an `akan.config.ts`. It rereads files changed on disk when the editor supports watched-file
50
+ registration.
51
+
52
+ Add it to the workspace, as in Install, so the bin lands in `node_modules/.bin`. `bunx --no-install` then runs that
53
+ copy and never fetches one from the registry.
54
+
55
+ Neovim 0.11 or later:
56
+
57
+ ```lua
58
+ vim.lsp.config("akan", {
59
+ cmd = function(dispatchers, config)
60
+ return vim.lsp.rpc.start({ "bunx", "--no-install", "akan-language-server", "--stdio" }, dispatchers, {
61
+ cwd = config.root_dir,
62
+ })
63
+ end,
64
+ filetypes = { "typescript", "typescriptreact" },
65
+ root_markers = { "bun.lock" },
66
+ })
67
+ vim.lsp.enable("akan")
68
+ ```
69
+
70
+ Helix, in `languages.toml`:
71
+
72
+ ```toml
73
+ [language-server.akan]
74
+ command = "bunx"
75
+ args = ["--no-install", "akan-language-server", "--stdio"]
76
+
77
+ [[language]]
78
+ name = "typescript"
79
+ language-servers = ["typescript-language-server", "akan"]
80
+
81
+ [[language]]
82
+ name = "tsx"
83
+ language-servers = ["typescript-language-server", "akan"]
84
+ ```
package/akanGraph.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface AkanGraphSource {
12
12
  resolve: (fileName: string, identifier: TS.Identifier) => AkanTarget[];
13
13
  relative: (fileName: string) => string;
14
14
  ownerOf: (fileName: string) => string | null;
15
+ digests?: boolean;
15
16
  }
16
17
  export declare class GraphBuilder {
17
18
  #private;
@@ -0,0 +1,10 @@
1
+ import type { AkanGraph, AkanGraphChange } from "akanjs/common/akanGraph.ts";
2
+ export interface AkanGraphChanges extends AkanGraph {
3
+ summary: {
4
+ [change in AkanGraphChange]: number;
5
+ };
6
+ }
7
+ export declare class AkanGraphDiff {
8
+ #private;
9
+ static between(base: AkanGraph, head: AkanGraph): AkanGraphChanges;
10
+ }
package/akanIndex.d.ts CHANGED
@@ -33,7 +33,9 @@ export declare class AkanIndex {
33
33
  sourceKeysOf(givenName: string): AkanSourceKey[];
34
34
  sourceKeyAt(givenName: string, offset: number): AkanSourceKey | null;
35
35
  referencesAt(givenName: string, offset: number, readText: (fileName: string) => string | undefined): AkanSpan[];
36
- graph(readText: (fileName: string) => string | undefined): AkanGraph;
36
+ graph(readText: (fileName: string) => string | undefined, { digests }?: {
37
+ digests?: boolean | undefined;
38
+ }): AkanGraph;
37
39
  renameTargetAt(givenName: string, offset: number): {
38
40
  family: AkanFamily;
39
41
  sourceKey: string;
package/index.cjs CHANGED
@@ -39,6 +39,7 @@ var __export = (target, all) => {
39
39
  // pkgs/@akanjs/language/index.ts
40
40
  var exports_language = {};
41
41
  __export(exports_language, {
42
+ AkanGraphDiff: () => AkanGraphDiff,
42
43
  AkanIndex: () => AkanIndex,
43
44
  createAst: () => createAst,
44
45
  createFactReader: () => createFactReader,
@@ -46,6 +47,79 @@ __export(exports_language, {
46
47
  });
47
48
  module.exports = __toCommonJS(exports_language);
48
49
 
50
+ // pkgs/@akanjs/language/akanGraphDiff.ts
51
+ var edgeKeyOf = ({ from, to, kind, label }) => `${from}
52
+ ${to}
53
+ ${kind}
54
+ ${label ?? ""}`;
55
+ var shapeOf = ({ type, array, optional, declaredIn }) => `${type ?? ""}${array ? "[]" : ""}${optional ? "?" : ""} ${declaredIn}`;
56
+ var isEdited = (before, after) => before.digest !== undefined && after.digest !== undefined && before.digest !== after.digest;
57
+ var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
58
+
59
+ class AkanGraphDiff {
60
+ static between(base, head) {
61
+ const baseNodes = new Map(base.nodes.map((node) => [node.id, node]));
62
+ const headNodes = new Map(head.nodes.map((node) => [node.id, node]));
63
+ const marked = new Map;
64
+ for (const node of head.nodes) {
65
+ const before = baseNodes.get(node.id);
66
+ const next = before ? AkanGraphDiff.#compare(before, node) : AkanGraphDiff.#mark(node, "added");
67
+ if (next)
68
+ marked.set(node.id, next);
69
+ }
70
+ for (const node of base.nodes)
71
+ if (!headNodes.has(node.id))
72
+ marked.set(node.id, AkanGraphDiff.#mark(node, "removed"));
73
+ const baseKeys = new Set(base.edges.map(edgeKeyOf));
74
+ const headKeys = new Set(head.edges.map(edgeKeyOf));
75
+ const edges = [
76
+ ...head.edges.flatMap((edge) => !baseKeys.has(edgeKeyOf(edge)) ? [{ ...edge, change: "added" }] : marked.has(edge.from) || marked.has(edge.to) ? [edge] : []),
77
+ ...base.edges.filter((edge) => !headKeys.has(edgeKeyOf(edge))).map((edge) => ({ ...edge, change: "removed" }))
78
+ ];
79
+ const context = edges.flatMap((edge) => [edge.from, edge.to]).filter((id) => !marked.has(id)).flatMap((id) => {
80
+ const node = headNodes.get(id) ?? baseNodes.get(id);
81
+ return node ? [node] : [];
82
+ });
83
+ const nodes = [...new Map([...marked.values(), ...context].map((node) => [node.id, node])).values()].map(AkanGraphDiff.#withoutDigest).sort(byId);
84
+ const summary = { added: 0, removed: 0, changed: 0 };
85
+ for (const node of marked.values())
86
+ if (node.change)
87
+ summary[node.change] += 1;
88
+ return { version: 1, nodes, edges, summary };
89
+ }
90
+ static #mark(node, change) {
91
+ return node.fields ? { ...node, change, fields: node.fields.map((field) => ({ ...field, change })) } : { ...node, change };
92
+ }
93
+ static #compare(before, after) {
94
+ const fields = AkanGraphDiff.#fieldsBetween(before.fields, after.fields);
95
+ const isChanged = isEdited(before, after) || before.label !== after.label || before.detail.join(`
96
+ `) !== after.detail.join(`
97
+ `) || (before.values ?? []).join(`
98
+ `) !== (after.values ?? []).join(`
99
+ `) || !!fields?.some((field) => field.change);
100
+ if (!isChanged)
101
+ return null;
102
+ return fields ? { ...after, change: "changed", fields } : { ...after, change: "changed" };
103
+ }
104
+ static #fieldsBetween(before, after) {
105
+ if (!before && !after)
106
+ return;
107
+ const previous = new Map((before ?? []).map((field) => [field.name, field]));
108
+ const names = new Set((after ?? []).map((field) => field.name));
109
+ return [
110
+ ...(after ?? []).map((field) => {
111
+ const old = previous.get(field.name);
112
+ if (!old)
113
+ return { ...field, change: "added" };
114
+ return shapeOf(old) === shapeOf(field) && !isEdited(old, field) ? field : { ...field, change: "changed" };
115
+ }),
116
+ ...(before ?? []).filter((field) => !names.has(field.name)).map((field) => ({ ...field, change: "removed" }))
117
+ ];
118
+ }
119
+ static #withoutDigest({ digest: _digest, ...node }) {
120
+ return node.fields ? { ...node, fields: node.fields.map(({ digest: _field, ...field }) => field) } : node;
121
+ }
122
+ }
49
123
  // pkgs/@akanjs/language/akanGraph.ts
50
124
  var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
51
125
  var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
@@ -112,7 +186,7 @@ class GraphBuilder {
112
186
  const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
113
187
  this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
114
188
  for (const query of filter.queries)
115
- this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span);
189
+ this.#digest(this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span), this.#source.readText(file.fileName), query.extent);
116
190
  }
117
191
  for (const slice of file.slices) {
118
192
  const refName = slice.model;
@@ -189,6 +263,7 @@ class GraphBuilder {
189
263
  #addMember(kind, owner, refName, member, label, fileName, detail = []) {
190
264
  const id = this.#key(kind, owner, refName, member.name);
191
265
  const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
266
+ this.#digest(node, this.#source.readText(fileName), member.extent);
192
267
  this.#contexts.set(fileName, [
193
268
  ...this.#contexts.get(fileName) ?? [],
194
269
  { id, kind, owner, module: refName, extent: member.extent }
@@ -197,6 +272,22 @@ class GraphBuilder {
197
272
  this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
198
273
  return node;
199
274
  }
275
+ #hashOf(text, { start, end }) {
276
+ if (!this.#source.digests || text === undefined)
277
+ return null;
278
+ let hash = 2166136261;
279
+ for (let idx = start;idx < end; idx++) {
280
+ const code = text.charCodeAt(idx);
281
+ if (code !== 13)
282
+ hash = Math.imul(hash ^ code, 16777619);
283
+ }
284
+ return (hash >>> 0).toString(16).padStart(8, "0");
285
+ }
286
+ #digest(node, text, extent) {
287
+ const digest = node.digest ? null : this.#hashOf(text, extent);
288
+ if (digest)
289
+ node.digest = digest;
290
+ }
200
291
  #addEdge(from, to, kind, label, at) {
201
292
  if (from === to)
202
293
  return;
@@ -220,7 +311,8 @@ ${label ?? ""}`;
220
311
  this.#addEdge(node.id, id, "guard", role, node.at);
221
312
  }
222
313
  #moduleDirOf(fileName) {
223
- return this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
314
+ const dir = this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
315
+ return dir?.startsWith("_") && !dir.startsWith("__") ? dir.slice(1) : dir;
224
316
  }
225
317
  #fieldsOf(constant, scope) {
226
318
  const fields = new Map;
@@ -234,17 +326,21 @@ ${label ?? ""}`;
234
326
  if (parentClass)
235
327
  visit(parentClass);
236
328
  }
237
- for (const field of current.fields)
238
- if (!fields.has(field.name))
239
- fields.set(field.name, {
240
- name: field.name,
241
- type: field.type,
242
- array: field.array,
243
- optional: field.optional,
244
- ref: null,
245
- declaredIn: current.name,
246
- at: this.#location(field.span)
247
- });
329
+ for (const field of current.fields) {
330
+ if (fields.has(field.name))
331
+ continue;
332
+ const digest = this.#hashOf(this.#source.readText(field.span.fileName), field.extent);
333
+ fields.set(field.name, {
334
+ name: field.name,
335
+ type: field.type,
336
+ array: field.array,
337
+ optional: field.optional,
338
+ ref: null,
339
+ declaredIn: current.name,
340
+ at: this.#location(field.span),
341
+ ...digest ? { digest } : {}
342
+ });
343
+ }
248
344
  };
249
345
  visit(constant);
250
346
  return [...fields.values()];
@@ -307,8 +403,10 @@ st
307
403
  const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
308
404
  if (!targets.length)
309
405
  return;
310
- if (context.pending && !this.#nodes.has(context.id))
406
+ if (context.pending && !this.#nodes.has(context.id)) {
311
407
  this.#nodes.set(context.id, context.pending);
408
+ this.#digest(context.pending, source.text, context.extent);
409
+ }
312
410
  const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
313
411
  const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
314
412
  const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
@@ -503,7 +601,7 @@ var createFactReader = (ts, ast) => {
503
601
  const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
504
602
  const name = ast.propertyName(property);
505
603
  const node = keyName(property);
506
- return name && node ? [{ name, span: spanOf(source, node) }] : [];
604
+ return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
507
605
  });
508
606
  const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
509
607
  if (!ts.isPropertyAssignment(property))
@@ -578,7 +676,13 @@ var createFactReader = (ts, ast) => {
578
676
  return [];
579
677
  const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
580
678
  return [
581
- { name, span: spanOf(source, node), ...typeOf(root?.arguments[0]), optional: methods.includes("optional") }
679
+ {
680
+ name,
681
+ span: spanOf(source, node),
682
+ extent: extentOf(source, property),
683
+ ...typeOf(root?.arguments[0]),
684
+ optional: methods.includes("optional")
685
+ }
582
686
  ];
583
687
  });
584
688
  const constantOf = (source, name, call) => {
@@ -1196,8 +1300,9 @@ ${[...new Set(text.match(wordPattern))].join(`
1196
1300
  referencesAt(givenName, offset, readText) {
1197
1301
  return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
1198
1302
  }
1199
- graph(readText) {
1303
+ graph(readText, { digests = false } = {}) {
1200
1304
  return new GraphBuilder({
1305
+ digests,
1201
1306
  ts: this.#ts,
1202
1307
  facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
1203
1308
  files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export type { AkanGraph, AkanGraphEdge, AkanGraphEdgeKind, AkanGraphField, AkanGraphLocation, AkanGraphNode, AkanGraphNodeKind, } from "akanjs/common/akanGraph.ts";
1
+ export type { AkanChangedFile, AkanChangeMap, AkanGraph, AkanGraphChange, AkanGraphEdge, AkanGraphEdgeKind, AkanGraphField, AkanGraphLocation, AkanGraphNode, AkanGraphNodeKind, } from "akanjs/common/akanGraph.ts";
2
+ export { type AkanGraphChanges, AkanGraphDiff } from "./akanGraphDiff";
2
3
  export { type AkanHover, AkanIndex, type AkanIndexOptions, type AkanRenameEdit, type AkanSourceKey } from "./akanIndex";
3
4
  export { type AkanAst, createAst, type Ts } from "./ast";
4
5
  export { type AkanFactReader, type AkanSpan, type ConstantClassFact, createFactReader, type EndpointClassFact, type EndpointKeyFact, type EnumClassFact, type Extent, type FieldFact, type FileFacts, type FilterClassFact, type MemberFact, type ModuleClassFact, type NamedFact, ownerOf, type ServiceClassFact, type SliceClassFact, type SliceKeyFact, type StoreClassFact, } from "./moduleFacts";
package/index.js CHANGED
@@ -1,3 +1,76 @@
1
+ // pkgs/@akanjs/language/akanGraphDiff.ts
2
+ var edgeKeyOf = ({ from, to, kind, label }) => `${from}
3
+ ${to}
4
+ ${kind}
5
+ ${label ?? ""}`;
6
+ var shapeOf = ({ type, array, optional, declaredIn }) => `${type ?? ""}${array ? "[]" : ""}${optional ? "?" : ""} ${declaredIn}`;
7
+ var isEdited = (before, after) => before.digest !== undefined && after.digest !== undefined && before.digest !== after.digest;
8
+ var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
9
+
10
+ class AkanGraphDiff {
11
+ static between(base, head) {
12
+ const baseNodes = new Map(base.nodes.map((node) => [node.id, node]));
13
+ const headNodes = new Map(head.nodes.map((node) => [node.id, node]));
14
+ const marked = new Map;
15
+ for (const node of head.nodes) {
16
+ const before = baseNodes.get(node.id);
17
+ const next = before ? AkanGraphDiff.#compare(before, node) : AkanGraphDiff.#mark(node, "added");
18
+ if (next)
19
+ marked.set(node.id, next);
20
+ }
21
+ for (const node of base.nodes)
22
+ if (!headNodes.has(node.id))
23
+ marked.set(node.id, AkanGraphDiff.#mark(node, "removed"));
24
+ const baseKeys = new Set(base.edges.map(edgeKeyOf));
25
+ const headKeys = new Set(head.edges.map(edgeKeyOf));
26
+ const edges = [
27
+ ...head.edges.flatMap((edge) => !baseKeys.has(edgeKeyOf(edge)) ? [{ ...edge, change: "added" }] : marked.has(edge.from) || marked.has(edge.to) ? [edge] : []),
28
+ ...base.edges.filter((edge) => !headKeys.has(edgeKeyOf(edge))).map((edge) => ({ ...edge, change: "removed" }))
29
+ ];
30
+ const context = edges.flatMap((edge) => [edge.from, edge.to]).filter((id) => !marked.has(id)).flatMap((id) => {
31
+ const node = headNodes.get(id) ?? baseNodes.get(id);
32
+ return node ? [node] : [];
33
+ });
34
+ const nodes = [...new Map([...marked.values(), ...context].map((node) => [node.id, node])).values()].map(AkanGraphDiff.#withoutDigest).sort(byId);
35
+ const summary = { added: 0, removed: 0, changed: 0 };
36
+ for (const node of marked.values())
37
+ if (node.change)
38
+ summary[node.change] += 1;
39
+ return { version: 1, nodes, edges, summary };
40
+ }
41
+ static #mark(node, change) {
42
+ return node.fields ? { ...node, change, fields: node.fields.map((field) => ({ ...field, change })) } : { ...node, change };
43
+ }
44
+ static #compare(before, after) {
45
+ const fields = AkanGraphDiff.#fieldsBetween(before.fields, after.fields);
46
+ const isChanged = isEdited(before, after) || before.label !== after.label || before.detail.join(`
47
+ `) !== after.detail.join(`
48
+ `) || (before.values ?? []).join(`
49
+ `) !== (after.values ?? []).join(`
50
+ `) || !!fields?.some((field) => field.change);
51
+ if (!isChanged)
52
+ return null;
53
+ return fields ? { ...after, change: "changed", fields } : { ...after, change: "changed" };
54
+ }
55
+ static #fieldsBetween(before, after) {
56
+ if (!before && !after)
57
+ return;
58
+ const previous = new Map((before ?? []).map((field) => [field.name, field]));
59
+ const names = new Set((after ?? []).map((field) => field.name));
60
+ return [
61
+ ...(after ?? []).map((field) => {
62
+ const old = previous.get(field.name);
63
+ if (!old)
64
+ return { ...field, change: "added" };
65
+ return shapeOf(old) === shapeOf(field) && !isEdited(old, field) ? field : { ...field, change: "changed" };
66
+ }),
67
+ ...(before ?? []).filter((field) => !names.has(field.name)).map((field) => ({ ...field, change: "removed" }))
68
+ ];
69
+ }
70
+ static #withoutDigest({ digest: _digest, ...node }) {
71
+ return node.fields ? { ...node, fields: node.fields.map(({ digest: _field, ...field }) => field) } : node;
72
+ }
73
+ }
1
74
  // pkgs/@akanjs/language/akanGraph.ts
2
75
  var capitalize = (text) => text.charAt(0).toUpperCase() + text.slice(1);
3
76
  var lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
@@ -64,7 +137,7 @@ class GraphBuilder {
64
137
  const refName = lowerFirst(filter.name.slice(0, -"Filter".length));
65
138
  this.#addNode(this.#key("query", owner, refName), "query", owner, refName, "", filter.span);
66
139
  for (const query of filter.queries)
67
- this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span);
140
+ this.#digest(this.#addNode(this.#key("query", owner, refName, query.name), "query", owner, refName, query.name, query.span), this.#source.readText(file.fileName), query.extent);
68
141
  }
69
142
  for (const slice of file.slices) {
70
143
  const refName = slice.model;
@@ -141,6 +214,7 @@ class GraphBuilder {
141
214
  #addMember(kind, owner, refName, member, label, fileName, detail = []) {
142
215
  const id = this.#key(kind, owner, refName, member.name);
143
216
  const node = this.#addNode(id, kind, owner, refName, member.name, member.span, detail, label);
217
+ this.#digest(node, this.#source.readText(fileName), member.extent);
144
218
  this.#contexts.set(fileName, [
145
219
  ...this.#contexts.get(fileName) ?? [],
146
220
  { id, kind, owner, module: refName, extent: member.extent }
@@ -149,6 +223,22 @@ class GraphBuilder {
149
223
  this.#members.set(byName, [...this.#members.get(byName) ?? [], node]);
150
224
  return node;
151
225
  }
226
+ #hashOf(text, { start, end }) {
227
+ if (!this.#source.digests || text === undefined)
228
+ return null;
229
+ let hash = 2166136261;
230
+ for (let idx = start;idx < end; idx++) {
231
+ const code = text.charCodeAt(idx);
232
+ if (code !== 13)
233
+ hash = Math.imul(hash ^ code, 16777619);
234
+ }
235
+ return (hash >>> 0).toString(16).padStart(8, "0");
236
+ }
237
+ #digest(node, text, extent) {
238
+ const digest = node.digest ? null : this.#hashOf(text, extent);
239
+ if (digest)
240
+ node.digest = digest;
241
+ }
152
242
  #addEdge(from, to, kind, label, at) {
153
243
  if (from === to)
154
244
  return;
@@ -172,7 +262,8 @@ ${label ?? ""}`;
172
262
  this.#addEdge(node.id, id, "guard", role, node.at);
173
263
  }
174
264
  #moduleDirOf(fileName) {
175
- return this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
265
+ const dir = this.#source.relative(fileName).match(moduleDirPattern)?.[1] ?? null;
266
+ return dir?.startsWith("_") && !dir.startsWith("__") ? dir.slice(1) : dir;
176
267
  }
177
268
  #fieldsOf(constant, scope) {
178
269
  const fields = new Map;
@@ -186,17 +277,21 @@ ${label ?? ""}`;
186
277
  if (parentClass)
187
278
  visit(parentClass);
188
279
  }
189
- for (const field of current.fields)
190
- if (!fields.has(field.name))
191
- fields.set(field.name, {
192
- name: field.name,
193
- type: field.type,
194
- array: field.array,
195
- optional: field.optional,
196
- ref: null,
197
- declaredIn: current.name,
198
- at: this.#location(field.span)
199
- });
280
+ for (const field of current.fields) {
281
+ if (fields.has(field.name))
282
+ continue;
283
+ const digest = this.#hashOf(this.#source.readText(field.span.fileName), field.extent);
284
+ fields.set(field.name, {
285
+ name: field.name,
286
+ type: field.type,
287
+ array: field.array,
288
+ optional: field.optional,
289
+ ref: null,
290
+ declaredIn: current.name,
291
+ at: this.#location(field.span),
292
+ ...digest ? { digest } : {}
293
+ });
294
+ }
200
295
  };
201
296
  visit(constant);
202
297
  return [...fields.values()];
@@ -259,8 +354,10 @@ st
259
354
  const targets = resolved.length ? resolved : this.#plainTargetsOf(chain, identifier.text, context, owner);
260
355
  if (!targets.length)
261
356
  return;
262
- if (context.pending && !this.#nodes.has(context.id))
357
+ if (context.pending && !this.#nodes.has(context.id)) {
263
358
  this.#nodes.set(context.id, context.pending);
359
+ this.#digest(context.pending, source.text, context.extent);
360
+ }
264
361
  const own = targets.filter((id) => this.#nodes.get(id)?.owner === owner);
265
362
  const kind = context.kind === "component" ? "uses" : chain?.[0] === "fetch" ? "fetches" : "calls";
266
363
  const position = source.getLineAndCharacterOfPosition(identifier.getStart(source));
@@ -455,7 +552,7 @@ var createFactReader = (ts, ast) => {
455
552
  const objectKeys = (source, object) => (object?.properties ?? []).flatMap((property) => {
456
553
  const name = ast.propertyName(property);
457
554
  const node = keyName(property);
458
- return name && node ? [{ name, span: spanOf(source, node) }] : [];
555
+ return name && node ? [{ name, span: spanOf(source, node), extent: extentOf(source, property) }] : [];
459
556
  });
460
557
  const sliceKeysOf = (source, builder) => (returnedObject(builder)?.properties ?? []).flatMap((property) => {
461
558
  if (!ts.isPropertyAssignment(property))
@@ -530,7 +627,13 @@ var createFactReader = (ts, ast) => {
530
627
  return [];
531
628
  const { root, methods } = ts.isPropertyAssignment(property) ? chainOf(property.initializer) : { root: null, methods: [] };
532
629
  return [
533
- { name, span: spanOf(source, node), ...typeOf(root?.arguments[0]), optional: methods.includes("optional") }
630
+ {
631
+ name,
632
+ span: spanOf(source, node),
633
+ extent: extentOf(source, property),
634
+ ...typeOf(root?.arguments[0]),
635
+ optional: methods.includes("optional")
636
+ }
534
637
  ];
535
638
  });
536
639
  const constantOf = (source, name, call) => {
@@ -1148,8 +1251,9 @@ ${[...new Set(text.match(wordPattern))].join(`
1148
1251
  referencesAt(givenName, offset, readText) {
1149
1252
  return this.#usesOf(this.#sourceEntriesAt(pathOf(givenName), offset), readText).map((use) => use.span);
1150
1253
  }
1151
- graph(readText) {
1254
+ graph(readText, { digests = false } = {}) {
1152
1255
  return new GraphBuilder({
1256
+ digests,
1153
1257
  ts: this.#ts,
1154
1258
  facts: [...this.#facts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, facts]) => facts),
1155
1259
  files: [...this.#fileWords].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
@@ -1410,6 +1514,7 @@ ${dictionary.className}
1410
1514
  }
1411
1515
  }
1412
1516
  export {
1517
+ AkanGraphDiff,
1413
1518
  AkanIndex,
1414
1519
  createAst,
1415
1520
  createFactReader,
package/moduleFacts.d.ts CHANGED
@@ -31,6 +31,7 @@ export interface EndpointKeyFact extends MemberFact {
31
31
  argCount: number;
32
32
  }
33
33
  export interface FieldFact extends NamedFact {
34
+ extent: Extent;
34
35
  type: string | null;
35
36
  array: boolean;
36
37
  optional: boolean;
@@ -43,7 +44,7 @@ export interface EnumClassFact extends NamedFact {
43
44
  values: string[];
44
45
  }
45
46
  export interface FilterClassFact extends NamedFact {
46
- queries: NamedFact[];
47
+ queries: MemberFact[];
47
48
  }
48
49
  export interface ModuleClassFact extends NamedFact {
49
50
  model: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/language",
3
- "version": "3.1.2-rc.3",
3
+ "version": "3.1.2-rc.4",
4
4
  "description": "Editor-independent Akan.js language core: resolves derived names (slices, queries, CRUD, form setters) back to the source key that produced them.",
5
5
  "sourceType": "module",
6
6
  "type": "module",
@@ -26,6 +26,9 @@
26
26
  "module": "./index.js",
27
27
  "types": "./index.d.ts",
28
28
  "sideEffects": false,
29
+ "bin": {
30
+ "akan-language-server": "./server.js"
31
+ },
29
32
  "exports": {
30
33
  ".": {
31
34
  "types": "./index.d.ts",