@oh-my-pi/pi-coding-agent 10.2.2 → 10.3.0
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 +51 -0
- package/package.json +7 -7
- package/src/commit/agentic/prompts/analyze-file.md +7 -7
- package/src/commit/agentic/prompts/session-user.md +4 -4
- package/src/commit/agentic/prompts/system.md +14 -16
- package/src/commit/prompts/analysis-system.md +7 -9
- package/src/commit/prompts/analysis-user.md +0 -3
- package/src/commit/prompts/changelog-system.md +14 -19
- package/src/commit/prompts/file-observer-system.md +2 -2
- package/src/commit/prompts/reduce-system.md +13 -23
- package/src/commit/prompts/summary-system.md +7 -21
- package/src/config/settings-schema.ts +135 -56
- package/src/config/settings.ts +0 -6
- package/src/cursor.ts +2 -1
- package/src/extensibility/extensions/index.ts +0 -11
- package/src/extensibility/extensions/types.ts +1 -30
- package/src/extensibility/hooks/types.ts +1 -31
- package/src/index.ts +0 -11
- package/src/ipy/prelude.py +1 -113
- package/src/lsp/index.ts +66 -515
- package/src/lsp/render.ts +0 -11
- package/src/lsp/types.ts +3 -87
- package/src/modes/components/settings-defs.ts +3 -2
- package/src/modes/components/settings-selector.ts +14 -14
- package/src/modes/theme/theme.ts +45 -1
- package/src/prompts/agents/designer.md +23 -27
- package/src/prompts/agents/explore.md +28 -38
- package/src/prompts/agents/init.md +17 -17
- package/src/prompts/agents/plan.md +21 -27
- package/src/prompts/agents/reviewer.md +37 -37
- package/src/prompts/compaction/branch-summary.md +9 -9
- package/src/prompts/compaction/compaction-summary.md +8 -12
- package/src/prompts/compaction/compaction-update-summary.md +17 -19
- package/src/prompts/review-request.md +12 -13
- package/src/prompts/system/custom-system-prompt.md +6 -26
- package/src/prompts/system/plan-mode-active.md +23 -35
- package/src/prompts/system/plan-mode-subagent.md +7 -7
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/system-prompt.md +134 -149
- package/src/prompts/system/web-search.md +10 -10
- package/src/prompts/tools/ask.md +12 -15
- package/src/prompts/tools/bash.md +7 -7
- package/src/prompts/tools/exit-plan-mode.md +6 -6
- package/src/prompts/tools/gemini-image.md +4 -4
- package/src/prompts/tools/grep.md +4 -4
- package/src/prompts/tools/lsp.md +12 -19
- package/src/prompts/tools/patch.md +26 -30
- package/src/prompts/tools/python.md +14 -57
- package/src/prompts/tools/read.md +4 -4
- package/src/prompts/tools/replace.md +8 -8
- package/src/prompts/tools/ssh.md +14 -27
- package/src/prompts/tools/task.md +23 -35
- package/src/prompts/tools/todo-write.md +29 -38
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +0 -2
- package/src/session/agent-session.ts +27 -6
- package/src/system-prompt.ts +1 -219
- package/src/task/agents.ts +2 -1
- package/src/tools/bash-interceptor.ts +0 -24
- package/src/tools/bash.ts +1 -7
- package/src/tools/index.ts +8 -3
- package/src/tools/read.ts +74 -17
- package/src/tools/renderers.ts +0 -2
- package/src/lsp/rust-analyzer.ts +0 -184
- package/src/tools/ls.ts +0 -307
package/src/lsp/index.ts
CHANGED
|
@@ -22,17 +22,11 @@ import {
|
|
|
22
22
|
WARMUP_TIMEOUT_MS,
|
|
23
23
|
} from "./client";
|
|
24
24
|
import { getLinterClient } from "./clients";
|
|
25
|
-
import { getServersForFile,
|
|
25
|
+
import { getServersForFile, type LspConfig, loadConfig } from "./config";
|
|
26
26
|
import { applyTextEditsToString, applyWorkspaceEdit } from "./edits";
|
|
27
27
|
import { detectLspmux } from "./lspmux";
|
|
28
28
|
import { renderCall, renderResult } from "./render";
|
|
29
|
-
import * as rustAnalyzer from "./rust-analyzer";
|
|
30
29
|
import {
|
|
31
|
-
type CallHierarchyIncomingCall,
|
|
32
|
-
type CallHierarchyItem,
|
|
33
|
-
type CallHierarchyOutgoingCall,
|
|
34
|
-
type CodeAction,
|
|
35
|
-
type Command,
|
|
36
30
|
type Diagnostic,
|
|
37
31
|
type DocumentSymbol,
|
|
38
32
|
type Hover,
|
|
@@ -57,7 +51,6 @@ import {
|
|
|
57
51
|
formatSymbolInformation,
|
|
58
52
|
formatWorkspaceEdit,
|
|
59
53
|
symbolKindToIcon,
|
|
60
|
-
uriToFile,
|
|
61
54
|
} from "./utils";
|
|
62
55
|
|
|
63
56
|
export type { LspServerStatus } from "./client";
|
|
@@ -243,8 +236,6 @@ function getLspServerForFile(config: LspConfig, filePath: string): [string, Serv
|
|
|
243
236
|
return servers.length > 0 ? servers[0] : null;
|
|
244
237
|
}
|
|
245
238
|
|
|
246
|
-
const FILE_SEARCH_MAX_DEPTH = 5;
|
|
247
|
-
const IGNORED_DIRS = new Set(["node_modules", "target", "dist", "build", ".git"]);
|
|
248
239
|
const DIAGNOSTIC_MESSAGE_LIMIT = 50;
|
|
249
240
|
|
|
250
241
|
function limitDiagnosticMessages(messages: string[]): string[] {
|
|
@@ -254,84 +245,14 @@ function limitDiagnosticMessages(messages: string[]): string[] {
|
|
|
254
245
|
return messages.slice(0, DIAGNOSTIC_MESSAGE_LIMIT);
|
|
255
246
|
}
|
|
256
247
|
|
|
257
|
-
function findFileByExtensions(baseDir: string, extensions: string[], maxDepth: number): string | null {
|
|
258
|
-
const normalized = extensions.map(ext => ext.toLowerCase());
|
|
259
|
-
const search = (dir: string, depth: number): string | null => {
|
|
260
|
-
if (depth > maxDepth) return null;
|
|
261
|
-
const entries: fs.Dirent[] = [];
|
|
262
|
-
try {
|
|
263
|
-
const names = Array.from(new Bun.Glob("*").scanSync({ cwd: dir, onlyFiles: false }));
|
|
264
|
-
for (const name of names) {
|
|
265
|
-
const fullPath = path.join(dir, name);
|
|
266
|
-
let isDir = false;
|
|
267
|
-
try {
|
|
268
|
-
isDir = fs.statSync(fullPath).isDirectory();
|
|
269
|
-
} catch {
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
entries.push({ name, isFile: () => !isDir, isDirectory: () => isDir } as fs.Dirent);
|
|
273
|
-
}
|
|
274
|
-
} catch {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
for (const entry of entries) {
|
|
279
|
-
if (entry.name.startsWith(".")) continue;
|
|
280
|
-
if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
|
|
281
|
-
const fullPath = path.join(dir, entry.name);
|
|
282
|
-
|
|
283
|
-
if (entry.isFile()) {
|
|
284
|
-
const lowerName = entry.name.toLowerCase();
|
|
285
|
-
if (normalized.some(ext => lowerName.endsWith(ext))) {
|
|
286
|
-
return fullPath;
|
|
287
|
-
}
|
|
288
|
-
} else if (entry.isDirectory()) {
|
|
289
|
-
const found = search(fullPath, depth + 1);
|
|
290
|
-
if (found) return found;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
return null;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
return search(baseDir, 0);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function findFileForServer(cwd: string, serverConfig: ServerConfig): string | null {
|
|
300
|
-
return findFileByExtensions(cwd, serverConfig.fileTypes, FILE_SEARCH_MAX_DEPTH);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function getRustServer(config: LspConfig): [string, ServerConfig] | null {
|
|
304
|
-
const entries = getLspServers(config);
|
|
305
|
-
const byName = entries.find(([name, server]) => name === "rust-analyzer" || server.command === "rust-analyzer");
|
|
306
|
-
if (byName) return byName;
|
|
307
|
-
|
|
308
|
-
for (const [name, server] of entries) {
|
|
309
|
-
if (
|
|
310
|
-
hasCapability(server, "flycheck") ||
|
|
311
|
-
hasCapability(server, "ssr") ||
|
|
312
|
-
hasCapability(server, "runnables") ||
|
|
313
|
-
hasCapability(server, "expandMacro") ||
|
|
314
|
-
hasCapability(server, "relatedTests")
|
|
315
|
-
) {
|
|
316
|
-
return [name, server];
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
248
|
function getServerForWorkspaceAction(config: LspConfig, action: string): [string, ServerConfig] | null {
|
|
324
249
|
const entries = getLspServers(config);
|
|
325
250
|
if (entries.length === 0) return null;
|
|
326
251
|
|
|
327
|
-
if (action === "
|
|
252
|
+
if (action === "symbols" || action === "reload") {
|
|
328
253
|
return entries[0];
|
|
329
254
|
}
|
|
330
255
|
|
|
331
|
-
if (action === "flycheck" || action === "ssr" || action === "runnables" || action === "reload_workspace") {
|
|
332
|
-
return getRustServer(config);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
256
|
return null;
|
|
336
257
|
}
|
|
337
258
|
|
|
@@ -386,45 +307,9 @@ function detectProjectType(cwd: string): ProjectType {
|
|
|
386
307
|
}
|
|
387
308
|
|
|
388
309
|
/** Run workspace diagnostics command and parse output */
|
|
389
|
-
async function runWorkspaceDiagnostics(
|
|
390
|
-
cwd: string,
|
|
391
|
-
config: LspConfig,
|
|
392
|
-
): Promise<{ output: string; projectType: ProjectType }> {
|
|
310
|
+
async function runWorkspaceDiagnostics(cwd: string): Promise<{ output: string; projectType: ProjectType }> {
|
|
393
311
|
const projectType = detectProjectType(cwd);
|
|
394
312
|
|
|
395
|
-
// For Rust, use flycheck via rust-analyzer if available
|
|
396
|
-
if (projectType.type === "rust") {
|
|
397
|
-
const rustServer = getRustServer(config);
|
|
398
|
-
if (rustServer && hasCapability(rustServer[1], "flycheck")) {
|
|
399
|
-
const [_serverName, serverConfig] = rustServer;
|
|
400
|
-
try {
|
|
401
|
-
const client = await getOrCreateClient(serverConfig, cwd);
|
|
402
|
-
await rustAnalyzer.flycheck(client);
|
|
403
|
-
|
|
404
|
-
const collected: Array<{ filePath: string; diagnostic: Diagnostic }> = [];
|
|
405
|
-
for (const [diagUri, diags] of client.diagnostics.entries()) {
|
|
406
|
-
const relPath = path.relative(cwd, uriToFile(diagUri));
|
|
407
|
-
for (const diag of diags) {
|
|
408
|
-
collected.push({ filePath: relPath, diagnostic: diag });
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (collected.length === 0) {
|
|
413
|
-
return { output: "No issues found", projectType };
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const summary = formatDiagnosticsSummary(collected.map(d => d.diagnostic));
|
|
417
|
-
const formatted = collected.slice(0, 50).map(d => formatDiagnostic(d.diagnostic, d.filePath));
|
|
418
|
-
const more = collected.length > 50 ? `\n ... and ${collected.length - 50} more` : "";
|
|
419
|
-
return { output: `${summary}:\n${formatted.map(f => ` ${f}`).join("\n")}${more}`, projectType };
|
|
420
|
-
} catch (err) {
|
|
421
|
-
logger.debug("LSP diagnostics failed, falling back to shell", { error: String(err) });
|
|
422
|
-
// Fall through to shell command
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Fall back to shell command
|
|
428
313
|
if (!projectType.command) {
|
|
429
314
|
return {
|
|
430
315
|
output: `Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.mod), Python (pyproject.toml)`,
|
|
@@ -979,22 +864,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
979
864
|
_onUpdate?: AgentToolUpdateCallback<LspToolDetails>,
|
|
980
865
|
_context?: AgentToolContext,
|
|
981
866
|
): Promise<AgentToolResult<LspToolDetails>> {
|
|
982
|
-
const {
|
|
983
|
-
action,
|
|
984
|
-
file,
|
|
985
|
-
files,
|
|
986
|
-
line,
|
|
987
|
-
column,
|
|
988
|
-
end_line,
|
|
989
|
-
end_character,
|
|
990
|
-
query,
|
|
991
|
-
new_name,
|
|
992
|
-
replacement,
|
|
993
|
-
kind,
|
|
994
|
-
apply,
|
|
995
|
-
action_index,
|
|
996
|
-
include_declaration,
|
|
997
|
-
} = params;
|
|
867
|
+
const { action, file, files, line, column, query, new_name, apply, include_declaration } = params;
|
|
998
868
|
|
|
999
869
|
const config = getConfig(this.session.cwd);
|
|
1000
870
|
|
|
@@ -1020,27 +890,20 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1020
890
|
};
|
|
1021
891
|
}
|
|
1022
892
|
|
|
1023
|
-
// Workspace diagnostics - check entire project
|
|
1024
|
-
if (action === "workspace_diagnostics") {
|
|
1025
|
-
const result = await runWorkspaceDiagnostics(this.session.cwd, config);
|
|
1026
|
-
return {
|
|
1027
|
-
content: [
|
|
1028
|
-
{
|
|
1029
|
-
type: "text",
|
|
1030
|
-
text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
|
|
1031
|
-
},
|
|
1032
|
-
],
|
|
1033
|
-
details: { action, success: true, request: params },
|
|
1034
|
-
};
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
893
|
// Diagnostics can be batch or single-file - queries all applicable servers
|
|
1038
894
|
if (action === "diagnostics") {
|
|
1039
895
|
const targets = files?.length ? files : file ? [file] : null;
|
|
1040
896
|
if (!targets) {
|
|
897
|
+
// No file specified - run workspace diagnostics
|
|
898
|
+
const result = await runWorkspaceDiagnostics(this.session.cwd);
|
|
1041
899
|
return {
|
|
1042
|
-
content: [
|
|
1043
|
-
|
|
900
|
+
content: [
|
|
901
|
+
{
|
|
902
|
+
type: "text",
|
|
903
|
+
text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
details: { action, success: true, request: params },
|
|
1044
907
|
};
|
|
1045
908
|
}
|
|
1046
909
|
|
|
@@ -1125,13 +988,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1125
988
|
};
|
|
1126
989
|
}
|
|
1127
990
|
|
|
1128
|
-
const requiresFile =
|
|
1129
|
-
!file &&
|
|
1130
|
-
action !== "workspace_symbols" &&
|
|
1131
|
-
action !== "flycheck" &&
|
|
1132
|
-
action !== "ssr" &&
|
|
1133
|
-
action !== "runnables" &&
|
|
1134
|
-
action !== "reload_workspace";
|
|
991
|
+
const requiresFile = !file && action !== "symbols" && action !== "reload";
|
|
1135
992
|
|
|
1136
993
|
if (requiresFile) {
|
|
1137
994
|
return {
|
|
@@ -1156,16 +1013,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1156
1013
|
|
|
1157
1014
|
try {
|
|
1158
1015
|
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1159
|
-
|
|
1160
|
-
if (action === "runnables" && !targetFile) {
|
|
1161
|
-
targetFile = findFileForServer(this.session.cwd, serverConfig);
|
|
1162
|
-
if (!targetFile) {
|
|
1163
|
-
return {
|
|
1164
|
-
content: [{ type: "text", text: "Error: no matching files found for runnables" }],
|
|
1165
|
-
details: { action, serverName, success: false },
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1016
|
+
const targetFile = resolvedFile;
|
|
1169
1017
|
|
|
1170
1018
|
if (targetFile) {
|
|
1171
1019
|
await ensureFileOpen(client, targetFile);
|
|
@@ -1245,52 +1093,47 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1245
1093
|
}
|
|
1246
1094
|
|
|
1247
1095
|
case "symbols": {
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
// Hierarchical
|
|
1264
|
-
const lines = (result as DocumentSymbol[]).flatMap(s => formatDocumentSymbol(s));
|
|
1265
|
-
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
1096
|
+
// If no file, do workspace symbol search (requires query)
|
|
1097
|
+
if (!targetFile) {
|
|
1098
|
+
if (!query) {
|
|
1099
|
+
return {
|
|
1100
|
+
content: [
|
|
1101
|
+
{ type: "text", text: "Error: query parameter required for workspace symbol search" },
|
|
1102
|
+
],
|
|
1103
|
+
details: { action, serverName, success: false },
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
const result = (await sendRequest(client, "workspace/symbol", { query })) as
|
|
1107
|
+
| SymbolInformation[]
|
|
1108
|
+
| null;
|
|
1109
|
+
if (!result || result.length === 0) {
|
|
1110
|
+
output = `No symbols matching "${query}"`;
|
|
1266
1111
|
} else {
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const line = s.location.range.start.line + 1;
|
|
1270
|
-
const icon = symbolKindToIcon(s.kind);
|
|
1271
|
-
return `${icon} ${s.name} @ line ${line}`;
|
|
1272
|
-
});
|
|
1273
|
-
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
1112
|
+
const lines = result.map(s => formatSymbolInformation(s, this.session.cwd));
|
|
1113
|
+
output = `Found ${result.length} symbol(s) matching "${query}":\n${lines.map(l => ` ${l}`).join("\n")}`;
|
|
1274
1114
|
}
|
|
1275
|
-
}
|
|
1276
|
-
break;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
case "workspace_symbols": {
|
|
1280
|
-
if (!query) {
|
|
1281
|
-
return {
|
|
1282
|
-
content: [{ type: "text", text: "Error: query parameter required for workspace_symbols" }],
|
|
1283
|
-
details: { action, serverName, success: false },
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
const result = (await sendRequest(client, "workspace/symbol", { query })) as SymbolInformation[] | null;
|
|
1288
|
-
|
|
1289
|
-
if (!result || result.length === 0) {
|
|
1290
|
-
output = `No symbols matching "${query}"`;
|
|
1291
1115
|
} else {
|
|
1292
|
-
|
|
1293
|
-
|
|
1116
|
+
// File-based document symbols
|
|
1117
|
+
const result = (await sendRequest(client, "textDocument/documentSymbol", {
|
|
1118
|
+
textDocument: { uri },
|
|
1119
|
+
})) as (DocumentSymbol | SymbolInformation)[] | null;
|
|
1120
|
+
|
|
1121
|
+
if (!result || result.length === 0) {
|
|
1122
|
+
output = "No symbols found";
|
|
1123
|
+
} else {
|
|
1124
|
+
const relPath = path.relative(this.session.cwd, targetFile);
|
|
1125
|
+
if ("selectionRange" in result[0]) {
|
|
1126
|
+
const lines = (result as DocumentSymbol[]).flatMap(s => formatDocumentSymbol(s));
|
|
1127
|
+
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
1128
|
+
} else {
|
|
1129
|
+
const lines = (result as SymbolInformation[]).map(s => {
|
|
1130
|
+
const line = s.location.range.start.line + 1;
|
|
1131
|
+
const icon = symbolKindToIcon(s.kind);
|
|
1132
|
+
return `${icon} ${s.name} @ line ${line}`;
|
|
1133
|
+
});
|
|
1134
|
+
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1294
1137
|
}
|
|
1295
1138
|
break;
|
|
1296
1139
|
}
|
|
@@ -1324,314 +1167,22 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
|
|
|
1324
1167
|
break;
|
|
1325
1168
|
}
|
|
1326
1169
|
|
|
1327
|
-
case "
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const endLine = (end_line ?? line ?? 1) - 1;
|
|
1339
|
-
const endCharacter = (end_character ?? column ?? 1) - 1;
|
|
1340
|
-
const range = { start: position, end: { line: endLine, character: endCharacter } };
|
|
1341
|
-
const relevantDiagnostics = diagnostics.filter(
|
|
1342
|
-
d => d.range.start.line <= range.end.line && d.range.end.line >= range.start.line,
|
|
1343
|
-
);
|
|
1344
|
-
|
|
1345
|
-
const codeActionContext: { diagnostics: Diagnostic[]; only?: string[] } = {
|
|
1346
|
-
diagnostics: relevantDiagnostics,
|
|
1347
|
-
};
|
|
1348
|
-
if (kind) {
|
|
1349
|
-
codeActionContext.only = [kind];
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
const result = (await sendRequest(client, "textDocument/codeAction", {
|
|
1353
|
-
textDocument: { uri },
|
|
1354
|
-
range,
|
|
1355
|
-
context: codeActionContext,
|
|
1356
|
-
})) as Array<CodeAction | Command> | null;
|
|
1357
|
-
|
|
1358
|
-
if (!result || result.length === 0) {
|
|
1359
|
-
output = "No code actions available";
|
|
1360
|
-
} else if (action_index !== undefined) {
|
|
1361
|
-
// Apply specific action
|
|
1362
|
-
if (action_index < 0 || action_index >= result.length) {
|
|
1363
|
-
return {
|
|
1364
|
-
content: [
|
|
1365
|
-
{
|
|
1366
|
-
type: "text",
|
|
1367
|
-
text: `Error: action_index ${action_index} out of range (0-${result.length - 1})`,
|
|
1368
|
-
},
|
|
1369
|
-
],
|
|
1370
|
-
details: { action, serverName, success: false },
|
|
1371
|
-
};
|
|
1170
|
+
case "reload": {
|
|
1171
|
+
// Try graceful reload first, fall back to kill
|
|
1172
|
+
output = `Restarted ${serverName}`;
|
|
1173
|
+
const reloadMethods = ["rust-analyzer/reloadWorkspace", "workspace/didChangeConfiguration"];
|
|
1174
|
+
for (const method of reloadMethods) {
|
|
1175
|
+
try {
|
|
1176
|
+
await sendRequest(client, method, method.includes("Configuration") ? { settings: {} } : null);
|
|
1177
|
+
output = `Reloaded ${serverName}`;
|
|
1178
|
+
break;
|
|
1179
|
+
} catch {
|
|
1180
|
+
// Method not supported, try next
|
|
1372
1181
|
}
|
|
1373
|
-
|
|
1374
|
-
const isCommand = (candidate: CodeAction | Command): candidate is Command =>
|
|
1375
|
-
typeof (candidate as Command).command === "string";
|
|
1376
|
-
const isCodeAction = (candidate: CodeAction | Command): candidate is CodeAction =>
|
|
1377
|
-
!isCommand(candidate);
|
|
1378
|
-
const getCommandPayload = (
|
|
1379
|
-
candidate: CodeAction | Command,
|
|
1380
|
-
): { command: string; arguments?: unknown[] } | null => {
|
|
1381
|
-
if (isCommand(candidate)) {
|
|
1382
|
-
return { command: candidate.command, arguments: candidate.arguments };
|
|
1383
|
-
}
|
|
1384
|
-
if (candidate.command) {
|
|
1385
|
-
return { command: candidate.command.command, arguments: candidate.command.arguments };
|
|
1386
|
-
}
|
|
1387
|
-
return null;
|
|
1388
|
-
};
|
|
1389
|
-
|
|
1390
|
-
const codeAction = result[action_index];
|
|
1391
|
-
|
|
1392
|
-
// Resolve if needed
|
|
1393
|
-
let resolvedAction = codeAction;
|
|
1394
|
-
if (
|
|
1395
|
-
isCodeAction(codeAction) &&
|
|
1396
|
-
!codeAction.edit &&
|
|
1397
|
-
codeAction.data &&
|
|
1398
|
-
client.serverCapabilities?.codeActionProvider
|
|
1399
|
-
) {
|
|
1400
|
-
const provider = client.serverCapabilities.codeActionProvider;
|
|
1401
|
-
if (typeof provider === "object" && provider.resolveProvider) {
|
|
1402
|
-
resolvedAction = (await sendRequest(client, "codeAction/resolve", codeAction)) as CodeAction;
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
if (isCodeAction(resolvedAction) && resolvedAction.edit) {
|
|
1407
|
-
const applied = await applyWorkspaceEdit(resolvedAction.edit, this.session.cwd);
|
|
1408
|
-
output = `Applied "${codeAction.title}":\n${applied.map(a => ` ${a}`).join("\n")}`;
|
|
1409
|
-
} else {
|
|
1410
|
-
const commandPayload = getCommandPayload(resolvedAction);
|
|
1411
|
-
if (commandPayload) {
|
|
1412
|
-
await sendRequest(client, "workspace/executeCommand", commandPayload);
|
|
1413
|
-
output = `Executed "${codeAction.title}"`;
|
|
1414
|
-
} else {
|
|
1415
|
-
output = `Code action "${codeAction.title}" has no edits or command to apply`;
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
} else {
|
|
1419
|
-
// List available actions
|
|
1420
|
-
const lines = result.map((actionItem, i) => {
|
|
1421
|
-
if ("kind" in actionItem || "isPreferred" in actionItem || "edit" in actionItem) {
|
|
1422
|
-
const actionDetails = actionItem as CodeAction;
|
|
1423
|
-
const preferred = actionDetails.isPreferred ? " (preferred)" : "";
|
|
1424
|
-
const kindInfo = actionDetails.kind ? ` [${actionDetails.kind}]` : "";
|
|
1425
|
-
return ` [${i}] ${actionDetails.title}${kindInfo}${preferred}`;
|
|
1426
|
-
}
|
|
1427
|
-
return ` [${i}] ${actionItem.title}`;
|
|
1428
|
-
});
|
|
1429
|
-
output = `Available code actions:\n${lines.join("\n")}\n\nUse action_index parameter to apply a specific action.`;
|
|
1430
1182
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
case "incoming_calls":
|
|
1435
|
-
case "outgoing_calls": {
|
|
1436
|
-
// First, prepare the call hierarchy item at the cursor position
|
|
1437
|
-
const prepareResult = (await sendRequest(client, "textDocument/prepareCallHierarchy", {
|
|
1438
|
-
textDocument: { uri },
|
|
1439
|
-
position,
|
|
1440
|
-
})) as CallHierarchyItem[] | null;
|
|
1441
|
-
|
|
1442
|
-
if (!prepareResult || prepareResult.length === 0) {
|
|
1443
|
-
output = "No callable symbol found at this position";
|
|
1444
|
-
break;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
const item = prepareResult[0];
|
|
1448
|
-
|
|
1449
|
-
if (action === "incoming_calls") {
|
|
1450
|
-
const calls = (await sendRequest(client, "callHierarchy/incomingCalls", { item })) as
|
|
1451
|
-
| CallHierarchyIncomingCall[]
|
|
1452
|
-
| null;
|
|
1453
|
-
|
|
1454
|
-
if (!calls || calls.length === 0) {
|
|
1455
|
-
output = `No callers found for "${item.name}"`;
|
|
1456
|
-
} else {
|
|
1457
|
-
const lines = calls.map(call => {
|
|
1458
|
-
const loc = { uri: call.from.uri, range: call.from.selectionRange };
|
|
1459
|
-
const detail = call.from.detail ? ` (${call.from.detail})` : "";
|
|
1460
|
-
return ` ${call.from.name}${detail} @ ${formatLocation(loc, this.session.cwd)}`;
|
|
1461
|
-
});
|
|
1462
|
-
output = `Found ${calls.length} caller(s) of "${item.name}":\n${lines.join("\n")}`;
|
|
1463
|
-
}
|
|
1464
|
-
} else {
|
|
1465
|
-
const calls = (await sendRequest(client, "callHierarchy/outgoingCalls", { item })) as
|
|
1466
|
-
| CallHierarchyOutgoingCall[]
|
|
1467
|
-
| null;
|
|
1468
|
-
|
|
1469
|
-
if (!calls || calls.length === 0) {
|
|
1470
|
-
output = `"${item.name}" doesn't call any functions`;
|
|
1471
|
-
} else {
|
|
1472
|
-
const lines = calls.map(call => {
|
|
1473
|
-
const loc = { uri: call.to.uri, range: call.to.selectionRange };
|
|
1474
|
-
const detail = call.to.detail ? ` (${call.to.detail})` : "";
|
|
1475
|
-
return ` ${call.to.name}${detail} @ ${formatLocation(loc, this.session.cwd)}`;
|
|
1476
|
-
});
|
|
1477
|
-
output = `"${item.name}" calls ${calls.length} function(s):\n${lines.join("\n")}`;
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
break;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
// =====================================================================
|
|
1484
|
-
// Rust-Analyzer Specific Operations
|
|
1485
|
-
// =====================================================================
|
|
1486
|
-
|
|
1487
|
-
case "flycheck": {
|
|
1488
|
-
if (!hasCapability(serverConfig, "flycheck")) {
|
|
1489
|
-
return {
|
|
1490
|
-
content: [{ type: "text", text: "Error: flycheck requires rust-analyzer" }],
|
|
1491
|
-
details: { action, serverName, success: false },
|
|
1492
|
-
};
|
|
1183
|
+
if (output.startsWith("Restarted")) {
|
|
1184
|
+
client.proc.kill();
|
|
1493
1185
|
}
|
|
1494
|
-
|
|
1495
|
-
await rustAnalyzer.flycheck(client, resolvedFile ?? undefined);
|
|
1496
|
-
const collected: Array<{ filePath: string; diagnostic: Diagnostic }> = [];
|
|
1497
|
-
for (const [diagUri, diags] of client.diagnostics.entries()) {
|
|
1498
|
-
const relPath = path.relative(this.session.cwd, uriToFile(diagUri));
|
|
1499
|
-
for (const diag of diags) {
|
|
1500
|
-
collected.push({ filePath: relPath, diagnostic: diag });
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
if (collected.length === 0) {
|
|
1505
|
-
output = "Flycheck: no issues found";
|
|
1506
|
-
} else {
|
|
1507
|
-
const summary = formatDiagnosticsSummary(collected.map(d => d.diagnostic));
|
|
1508
|
-
const formatted = collected.slice(0, 20).map(d => formatDiagnostic(d.diagnostic, d.filePath));
|
|
1509
|
-
const more = collected.length > 20 ? `\n ... and ${collected.length - 20} more` : "";
|
|
1510
|
-
output = `Flycheck ${summary}:\n${formatted.map(f => ` ${f}`).join("\n")}${more}`;
|
|
1511
|
-
}
|
|
1512
|
-
break;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
case "expand_macro": {
|
|
1516
|
-
if (!hasCapability(serverConfig, "expandMacro")) {
|
|
1517
|
-
return {
|
|
1518
|
-
content: [{ type: "text", text: "Error: expand_macro requires rust-analyzer" }],
|
|
1519
|
-
details: { action, serverName, success: false },
|
|
1520
|
-
};
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
if (!targetFile) {
|
|
1524
|
-
return {
|
|
1525
|
-
content: [{ type: "text", text: "Error: file parameter required for expand_macro" }],
|
|
1526
|
-
details: { action, serverName, success: false },
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
const result = await rustAnalyzer.expandMacro(client, targetFile, line || 1, column || 1);
|
|
1531
|
-
if (!result) {
|
|
1532
|
-
output = "No macro expansion at this position";
|
|
1533
|
-
} else {
|
|
1534
|
-
output = `Macro: ${result.name}\n\nExpansion:\n${result.expansion}`;
|
|
1535
|
-
}
|
|
1536
|
-
break;
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
case "ssr": {
|
|
1540
|
-
if (!hasCapability(serverConfig, "ssr")) {
|
|
1541
|
-
return {
|
|
1542
|
-
content: [{ type: "text", text: "Error: ssr requires rust-analyzer" }],
|
|
1543
|
-
details: { action, serverName, success: false },
|
|
1544
|
-
};
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
if (!query) {
|
|
1548
|
-
return {
|
|
1549
|
-
content: [{ type: "text", text: "Error: query parameter (pattern) required for ssr" }],
|
|
1550
|
-
details: { action, serverName, success: false },
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
if (!replacement) {
|
|
1555
|
-
return {
|
|
1556
|
-
content: [{ type: "text", text: "Error: replacement parameter required for ssr" }],
|
|
1557
|
-
details: { action, serverName, success: false },
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
const shouldApply = apply === true;
|
|
1562
|
-
const result = await rustAnalyzer.ssr(client, query, replacement, !shouldApply);
|
|
1563
|
-
|
|
1564
|
-
if (shouldApply) {
|
|
1565
|
-
const applied = await applyWorkspaceEdit(result, this.session.cwd);
|
|
1566
|
-
output =
|
|
1567
|
-
applied.length > 0
|
|
1568
|
-
? `Applied SSR:\n${applied.map(a => ` ${a}`).join("\n")}`
|
|
1569
|
-
: "SSR: no matches found";
|
|
1570
|
-
} else {
|
|
1571
|
-
const preview = formatWorkspaceEdit(result, this.session.cwd);
|
|
1572
|
-
output =
|
|
1573
|
-
preview.length > 0
|
|
1574
|
-
? `SSR preview:\n${preview.map(p => ` ${p}`).join("\n")}`
|
|
1575
|
-
: "SSR: no matches found";
|
|
1576
|
-
}
|
|
1577
|
-
break;
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
case "runnables": {
|
|
1581
|
-
if (!hasCapability(serverConfig, "runnables")) {
|
|
1582
|
-
return {
|
|
1583
|
-
content: [{ type: "text", text: "Error: runnables requires rust-analyzer" }],
|
|
1584
|
-
details: { action, serverName, success: false },
|
|
1585
|
-
};
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
if (!targetFile) {
|
|
1589
|
-
return {
|
|
1590
|
-
content: [{ type: "text", text: "Error: file parameter required for runnables" }],
|
|
1591
|
-
details: { action, serverName, success: false },
|
|
1592
|
-
};
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
const result = await rustAnalyzer.runnables(client, targetFile, line);
|
|
1596
|
-
if (result.length === 0) {
|
|
1597
|
-
output = "No runnables found";
|
|
1598
|
-
} else {
|
|
1599
|
-
const lines = result.map(r => {
|
|
1600
|
-
const args = r.args?.cargoArgs?.join(" ") || "";
|
|
1601
|
-
return ` [${r.kind}] ${r.label}${args ? ` (cargo ${args})` : ""}`;
|
|
1602
|
-
});
|
|
1603
|
-
output = `Found ${result.length} runnable(s):\n${lines.join("\n")}`;
|
|
1604
|
-
}
|
|
1605
|
-
break;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
case "related_tests": {
|
|
1609
|
-
if (!hasCapability(serverConfig, "relatedTests")) {
|
|
1610
|
-
return {
|
|
1611
|
-
content: [{ type: "text", text: "Error: related_tests requires rust-analyzer" }],
|
|
1612
|
-
details: { action, serverName, success: false },
|
|
1613
|
-
};
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
if (!targetFile) {
|
|
1617
|
-
return {
|
|
1618
|
-
content: [{ type: "text", text: "Error: file parameter required for related_tests" }],
|
|
1619
|
-
details: { action, serverName, success: false },
|
|
1620
|
-
};
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
const result = await rustAnalyzer.relatedTests(client, targetFile, line || 1, column || 1);
|
|
1624
|
-
if (result.length === 0) {
|
|
1625
|
-
output = "No related tests found";
|
|
1626
|
-
} else {
|
|
1627
|
-
output = `Found ${result.length} related test(s):\n${result.map(t => ` ${t}`).join("\n")}`;
|
|
1628
|
-
}
|
|
1629
|
-
break;
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
case "reload_workspace": {
|
|
1633
|
-
await rustAnalyzer.reloadWorkspace(client);
|
|
1634
|
-
output = "Workspace reloaded successfully";
|
|
1635
1186
|
break;
|
|
1636
1187
|
}
|
|
1637
1188
|
|