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.
- package/README.md +7 -4
- package/dist/index.js +147 -7
- 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
|
|
68
|
+
The API surface is 18 functions plus `getDiagnostics`: 10 read ops (`readFile`,
|
|
69
69
|
`getSymbolBody`, `getSymbols`, `findSymbol`, `findReferences`,
|
|
70
|
-
`goToDefinition`, `searchText`, `listFiles`)
|
|
71
|
-
`
|
|
72
|
-
`deleteSymbol`, `writeFile`, `deleteFile`).
|
|
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
|
-
|
|
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
|
-
|
|
39516
|
+
chain.push(candidate);
|
|
39517
|
+
visit(candidate.children);
|
|
39518
|
+
return;
|
|
39515
39519
|
}
|
|
39516
|
-
return;
|
|
39517
39520
|
}
|
|
39518
|
-
|
|
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
|
|
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
|
|
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.
|