codemode-lsp 0.1.3 → 0.2.1

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.
Files changed (3) hide show
  1. package/README.md +7 -4
  2. package/dist/index.js +147 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,11 +65,14 @@ returns `{ result, logs, changes }`:
65
65
  - **changes** — every file that hit disk, as `{ file, kind, diff }` with a
66
66
  unified diff against the pre-script content. Empty for read-only scripts.
67
67
 
68
- The API surface is 16 functions plus `getDiagnostics`: 8 read ops (`readFile`,
68
+ The API surface is 18 functions plus `getDiagnostics`: 10 read ops (`readFile`,
69
69
  `getSymbolBody`, `getSymbols`, `findSymbol`, `findReferences`,
70
- `goToDefinition`, `searchText`, `listFiles`) and 7 write ops (`renameSymbol`,
71
- `replaceSymbolBody`, `insertBeforeSymbol`, `insertAfterSymbol`,
72
- `deleteSymbol`, `writeFile`, `deleteFile`). Symbols are addressed by
70
+ `goToDefinition`, `incomingCalls`, `outgoingCalls`, `searchText`, `listFiles`)
71
+ and 7 write ops (`renameSymbol`, `replaceSymbolBody`, `insertBeforeSymbol`,
72
+ `insertAfterSymbol`, `deleteSymbol`, `writeFile`, `deleteFile`). The call
73
+ hierarchy ops return only true calls — attributed to the enclosing function,
74
+ resolved across modules — where `findReferences` mixes calls with imports and
75
+ re-exports. Symbols are addressed by
73
76
  slash-separated paths (`MyClass/myMethod`) discovered via `getSymbols`. The
74
77
  full type definitions are embedded in the tool description, generated straight
75
78
  from the source (`bun run generate:types`). If a client truncates the
package/dist/index.js CHANGED
@@ -39506,16 +39506,26 @@ ${prefix}has no matching children.` : "")}`);
39506
39506
  }
39507
39507
  return resolved;
39508
39508
  }
