pi-lens 3.8.21 → 3.8.22
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 +8 -0
- package/README.md +2 -0
- package/clients/dispatch/runners/lsp.ts +58 -3
- package/clients/dispatch/runners/tree-sitter.ts +467 -0
- package/clients/lsp/client.ts +229 -3
- package/clients/lsp/index.ts +111 -1
- package/clients/pipeline.ts +2 -2
- package/clients/runtime-session.ts +43 -5
- 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/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 +259 -28
- package/tools/lsp-navigation.ts +294 -29
package/tools/lsp-navigation.ts
CHANGED
|
@@ -10,24 +10,96 @@ import { Type } from "@sinclair/typebox";
|
|
|
10
10
|
import type { LSPCallHierarchyItem } from "../clients/lsp/client.js";
|
|
11
11
|
import { getLSPService } from "../clients/lsp/index.js";
|
|
12
12
|
|
|
13
|
+
function operationSupportStatus(
|
|
14
|
+
operation: string,
|
|
15
|
+
support: import("../clients/lsp/client.js").LSPOperationSupport | null,
|
|
16
|
+
): boolean | null {
|
|
17
|
+
if (!support) return null;
|
|
18
|
+
if (operation === "definition") return support.definition;
|
|
19
|
+
if (operation === "references") return support.references;
|
|
20
|
+
if (operation === "hover") return support.hover;
|
|
21
|
+
if (operation === "signatureHelp") return support.signatureHelp;
|
|
22
|
+
if (operation === "documentSymbol") return support.documentSymbol;
|
|
23
|
+
if (operation === "workspaceSymbol") return support.workspaceSymbol;
|
|
24
|
+
if (operation === "codeAction") return support.codeAction;
|
|
25
|
+
if (operation === "rename") return support.rename;
|
|
26
|
+
if (operation === "implementation") return support.implementation;
|
|
27
|
+
if (
|
|
28
|
+
operation === "prepareCallHierarchy" ||
|
|
29
|
+
operation === "incomingCalls" ||
|
|
30
|
+
operation === "outgoingCalls"
|
|
31
|
+
)
|
|
32
|
+
return support.callHierarchy;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function emptyReasonForOperation(operation: string): string {
|
|
37
|
+
if (operation === "signatureHelp") return "position-sensitive-or-no-signature";
|
|
38
|
+
if (operation === "codeAction") return "no-applicable-actions";
|
|
39
|
+
if (operation === "rename") return "no-rename-edits-or-symbol-not-renamable";
|
|
40
|
+
if (operation === "workspaceSymbol")
|
|
41
|
+
return "no-matching-symbols-or-server-index-unavailable";
|
|
42
|
+
if (operation === "incomingCalls" || operation === "outgoingCalls")
|
|
43
|
+
return "no-call-hierarchy-results";
|
|
44
|
+
return "no-results";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function classifyCodeActions(
|
|
48
|
+
actions: Array<{ kind?: string }> | undefined,
|
|
49
|
+
): { quickfix: number; refactor: number; other: number } {
|
|
50
|
+
if (!actions || actions.length === 0) return { quickfix: 0, refactor: 0, other: 0 };
|
|
51
|
+
let quickfix = 0;
|
|
52
|
+
let refactor = 0;
|
|
53
|
+
let other = 0;
|
|
54
|
+
for (const action of actions) {
|
|
55
|
+
const kind = action.kind ?? "";
|
|
56
|
+
if (kind.startsWith("quickfix")) quickfix += 1;
|
|
57
|
+
else if (kind.startsWith("refactor")) refactor += 1;
|
|
58
|
+
else other += 1;
|
|
59
|
+
}
|
|
60
|
+
return { quickfix, refactor, other };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function openFileBestEffort(
|
|
64
|
+
lspService: ReturnType<typeof getLSPService>,
|
|
65
|
+
filePath: string,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
let fileContent: string | undefined;
|
|
68
|
+
try {
|
|
69
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
70
|
+
} catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!fileContent) return;
|
|
74
|
+
try {
|
|
75
|
+
await lspService.openFile(filePath, fileContent);
|
|
76
|
+
} catch {
|
|
77
|
+
/* LSP server may not be ready yet — proceed anyway */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
13
81
|
export function createLspNavigationTool(
|
|
14
82
|
getFlag: (name: string) => boolean | string | undefined,
|
|
15
83
|
) {
|
|
16
84
|
return {
|
|
17
85
|
name: "lsp_navigation" as const,
|
|
18
86
|
label: "LSP Navigate",
|
|
19
|
-
|
|
87
|
+
description:
|
|
20
88
|
"Navigate code using LSP (Language Server Protocol). Requires --lens-lsp flag.\n" +
|
|
21
89
|
"Operations:\n" +
|
|
22
90
|
"- definition: Jump to where a symbol is defined\n" +
|
|
23
91
|
"- references: Find all usages of a symbol\n" +
|
|
24
92
|
"- hover: Get type/doc info at a position\n" +
|
|
93
|
+
"- signatureHelp: Show callable signatures at cursor\n" +
|
|
25
94
|
"- documentSymbol: List all symbols (functions/classes/vars) in a file\n" +
|
|
26
|
-
"- workspaceSymbol: Search symbols across the whole project\n" +
|
|
95
|
+
"- workspaceSymbol: Search symbols across the whole project (best with filePath context)\n" +
|
|
96
|
+
"- codeAction: Find available quick fixes/refactors at a range\n" +
|
|
97
|
+
"- rename: Compute workspace edits for renaming a symbol\n" +
|
|
27
98
|
"- implementation: Jump to interface implementations\n" +
|
|
28
99
|
"- prepareCallHierarchy: Get callable item at position (for incoming/outgoing)\n" +
|
|
29
100
|
"- incomingCalls: Find all functions/methods that CALL this function\n" +
|
|
30
|
-
"- outgoingCalls: Find all functions/methods CALLED by this function\n
|
|
101
|
+
"- outgoingCalls: Find all functions/methods CALLED by this function\n" +
|
|
102
|
+
"- workspaceDiagnostics: List all diagnostics tracked by active LSP clients\n\n" +
|
|
31
103
|
"Line and character are 1-based (as shown in editors).",
|
|
32
104
|
promptSnippet:
|
|
33
105
|
"Use lsp_navigation to find definitions, references, and hover info via LSP",
|
|
@@ -37,18 +109,25 @@ export function createLspNavigationTool(
|
|
|
37
109
|
Type.Literal("definition"),
|
|
38
110
|
Type.Literal("references"),
|
|
39
111
|
Type.Literal("hover"),
|
|
112
|
+
Type.Literal("signatureHelp"),
|
|
40
113
|
Type.Literal("documentSymbol"),
|
|
41
114
|
Type.Literal("workspaceSymbol"),
|
|
115
|
+
Type.Literal("codeAction"),
|
|
116
|
+
Type.Literal("rename"),
|
|
42
117
|
Type.Literal("implementation"),
|
|
43
118
|
Type.Literal("prepareCallHierarchy"),
|
|
44
119
|
Type.Literal("incomingCalls"),
|
|
45
120
|
Type.Literal("outgoingCalls"),
|
|
121
|
+
Type.Literal("workspaceDiagnostics"),
|
|
46
122
|
],
|
|
47
123
|
{ description: "LSP operation to perform" },
|
|
48
124
|
),
|
|
49
|
-
filePath: Type.
|
|
50
|
-
|
|
51
|
-
|
|
125
|
+
filePath: Type.Optional(
|
|
126
|
+
Type.String({
|
|
127
|
+
description:
|
|
128
|
+
"Absolute or relative file path. Required for file-scoped operations; optional for workspaceSymbol/workspaceDiagnostics.",
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
52
131
|
line: Type.Optional(
|
|
53
132
|
Type.Number({
|
|
54
133
|
description:
|
|
@@ -61,9 +140,27 @@ export function createLspNavigationTool(
|
|
|
61
140
|
"Character offset (1-based). Required for definition/references/hover/implementation",
|
|
62
141
|
}),
|
|
63
142
|
),
|
|
143
|
+
endLine: Type.Optional(
|
|
144
|
+
Type.Number({
|
|
145
|
+
description:
|
|
146
|
+
"End line (1-based). Optional; used by codeAction range.",
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
149
|
+
endCharacter: Type.Optional(
|
|
150
|
+
Type.Number({
|
|
151
|
+
description:
|
|
152
|
+
"End character (1-based). Optional; used by codeAction range.",
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
newName: Type.Optional(
|
|
156
|
+
Type.String({
|
|
157
|
+
description: "Required for rename operation.",
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
64
160
|
query: Type.Optional(
|
|
65
161
|
Type.String({
|
|
66
|
-
description:
|
|
162
|
+
description:
|
|
163
|
+
"Symbol name to search. Used by workspaceSymbol (best with filePath for active project context).",
|
|
67
164
|
}),
|
|
68
165
|
),
|
|
69
166
|
callHierarchyItem: Type.Optional(
|
|
@@ -107,6 +204,9 @@ export function createLspNavigationTool(
|
|
|
107
204
|
_onUpdate: unknown,
|
|
108
205
|
ctx: { cwd?: string },
|
|
109
206
|
) {
|
|
207
|
+
let supported: boolean | null = null;
|
|
208
|
+
let diagnosticsMode: "pull" | "push-only" | "unknown" = "unknown";
|
|
209
|
+
|
|
110
210
|
if (!getFlag("lens-lsp") || getFlag("no-lsp")) {
|
|
111
211
|
return {
|
|
112
212
|
content: [
|
|
@@ -125,22 +225,84 @@ export function createLspNavigationTool(
|
|
|
125
225
|
filePath: rawPath,
|
|
126
226
|
line,
|
|
127
227
|
character,
|
|
228
|
+
endLine,
|
|
229
|
+
endCharacter,
|
|
230
|
+
newName,
|
|
128
231
|
query,
|
|
129
232
|
} = params as {
|
|
130
233
|
operation: string;
|
|
131
|
-
filePath
|
|
234
|
+
filePath?: string;
|
|
132
235
|
line?: number;
|
|
133
236
|
character?: number;
|
|
237
|
+
endLine?: number;
|
|
238
|
+
endCharacter?: number;
|
|
239
|
+
newName?: string;
|
|
134
240
|
query?: string;
|
|
135
241
|
};
|
|
136
242
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
243
|
+
const isCallHierarchyTraversal =
|
|
244
|
+
operation === "incomingCalls" || operation === "outgoingCalls";
|
|
245
|
+
const needsFilePath =
|
|
246
|
+
operation !== "workspaceDiagnostics" &&
|
|
247
|
+
operation !== "workspaceSymbol" &&
|
|
248
|
+
!isCallHierarchyTraversal;
|
|
249
|
+
if (needsFilePath && (!rawPath || rawPath.trim().length === 0)) {
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: "text" as const,
|
|
254
|
+
text: `filePath is required for ${operation}`,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
isError: true,
|
|
258
|
+
details: {},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const filePath = rawPath
|
|
263
|
+
? path.isAbsolute(rawPath)
|
|
264
|
+
? rawPath
|
|
265
|
+
: path.resolve(ctx.cwd || ".", rawPath)
|
|
266
|
+
: "";
|
|
140
267
|
|
|
141
268
|
const lspService = getLSPService();
|
|
142
|
-
|
|
143
|
-
|
|
269
|
+
if (operation === "workspaceDiagnostics") {
|
|
270
|
+
const allDiagnostics = await lspService.getAllDiagnostics();
|
|
271
|
+
const wsDiagSupport = await lspService.getWorkspaceDiagnosticsSupport(
|
|
272
|
+
rawPath ? filePath : undefined,
|
|
273
|
+
);
|
|
274
|
+
diagnosticsMode = wsDiagSupport?.mode ?? "unknown";
|
|
275
|
+
const result = Array.from(allDiagnostics.entries()).map(
|
|
276
|
+
([trackedFile, diags]) => ({
|
|
277
|
+
filePath: trackedFile,
|
|
278
|
+
diagnostics: diags,
|
|
279
|
+
count: diags.length,
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
const note =
|
|
283
|
+
diagnosticsMode === "push-only"
|
|
284
|
+
? "Note: push-only tracked diagnostics snapshot (not full workspace pull diagnostics)."
|
|
285
|
+
: diagnosticsMode === "pull"
|
|
286
|
+
? "Note: server advertises workspace pull diagnostics support."
|
|
287
|
+
: "Note: workspace diagnostics mode unknown (no active capability snapshot).";
|
|
288
|
+
return {
|
|
289
|
+
content: [
|
|
290
|
+
{
|
|
291
|
+
type: "text" as const,
|
|
292
|
+
text: `${note}\n${JSON.stringify(result, null, 2)}`,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
details: {
|
|
296
|
+
operation,
|
|
297
|
+
resultCount: result.length,
|
|
298
|
+
diagnosticsMode,
|
|
299
|
+
coverage: "tracked-open-files",
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const hasLSP = filePath ? await lspService.hasLSP(filePath) : false;
|
|
305
|
+
if (needsFilePath && !hasLSP) {
|
|
144
306
|
return {
|
|
145
307
|
content: [
|
|
146
308
|
{
|
|
@@ -153,24 +315,30 @@ export function createLspNavigationTool(
|
|
|
153
315
|
};
|
|
154
316
|
}
|
|
155
317
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
318
|
+
if (needsFilePath) {
|
|
319
|
+
const support = await lspService.getOperationSupport(filePath);
|
|
320
|
+
supported = operationSupportStatus(operation, support);
|
|
321
|
+
if (supported === false) {
|
|
322
|
+
return {
|
|
323
|
+
content: [
|
|
324
|
+
{
|
|
325
|
+
type: "text" as const,
|
|
326
|
+
text: `LSP server for ${path.basename(filePath)} does not advertise support for ${operation}`,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
isError: true,
|
|
330
|
+
details: { operation, supported: false, emptyReason: "unsupported" },
|
|
331
|
+
};
|
|
168
332
|
}
|
|
333
|
+
|
|
334
|
+
await openFileBestEffort(lspService, filePath);
|
|
169
335
|
}
|
|
170
336
|
|
|
171
337
|
// Convert 1-based editor coords to 0-based LSP coords
|
|
172
338
|
const lspLine = (line ?? 1) - 1;
|
|
173
339
|
const lspChar = (character ?? 1) - 1;
|
|
340
|
+
const lspEndLine = (endLine ?? line ?? 1) - 1;
|
|
341
|
+
const lspEndChar = (endCharacter ?? character ?? 1) - 1;
|
|
174
342
|
|
|
175
343
|
let result: unknown;
|
|
176
344
|
try {
|
|
@@ -184,19 +352,91 @@ export function createLspNavigationTool(
|
|
|
184
352
|
case "hover":
|
|
185
353
|
result = await lspService.hover(filePath, lspLine, lspChar);
|
|
186
354
|
break;
|
|
355
|
+
case "signatureHelp":
|
|
356
|
+
result = await lspService.signatureHelp(filePath, lspLine, lspChar);
|
|
357
|
+
break;
|
|
187
358
|
case "documentSymbol":
|
|
188
359
|
result = await lspService.documentSymbol(filePath);
|
|
189
360
|
break;
|
|
190
361
|
case "workspaceSymbol":
|
|
191
|
-
|
|
362
|
+
supported = operationSupportStatus(
|
|
363
|
+
operation,
|
|
364
|
+
await lspService.getOperationSupport(rawPath ? filePath : undefined),
|
|
365
|
+
);
|
|
366
|
+
if (supported === false) {
|
|
367
|
+
return {
|
|
368
|
+
content: [
|
|
369
|
+
{
|
|
370
|
+
type: "text" as const,
|
|
371
|
+
text: "Active LSP server does not advertise support for workspaceSymbol",
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
isError: true,
|
|
375
|
+
details: {
|
|
376
|
+
operation,
|
|
377
|
+
supported: false,
|
|
378
|
+
emptyReason: "unsupported",
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
if (!query || query.trim().length === 0) {
|
|
383
|
+
return {
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: "text" as const,
|
|
387
|
+
text: "query parameter required for workspaceSymbol",
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
isError: true,
|
|
391
|
+
details: {},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (rawPath) {
|
|
395
|
+
await openFileBestEffort(lspService, filePath);
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
result = await lspService.workspaceSymbol(
|
|
399
|
+
query ?? "",
|
|
400
|
+
rawPath ? filePath : undefined,
|
|
401
|
+
);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
404
|
+
if (rawPath && /No Project/i.test(msg)) {
|
|
405
|
+
await openFileBestEffort(lspService, filePath);
|
|
406
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
407
|
+
result = await lspService.workspaceSymbol(query ?? "", filePath);
|
|
408
|
+
} else {
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
192
412
|
break;
|
|
193
|
-
case "
|
|
194
|
-
result = await lspService.
|
|
413
|
+
case "codeAction":
|
|
414
|
+
result = await lspService.codeAction(
|
|
195
415
|
filePath,
|
|
196
416
|
lspLine,
|
|
197
417
|
lspChar,
|
|
418
|
+
lspEndLine,
|
|
419
|
+
lspEndChar,
|
|
198
420
|
);
|
|
199
421
|
break;
|
|
422
|
+
case "rename":
|
|
423
|
+
if (!newName || newName.trim().length === 0) {
|
|
424
|
+
return {
|
|
425
|
+
content: [
|
|
426
|
+
{
|
|
427
|
+
type: "text" as const,
|
|
428
|
+
text: "newName parameter required for rename",
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
isError: true,
|
|
432
|
+
details: {},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
result = await lspService.rename(filePath, lspLine, lspChar, newName);
|
|
436
|
+
break;
|
|
437
|
+
case "implementation":
|
|
438
|
+
result = await lspService.implementation(filePath, lspLine, lspChar);
|
|
439
|
+
break;
|
|
200
440
|
case "prepareCallHierarchy":
|
|
201
441
|
result = await lspService.prepareCallHierarchy(
|
|
202
442
|
filePath,
|
|
@@ -259,14 +499,39 @@ export function createLspNavigationTool(
|
|
|
259
499
|
}
|
|
260
500
|
|
|
261
501
|
const isEmpty = !result || (Array.isArray(result) && result.length === 0);
|
|
262
|
-
|
|
502
|
+
let output = isEmpty
|
|
263
503
|
? `No results for ${operation} at ${path.basename(filePath)}${line ? `:${line}:${character}` : ""}`
|
|
264
504
|
: JSON.stringify(result, null, 2);
|
|
505
|
+
if (isEmpty && operation === "workspaceSymbol" && !rawPath) {
|
|
506
|
+
output +=
|
|
507
|
+
"\nHint: provide filePath to scope workspaceSymbol to the active language server/root.";
|
|
508
|
+
}
|
|
509
|
+
if (
|
|
510
|
+
operation === "references" &&
|
|
511
|
+
Array.isArray(result) &&
|
|
512
|
+
result.length <= 2
|
|
513
|
+
) {
|
|
514
|
+
output +=
|
|
515
|
+
"\nHint: references from usage sites can be partial; retry from the symbol definition for broader cross-file results.";
|
|
516
|
+
}
|
|
517
|
+
const actionStats =
|
|
518
|
+
operation === "codeAction" && Array.isArray(result)
|
|
519
|
+
? classifyCodeActions(result as Array<{ kind?: string }>)
|
|
520
|
+
: null;
|
|
521
|
+
if (operation === "codeAction" && actionStats) {
|
|
522
|
+
if (actionStats.quickfix === 0 && actionStats.refactor > 0) {
|
|
523
|
+
output +=
|
|
524
|
+
"\nNote: no diagnostic quick fixes returned; refactor-only actions available.";
|
|
525
|
+
}
|
|
526
|
+
}
|
|
265
527
|
|
|
266
528
|
return {
|
|
267
529
|
content: [{ type: "text" as const, text: output }],
|
|
268
530
|
details: {
|
|
269
531
|
operation,
|
|
532
|
+
supported,
|
|
533
|
+
emptyReason: isEmpty ? emptyReasonForOperation(operation) : undefined,
|
|
534
|
+
codeActionKinds: actionStats ?? undefined,
|
|
270
535
|
resultCount: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
271
536
|
},
|
|
272
537
|
};
|