pi-lens 3.8.21 → 3.8.23
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 +28 -0
- package/README.md +2 -0
- package/clients/dispatch/dispatcher.ts +75 -91
- package/clients/dispatch/fact-provider-types.ts +22 -0
- package/clients/dispatch/fact-rule-runner.ts +22 -0
- package/clients/dispatch/fact-runner.ts +28 -0
- package/clients/dispatch/fact-scheduler.ts +78 -0
- package/clients/dispatch/fact-store.ts +67 -0
- package/clients/dispatch/facts/comment-facts.ts +59 -0
- package/clients/dispatch/facts/file-content.ts +20 -0
- package/clients/dispatch/facts/function-facts.ts +177 -0
- package/clients/dispatch/facts/try-catch-facts.ts +80 -0
- package/clients/dispatch/integration.ts +130 -24
- package/clients/dispatch/priorities.ts +22 -0
- package/clients/dispatch/rules/async-noise.ts +43 -0
- package/clients/dispatch/rules/error-obscuring.ts +40 -0
- package/clients/dispatch/rules/error-swallowing.ts +35 -0
- package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
- package/clients/dispatch/rules/placeholder-comments.ts +47 -0
- package/clients/dispatch/runners/architect.ts +2 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
- package/clients/dispatch/runners/biome-check.ts +40 -8
- package/clients/dispatch/runners/biome.ts +2 -1
- package/clients/dispatch/runners/eslint.ts +34 -6
- package/clients/dispatch/runners/go-vet.ts +2 -1
- package/clients/dispatch/runners/golangci-lint.ts +2 -1
- package/clients/dispatch/runners/index.ts +29 -27
- package/clients/dispatch/runners/lsp.ts +60 -4
- package/clients/dispatch/runners/oxlint.ts +2 -1
- package/clients/dispatch/runners/pyright.ts +2 -1
- package/clients/dispatch/runners/python-slop.ts +2 -1
- package/clients/dispatch/runners/rubocop.ts +2 -1
- package/clients/dispatch/runners/ruff.ts +2 -1
- package/clients/dispatch/runners/rust-clippy.ts +2 -1
- package/clients/dispatch/runners/shellcheck.ts +2 -1
- package/clients/dispatch/runners/similarity.ts +2 -1
- package/clients/dispatch/runners/spellcheck.ts +2 -1
- package/clients/dispatch/runners/sqlfluff.ts +2 -1
- package/clients/dispatch/runners/tree-sitter.ts +469 -1
- package/clients/dispatch/runners/ts-lsp.ts +2 -1
- package/clients/dispatch/runners/type-safety.ts +2 -1
- package/clients/dispatch/runners/yamllint.ts +2 -1
- package/clients/dispatch/tool-profile.ts +40 -0
- package/clients/dispatch/types.ts +3 -13
- package/clients/lsp/client.ts +366 -12
- package/clients/lsp/index.ts +374 -76
- package/clients/lsp/launch.ts +42 -2
- package/clients/lsp/server.ts +186 -12
- package/clients/pipeline.ts +2 -2
- package/clients/runtime-context.ts +2 -2
- package/clients/runtime-session.ts +43 -5
- package/clients/session-summary.ts +21 -0
- package/clients/tree-sitter-client.ts +162 -0
- package/clients/tree-sitter-logger.ts +47 -0
- package/clients/tree-sitter-query-loader.ts +13 -2
- package/index.ts +67 -17
- package/package.json +3 -1
- package/rules/rule-catalog.json +64 -0
- package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
- package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
- package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
- package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
- package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
- package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
- package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
- package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
- package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
- package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
- package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
- package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
- package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
- package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
- package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
- package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
- package/scripts/validate-rule-catalog.mjs +227 -0
- package/skills/lsp-navigation/SKILL.md +15 -3
- package/tools/lsp-navigation.js +466 -79
- package/tools/lsp-navigation.ts +587 -85
package/tools/lsp-navigation.ts
CHANGED
|
@@ -6,9 +6,152 @@
|
|
|
6
6
|
|
|
7
7
|
import * as nodeFs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
9
10
|
import { Type } from "@sinclair/typebox";
|
|
10
11
|
import type { LSPCallHierarchyItem } from "../clients/lsp/client.js";
|
|
11
12
|
import { getLSPService } from "../clients/lsp/index.js";
|
|
13
|
+
import { logLatency } from "../clients/latency-logger.js";
|
|
14
|
+
|
|
15
|
+
function operationSupportStatus(
|
|
16
|
+
operation: string,
|
|
17
|
+
support: import("../clients/lsp/client.js").LSPOperationSupport | null,
|
|
18
|
+
): boolean | null {
|
|
19
|
+
if (!support) return null;
|
|
20
|
+
if (operation === "definition") return support.definition;
|
|
21
|
+
if (operation === "references") return support.references;
|
|
22
|
+
if (operation === "hover") return support.hover;
|
|
23
|
+
if (operation === "signatureHelp") return support.signatureHelp;
|
|
24
|
+
if (operation === "documentSymbol") return support.documentSymbol;
|
|
25
|
+
if (operation === "workspaceSymbol") return support.workspaceSymbol;
|
|
26
|
+
if (operation === "codeAction") return support.codeAction;
|
|
27
|
+
if (operation === "rename") return support.rename;
|
|
28
|
+
if (operation === "implementation") return support.implementation;
|
|
29
|
+
if (
|
|
30
|
+
operation === "prepareCallHierarchy" ||
|
|
31
|
+
operation === "incomingCalls" ||
|
|
32
|
+
operation === "outgoingCalls"
|
|
33
|
+
)
|
|
34
|
+
return support.callHierarchy;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function emptyReasonForOperation(operation: string): string {
|
|
39
|
+
if (operation === "signatureHelp") return "position-sensitive-or-no-signature";
|
|
40
|
+
if (operation === "codeAction") return "no-applicable-actions";
|
|
41
|
+
if (operation === "rename") return "no-rename-edits-or-symbol-not-renamable";
|
|
42
|
+
if (operation === "workspaceSymbol")
|
|
43
|
+
return "no-matching-symbols-or-server-index-unavailable";
|
|
44
|
+
if (operation === "incomingCalls" || operation === "outgoingCalls")
|
|
45
|
+
return "no-call-hierarchy-results";
|
|
46
|
+
return "no-results";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tokenAtPosition(
|
|
50
|
+
content: string,
|
|
51
|
+
line1: number,
|
|
52
|
+
char1: number,
|
|
53
|
+
): string | undefined {
|
|
54
|
+
const lines = content.split(/\r?\n/);
|
|
55
|
+
const line = lines[line1 - 1];
|
|
56
|
+
if (!line) return undefined;
|
|
57
|
+
const chars = [...line];
|
|
58
|
+
const idx = Math.max(0, Math.min(chars.length - 1, char1 - 1));
|
|
59
|
+
const isWord = (ch: string | undefined) =>
|
|
60
|
+
!!ch && /[A-Za-z0-9_?!]/.test(ch);
|
|
61
|
+
|
|
62
|
+
let left = idx;
|
|
63
|
+
let right = idx;
|
|
64
|
+
if (!isWord(chars[idx]) && isWord(chars[idx + 1])) {
|
|
65
|
+
left = idx + 1;
|
|
66
|
+
right = idx + 1;
|
|
67
|
+
}
|
|
68
|
+
while (left > 0 && isWord(chars[left - 1])) left -= 1;
|
|
69
|
+
while (right < chars.length - 1 && isWord(chars[right + 1])) right += 1;
|
|
70
|
+
const token = chars.slice(left, right + 1).join("").trim();
|
|
71
|
+
return token.length > 0 ? token : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type SymbolNode = {
|
|
75
|
+
name?: string;
|
|
76
|
+
location?: { uri: string; range: Record<string, unknown> };
|
|
77
|
+
range?: Record<string, unknown>;
|
|
78
|
+
children?: SymbolNode[];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function flattenSymbols(symbols: SymbolNode[]): SymbolNode[] {
|
|
82
|
+
const all: SymbolNode[] = [];
|
|
83
|
+
for (const symbol of symbols) {
|
|
84
|
+
all.push(symbol);
|
|
85
|
+
if (symbol.children && symbol.children.length > 0) {
|
|
86
|
+
all.push(...flattenSymbols(symbol.children));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return all;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function pickLocalSymbolLocation(
|
|
93
|
+
symbols: SymbolNode[],
|
|
94
|
+
token: string,
|
|
95
|
+
filePath: string,
|
|
96
|
+
): Array<{ uri: string; range: Record<string, unknown> }> {
|
|
97
|
+
const flat = flattenSymbols(symbols).filter(
|
|
98
|
+
(symbol) => symbol.name === token,
|
|
99
|
+
);
|
|
100
|
+
if (flat.length === 0) return [];
|
|
101
|
+
const uri = pathToFileURL(filePath).href;
|
|
102
|
+
return flat
|
|
103
|
+
.map((symbol) => {
|
|
104
|
+
if (symbol.location?.uri && symbol.location.range) {
|
|
105
|
+
return { uri: symbol.location.uri, range: symbol.location.range };
|
|
106
|
+
}
|
|
107
|
+
if (symbol.range) {
|
|
108
|
+
return { uri, range: symbol.range };
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
})
|
|
112
|
+
.filter((entry): entry is { uri: string; range: Record<string, unknown> } =>
|
|
113
|
+
Boolean(entry),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function classifyCodeActions(
|
|
118
|
+
actions: Array<{ kind?: string }> | undefined,
|
|
119
|
+
): { quickfix: number; refactor: number; other: number } {
|
|
120
|
+
if (!actions || actions.length === 0) return { quickfix: 0, refactor: 0, other: 0 };
|
|
121
|
+
let quickfix = 0;
|
|
122
|
+
let refactor = 0;
|
|
123
|
+
let other = 0;
|
|
124
|
+
for (const action of actions) {
|
|
125
|
+
const kind = action.kind ?? "";
|
|
126
|
+
if (kind.startsWith("quickfix")) quickfix += 1;
|
|
127
|
+
else if (kind.startsWith("refactor")) refactor += 1;
|
|
128
|
+
else other += 1;
|
|
129
|
+
}
|
|
130
|
+
return { quickfix, refactor, other };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function openFileBestEffort(
|
|
134
|
+
lspService: ReturnType<typeof getLSPService>,
|
|
135
|
+
filePath: string,
|
|
136
|
+
waitForDiagnostics = false,
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
let fileContent: string | undefined;
|
|
139
|
+
try {
|
|
140
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
141
|
+
} catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!fileContent) return;
|
|
145
|
+
try {
|
|
146
|
+
if (typeof lspService.touchFile === "function") {
|
|
147
|
+
await lspService.touchFile(filePath, fileContent, waitForDiagnostics);
|
|
148
|
+
} else {
|
|
149
|
+
await lspService.openFile(filePath, fileContent);
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
/* LSP server may not be ready yet — proceed anyway */
|
|
153
|
+
}
|
|
154
|
+
}
|
|
12
155
|
|
|
13
156
|
export function createLspNavigationTool(
|
|
14
157
|
getFlag: (name: string) => boolean | string | undefined,
|
|
@@ -16,18 +159,22 @@ export function createLspNavigationTool(
|
|
|
16
159
|
return {
|
|
17
160
|
name: "lsp_navigation" as const,
|
|
18
161
|
label: "LSP Navigate",
|
|
19
|
-
|
|
162
|
+
description:
|
|
20
163
|
"Navigate code using LSP (Language Server Protocol). Requires --lens-lsp flag.\n" +
|
|
21
164
|
"Operations:\n" +
|
|
22
165
|
"- definition: Jump to where a symbol is defined\n" +
|
|
23
166
|
"- references: Find all usages of a symbol\n" +
|
|
24
167
|
"- hover: Get type/doc info at a position\n" +
|
|
168
|
+
"- signatureHelp: Show callable signatures at cursor\n" +
|
|
25
169
|
"- documentSymbol: List all symbols (functions/classes/vars) in a file\n" +
|
|
26
|
-
"- workspaceSymbol: Search symbols across the whole project\n" +
|
|
170
|
+
"- workspaceSymbol: Search symbols across the whole project (best with filePath context)\n" +
|
|
171
|
+
"- codeAction: Find available quick fixes/refactors at a range\n" +
|
|
172
|
+
"- rename: Compute workspace edits for renaming a symbol\n" +
|
|
27
173
|
"- implementation: Jump to interface implementations\n" +
|
|
28
174
|
"- prepareCallHierarchy: Get callable item at position (for incoming/outgoing)\n" +
|
|
29
175
|
"- incomingCalls: Find all functions/methods that CALL this function\n" +
|
|
30
|
-
"- outgoingCalls: Find all functions/methods CALLED by this function\n
|
|
176
|
+
"- outgoingCalls: Find all functions/methods CALLED by this function\n" +
|
|
177
|
+
"- workspaceDiagnostics: List all diagnostics tracked by active LSP clients\n\n" +
|
|
31
178
|
"Line and character are 1-based (as shown in editors).",
|
|
32
179
|
promptSnippet:
|
|
33
180
|
"Use lsp_navigation to find definitions, references, and hover info via LSP",
|
|
@@ -37,18 +184,25 @@ export function createLspNavigationTool(
|
|
|
37
184
|
Type.Literal("definition"),
|
|
38
185
|
Type.Literal("references"),
|
|
39
186
|
Type.Literal("hover"),
|
|
187
|
+
Type.Literal("signatureHelp"),
|
|
40
188
|
Type.Literal("documentSymbol"),
|
|
41
189
|
Type.Literal("workspaceSymbol"),
|
|
190
|
+
Type.Literal("codeAction"),
|
|
191
|
+
Type.Literal("rename"),
|
|
42
192
|
Type.Literal("implementation"),
|
|
43
193
|
Type.Literal("prepareCallHierarchy"),
|
|
44
194
|
Type.Literal("incomingCalls"),
|
|
45
195
|
Type.Literal("outgoingCalls"),
|
|
196
|
+
Type.Literal("workspaceDiagnostics"),
|
|
46
197
|
],
|
|
47
198
|
{ description: "LSP operation to perform" },
|
|
48
199
|
),
|
|
49
|
-
filePath: Type.
|
|
50
|
-
|
|
51
|
-
|
|
200
|
+
filePath: Type.Optional(
|
|
201
|
+
Type.String({
|
|
202
|
+
description:
|
|
203
|
+
"Absolute or relative file path. Required for file-scoped operations; optional for workspaceSymbol/workspaceDiagnostics.",
|
|
204
|
+
}),
|
|
205
|
+
),
|
|
52
206
|
line: Type.Optional(
|
|
53
207
|
Type.Number({
|
|
54
208
|
description:
|
|
@@ -61,9 +215,27 @@ export function createLspNavigationTool(
|
|
|
61
215
|
"Character offset (1-based). Required for definition/references/hover/implementation",
|
|
62
216
|
}),
|
|
63
217
|
),
|
|
218
|
+
endLine: Type.Optional(
|
|
219
|
+
Type.Number({
|
|
220
|
+
description:
|
|
221
|
+
"End line (1-based). Optional; used by codeAction range.",
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
endCharacter: Type.Optional(
|
|
225
|
+
Type.Number({
|
|
226
|
+
description:
|
|
227
|
+
"End character (1-based). Optional; used by codeAction range.",
|
|
228
|
+
}),
|
|
229
|
+
),
|
|
230
|
+
newName: Type.Optional(
|
|
231
|
+
Type.String({
|
|
232
|
+
description: "Required for rename operation.",
|
|
233
|
+
}),
|
|
234
|
+
),
|
|
64
235
|
query: Type.Optional(
|
|
65
236
|
Type.String({
|
|
66
|
-
description:
|
|
237
|
+
description:
|
|
238
|
+
"Symbol name to search. Used by workspaceSymbol (best with filePath for active project context).",
|
|
67
239
|
}),
|
|
68
240
|
),
|
|
69
241
|
callHierarchyItem: Type.Optional(
|
|
@@ -107,8 +279,50 @@ export function createLspNavigationTool(
|
|
|
107
279
|
_onUpdate: unknown,
|
|
108
280
|
ctx: { cwd?: string },
|
|
109
281
|
) {
|
|
110
|
-
|
|
282
|
+
const startedAt = Date.now();
|
|
283
|
+
let supported: boolean | null = null;
|
|
284
|
+
let diagnosticsMode: "pull" | "push-only" | "unknown" = "unknown";
|
|
285
|
+
|
|
286
|
+
const finalize = (
|
|
287
|
+
payload: {
|
|
288
|
+
content: Array<{ type: "text"; text: string }>;
|
|
289
|
+
isError?: boolean;
|
|
290
|
+
details?: Record<string, unknown>;
|
|
291
|
+
},
|
|
292
|
+
meta: {
|
|
293
|
+
operation: string;
|
|
294
|
+
filePath: string;
|
|
295
|
+
failureKind: string;
|
|
296
|
+
resultCount: number;
|
|
297
|
+
},
|
|
298
|
+
) => {
|
|
299
|
+
const normalizedFilePath = meta.filePath.replace(/\\/g, "/");
|
|
300
|
+
logLatency({
|
|
301
|
+
type: "phase",
|
|
302
|
+
phase: "lsp_navigation_result",
|
|
303
|
+
filePath: normalizedFilePath,
|
|
304
|
+
durationMs: Date.now() - startedAt,
|
|
305
|
+
metadata: {
|
|
306
|
+
operation: meta.operation,
|
|
307
|
+
failureKind: meta.failureKind,
|
|
308
|
+
resultCount: meta.resultCount,
|
|
309
|
+
supported,
|
|
310
|
+
diagnosticsMode,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
111
314
|
return {
|
|
315
|
+
...payload,
|
|
316
|
+
details: {
|
|
317
|
+
...(payload.details ?? {}),
|
|
318
|
+
failureKind: meta.failureKind,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
if (!getFlag("lens-lsp") || getFlag("no-lsp")) {
|
|
324
|
+
return finalize(
|
|
325
|
+
{
|
|
112
326
|
content: [
|
|
113
327
|
{
|
|
114
328
|
type: "text" as const,
|
|
@@ -116,8 +330,14 @@ export function createLspNavigationTool(
|
|
|
116
330
|
},
|
|
117
331
|
],
|
|
118
332
|
isError: true,
|
|
119
|
-
|
|
120
|
-
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
operation: "precheck",
|
|
336
|
+
filePath: "(workspace)",
|
|
337
|
+
failureKind: "lsp_disabled",
|
|
338
|
+
resultCount: 0,
|
|
339
|
+
},
|
|
340
|
+
);
|
|
121
341
|
}
|
|
122
342
|
|
|
123
343
|
const {
|
|
@@ -125,23 +345,165 @@ export function createLspNavigationTool(
|
|
|
125
345
|
filePath: rawPath,
|
|
126
346
|
line,
|
|
127
347
|
character,
|
|
348
|
+
endLine,
|
|
349
|
+
endCharacter,
|
|
350
|
+
newName,
|
|
128
351
|
query,
|
|
129
352
|
} = params as {
|
|
130
353
|
operation: string;
|
|
131
|
-
filePath
|
|
354
|
+
filePath?: string;
|
|
132
355
|
line?: number;
|
|
133
356
|
character?: number;
|
|
357
|
+
endLine?: number;
|
|
358
|
+
endCharacter?: number;
|
|
359
|
+
newName?: string;
|
|
134
360
|
query?: string;
|
|
135
361
|
};
|
|
136
362
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
363
|
+
const isCallHierarchyTraversal =
|
|
364
|
+
operation === "incomingCalls" || operation === "outgoingCalls";
|
|
365
|
+
const needsFilePath =
|
|
366
|
+
operation !== "workspaceDiagnostics" &&
|
|
367
|
+
operation !== "workspaceSymbol" &&
|
|
368
|
+
!isCallHierarchyTraversal;
|
|
369
|
+
if (needsFilePath && (!rawPath || rawPath.trim().length === 0)) {
|
|
370
|
+
return finalize(
|
|
371
|
+
{
|
|
372
|
+
content: [
|
|
373
|
+
{
|
|
374
|
+
type: "text" as const,
|
|
375
|
+
text: `filePath is required for ${operation}`,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
isError: true,
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
operation,
|
|
382
|
+
filePath: "(workspace)",
|
|
383
|
+
failureKind: "missing_file_path",
|
|
384
|
+
resultCount: 0,
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const filePath = rawPath
|
|
390
|
+
? path.isAbsolute(rawPath)
|
|
391
|
+
? rawPath
|
|
392
|
+
: path.resolve(ctx.cwd || ".", rawPath)
|
|
393
|
+
: "";
|
|
140
394
|
|
|
141
395
|
const lspService = getLSPService();
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
396
|
+
if (operation === "workspaceDiagnostics") {
|
|
397
|
+
const wsDiagSupport = await lspService.getWorkspaceDiagnosticsSupport(
|
|
398
|
+
rawPath ? filePath : undefined,
|
|
399
|
+
);
|
|
400
|
+
diagnosticsMode = wsDiagSupport?.mode ?? "unknown";
|
|
401
|
+
|
|
402
|
+
if (rawPath) {
|
|
403
|
+
const hasLSP = await lspService.hasLSP(filePath);
|
|
404
|
+
if (!hasLSP) {
|
|
405
|
+
return finalize(
|
|
406
|
+
{
|
|
407
|
+
content: [
|
|
408
|
+
{
|
|
409
|
+
type: "text" as const,
|
|
410
|
+
text: `No LSP server available for ${path.basename(filePath)}. Check that the language server is installed.`,
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
isError: true,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
operation,
|
|
417
|
+
filePath,
|
|
418
|
+
failureKind: "no_server",
|
|
419
|
+
resultCount: 0,
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
await openFileBestEffort(lspService, filePath, true);
|
|
425
|
+
const diagnostics = await lspService.getDiagnostics(filePath);
|
|
426
|
+
const result = [
|
|
427
|
+
{
|
|
428
|
+
filePath,
|
|
429
|
+
diagnostics,
|
|
430
|
+
count: diagnostics.length,
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
const note =
|
|
434
|
+
diagnosticsMode === "pull"
|
|
435
|
+
? "Note: filePath mode requests pull diagnostics for this file and returns the aggregated result."
|
|
436
|
+
: diagnosticsMode === "push-only"
|
|
437
|
+
? "Note: server is push-only; result depends on published diagnostics for this file."
|
|
438
|
+
: "Note: workspace diagnostics mode unknown (no active capability snapshot).";
|
|
439
|
+
const resultCount = diagnostics.length;
|
|
440
|
+
return finalize(
|
|
441
|
+
{
|
|
442
|
+
content: [
|
|
443
|
+
{
|
|
444
|
+
type: "text" as const,
|
|
445
|
+
text: `${note}\n${JSON.stringify(result, null, 2)}`,
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
details: {
|
|
449
|
+
operation,
|
|
450
|
+
resultCount,
|
|
451
|
+
diagnosticsMode,
|
|
452
|
+
coverage: "requested-file",
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
operation,
|
|
457
|
+
filePath,
|
|
458
|
+
failureKind: resultCount === 0 ? "empty_result" : "success",
|
|
459
|
+
resultCount,
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const allDiagnostics = await lspService.getAllDiagnostics();
|
|
465
|
+
const result = Array.from(allDiagnostics.entries()).map(([trackedFile, diags]) => ({
|
|
466
|
+
filePath: trackedFile,
|
|
467
|
+
diagnostics: diags,
|
|
468
|
+
count: diags.length,
|
|
469
|
+
}));
|
|
470
|
+
const note =
|
|
471
|
+
diagnosticsMode === "push-only"
|
|
472
|
+
? "Note: push-only tracked diagnostics snapshot (not full workspace pull diagnostics)."
|
|
473
|
+
: diagnosticsMode === "pull"
|
|
474
|
+
? "Note: tracked diagnostics snapshot from active clients. Provide filePath to force file-level diagnostics collection."
|
|
475
|
+
: "Note: workspace diagnostics mode unknown (no active capability snapshot).";
|
|
476
|
+
return finalize(
|
|
477
|
+
{
|
|
478
|
+
content: [
|
|
479
|
+
{
|
|
480
|
+
type: "text" as const,
|
|
481
|
+
text: `${note}\n${JSON.stringify(result, null, 2)}`,
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
details: {
|
|
485
|
+
operation,
|
|
486
|
+
resultCount: result.length,
|
|
487
|
+
diagnosticsMode,
|
|
488
|
+
coverage: "tracked-open-files",
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
operation,
|
|
493
|
+
filePath: rawPath ? filePath : "(workspace)",
|
|
494
|
+
failureKind:
|
|
495
|
+
diagnosticsMode === "push-only"
|
|
496
|
+
? "tracked_snapshot"
|
|
497
|
+
: "success",
|
|
498
|
+
resultCount: result.length,
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const hasLSP = filePath ? await lspService.hasLSP(filePath) : false;
|
|
504
|
+
if (needsFilePath && !hasLSP) {
|
|
505
|
+
return finalize(
|
|
506
|
+
{
|
|
145
507
|
content: [
|
|
146
508
|
{
|
|
147
509
|
type: "text" as const,
|
|
@@ -149,104 +511,201 @@ export function createLspNavigationTool(
|
|
|
149
511
|
},
|
|
150
512
|
],
|
|
151
513
|
isError: true,
|
|
152
|
-
|
|
153
|
-
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
operation,
|
|
517
|
+
filePath,
|
|
518
|
+
failureKind: "no_server",
|
|
519
|
+
resultCount: 0,
|
|
520
|
+
},
|
|
521
|
+
);
|
|
154
522
|
}
|
|
155
523
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
524
|
+
if (needsFilePath) {
|
|
525
|
+
const support = await lspService.getOperationSupport(filePath);
|
|
526
|
+
supported = operationSupportStatus(operation, support);
|
|
527
|
+
if (supported === false) {
|
|
528
|
+
return finalize(
|
|
529
|
+
{
|
|
530
|
+
content: [
|
|
531
|
+
{
|
|
532
|
+
type: "text" as const,
|
|
533
|
+
text: `LSP server for ${path.basename(filePath)} does not advertise support for ${operation}`,
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
isError: true,
|
|
537
|
+
details: { operation, supported: false, emptyReason: "unsupported" },
|
|
538
|
+
},
|
|
539
|
+
{ operation, filePath, failureKind: "unsupported", resultCount: 0 },
|
|
540
|
+
);
|
|
168
541
|
}
|
|
542
|
+
|
|
543
|
+
await openFileBestEffort(lspService, filePath);
|
|
169
544
|
}
|
|
170
545
|
|
|
171
546
|
// Convert 1-based editor coords to 0-based LSP coords
|
|
172
547
|
const lspLine = (line ?? 1) - 1;
|
|
173
548
|
const lspChar = (character ?? 1) - 1;
|
|
549
|
+
const lspEndLine = (endLine ?? line ?? 1) - 1;
|
|
550
|
+
const lspEndChar = (endCharacter ?? character ?? 1) - 1;
|
|
174
551
|
|
|
175
|
-
|
|
176
|
-
try {
|
|
552
|
+
const runOperation = async (): Promise<unknown> => {
|
|
177
553
|
switch (operation) {
|
|
178
554
|
case "definition":
|
|
179
|
-
|
|
180
|
-
break;
|
|
555
|
+
return lspService.definition(filePath, lspLine, lspChar);
|
|
181
556
|
case "references":
|
|
182
|
-
|
|
183
|
-
break;
|
|
557
|
+
return lspService.references(filePath, lspLine, lspChar);
|
|
184
558
|
case "hover":
|
|
185
|
-
|
|
186
|
-
|
|
559
|
+
return lspService.hover(filePath, lspLine, lspChar);
|
|
560
|
+
case "signatureHelp":
|
|
561
|
+
return lspService.signatureHelp(filePath, lspLine, lspChar);
|
|
187
562
|
case "documentSymbol":
|
|
188
|
-
|
|
189
|
-
break;
|
|
563
|
+
return lspService.documentSymbol(filePath);
|
|
190
564
|
case "workspaceSymbol":
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
result = await lspService.implementation(
|
|
195
|
-
filePath,
|
|
196
|
-
lspLine,
|
|
197
|
-
lspChar,
|
|
565
|
+
supported = operationSupportStatus(
|
|
566
|
+
operation,
|
|
567
|
+
await lspService.getOperationSupport(rawPath ? filePath : undefined),
|
|
198
568
|
);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
569
|
+
if (supported === false) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
"__UNSUPPORTED__ Active LSP server does not advertise support for workspaceSymbol",
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (!query || query.trim().length === 0) {
|
|
575
|
+
throw new Error("__BADINPUT__ query parameter required for workspaceSymbol");
|
|
576
|
+
}
|
|
577
|
+
if (rawPath) {
|
|
578
|
+
await openFileBestEffort(lspService, filePath);
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
return await lspService.workspaceSymbol(
|
|
582
|
+
query ?? "",
|
|
583
|
+
rawPath ? filePath : undefined,
|
|
584
|
+
);
|
|
585
|
+
} catch (err) {
|
|
586
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
587
|
+
if (rawPath && /No Project/i.test(msg)) {
|
|
588
|
+
await openFileBestEffort(lspService, filePath);
|
|
589
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
590
|
+
return lspService.workspaceSymbol(query ?? "", filePath);
|
|
591
|
+
}
|
|
592
|
+
throw err;
|
|
593
|
+
}
|
|
594
|
+
case "codeAction":
|
|
595
|
+
return lspService.codeAction(
|
|
202
596
|
filePath,
|
|
203
597
|
lspLine,
|
|
204
598
|
lspChar,
|
|
599
|
+
lspEndLine,
|
|
600
|
+
lspEndChar,
|
|
205
601
|
);
|
|
206
|
-
|
|
602
|
+
case "rename":
|
|
603
|
+
if (!newName || newName.trim().length === 0) {
|
|
604
|
+
throw new Error("__BADINPUT__ newName parameter required for rename");
|
|
605
|
+
}
|
|
606
|
+
return lspService.rename(filePath, lspLine, lspChar, newName);
|
|
607
|
+
case "implementation":
|
|
608
|
+
return lspService.implementation(filePath, lspLine, lspChar);
|
|
609
|
+
case "prepareCallHierarchy":
|
|
610
|
+
return lspService.prepareCallHierarchy(filePath, lspLine, lspChar);
|
|
207
611
|
case "incomingCalls": {
|
|
208
612
|
const callItem = (
|
|
209
613
|
params as { callHierarchyItem?: LSPCallHierarchyItem }
|
|
210
614
|
).callHierarchyItem;
|
|
211
615
|
if (!callItem) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
type: "text" as const,
|
|
216
|
-
text: "callHierarchyItem parameter required for incomingCalls",
|
|
217
|
-
},
|
|
218
|
-
],
|
|
219
|
-
isError: true,
|
|
220
|
-
details: {},
|
|
221
|
-
};
|
|
616
|
+
throw new Error(
|
|
617
|
+
"__BADINPUT__ callHierarchyItem parameter required for incomingCalls",
|
|
618
|
+
);
|
|
222
619
|
}
|
|
223
|
-
|
|
224
|
-
break;
|
|
620
|
+
return lspService.incomingCalls(callItem);
|
|
225
621
|
}
|
|
226
622
|
case "outgoingCalls": {
|
|
227
623
|
const callItem = (
|
|
228
624
|
params as { callHierarchyItem?: LSPCallHierarchyItem }
|
|
229
625
|
).callHierarchyItem;
|
|
230
626
|
if (!callItem) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
type: "text" as const,
|
|
235
|
-
text: "callHierarchyItem parameter required for outgoingCalls",
|
|
236
|
-
},
|
|
237
|
-
],
|
|
238
|
-
isError: true,
|
|
239
|
-
details: {},
|
|
240
|
-
};
|
|
627
|
+
throw new Error(
|
|
628
|
+
"__BADINPUT__ callHierarchyItem parameter required for outgoingCalls",
|
|
629
|
+
);
|
|
241
630
|
}
|
|
242
|
-
|
|
243
|
-
break;
|
|
631
|
+
return lspService.outgoingCalls(callItem);
|
|
244
632
|
}
|
|
245
633
|
default:
|
|
246
|
-
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
let result: unknown;
|
|
639
|
+
let usedDocumentSymbolFallback = false;
|
|
640
|
+
try {
|
|
641
|
+
result = await runOperation();
|
|
642
|
+
const isEmptyInitial =
|
|
643
|
+
!result || (Array.isArray(result) && result.length === 0);
|
|
644
|
+
const shouldRetryOnEmpty =
|
|
645
|
+
isEmptyInitial &&
|
|
646
|
+
needsFilePath &&
|
|
647
|
+
[
|
|
648
|
+
"definition",
|
|
649
|
+
"references",
|
|
650
|
+
"hover",
|
|
651
|
+
"signatureHelp",
|
|
652
|
+
"workspaceSymbol",
|
|
653
|
+
"codeAction",
|
|
654
|
+
"rename",
|
|
655
|
+
"implementation",
|
|
656
|
+
].includes(operation);
|
|
657
|
+
if (shouldRetryOnEmpty) {
|
|
658
|
+
await openFileBestEffort(lspService, filePath, true);
|
|
659
|
+
result = await runOperation();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const stillEmpty =
|
|
663
|
+
!result || (Array.isArray(result) && result.length === 0);
|
|
664
|
+
if (
|
|
665
|
+
stillEmpty &&
|
|
666
|
+
needsFilePath &&
|
|
667
|
+
(operation === "definition" || operation === "workspaceSymbol")
|
|
668
|
+
) {
|
|
669
|
+
const content = nodeFs.readFileSync(filePath, "utf-8");
|
|
670
|
+
const token =
|
|
671
|
+
operation === "workspaceSymbol"
|
|
672
|
+
? (query?.trim() || undefined)
|
|
673
|
+
: line && character
|
|
674
|
+
? tokenAtPosition(content, line, character)
|
|
675
|
+
: undefined;
|
|
676
|
+
if (token) {
|
|
677
|
+
const docSymbols = (await lspService.documentSymbol(filePath)) as SymbolNode[];
|
|
678
|
+
const locations = pickLocalSymbolLocation(docSymbols, token, filePath);
|
|
679
|
+
if (locations.length > 0) {
|
|
680
|
+
result = locations;
|
|
681
|
+
usedDocumentSymbolFallback = true;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
247
684
|
}
|
|
248
685
|
} catch (err) {
|
|
249
|
-
|
|
686
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
687
|
+
if (msg.startsWith("__UNSUPPORTED__ ")) {
|
|
688
|
+
return finalize(
|
|
689
|
+
{
|
|
690
|
+
content: [{ type: "text" as const, text: msg.replace("__UNSUPPORTED__ ", "") }],
|
|
691
|
+
isError: true,
|
|
692
|
+
details: { operation, supported: false, emptyReason: "unsupported" },
|
|
693
|
+
},
|
|
694
|
+
{ operation, filePath, failureKind: "unsupported", resultCount: 0 },
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (msg.startsWith("__BADINPUT__ ")) {
|
|
698
|
+
return finalize(
|
|
699
|
+
{
|
|
700
|
+
content: [{ type: "text" as const, text: msg.replace("__BADINPUT__ ", "") }],
|
|
701
|
+
isError: true,
|
|
702
|
+
details: {},
|
|
703
|
+
},
|
|
704
|
+
{ operation, filePath, failureKind: "bad_input", resultCount: 0 },
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return finalize(
|
|
708
|
+
{
|
|
250
709
|
content: [
|
|
251
710
|
{
|
|
252
711
|
type: "text" as const,
|
|
@@ -255,21 +714,64 @@ export function createLspNavigationTool(
|
|
|
255
714
|
],
|
|
256
715
|
isError: true,
|
|
257
716
|
details: {},
|
|
258
|
-
|
|
717
|
+
},
|
|
718
|
+
{ operation, filePath, failureKind: "lsp_error", resultCount: 0 },
|
|
719
|
+
);
|
|
259
720
|
}
|
|
260
721
|
|
|
261
722
|
const isEmpty = !result || (Array.isArray(result) && result.length === 0);
|
|
262
|
-
|
|
723
|
+
let output = isEmpty
|
|
263
724
|
? `No results for ${operation} at ${path.basename(filePath)}${line ? `:${line}:${character}` : ""}`
|
|
264
725
|
: JSON.stringify(result, null, 2);
|
|
726
|
+
if (isEmpty && operation === "workspaceSymbol" && !rawPath) {
|
|
727
|
+
output +=
|
|
728
|
+
"\nHint: provide filePath to scope workspaceSymbol to the active language server/root.";
|
|
729
|
+
}
|
|
730
|
+
if (usedDocumentSymbolFallback) {
|
|
731
|
+
output += "\nNote: served from documentSymbol fallback due to empty primary result.";
|
|
732
|
+
}
|
|
733
|
+
if (
|
|
734
|
+
operation === "references" &&
|
|
735
|
+
Array.isArray(result) &&
|
|
736
|
+
result.length <= 2
|
|
737
|
+
) {
|
|
738
|
+
output +=
|
|
739
|
+
"\nHint: references from usage sites can be partial; retry from the symbol definition for broader cross-file results.";
|
|
740
|
+
}
|
|
741
|
+
const actionStats =
|
|
742
|
+
operation === "codeAction" && Array.isArray(result)
|
|
743
|
+
? classifyCodeActions(result as Array<{ kind?: string }>)
|
|
744
|
+
: null;
|
|
745
|
+
if (operation === "codeAction" && actionStats) {
|
|
746
|
+
if (actionStats.quickfix === 0 && actionStats.refactor > 0) {
|
|
747
|
+
output +=
|
|
748
|
+
"\nNote: no diagnostic quick fixes returned; refactor-only actions available.";
|
|
749
|
+
}
|
|
750
|
+
}
|
|
265
751
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
752
|
+
const resultCount = Array.isArray(result) ? result.length : result ? 1 : 0;
|
|
753
|
+
return finalize(
|
|
754
|
+
{
|
|
755
|
+
content: [{ type: "text" as const, text: output }],
|
|
756
|
+
details: {
|
|
757
|
+
operation,
|
|
758
|
+
supported,
|
|
759
|
+
emptyReason: isEmpty ? emptyReasonForOperation(operation) : undefined,
|
|
760
|
+
codeActionKinds: actionStats ?? undefined,
|
|
761
|
+
resultCount,
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
{
|
|
269
765
|
operation,
|
|
270
|
-
|
|
766
|
+
filePath: rawPath ? filePath : "(workspace)",
|
|
767
|
+
failureKind: isEmpty
|
|
768
|
+
? "empty_result"
|
|
769
|
+
: usedDocumentSymbolFallback
|
|
770
|
+
? "fallback_success"
|
|
771
|
+
: "success",
|
|
772
|
+
resultCount,
|
|
271
773
|
},
|
|
272
|
-
|
|
774
|
+
);
|
|
273
775
|
},
|
|
274
776
|
};
|
|
275
777
|
}
|