39509
- function containingSymbolPath(symbols, position) {
39509
+ var CALLABLE_KIND_NAMES = new Set(["function", "method", "constructor"]);
39510
+ function containingFunctionPath(symbols, position) {
39511
+ const chain = [];
39510
39512
  function visit(candidates) {
39511
39513
  for (const candidate of candidates) {
39512
39514
  if (!positionContains(candidate.range, position))
39513
39515
  continue;
39514
- return visit(candidate.children) ?? candidate;
39516
+ chain.push(candidate);
39517
+ visit(candidate.children);
39518
+ return;
39515
39519
  }
39516
- return;
39517
39520
  }
39518
- return visit(buildResolvedSymbolTree(addressableSymbols(symbols)))?.path;
39521
+ visit(buildResolvedSymbolTree(addressableSymbols(symbols)));
39522
+ for (let i = chain.length - 1;i >= 0; i -= 1) {
39523
+ const candidate = chain[i];
39524
+ if (candidate && CALLABLE_KIND_NAMES.has(candidate.kind)) {
39525
+ return candidate.path;
39526
+ }
39527
+ }
39528
+ return chain[chain.length - 1]?.path;
39519
39529
  }
39520
39530
  function symbolPathForRange(symbols, range) {
39521
39531
  function visit(candidates) {
@@ -39859,6 +39869,76 @@ class LspApi {
39859
39869
  }
39860
39870
  return this.locationToApiLocation(first);
39861
39871
  }
39872
+ async incomingCalls(file2, symbolPath) {
39873
+ const item = await this.callHierarchyItemFor("incomingCalls(file, symbolPath)", 'await lsp.incomingCalls("src/auth.ts", "AuthService/validate")', file2, symbolPath);
39874
+ const calls = await this.client.incomingCalls(item);
39875
+ const results = [];
39876
+ for (const call of calls) {
39877
+ const info = await this.callHierarchyItemToInfo(call.from, call.fromRanges, call.from.uri);
39878
+ if (info)
39879
+ results.push(info);
39880
+ }
39881
+ return results;
39882
+ }
39883
+ async outgoingCalls(file2, symbolPath) {
39884
+ const item = await this.callHierarchyItemFor("outgoingCalls(file, symbolPath)", 'await lsp.outgoingCalls("src/payments.ts", "recordPayment")', file2, symbolPath);
39885
+ const calls = await this.client.outgoingCalls(item);
39886
+ const results = [];
39887
+ for (const call of calls) {
39888
+ const info = await this.callHierarchyItemToInfo(call.to, call.fromRanges, item.uri);
39889
+ if (info)
39890
+ results.push(info);
39891
+ }
39892
+ return results;
39893
+ }
39894
+ async callHierarchyItemFor(signature, example, file2, symbolPath) {
39895
+ this.requireStrings(signature, example, { file: file2, symbolPath });
39896
+ const resolved = this.resolveWorkspacePath(file2);
39897
+ const symbol2 = resolveSymbolPath({
39898
+ file: resolved.relPath,
39899
+ symbolPath,
39900
+ symbols: await this.documentSymbols(resolved)
39901
+ });
39902
+ const items = await this.client.prepareCallHierarchy({
39903
+ uri: resolved.uri,
39904
+ position: symbol2.selectionRange.start
39905
+ });
39906
+ const item = items[0];
39907
+ if (!item) {
39908
+ throw new Error(`"${symbolPath}" in "${resolved.relPath}" has no call hierarchy — the symbol must be callable (a function, method, or constructor; its kind is "${symbol2.kind}"). Pick a function-like symbol from getSymbols("${resolved.relPath}").`);
39909
+ }
39910
+ return item;
39911
+ }
39912
+ async callHierarchyItemToInfo(item, fromRanges, rangesUri) {
39913
+ const itemPath = this.workspacePathFromUri(item.uri);
39914
+ if (!itemPath)
39915
+ return;
39916
+ const rangesPath = this.workspacePathFromUri(rangesUri);
39917
+ const rangesText = rangesPath ? this.readTextSafe(rangesPath) : "";
39918
+ let symbolPath;
39919
+ try {
39920
+ const symbols = await this.documentSymbols(itemPath);
39921
+ symbolPath = symbolPathForRange(symbols, item.selectionRange) ?? containingFunctionPath(symbols, item.selectionRange.start);
39922
+ } catch {
39923
+ symbolPath = undefined;
39924
+ }
39925
+ return {
39926
+ file: itemPath.relPath,
39927
+ symbolPath: symbolPath ?? "",
39928
+ name: item.name,
39929
+ kind: symbolKindName(item.kind),
39930
+ callSites: [
39931
+ ...new Map(fromRanges.map((range) => [
39932
+ `${range.start.line}:${range.start.character}`,
39933
+ {
39934
+ line: range.start.line + 1,
39935
+ column: range.start.character + 1,
39936
+ context: rangesPath ? lineContext(rangesText, range.start.line) : ""
39937
+ }
39938
+ ])).values()
39939
+ ]
39940
+ };
39941
+ }
39862
39942
  async searchText(pattern, glob) {
39863
39943
  this.requireStrings("searchText(pattern, glob?)", 'await lsp.searchText("new NotFoundError\\\\(", "src/**") — the pattern is a regex; escape metacharacters for literal text', { pattern });
39864
39944
  if (glob !== undefined && glob !== null && typeof glob !== "string") {
@@ -40203,7 +40283,7 @@ ${text}`
40203
40283
  async containingPathForLocation(workspacePath, location) {
40204
40284
  try {
40205
40285
  const symbols = await this.documentSymbols(workspacePath);
40206
- return containingSymbolPath(symbols, location.range.start);
40286
+ return containingFunctionPath(symbols, location.range.start);
40207
40287
  } catch {
40208
40288
  return;
40209
40289
  }
@@ -40397,6 +40477,21 @@ class LspClient {
40397
40477
  const result = await this.request("textDocument/definition", { textDocument: { uri: params.uri }, position: params.position });
40398
40478
  return normalizeDefinition(result);
40399
40479
  }
40480
+ async prepareCallHierarchy(params) {
40481
+ await this.ensureAlive();
40482
+ const result = await this.request("textDocument/prepareCallHierarchy", { textDocument: { uri: params.uri }, position: params.position });
40483
+ return result ?? [];
40484
+ }
40485
+ async incomingCalls(item) {
40486
+ await this.ensureAlive();
40487
+ const result = await this.request("callHierarchy/incomingCalls", { item });
40488
+ return result ?? [];
40489
+ }
40490
+ async outgoingCalls(item) {
40491
+ await this.ensureAlive();
40492
+ const result = await this.request("callHierarchy/outgoingCalls", { item });
40493
+ return result ?? [];
40494
+ }
40400
40495
  openTextDocument(abs) {
40401
40496
  const uri = pathToFileURL2(abs).href;
40402
40497
  if (this.openVersions.has(uri))
@@ -40606,6 +40701,7 @@ class LspClient {
40606
40701
  definition: { linkSupport: false },
40607
40702
  references: {},
40608
40703
  rename: { prepareSupport: true },
40704
+ callHierarchy: {},
40609
40705
  synchronization: { didSave: false, willSave: false },
40610
40706
  publishDiagnostics: {}
40611
40707
  },
@@ -46035,6 +46131,8 @@ var READ_OP_NAMES = [
46035
46131
  "findSymbol",
46036
46132
  "findReferences",
46037
46133
  "goToDefinition",
46134
+ "incomingCalls",
46135
+ "outgoingCalls",
46038
46136
  "searchText",
46039
46137
  "listFiles",
46040
46138
  "getDiagnostics"
@@ -46360,12 +46458,32 @@ interface Reference {
46360
46458
  column: number;
46361
46459
  /** The actual line of code containing the reference. */
46362
46460
  context: string;
46363
- /** Path of the symbol containing the reference (a reusable handle); "" at top level. */
46461
+ /** Path of the nearest enclosing function/method (a reusable handle); "" at top level. */
46364
46462
  symbolPath: string;
46365
46463
  /** Always false in v1 — the language server does not classify accesses. */
46366
46464
  isWriteAccess: boolean;
46367
46465
  }
46368
46466
 
46467
+ interface CallInfo {
46468
+ /** The other function: the caller (incomingCalls) or the callee (outgoingCalls). */
46469
+ file: string;
46470
+ /** Its symbol path — round-trips into other lsp.* calls together with \`file\`; "" when the call sits at module top level (no enclosing function). */
46471
+ symbolPath: string;
46472
+ name: string;
46473
+ kind: string;
46474
+ /** Call sites in the caller's body (so in \`file\` for incomingCalls, in the queried symbol's file for outgoingCalls). */
46475
+ callSites: CallSite[];
46476
+ }
46477
+
46478
+ interface CallSite {
46479
+ /** 1-based. */
46480
+ line: number;
46481
+ /** 1-based. */
46482
+ column: number;
46483
+ /** The line of code containing the call. */
46484
+ context: string;
46485
+ }
46486
+
46369
46487
  interface Location {
46370
46488
  file: string;
46371
46489
  /** 1-based. */
@@ -46424,6 +46542,10 @@ var LSP_READ_OP_SIGNATURES = ` /** File contents as a raw string (no line numbe
46424
46542
  findReferences(file: string, symbolPath: string): Promise<Reference[]>;
46425
46543
  /** Jump to the definition of a symbol DEFINED in \`file\` — imports/callees in a body are not addressable; resolve those names with findSymbol. */
46426
46544
  goToDefinition(file: string, symbolPath: string): Promise<Location>;
46545
+ /** Functions that CALL this symbol — true calls only (no imports/re-exports), each attributed to its enclosing function with exact call sites. */
46546
+ incomingCalls(file: string, symbolPath: string): Promise<CallInfo[]>;
46547
+ /** Functions this symbol's body CALLS, each resolved to its definition (follows imports across modules); workspace functions only — library calls are omitted. */
46548
+ outgoingCalls(file: string, symbolPath: string): Promise<CallInfo[]>;
46427
46549
  /** Regex search across project files — escape metacharacters for literal text; optional second arg is a glob string. */
46428
46550
  searchText(pattern: string, glob?: string): Promise<SearchResult[]>;
46429
46551
  /** Project files matching a glob; no glob = all files, a directory name means everything under it. */
@@ -46457,6 +46579,17 @@ for (const file of files) {
46457
46579
  outline.push({ file, symbols: symbols.map((s) => s.kind + " " + s.path) });
46458
46580
  }
46459
46581
  outline;`
46582
+ },
46583
+ {
46584
+ title: "Trace the call graph",
46585
+ writes: false,
46586
+ code: `// Who calls findUser, and what does findUser call?
46587
+ const callers = await lsp.incomingCalls("src/users.ts", "findUser");
46588
+ const callees = await lsp.outgoingCalls("src/users.ts", "findUser");
46589
+ ({
46590
+ calledBy: callers.map((c) => c.file + "::" + c.symbolPath),
46591
+ calls: callees.map((c) => c.file + "::" + c.symbolPath),
46592
+ });`
46460
46593
  },
46461
46594
  {
46462
46595
  title: "Batch refactor: migrate every caller",
@@ -46540,10 +46673,17 @@ script's last expression, JSON-serialized.
46540
46673
  the write ops.
46541
46674
  - \`searchText\` patterns are regexes — escape metacharacters for literal text:
46542
46675
  \`searchText("new NotFoundError\\\\(")\`.
46676
+ - To trace calls, use \`incomingCalls\`/\`outgoingCalls\` — they return only TRUE
46677
+ calls, attributed to the enclosing function. \`findReferences\` mixes calls
46678
+ with imports, re-exports, and type references.
46543
46679
  - \`goToDefinition\` only addresses symbols DEFINED in the given file — it cannot
46544
46680
  follow an imported or called name into another module. To resolve a name to
46545
46681
  its definition anywhere in the workspace, use \`findSymbol(name)\` and filter
46546
- for an exact \`name\` match (it also returns substring matches).
46682
+ for an exact \`name\` match (it also returns substring matches), or
46683
+ \`outgoingCalls\` on the containing function.
46684
+ - \`result\` is capped at 50,000 chars (logs at 10,000). Aggregate INSIDE the
46685
+ script — return counts, top-N lists, and compact summaries, never a raw
46686
+ inventory of files/symbols/references.
46547
46687
  - File paths are relative to the workspace root; anything outside it is
46548
46688
  rejected.
46549
46689
  - Diagnostics cover files touched this session only, never the whole project.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemode-lsp",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "MCP server exposing a single code-mode tool backed by LSP",
5
5
  "keywords": [
6
6
  "mcp",