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.js
CHANGED
|
@@ -5,8 +5,137 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as nodeFs from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
8
9
|
import { Type } from "@sinclair/typebox";
|
|
9
10
|
import { getLSPService } from "../clients/lsp/index.js";
|
|
11
|
+
import { logLatency } from "../clients/latency-logger.js";
|
|
12
|
+
function operationSupportStatus(operation, support) {
|
|
13
|
+
if (!support)
|
|
14
|
+
return null;
|
|
15
|
+
if (operation === "definition")
|
|
16
|
+
return support.definition;
|
|
17
|
+
if (operation === "references")
|
|
18
|
+
return support.references;
|
|
19
|
+
if (operation === "hover")
|
|
20
|
+
return support.hover;
|
|
21
|
+
if (operation === "signatureHelp")
|
|
22
|
+
return support.signatureHelp;
|
|
23
|
+
if (operation === "documentSymbol")
|
|
24
|
+
return support.documentSymbol;
|
|
25
|
+
if (operation === "workspaceSymbol")
|
|
26
|
+
return support.workspaceSymbol;
|
|
27
|
+
if (operation === "codeAction")
|
|
28
|
+
return support.codeAction;
|
|
29
|
+
if (operation === "rename")
|
|
30
|
+
return support.rename;
|
|
31
|
+
if (operation === "implementation")
|
|
32
|
+
return support.implementation;
|
|
33
|
+
if (operation === "prepareCallHierarchy" ||
|
|
34
|
+
operation === "incomingCalls" ||
|
|
35
|
+
operation === "outgoingCalls")
|
|
36
|
+
return support.callHierarchy;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function emptyReasonForOperation(operation) {
|
|
40
|
+
if (operation === "signatureHelp")
|
|
41
|
+
return "position-sensitive-or-no-signature";
|
|
42
|
+
if (operation === "codeAction")
|
|
43
|
+
return "no-applicable-actions";
|
|
44
|
+
if (operation === "rename")
|
|
45
|
+
return "no-rename-edits-or-symbol-not-renamable";
|
|
46
|
+
if (operation === "workspaceSymbol")
|
|
47
|
+
return "no-matching-symbols-or-server-index-unavailable";
|
|
48
|
+
if (operation === "incomingCalls" || operation === "outgoingCalls")
|
|
49
|
+
return "no-call-hierarchy-results";
|
|
50
|
+
return "no-results";
|
|
51
|
+
}
|
|
52
|
+
function tokenAtPosition(content, line1, char1) {
|
|
53
|
+
const lines = content.split(/\r?\n/);
|
|
54
|
+
const line = lines[line1 - 1];
|
|
55
|
+
if (!line)
|
|
56
|
+
return undefined;
|
|
57
|
+
const chars = [...line];
|
|
58
|
+
const idx = Math.max(0, Math.min(chars.length - 1, char1 - 1));
|
|
59
|
+
const isWord = (ch) => !!ch && /[A-Za-z0-9_?!]/.test(ch);
|
|
60
|
+
let left = idx;
|
|
61
|
+
let right = idx;
|
|
62
|
+
if (!isWord(chars[idx]) && isWord(chars[idx + 1])) {
|
|
63
|
+
left = idx + 1;
|
|
64
|
+
right = idx + 1;
|
|
65
|
+
}
|
|
66
|
+
while (left > 0 && isWord(chars[left - 1]))
|
|
67
|
+
left -= 1;
|
|
68
|
+
while (right < chars.length - 1 && isWord(chars[right + 1]))
|
|
69
|
+
right += 1;
|
|
70
|
+
const token = chars.slice(left, right + 1).join("").trim();
|
|
71
|
+
return token.length > 0 ? token : undefined;
|
|
72
|
+
}
|
|
73
|
+
function flattenSymbols(symbols) {
|
|
74
|
+
const all = [];
|
|
75
|
+
for (const symbol of symbols) {
|
|
76
|
+
all.push(symbol);
|
|
77
|
+
if (symbol.children && symbol.children.length > 0) {
|
|
78
|
+
all.push(...flattenSymbols(symbol.children));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return all;
|
|
82
|
+
}
|
|
83
|
+
function pickLocalSymbolLocation(symbols, token, filePath) {
|
|
84
|
+
const flat = flattenSymbols(symbols).filter((symbol) => symbol.name === token);
|
|
85
|
+
if (flat.length === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const uri = pathToFileURL(filePath).href;
|
|
88
|
+
return flat
|
|
89
|
+
.map((symbol) => {
|
|
90
|
+
if (symbol.location?.uri && symbol.location.range) {
|
|
91
|
+
return { uri: symbol.location.uri, range: symbol.location.range };
|
|
92
|
+
}
|
|
93
|
+
if (symbol.range) {
|
|
94
|
+
return { uri, range: symbol.range };
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
})
|
|
98
|
+
.filter((entry) => Boolean(entry));
|
|
99
|
+
}
|
|
100
|
+
function classifyCodeActions(actions) {
|
|
101
|
+
if (!actions || actions.length === 0)
|
|
102
|
+
return { quickfix: 0, refactor: 0, other: 0 };
|
|
103
|
+
let quickfix = 0;
|
|
104
|
+
let refactor = 0;
|
|
105
|
+
let other = 0;
|
|
106
|
+
for (const action of actions) {
|
|
107
|
+
const kind = action.kind ?? "";
|
|
108
|
+
if (kind.startsWith("quickfix"))
|
|
109
|
+
quickfix += 1;
|
|
110
|
+
else if (kind.startsWith("refactor"))
|
|
111
|
+
refactor += 1;
|
|
112
|
+
else
|
|
113
|
+
other += 1;
|
|
114
|
+
}
|
|
115
|
+
return { quickfix, refactor, other };
|
|
116
|
+
}
|
|
117
|
+
async function openFileBestEffort(lspService, filePath, waitForDiagnostics = false) {
|
|
118
|
+
let fileContent;
|
|
119
|
+
try {
|
|
120
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!fileContent)
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
if (typeof lspService.touchFile === "function") {
|
|
129
|
+
await lspService.touchFile(filePath, fileContent, waitForDiagnostics);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
await lspService.openFile(filePath, fileContent);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* LSP server may not be ready yet — proceed anyway */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
10
139
|
export function createLspNavigationTool(getFlag) {
|
|
11
140
|
return {
|
|
12
141
|
name: "lsp_navigation",
|
|
@@ -16,12 +145,16 @@ export function createLspNavigationTool(getFlag) {
|
|
|
16
145
|
"- definition: Jump to where a symbol is defined\n" +
|
|
17
146
|
"- references: Find all usages of a symbol\n" +
|
|
18
147
|
"- hover: Get type/doc info at a position\n" +
|
|
148
|
+
"- signatureHelp: Show callable signatures at cursor\n" +
|
|
19
149
|
"- documentSymbol: List all symbols (functions/classes/vars) in a file\n" +
|
|
20
|
-
"- workspaceSymbol: Search symbols across the whole project\n" +
|
|
150
|
+
"- workspaceSymbol: Search symbols across the whole project (best with filePath context)\n" +
|
|
151
|
+
"- codeAction: Find available quick fixes/refactors at a range\n" +
|
|
152
|
+
"- rename: Compute workspace edits for renaming a symbol\n" +
|
|
21
153
|
"- implementation: Jump to interface implementations\n" +
|
|
22
154
|
"- prepareCallHierarchy: Get callable item at position (for incoming/outgoing)\n" +
|
|
23
155
|
"- incomingCalls: Find all functions/methods that CALL this function\n" +
|
|
24
|
-
"- outgoingCalls: Find all functions/methods CALLED by this function\n
|
|
156
|
+
"- outgoingCalls: Find all functions/methods CALLED by this function\n" +
|
|
157
|
+
"- workspaceDiagnostics: List all diagnostics tracked by active LSP clients\n\n" +
|
|
25
158
|
"Line and character are 1-based (as shown in editors).",
|
|
26
159
|
promptSnippet: "Use lsp_navigation to find definitions, references, and hover info via LSP",
|
|
27
160
|
parameters: Type.Object({
|
|
@@ -29,24 +162,37 @@ export function createLspNavigationTool(getFlag) {
|
|
|
29
162
|
Type.Literal("definition"),
|
|
30
163
|
Type.Literal("references"),
|
|
31
164
|
Type.Literal("hover"),
|
|
165
|
+
Type.Literal("signatureHelp"),
|
|
32
166
|
Type.Literal("documentSymbol"),
|
|
33
167
|
Type.Literal("workspaceSymbol"),
|
|
168
|
+
Type.Literal("codeAction"),
|
|
169
|
+
Type.Literal("rename"),
|
|
34
170
|
Type.Literal("implementation"),
|
|
35
171
|
Type.Literal("prepareCallHierarchy"),
|
|
36
172
|
Type.Literal("incomingCalls"),
|
|
37
173
|
Type.Literal("outgoingCalls"),
|
|
174
|
+
Type.Literal("workspaceDiagnostics"),
|
|
38
175
|
], { description: "LSP operation to perform" }),
|
|
39
|
-
filePath: Type.String({
|
|
40
|
-
description: "Absolute or relative path
|
|
41
|
-
}),
|
|
176
|
+
filePath: Type.Optional(Type.String({
|
|
177
|
+
description: "Absolute or relative file path. Required for file-scoped operations; optional for workspaceSymbol/workspaceDiagnostics.",
|
|
178
|
+
})),
|
|
42
179
|
line: Type.Optional(Type.Number({
|
|
43
180
|
description: "Line number (1-based). Required for definition/references/hover/implementation",
|
|
44
181
|
})),
|
|
45
182
|
character: Type.Optional(Type.Number({
|
|
46
183
|
description: "Character offset (1-based). Required for definition/references/hover/implementation",
|
|
47
184
|
})),
|
|
185
|
+
endLine: Type.Optional(Type.Number({
|
|
186
|
+
description: "End line (1-based). Optional; used by codeAction range.",
|
|
187
|
+
})),
|
|
188
|
+
endCharacter: Type.Optional(Type.Number({
|
|
189
|
+
description: "End character (1-based). Optional; used by codeAction range.",
|
|
190
|
+
})),
|
|
191
|
+
newName: Type.Optional(Type.String({
|
|
192
|
+
description: "Required for rename operation.",
|
|
193
|
+
})),
|
|
48
194
|
query: Type.Optional(Type.String({
|
|
49
|
-
description: "Symbol name to search. Used by workspaceSymbol",
|
|
195
|
+
description: "Symbol name to search. Used by workspaceSymbol (best with filePath for active project context).",
|
|
50
196
|
})),
|
|
51
197
|
callHierarchyItem: Type.Optional(Type.Object({
|
|
52
198
|
name: Type.String(),
|
|
@@ -77,8 +223,34 @@ export function createLspNavigationTool(getFlag) {
|
|
|
77
223
|
})),
|
|
78
224
|
}),
|
|
79
225
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
80
|
-
|
|
226
|
+
const startedAt = Date.now();
|
|
227
|
+
let supported = null;
|
|
228
|
+
let diagnosticsMode = "unknown";
|
|
229
|
+
const finalize = (payload, meta) => {
|
|
230
|
+
const normalizedFilePath = meta.filePath.replace(/\\/g, "/");
|
|
231
|
+
logLatency({
|
|
232
|
+
type: "phase",
|
|
233
|
+
phase: "lsp_navigation_result",
|
|
234
|
+
filePath: normalizedFilePath,
|
|
235
|
+
durationMs: Date.now() - startedAt,
|
|
236
|
+
metadata: {
|
|
237
|
+
operation: meta.operation,
|
|
238
|
+
failureKind: meta.failureKind,
|
|
239
|
+
resultCount: meta.resultCount,
|
|
240
|
+
supported,
|
|
241
|
+
diagnosticsMode,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
81
244
|
return {
|
|
245
|
+
...payload,
|
|
246
|
+
details: {
|
|
247
|
+
...(payload.details ?? {}),
|
|
248
|
+
failureKind: meta.failureKind,
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
if (!getFlag("lens-lsp") || getFlag("no-lsp")) {
|
|
253
|
+
return finalize({
|
|
82
254
|
content: [
|
|
83
255
|
{
|
|
84
256
|
type: "text",
|
|
@@ -86,110 +258,290 @@ export function createLspNavigationTool(getFlag) {
|
|
|
86
258
|
},
|
|
87
259
|
],
|
|
88
260
|
isError: true,
|
|
89
|
-
|
|
90
|
-
|
|
261
|
+
}, {
|
|
262
|
+
operation: "precheck",
|
|
263
|
+
filePath: "(workspace)",
|
|
264
|
+
failureKind: "lsp_disabled",
|
|
265
|
+
resultCount: 0,
|
|
266
|
+
});
|
|
91
267
|
}
|
|
92
|
-
const { operation, filePath: rawPath, line, character, query, } = params;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return {
|
|
268
|
+
const { operation, filePath: rawPath, line, character, endLine, endCharacter, newName, query, } = params;
|
|
269
|
+
const isCallHierarchyTraversal = operation === "incomingCalls" || operation === "outgoingCalls";
|
|
270
|
+
const needsFilePath = operation !== "workspaceDiagnostics" &&
|
|
271
|
+
operation !== "workspaceSymbol" &&
|
|
272
|
+
!isCallHierarchyTraversal;
|
|
273
|
+
if (needsFilePath && (!rawPath || rawPath.trim().length === 0)) {
|
|
274
|
+
return finalize({
|
|
100
275
|
content: [
|
|
101
276
|
{
|
|
102
277
|
type: "text",
|
|
103
|
-
text: `
|
|
278
|
+
text: `filePath is required for ${operation}`,
|
|
104
279
|
},
|
|
105
280
|
],
|
|
106
281
|
isError: true,
|
|
107
|
-
|
|
108
|
-
|
|
282
|
+
}, {
|
|
283
|
+
operation,
|
|
284
|
+
filePath: "(workspace)",
|
|
285
|
+
failureKind: "missing_file_path",
|
|
286
|
+
resultCount: 0,
|
|
287
|
+
});
|
|
109
288
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
289
|
+
const filePath = rawPath
|
|
290
|
+
? path.isAbsolute(rawPath)
|
|
291
|
+
? rawPath
|
|
292
|
+
: path.resolve(ctx.cwd || ".", rawPath)
|
|
293
|
+
: "";
|
|
294
|
+
const lspService = getLSPService();
|
|
295
|
+
if (operation === "workspaceDiagnostics") {
|
|
296
|
+
const wsDiagSupport = await lspService.getWorkspaceDiagnosticsSupport(rawPath ? filePath : undefined);
|
|
297
|
+
diagnosticsMode = wsDiagSupport?.mode ?? "unknown";
|
|
298
|
+
if (rawPath) {
|
|
299
|
+
const hasLSP = await lspService.hasLSP(filePath);
|
|
300
|
+
if (!hasLSP) {
|
|
301
|
+
return finalize({
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: "text",
|
|
305
|
+
text: `No LSP server available for ${path.basename(filePath)}. Check that the language server is installed.`,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
isError: true,
|
|
309
|
+
}, {
|
|
310
|
+
operation,
|
|
311
|
+
filePath,
|
|
312
|
+
failureKind: "no_server",
|
|
313
|
+
resultCount: 0,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
await openFileBestEffort(lspService, filePath, true);
|
|
317
|
+
const diagnostics = await lspService.getDiagnostics(filePath);
|
|
318
|
+
const result = [
|
|
319
|
+
{
|
|
320
|
+
filePath,
|
|
321
|
+
diagnostics,
|
|
322
|
+
count: diagnostics.length,
|
|
323
|
+
},
|
|
324
|
+
];
|
|
325
|
+
const note = diagnosticsMode === "pull"
|
|
326
|
+
? "Note: filePath mode requests pull diagnostics for this file and returns the aggregated result."
|
|
327
|
+
: diagnosticsMode === "push-only"
|
|
328
|
+
? "Note: server is push-only; result depends on published diagnostics for this file."
|
|
329
|
+
: "Note: workspace diagnostics mode unknown (no active capability snapshot).";
|
|
330
|
+
const resultCount = diagnostics.length;
|
|
331
|
+
return finalize({
|
|
332
|
+
content: [
|
|
333
|
+
{
|
|
334
|
+
type: "text",
|
|
335
|
+
text: `${note}\n${JSON.stringify(result, null, 2)}`,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
details: {
|
|
339
|
+
operation,
|
|
340
|
+
resultCount,
|
|
341
|
+
diagnosticsMode,
|
|
342
|
+
coverage: "requested-file",
|
|
343
|
+
},
|
|
344
|
+
}, {
|
|
345
|
+
operation,
|
|
346
|
+
filePath,
|
|
347
|
+
failureKind: resultCount === 0 ? "empty_result" : "success",
|
|
348
|
+
resultCount,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const allDiagnostics = await lspService.getAllDiagnostics();
|
|
352
|
+
const result = Array.from(allDiagnostics.entries()).map(([trackedFile, diags]) => ({
|
|
353
|
+
filePath: trackedFile,
|
|
354
|
+
diagnostics: diags,
|
|
355
|
+
count: diags.length,
|
|
356
|
+
}));
|
|
357
|
+
const note = diagnosticsMode === "push-only"
|
|
358
|
+
? "Note: push-only tracked diagnostics snapshot (not full workspace pull diagnostics)."
|
|
359
|
+
: diagnosticsMode === "pull"
|
|
360
|
+
? "Note: tracked diagnostics snapshot from active clients. Provide filePath to force file-level diagnostics collection."
|
|
361
|
+
: "Note: workspace diagnostics mode unknown (no active capability snapshot).";
|
|
362
|
+
return finalize({
|
|
363
|
+
content: [
|
|
364
|
+
{
|
|
365
|
+
type: "text",
|
|
366
|
+
text: `${note}\n${JSON.stringify(result, null, 2)}`,
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
details: {
|
|
370
|
+
operation,
|
|
371
|
+
resultCount: result.length,
|
|
372
|
+
diagnosticsMode,
|
|
373
|
+
coverage: "tracked-open-files",
|
|
374
|
+
},
|
|
375
|
+
}, {
|
|
376
|
+
operation,
|
|
377
|
+
filePath: rawPath ? filePath : "(workspace)",
|
|
378
|
+
failureKind: diagnosticsMode === "push-only"
|
|
379
|
+
? "tracked_snapshot"
|
|
380
|
+
: "success",
|
|
381
|
+
resultCount: result.length,
|
|
382
|
+
});
|
|
114
383
|
}
|
|
115
|
-
|
|
116
|
-
|
|
384
|
+
const hasLSP = filePath ? await lspService.hasLSP(filePath) : false;
|
|
385
|
+
if (needsFilePath && !hasLSP) {
|
|
386
|
+
return finalize({
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
type: "text",
|
|
390
|
+
text: `No LSP server available for ${path.basename(filePath)}. Check that the language server is installed.`,
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
isError: true,
|
|
394
|
+
}, {
|
|
395
|
+
operation,
|
|
396
|
+
filePath,
|
|
397
|
+
failureKind: "no_server",
|
|
398
|
+
resultCount: 0,
|
|
399
|
+
});
|
|
117
400
|
}
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
401
|
+
if (needsFilePath) {
|
|
402
|
+
const support = await lspService.getOperationSupport(filePath);
|
|
403
|
+
supported = operationSupportStatus(operation, support);
|
|
404
|
+
if (supported === false) {
|
|
405
|
+
return finalize({
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: "text",
|
|
409
|
+
text: `LSP server for ${path.basename(filePath)} does not advertise support for ${operation}`,
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
isError: true,
|
|
413
|
+
details: { operation, supported: false, emptyReason: "unsupported" },
|
|
414
|
+
}, { operation, filePath, failureKind: "unsupported", resultCount: 0 });
|
|
124
415
|
}
|
|
416
|
+
await openFileBestEffort(lspService, filePath);
|
|
125
417
|
}
|
|
126
418
|
// Convert 1-based editor coords to 0-based LSP coords
|
|
127
419
|
const lspLine = (line ?? 1) - 1;
|
|
128
420
|
const lspChar = (character ?? 1) - 1;
|
|
129
|
-
|
|
130
|
-
|
|
421
|
+
const lspEndLine = (endLine ?? line ?? 1) - 1;
|
|
422
|
+
const lspEndChar = (endCharacter ?? character ?? 1) - 1;
|
|
423
|
+
const runOperation = async () => {
|
|
131
424
|
switch (operation) {
|
|
132
425
|
case "definition":
|
|
133
|
-
|
|
134
|
-
break;
|
|
426
|
+
return lspService.definition(filePath, lspLine, lspChar);
|
|
135
427
|
case "references":
|
|
136
|
-
|
|
137
|
-
break;
|
|
428
|
+
return lspService.references(filePath, lspLine, lspChar);
|
|
138
429
|
case "hover":
|
|
139
|
-
|
|
140
|
-
|
|
430
|
+
return lspService.hover(filePath, lspLine, lspChar);
|
|
431
|
+
case "signatureHelp":
|
|
432
|
+
return lspService.signatureHelp(filePath, lspLine, lspChar);
|
|
141
433
|
case "documentSymbol":
|
|
142
|
-
|
|
143
|
-
break;
|
|
434
|
+
return lspService.documentSymbol(filePath);
|
|
144
435
|
case "workspaceSymbol":
|
|
145
|
-
|
|
146
|
-
|
|
436
|
+
supported = operationSupportStatus(operation, await lspService.getOperationSupport(rawPath ? filePath : undefined));
|
|
437
|
+
if (supported === false) {
|
|
438
|
+
throw new Error("__UNSUPPORTED__ Active LSP server does not advertise support for workspaceSymbol");
|
|
439
|
+
}
|
|
440
|
+
if (!query || query.trim().length === 0) {
|
|
441
|
+
throw new Error("__BADINPUT__ query parameter required for workspaceSymbol");
|
|
442
|
+
}
|
|
443
|
+
if (rawPath) {
|
|
444
|
+
await openFileBestEffort(lspService, filePath);
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
return await lspService.workspaceSymbol(query ?? "", rawPath ? filePath : undefined);
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
451
|
+
if (rawPath && /No Project/i.test(msg)) {
|
|
452
|
+
await openFileBestEffort(lspService, filePath);
|
|
453
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
454
|
+
return lspService.workspaceSymbol(query ?? "", filePath);
|
|
455
|
+
}
|
|
456
|
+
throw err;
|
|
457
|
+
}
|
|
458
|
+
case "codeAction":
|
|
459
|
+
return lspService.codeAction(filePath, lspLine, lspChar, lspEndLine, lspEndChar);
|
|
460
|
+
case "rename":
|
|
461
|
+
if (!newName || newName.trim().length === 0) {
|
|
462
|
+
throw new Error("__BADINPUT__ newName parameter required for rename");
|
|
463
|
+
}
|
|
464
|
+
return lspService.rename(filePath, lspLine, lspChar, newName);
|
|
147
465
|
case "implementation":
|
|
148
|
-
|
|
149
|
-
break;
|
|
466
|
+
return lspService.implementation(filePath, lspLine, lspChar);
|
|
150
467
|
case "prepareCallHierarchy":
|
|
151
|
-
|
|
152
|
-
break;
|
|
468
|
+
return lspService.prepareCallHierarchy(filePath, lspLine, lspChar);
|
|
153
469
|
case "incomingCalls": {
|
|
154
470
|
const callItem = params.callHierarchyItem;
|
|
155
471
|
if (!callItem) {
|
|
156
|
-
|
|
157
|
-
content: [
|
|
158
|
-
{
|
|
159
|
-
type: "text",
|
|
160
|
-
text: "callHierarchyItem parameter required for incomingCalls",
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
isError: true,
|
|
164
|
-
details: {},
|
|
165
|
-
};
|
|
472
|
+
throw new Error("__BADINPUT__ callHierarchyItem parameter required for incomingCalls");
|
|
166
473
|
}
|
|
167
|
-
|
|
168
|
-
break;
|
|
474
|
+
return lspService.incomingCalls(callItem);
|
|
169
475
|
}
|
|
170
476
|
case "outgoingCalls": {
|
|
171
477
|
const callItem = params.callHierarchyItem;
|
|
172
478
|
if (!callItem) {
|
|
173
|
-
|
|
174
|
-
content: [
|
|
175
|
-
{
|
|
176
|
-
type: "text",
|
|
177
|
-
text: "callHierarchyItem parameter required for outgoingCalls",
|
|
178
|
-
},
|
|
179
|
-
],
|
|
180
|
-
isError: true,
|
|
181
|
-
details: {},
|
|
182
|
-
};
|
|
479
|
+
throw new Error("__BADINPUT__ callHierarchyItem parameter required for outgoingCalls");
|
|
183
480
|
}
|
|
184
|
-
|
|
185
|
-
break;
|
|
481
|
+
return lspService.outgoingCalls(callItem);
|
|
186
482
|
}
|
|
187
483
|
default:
|
|
188
|
-
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
let result;
|
|
488
|
+
let usedDocumentSymbolFallback = false;
|
|
489
|
+
try {
|
|
490
|
+
result = await runOperation();
|
|
491
|
+
const isEmptyInitial = !result || (Array.isArray(result) && result.length === 0);
|
|
492
|
+
const shouldRetryOnEmpty = isEmptyInitial &&
|
|
493
|
+
needsFilePath &&
|
|
494
|
+
[
|
|
495
|
+
"definition",
|
|
496
|
+
"references",
|
|
497
|
+
"hover",
|
|
498
|
+
"signatureHelp",
|
|
499
|
+
"workspaceSymbol",
|
|
500
|
+
"codeAction",
|
|
501
|
+
"rename",
|
|
502
|
+
"implementation",
|
|
503
|
+
].includes(operation);
|
|
504
|
+
if (shouldRetryOnEmpty) {
|
|
505
|
+
await openFileBestEffort(lspService, filePath, true);
|
|
506
|
+
result = await runOperation();
|
|
507
|
+
}
|
|
508
|
+
const stillEmpty = !result || (Array.isArray(result) && result.length === 0);
|
|
509
|
+
if (stillEmpty &&
|
|
510
|
+
needsFilePath &&
|
|
511
|
+
(operation === "definition" || operation === "workspaceSymbol")) {
|
|
512
|
+
const content = nodeFs.readFileSync(filePath, "utf-8");
|
|
513
|
+
const token = operation === "workspaceSymbol"
|
|
514
|
+
? (query?.trim() || undefined)
|
|
515
|
+
: line && character
|
|
516
|
+
? tokenAtPosition(content, line, character)
|
|
517
|
+
: undefined;
|
|
518
|
+
if (token) {
|
|
519
|
+
const docSymbols = (await lspService.documentSymbol(filePath));
|
|
520
|
+
const locations = pickLocalSymbolLocation(docSymbols, token, filePath);
|
|
521
|
+
if (locations.length > 0) {
|
|
522
|
+
result = locations;
|
|
523
|
+
usedDocumentSymbolFallback = true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
189
526
|
}
|
|
190
527
|
}
|
|
191
528
|
catch (err) {
|
|
192
|
-
|
|
529
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
530
|
+
if (msg.startsWith("__UNSUPPORTED__ ")) {
|
|
531
|
+
return finalize({
|
|
532
|
+
content: [{ type: "text", text: msg.replace("__UNSUPPORTED__ ", "") }],
|
|
533
|
+
isError: true,
|
|
534
|
+
details: { operation, supported: false, emptyReason: "unsupported" },
|
|
535
|
+
}, { operation, filePath, failureKind: "unsupported", resultCount: 0 });
|
|
536
|
+
}
|
|
537
|
+
if (msg.startsWith("__BADINPUT__ ")) {
|
|
538
|
+
return finalize({
|
|
539
|
+
content: [{ type: "text", text: msg.replace("__BADINPUT__ ", "") }],
|
|
540
|
+
isError: true,
|
|
541
|
+
details: {},
|
|
542
|
+
}, { operation, filePath, failureKind: "bad_input", resultCount: 0 });
|
|
543
|
+
}
|
|
544
|
+
return finalize({
|
|
193
545
|
content: [
|
|
194
546
|
{
|
|
195
547
|
type: "text",
|
|
@@ -198,19 +550,54 @@ export function createLspNavigationTool(getFlag) {
|
|
|
198
550
|
],
|
|
199
551
|
isError: true,
|
|
200
552
|
details: {},
|
|
201
|
-
};
|
|
553
|
+
}, { operation, filePath, failureKind: "lsp_error", resultCount: 0 });
|
|
202
554
|
}
|
|
203
555
|
const isEmpty = !result || (Array.isArray(result) && result.length === 0);
|
|
204
|
-
|
|
556
|
+
let output = isEmpty
|
|
205
557
|
? `No results for ${operation} at ${path.basename(filePath)}${line ? `:${line}:${character}` : ""}`
|
|
206
558
|
: JSON.stringify(result, null, 2);
|
|
207
|
-
|
|
559
|
+
if (isEmpty && operation === "workspaceSymbol" && !rawPath) {
|
|
560
|
+
output +=
|
|
561
|
+
"\nHint: provide filePath to scope workspaceSymbol to the active language server/root.";
|
|
562
|
+
}
|
|
563
|
+
if (usedDocumentSymbolFallback) {
|
|
564
|
+
output += "\nNote: served from documentSymbol fallback due to empty primary result.";
|
|
565
|
+
}
|
|
566
|
+
if (operation === "references" &&
|
|
567
|
+
Array.isArray(result) &&
|
|
568
|
+
result.length <= 2) {
|
|
569
|
+
output +=
|
|
570
|
+
"\nHint: references from usage sites can be partial; retry from the symbol definition for broader cross-file results.";
|
|
571
|
+
}
|
|
572
|
+
const actionStats = operation === "codeAction" && Array.isArray(result)
|
|
573
|
+
? classifyCodeActions(result)
|
|
574
|
+
: null;
|
|
575
|
+
if (operation === "codeAction" && actionStats) {
|
|
576
|
+
if (actionStats.quickfix === 0 && actionStats.refactor > 0) {
|
|
577
|
+
output +=
|
|
578
|
+
"\nNote: no diagnostic quick fixes returned; refactor-only actions available.";
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const resultCount = Array.isArray(result) ? result.length : result ? 1 : 0;
|
|
582
|
+
return finalize({
|
|
208
583
|
content: [{ type: "text", text: output }],
|
|
209
584
|
details: {
|
|
210
585
|
operation,
|
|
211
|
-
|
|
586
|
+
supported,
|
|
587
|
+
emptyReason: isEmpty ? emptyReasonForOperation(operation) : undefined,
|
|
588
|
+
codeActionKinds: actionStats ?? undefined,
|
|
589
|
+
resultCount,
|
|
212
590
|
},
|
|
213
|
-
}
|
|
591
|
+
}, {
|
|
592
|
+
operation,
|
|
593
|
+
filePath: rawPath ? filePath : "(workspace)",
|
|
594
|
+
failureKind: isEmpty
|
|
595
|
+
? "empty_result"
|
|
596
|
+
: usedDocumentSymbolFallback
|
|
597
|
+
? "fallback_success"
|
|
598
|
+
: "success",
|
|
599
|
+
resultCount,
|
|
600
|
+
});
|
|
214
601
|
},
|
|
215
602
|
};
|
|
216
603
|
}
|