@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.67
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 +60 -0
- package/docs/sdk.md +1 -1
- package/package.json +5 -5
- package/scripts/generate-template.ts +6 -6
- package/src/cli/args.ts +3 -0
- package/src/core/agent-session.ts +39 -0
- package/src/core/bash-executor.ts +3 -3
- package/src/core/cursor/exec-bridge.ts +95 -88
- package/src/core/custom-commands/bundled/review/index.ts +142 -145
- package/src/core/custom-commands/bundled/wt/index.ts +68 -66
- package/src/core/custom-commands/loader.ts +4 -6
- package/src/core/custom-tools/index.ts +2 -2
- package/src/core/custom-tools/loader.ts +66 -61
- package/src/core/custom-tools/types.ts +4 -4
- package/src/core/custom-tools/wrapper.ts +61 -25
- package/src/core/event-bus.ts +19 -47
- package/src/core/extensions/index.ts +8 -4
- package/src/core/extensions/loader.ts +160 -120
- package/src/core/extensions/types.ts +4 -4
- package/src/core/extensions/wrapper.ts +149 -100
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +96 -70
- package/src/core/hooks/types.ts +1 -2
- package/src/core/index.ts +1 -0
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/json-rpc.ts +88 -0
- package/src/core/mcp/loader.ts +22 -4
- package/src/core/mcp/manager.ts +202 -48
- package/src/core/mcp/tool-bridge.ts +143 -55
- package/src/core/mcp/tool-cache.ts +122 -0
- package/src/core/python-executor.ts +3 -9
- package/src/core/sdk.ts +33 -32
- package/src/core/session-manager.ts +30 -0
- package/src/core/settings-manager.ts +54 -1
- package/src/core/ssh/ssh-executor.ts +6 -84
- package/src/core/streaming-output.ts +107 -53
- package/src/core/tools/ask.ts +92 -93
- package/src/core/tools/bash.ts +103 -94
- package/src/core/tools/calculator.ts +41 -26
- package/src/core/tools/complete.ts +76 -66
- package/src/core/tools/context.ts +22 -24
- package/src/core/tools/exa/index.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +56 -101
- package/src/core/tools/find.ts +250 -253
- package/src/core/tools/git.ts +39 -33
- package/src/core/tools/grep.ts +440 -427
- package/src/core/tools/index.ts +63 -61
- package/src/core/tools/ls.ts +119 -114
- package/src/core/tools/lsp/clients/biome-client.ts +5 -7
- package/src/core/tools/lsp/clients/index.ts +4 -4
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/index.ts +604 -578
- package/src/core/tools/notebook.ts +121 -119
- package/src/core/tools/output.ts +163 -147
- package/src/core/tools/patch/applicator.ts +1100 -0
- package/src/core/tools/patch/diff.ts +362 -0
- package/src/core/tools/patch/fuzzy.ts +647 -0
- package/src/core/tools/patch/index.ts +430 -0
- package/src/core/tools/patch/normalize.ts +220 -0
- package/src/core/tools/patch/normative.ts +73 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +257 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +239 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +6 -2
- package/src/core/tools/ssh.ts +99 -80
- package/src/core/tools/task/executor.ts +11 -7
- package/src/core/tools/task/index.ts +352 -343
- package/src/core/tools/task/worker.ts +13 -23
- package/src/core/tools/todo-write.ts +74 -59
- package/src/core/tools/web-fetch.ts +54 -47
- package/src/core/tools/web-search/index.ts +27 -16
- package/src/core/tools/write.ts +108 -47
- package/src/core/ttsr.ts +106 -152
- package/src/core/voice.ts +49 -39
- package/src/index.ts +16 -12
- package/src/lib/worktree/index.ts +1 -9
- package/src/modes/interactive/components/diff.ts +15 -8
- package/src/modes/interactive/components/settings-defs.ts +42 -0
- package/src/modes/interactive/components/tool-execution.ts +46 -8
- package/src/modes/interactive/controllers/event-controller.ts +6 -19
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/utils/ui-helpers.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +99 -81
- package/src/prompts/tools/patch.md +76 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/{edit.md → replace.md} +1 -0
- package/src/utils/shell.ts +0 -40
- package/src/core/tools/edit-diff.ts +0 -574
- package/src/core/tools/edit.ts +0 -345
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Dirent } from "node:fs";
|
|
2
2
|
import { existsSync, statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import type { BunFile } from "bun";
|
|
6
6
|
import { type Theme, theme } from "../../../modes/interactive/theme/theme";
|
|
7
7
|
import lspDescription from "../../../prompts/tools/lsp.md" with { type: "text" };
|
|
@@ -718,6 +718,19 @@ function getOrCreateWritethroughBatch(id: string, options: Required<Writethrough
|
|
|
718
718
|
return batch;
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
+
export async function flushLspWritethroughBatch(
|
|
722
|
+
id: string,
|
|
723
|
+
cwd: string,
|
|
724
|
+
signal?: AbortSignal,
|
|
725
|
+
): Promise<FileDiagnosticsResult | undefined> {
|
|
726
|
+
const state = writethroughBatches.get(id);
|
|
727
|
+
if (!state) {
|
|
728
|
+
return undefined;
|
|
729
|
+
}
|
|
730
|
+
writethroughBatches.delete(id);
|
|
731
|
+
return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal);
|
|
732
|
+
}
|
|
733
|
+
|
|
721
734
|
function summarizeDiagnosticMessages(messages: string[]): { summary: string; errored: boolean } {
|
|
722
735
|
const counts = { error: 0, warning: 0, info: 0, hint: 0 };
|
|
723
736
|
for (const message of messages) {
|
|
@@ -923,692 +936,705 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
923
936
|
};
|
|
924
937
|
}
|
|
925
938
|
|
|
926
|
-
/**
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
939
|
+
/**
|
|
940
|
+
* LSP tool for language server protocol operations.
|
|
941
|
+
*/
|
|
942
|
+
export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Theme> {
|
|
943
|
+
public readonly name = "lsp";
|
|
944
|
+
public readonly label = "LSP";
|
|
945
|
+
public readonly description: string;
|
|
946
|
+
public readonly parameters = lspSchema;
|
|
947
|
+
public readonly renderCall = renderCall;
|
|
948
|
+
public readonly renderResult = renderResult;
|
|
949
|
+
|
|
950
|
+
private readonly session: ToolSession;
|
|
951
|
+
|
|
952
|
+
constructor(session: ToolSession) {
|
|
953
|
+
this.session = session;
|
|
954
|
+
this.description = renderPromptTemplate(lspDescription);
|
|
930
955
|
}
|
|
931
|
-
return {
|
|
932
|
-
name: "lsp",
|
|
933
|
-
label: "LSP",
|
|
934
|
-
description: renderPromptTemplate(lspDescription),
|
|
935
|
-
parameters: lspSchema,
|
|
936
|
-
renderCall,
|
|
937
|
-
renderResult,
|
|
938
|
-
execute: async (_toolCallId, params: LspParams, _signal) => {
|
|
939
|
-
const {
|
|
940
|
-
action,
|
|
941
|
-
file,
|
|
942
|
-
files,
|
|
943
|
-
line,
|
|
944
|
-
column,
|
|
945
|
-
end_line,
|
|
946
|
-
end_character,
|
|
947
|
-
query,
|
|
948
|
-
new_name,
|
|
949
|
-
replacement,
|
|
950
|
-
kind,
|
|
951
|
-
apply,
|
|
952
|
-
action_index,
|
|
953
|
-
include_declaration,
|
|
954
|
-
} = params;
|
|
955
|
-
|
|
956
|
-
const config = await getConfig(session.cwd);
|
|
957
|
-
|
|
958
|
-
// Status action doesn't need a file
|
|
959
|
-
if (action === "status") {
|
|
960
|
-
const servers = Object.keys(config.servers);
|
|
961
|
-
const lspmuxState = await detectLspmux();
|
|
962
|
-
const lspmuxStatus = lspmuxState.available
|
|
963
|
-
? lspmuxState.running
|
|
964
|
-
? "lspmux: active (multiplexing enabled)"
|
|
965
|
-
: "lspmux: installed but server not running"
|
|
966
|
-
: "";
|
|
967
|
-
|
|
968
|
-
const serverStatus =
|
|
969
|
-
servers.length > 0
|
|
970
|
-
? `Active language servers: ${servers.join(", ")}`
|
|
971
|
-
: "No language servers configured for this project";
|
|
972
|
-
|
|
973
|
-
const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
|
|
974
|
-
return {
|
|
975
|
-
content: [{ type: "text", text: output }],
|
|
976
|
-
details: { action, success: true },
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
956
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
957
|
+
static createIf(session: ToolSession): LspTool | null {
|
|
958
|
+
return session.enableLsp === false ? null : new LspTool(session);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
public async execute(
|
|
962
|
+
_toolCallId: string,
|
|
963
|
+
params: LspParams,
|
|
964
|
+
_signal?: AbortSignal,
|
|
965
|
+
_onUpdate?: AgentToolUpdateCallback<LspToolDetails>,
|
|
966
|
+
_context?: AgentToolContext,
|
|
967
|
+
): Promise<AgentToolResult<LspToolDetails>> {
|
|
968
|
+
const {
|
|
969
|
+
action,
|
|
970
|
+
file,
|
|
971
|
+
files,
|
|
972
|
+
line,
|
|
973
|
+
column,
|
|
974
|
+
end_line,
|
|
975
|
+
end_character,
|
|
976
|
+
query,
|
|
977
|
+
new_name,
|
|
978
|
+
replacement,
|
|
979
|
+
kind,
|
|
980
|
+
apply,
|
|
981
|
+
action_index,
|
|
982
|
+
include_declaration,
|
|
983
|
+
} = params;
|
|
984
|
+
|
|
985
|
+
const config = await getConfig(this.session.cwd);
|
|
986
|
+
|
|
987
|
+
// Status action doesn't need a file
|
|
988
|
+
if (action === "status") {
|
|
989
|
+
const servers = Object.keys(config.servers);
|
|
990
|
+
const lspmuxState = await detectLspmux();
|
|
991
|
+
const lspmuxStatus = lspmuxState.available
|
|
992
|
+
? lspmuxState.running
|
|
993
|
+
? "lspmux: active (multiplexing enabled)"
|
|
994
|
+
: "lspmux: installed but server not running"
|
|
995
|
+
: "";
|
|
996
|
+
|
|
997
|
+
const serverStatus =
|
|
998
|
+
servers.length > 0
|
|
999
|
+
? `Active language servers: ${servers.join(", ")}`
|
|
1000
|
+
: "No language servers configured for this project";
|
|
1001
|
+
|
|
1002
|
+
const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
|
|
1003
|
+
return {
|
|
1004
|
+
content: [{ type: "text", text: output }],
|
|
1005
|
+
details: { action, success: true },
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Workspace diagnostics - check entire project
|
|
1010
|
+
if (action === "workspace_diagnostics") {
|
|
1011
|
+
const result = await runWorkspaceDiagnostics(this.session.cwd, config);
|
|
1012
|
+
return {
|
|
1013
|
+
content: [
|
|
1014
|
+
{
|
|
1015
|
+
type: "text",
|
|
1016
|
+
text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
|
|
1017
|
+
},
|
|
1018
|
+
],
|
|
1019
|
+
details: { action, success: true },
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Diagnostics can be batch or single-file - queries all applicable servers
|
|
1024
|
+
if (action === "diagnostics") {
|
|
1025
|
+
const targets = files?.length ? files : file ? [file] : null;
|
|
1026
|
+
if (!targets) {
|
|
983
1027
|
return {
|
|
984
|
-
content: [
|
|
985
|
-
|
|
986
|
-
type: "text",
|
|
987
|
-
text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
|
|
988
|
-
},
|
|
989
|
-
],
|
|
990
|
-
details: { action, success: true },
|
|
1028
|
+
content: [{ type: "text", text: "Error: file or files parameter required for diagnostics" }],
|
|
1029
|
+
details: { action, success: false },
|
|
991
1030
|
};
|
|
992
1031
|
}
|
|
993
1032
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
if (!targets) {
|
|
998
|
-
return {
|
|
999
|
-
content: [{ type: "text", text: "Error: file or files parameter required for diagnostics" }],
|
|
1000
|
-
details: { action, success: false },
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1033
|
+
const detailed = Boolean(files?.length);
|
|
1034
|
+
const results: string[] = [];
|
|
1035
|
+
const allServerNames = new Set<string>();
|
|
1003
1036
|
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
if (servers.length === 0) {
|
|
1012
|
-
results.push(`${theme.status.error} ${target}: No language server found`);
|
|
1013
|
-
continue;
|
|
1014
|
-
}
|
|
1037
|
+
for (const target of targets) {
|
|
1038
|
+
const resolved = resolveToCwd(target, this.session.cwd);
|
|
1039
|
+
const servers = getServersForFile(config, resolved);
|
|
1040
|
+
if (servers.length === 0) {
|
|
1041
|
+
results.push(`${theme.status.error} ${target}: No language server found`);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1015
1044
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
allDiagnostics.push(...diagnostics);
|
|
1028
|
-
continue;
|
|
1029
|
-
}
|
|
1030
|
-
const client = await getOrCreateClient(serverConfig, session.cwd);
|
|
1031
|
-
const minVersion = client.diagnosticsVersion;
|
|
1032
|
-
await refreshFile(client, resolved);
|
|
1033
|
-
const diagnostics = await waitForDiagnostics(client, uri, 3000, undefined, minVersion);
|
|
1045
|
+
const uri = fileToUri(resolved);
|
|
1046
|
+
const relPath = path.relative(this.session.cwd, resolved);
|
|
1047
|
+
const allDiagnostics: Diagnostic[] = [];
|
|
1048
|
+
|
|
1049
|
+
// Query all applicable servers for this file
|
|
1050
|
+
for (const [serverName, serverConfig] of servers) {
|
|
1051
|
+
allServerNames.add(serverName);
|
|
1052
|
+
try {
|
|
1053
|
+
if (serverConfig.createClient) {
|
|
1054
|
+
const linterClient = getLinterClient(serverName, serverConfig, this.session.cwd);
|
|
1055
|
+
const diagnostics = await linterClient.lint(resolved);
|
|
1034
1056
|
allDiagnostics.push(...diagnostics);
|
|
1035
|
-
|
|
1036
|
-
// Server failed, continue with others
|
|
1057
|
+
continue;
|
|
1037
1058
|
}
|
|
1059
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1060
|
+
const minVersion = client.diagnosticsVersion;
|
|
1061
|
+
await refreshFile(client, resolved);
|
|
1062
|
+
const diagnostics = await waitForDiagnostics(client, uri, 3000, undefined, minVersion);
|
|
1063
|
+
allDiagnostics.push(...diagnostics);
|
|
1064
|
+
} catch {
|
|
1065
|
+
// Server failed, continue with others
|
|
1038
1066
|
}
|
|
1067
|
+
}
|
|
1039
1068
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1069
|
+
// Deduplicate diagnostics
|
|
1070
|
+
const seen = new Set<string>();
|
|
1071
|
+
const uniqueDiagnostics: Diagnostic[] = [];
|
|
1072
|
+
for (const d of allDiagnostics) {
|
|
1073
|
+
const key = `${d.range.start.line}:${d.range.start.character}:${d.range.end.line}:${d.range.end.character}:${d.message}`;
|
|
1074
|
+
if (!seen.has(key)) {
|
|
1075
|
+
seen.add(key);
|
|
1076
|
+
uniqueDiagnostics.push(d);
|
|
1049
1077
|
}
|
|
1078
|
+
}
|
|
1050
1079
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
return {
|
|
1054
|
-
content: [{ type: "text", text: "No diagnostics" }],
|
|
1055
|
-
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
1060
|
-
const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
|
|
1061
|
-
const output = `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}`;
|
|
1080
|
+
if (!detailed && targets.length === 1) {
|
|
1081
|
+
if (uniqueDiagnostics.length === 0) {
|
|
1062
1082
|
return {
|
|
1063
|
-
content: [{ type: "text", text:
|
|
1083
|
+
content: [{ type: "text", text: "No diagnostics" }],
|
|
1064
1084
|
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
1065
1085
|
};
|
|
1066
1086
|
}
|
|
1067
1087
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1088
|
+
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
1089
|
+
const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
|
|
1090
|
+
const output = `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}`;
|
|
1091
|
+
return {
|
|
1092
|
+
content: [{ type: "text", text: output }],
|
|
1093
|
+
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
1094
|
+
};
|
|
1077
1095
|
}
|
|
1078
1096
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1097
|
+
if (uniqueDiagnostics.length === 0) {
|
|
1098
|
+
results.push(`${theme.status.success} ${relPath}: no issues`);
|
|
1099
|
+
} else {
|
|
1100
|
+
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
1101
|
+
results.push(`${theme.status.error} ${relPath}: ${summary}`);
|
|
1102
|
+
for (const diag of uniqueDiagnostics) {
|
|
1103
|
+
results.push(` ${formatDiagnostic(diag, relPath)}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1083
1106
|
}
|
|
1084
1107
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
action
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
action !== "runnables" &&
|
|
1091
|
-
action !== "reload_workspace";
|
|
1108
|
+
return {
|
|
1109
|
+
content: [{ type: "text", text: results.join("\n") }],
|
|
1110
|
+
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1092
1113
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1114
|
+
const requiresFile =
|
|
1115
|
+
!file &&
|
|
1116
|
+
action !== "workspace_symbols" &&
|
|
1117
|
+
action !== "flycheck" &&
|
|
1118
|
+
action !== "ssr" &&
|
|
1119
|
+
action !== "runnables" &&
|
|
1120
|
+
action !== "reload_workspace";
|
|
1121
|
+
|
|
1122
|
+
if (requiresFile) {
|
|
1123
|
+
return {
|
|
1124
|
+
content: [{ type: "text", text: "Error: file parameter required for this action" }],
|
|
1125
|
+
details: { action, success: false },
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1099
1128
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1129
|
+
const resolvedFile = file ? resolveToCwd(file, this.session.cwd) : null;
|
|
1130
|
+
const serverInfo = resolvedFile
|
|
1131
|
+
? getLspServerForFile(config, resolvedFile)
|
|
1132
|
+
: getServerForWorkspaceAction(config, action);
|
|
1104
1133
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1134
|
+
if (!serverInfo) {
|
|
1135
|
+
return {
|
|
1136
|
+
content: [{ type: "text", text: "No language server found for this action" }],
|
|
1137
|
+
details: { action, success: false },
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1111
1140
|
|
|
1112
|
-
|
|
1141
|
+
const [serverName, serverConfig] = serverInfo;
|
|
1113
1142
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
}
|
|
1143
|
+
try {
|
|
1144
|
+
const client = await getOrCreateClient(serverConfig, this.session.cwd);
|
|
1145
|
+
let targetFile = resolvedFile;
|
|
1146
|
+
if (action === "runnables" && !targetFile) {
|
|
1147
|
+
targetFile = findFileForServer(this.session.cwd, serverConfig);
|
|
1148
|
+
if (!targetFile) {
|
|
1149
|
+
return {
|
|
1150
|
+
content: [{ type: "text", text: "Error: no matching files found for runnables" }],
|
|
1151
|
+
details: { action, serverName, success: false },
|
|
1152
|
+
};
|
|
1125
1153
|
}
|
|
1154
|
+
}
|
|
1126
1155
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1156
|
+
if (targetFile) {
|
|
1157
|
+
await ensureFileOpen(client, targetFile);
|
|
1158
|
+
}
|
|
1130
1159
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1160
|
+
const uri = targetFile ? fileToUri(targetFile) : "";
|
|
1161
|
+
const position = { line: (line || 1) - 1, character: (column || 1) - 1 };
|
|
1133
1162
|
|
|
1134
|
-
|
|
1163
|
+
let output: string;
|
|
1135
1164
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1165
|
+
switch (action) {
|
|
1166
|
+
// =====================================================================
|
|
1167
|
+
// Standard LSP Operations
|
|
1168
|
+
// =====================================================================
|
|
1140
1169
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1170
|
+
case "definition": {
|
|
1171
|
+
const result = (await sendRequest(client, "textDocument/definition", {
|
|
1172
|
+
textDocument: { uri },
|
|
1173
|
+
position,
|
|
1174
|
+
})) as Location | Location[] | LocationLink | LocationLink[] | null;
|
|
1175
|
+
|
|
1176
|
+
if (!result) {
|
|
1177
|
+
output = "No definition found";
|
|
1178
|
+
} else {
|
|
1179
|
+
const raw = Array.isArray(result) ? result : [result];
|
|
1180
|
+
const locations = raw.flatMap((loc) => {
|
|
1181
|
+
if ("uri" in loc) {
|
|
1182
|
+
return [loc as Location];
|
|
1183
|
+
}
|
|
1184
|
+
if ("targetUri" in loc) {
|
|
1185
|
+
// Use targetSelectionRange (the precise identifier range) with fallback to targetRange
|
|
1186
|
+
const link = loc as LocationLink;
|
|
1187
|
+
return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
|
|
1188
|
+
}
|
|
1189
|
+
return [];
|
|
1190
|
+
});
|
|
1146
1191
|
|
|
1147
|
-
if (
|
|
1192
|
+
if (locations.length === 0) {
|
|
1148
1193
|
output = "No definition found";
|
|
1149
1194
|
} else {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
return [loc as Location];
|
|
1154
|
-
}
|
|
1155
|
-
if ("targetUri" in loc) {
|
|
1156
|
-
// Use targetSelectionRange (the precise identifier range) with fallback to targetRange
|
|
1157
|
-
const link = loc as LocationLink;
|
|
1158
|
-
return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
|
|
1159
|
-
}
|
|
1160
|
-
return [];
|
|
1161
|
-
});
|
|
1162
|
-
|
|
1163
|
-
if (locations.length === 0) {
|
|
1164
|
-
output = "No definition found";
|
|
1165
|
-
} else {
|
|
1166
|
-
output = `Found ${locations.length} definition(s):\n${locations
|
|
1167
|
-
.map((loc) => ` ${formatLocation(loc, session.cwd)}`)
|
|
1168
|
-
.join("\n")}`;
|
|
1169
|
-
}
|
|
1195
|
+
output = `Found ${locations.length} definition(s):\n${locations
|
|
1196
|
+
.map((loc) => ` ${formatLocation(loc, this.session.cwd)}`)
|
|
1197
|
+
.join("\n")}`;
|
|
1170
1198
|
}
|
|
1171
|
-
break;
|
|
1172
1199
|
}
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1173
1202
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1203
|
+
case "references": {
|
|
1204
|
+
const result = (await sendRequest(client, "textDocument/references", {
|
|
1205
|
+
textDocument: { uri },
|
|
1206
|
+
position,
|
|
1207
|
+
context: { includeDeclaration: include_declaration ?? true },
|
|
1208
|
+
})) as Location[] | null;
|
|
1180
1209
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1187
|
-
break;
|
|
1210
|
+
if (!result || result.length === 0) {
|
|
1211
|
+
output = "No references found";
|
|
1212
|
+
} else {
|
|
1213
|
+
const lines = result.map((loc) => ` ${formatLocation(loc, this.session.cwd)}`);
|
|
1214
|
+
output = `Found ${result.length} reference(s):\n${lines.join("\n")}`;
|
|
1188
1215
|
}
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1189
1218
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1219
|
+
case "hover": {
|
|
1220
|
+
const result = (await sendRequest(client, "textDocument/hover", {
|
|
1221
|
+
textDocument: { uri },
|
|
1222
|
+
position,
|
|
1223
|
+
})) as Hover | null;
|
|
1195
1224
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
}
|
|
1201
|
-
break;
|
|
1225
|
+
if (!result || !result.contents) {
|
|
1226
|
+
output = "No hover information";
|
|
1227
|
+
} else {
|
|
1228
|
+
output = extractHoverText(result.contents);
|
|
1202
1229
|
}
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1203
1232
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1233
|
+
case "symbols": {
|
|
1234
|
+
const result = (await sendRequest(client, "textDocument/documentSymbol", {
|
|
1235
|
+
textDocument: { uri },
|
|
1236
|
+
})) as (DocumentSymbol | SymbolInformation)[] | null;
|
|
1208
1237
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1238
|
+
if (!result || result.length === 0) {
|
|
1239
|
+
output = "No symbols found";
|
|
1240
|
+
} else if (!targetFile) {
|
|
1241
|
+
return {
|
|
1242
|
+
content: [{ type: "text", text: "Error: file parameter required for symbols" }],
|
|
1243
|
+
details: { action, serverName, success: false },
|
|
1244
|
+
};
|
|
1245
|
+
} else {
|
|
1246
|
+
const relPath = path.relative(this.session.cwd, targetFile);
|
|
1247
|
+
// Check if hierarchical (DocumentSymbol) or flat (SymbolInformation)
|
|
1248
|
+
if ("selectionRange" in result[0]) {
|
|
1249
|
+
// Hierarchical
|
|
1250
|
+
const lines = (result as DocumentSymbol[]).flatMap((s) => formatDocumentSymbol(s));
|
|
1251
|
+
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
1216
1252
|
} else {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
// Flat
|
|
1225
|
-
const lines = (result as SymbolInformation[]).map((s) => {
|
|
1226
|
-
const line = s.location.range.start.line + 1;
|
|
1227
|
-
const icon = symbolKindToIcon(s.kind);
|
|
1228
|
-
return `${icon} ${s.name} @ line ${line}`;
|
|
1229
|
-
});
|
|
1230
|
-
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
1231
|
-
}
|
|
1253
|
+
// Flat
|
|
1254
|
+
const lines = (result as SymbolInformation[]).map((s) => {
|
|
1255
|
+
const line = s.location.range.start.line + 1;
|
|
1256
|
+
const icon = symbolKindToIcon(s.kind);
|
|
1257
|
+
return `${icon} ${s.name} @ line ${line}`;
|
|
1258
|
+
});
|
|
1259
|
+
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
1232
1260
|
}
|
|
1233
|
-
break;
|
|
1234
1261
|
}
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1235
1264
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1265
|
+
case "workspace_symbols": {
|
|
1266
|
+
if (!query) {
|
|
1267
|
+
return {
|
|
1268
|
+
content: [{ type: "text", text: "Error: query parameter required for workspace_symbols" }],
|
|
1269
|
+
details: { action, serverName, success: false },
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1243
1272
|
|
|
1244
|
-
|
|
1245
|
-
| SymbolInformation[]
|
|
1246
|
-
| null;
|
|
1273
|
+
const result = (await sendRequest(client, "workspace/symbol", { query })) as SymbolInformation[] | null;
|
|
1247
1274
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1254
|
-
break;
|
|
1275
|
+
if (!result || result.length === 0) {
|
|
1276
|
+
output = `No symbols matching "${query}"`;
|
|
1277
|
+
} else {
|
|
1278
|
+
const lines = result.map((s) => formatSymbolInformation(s, this.session.cwd));
|
|
1279
|
+
output = `Found ${result.length} symbol(s) matching "${query}":\n${lines.map((l) => ` ${l}`).join("\n")}`;
|
|
1255
1280
|
}
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1256
1283
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1284
|
+
case "rename": {
|
|
1285
|
+
if (!new_name) {
|
|
1286
|
+
return {
|
|
1287
|
+
content: [{ type: "text", text: "Error: new_name parameter required for rename" }],
|
|
1288
|
+
details: { action, serverName, success: false },
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1264
1291
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1292
|
+
const result = (await sendRequest(client, "textDocument/rename", {
|
|
1293
|
+
textDocument: { uri },
|
|
1294
|
+
position,
|
|
1295
|
+
newName: new_name,
|
|
1296
|
+
})) as WorkspaceEdit | null;
|
|
1270
1297
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1298
|
+
if (!result) {
|
|
1299
|
+
output = "Rename returned no edits";
|
|
1300
|
+
} else {
|
|
1301
|
+
const shouldApply = apply !== false;
|
|
1302
|
+
if (shouldApply) {
|
|
1303
|
+
const applied = await applyWorkspaceEdit(result, this.session.cwd);
|
|
1304
|
+
output = `Applied rename:\n${applied.map((a) => ` ${a}`).join("\n")}`;
|
|
1273
1305
|
} else {
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
const applied = await applyWorkspaceEdit(result, session.cwd);
|
|
1277
|
-
output = `Applied rename:\n${applied.map((a) => ` ${a}`).join("\n")}`;
|
|
1278
|
-
} else {
|
|
1279
|
-
const preview = formatWorkspaceEdit(result, session.cwd);
|
|
1280
|
-
output = `Rename preview:\n${preview.map((p) => ` ${p}`).join("\n")}`;
|
|
1281
|
-
}
|
|
1306
|
+
const preview = formatWorkspaceEdit(result, this.session.cwd);
|
|
1307
|
+
output = `Rename preview:\n${preview.map((p) => ` ${p}`).join("\n")}`;
|
|
1282
1308
|
}
|
|
1283
|
-
|
|
1309
|
+
}
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
case "actions": {
|
|
1314
|
+
if (!targetFile) {
|
|
1315
|
+
return {
|
|
1316
|
+
content: [{ type: "text", text: "Error: file parameter required for actions" }],
|
|
1317
|
+
details: { action, serverName, success: false },
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const actionsMinVersion = client.diagnosticsVersion;
|
|
1322
|
+
await refreshFile(client, targetFile);
|
|
1323
|
+
const diagnostics = await waitForDiagnostics(client, uri, 3000, undefined, actionsMinVersion);
|
|
1324
|
+
const endLine = (end_line ?? line ?? 1) - 1;
|
|
1325
|
+
const endCharacter = (end_character ?? column ?? 1) - 1;
|
|
1326
|
+
const range = { start: position, end: { line: endLine, character: endCharacter } };
|
|
1327
|
+
const relevantDiagnostics = diagnostics.filter(
|
|
1328
|
+
(d) => d.range.start.line <= range.end.line && d.range.end.line >= range.start.line,
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
const codeActionContext: { diagnostics: Diagnostic[]; only?: string[] } = {
|
|
1332
|
+
diagnostics: relevantDiagnostics,
|
|
1333
|
+
};
|
|
1334
|
+
if (kind) {
|
|
1335
|
+
codeActionContext.only = [kind];
|
|
1284
1336
|
}
|
|
1285
1337
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1338
|
+
const result = (await sendRequest(client, "textDocument/codeAction", {
|
|
1339
|
+
textDocument: { uri },
|
|
1340
|
+
range,
|
|
1341
|
+
context: codeActionContext,
|
|
1342
|
+
})) as Array<CodeAction | Command> | null;
|
|
1343
|
+
|
|
1344
|
+
if (!result || result.length === 0) {
|
|
1345
|
+
output = "No code actions available";
|
|
1346
|
+
} else if (action_index !== undefined) {
|
|
1347
|
+
// Apply specific action
|
|
1348
|
+
if (action_index < 0 || action_index >= result.length) {
|
|
1288
1349
|
return {
|
|
1289
|
-
content: [
|
|
1350
|
+
content: [
|
|
1351
|
+
{
|
|
1352
|
+
type: "text",
|
|
1353
|
+
text: `Error: action_index ${action_index} out of range (0-${result.length - 1})`,
|
|
1354
|
+
},
|
|
1355
|
+
],
|
|
1290
1356
|
details: { action, serverName, success: false },
|
|
1291
1357
|
};
|
|
1292
1358
|
}
|
|
1293
1359
|
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
(
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
const codeActionContext: { diagnostics: Diagnostic[]; only?: string[] } = {
|
|
1305
|
-
diagnostics: relevantDiagnostics,
|
|
1306
|
-
};
|
|
1307
|
-
if (kind) {
|
|
1308
|
-
codeActionContext.only = [kind];
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
const result = (await sendRequest(client, "textDocument/codeAction", {
|
|
1312
|
-
textDocument: { uri },
|
|
1313
|
-
range,
|
|
1314
|
-
context: codeActionContext,
|
|
1315
|
-
})) as Array<CodeAction | Command> | null;
|
|
1316
|
-
|
|
1317
|
-
if (!result || result.length === 0) {
|
|
1318
|
-
output = "No code actions available";
|
|
1319
|
-
} else if (action_index !== undefined) {
|
|
1320
|
-
// Apply specific action
|
|
1321
|
-
if (action_index < 0 || action_index >= result.length) {
|
|
1322
|
-
return {
|
|
1323
|
-
content: [
|
|
1324
|
-
{
|
|
1325
|
-
type: "text",
|
|
1326
|
-
text: `Error: action_index ${action_index} out of range (0-${result.length - 1})`,
|
|
1327
|
-
},
|
|
1328
|
-
],
|
|
1329
|
-
details: { action, serverName, success: false },
|
|
1330
|
-
};
|
|
1360
|
+
const isCommand = (candidate: CodeAction | Command): candidate is Command =>
|
|
1361
|
+
typeof (candidate as Command).command === "string";
|
|
1362
|
+
const isCodeAction = (candidate: CodeAction | Command): candidate is CodeAction =>
|
|
1363
|
+
!isCommand(candidate);
|
|
1364
|
+
const getCommandPayload = (
|
|
1365
|
+
candidate: CodeAction | Command,
|
|
1366
|
+
): { command: string; arguments?: unknown[] } | null => {
|
|
1367
|
+
if (isCommand(candidate)) {
|
|
1368
|
+
return { command: candidate.command, arguments: candidate.arguments };
|
|
1331
1369
|
}
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
typeof (candidate as Command).command === "string";
|
|
1335
|
-
const isCodeAction = (candidate: CodeAction | Command): candidate is CodeAction =>
|
|
1336
|
-
!isCommand(candidate);
|
|
1337
|
-
const getCommandPayload = (
|
|
1338
|
-
candidate: CodeAction | Command,
|
|
1339
|
-
): { command: string; arguments?: unknown[] } | null => {
|
|
1340
|
-
if (isCommand(candidate)) {
|
|
1341
|
-
return { command: candidate.command, arguments: candidate.arguments };
|
|
1342
|
-
}
|
|
1343
|
-
if (candidate.command) {
|
|
1344
|
-
return { command: candidate.command.command, arguments: candidate.command.arguments };
|
|
1345
|
-
}
|
|
1346
|
-
return null;
|
|
1347
|
-
};
|
|
1348
|
-
|
|
1349
|
-
const codeAction = result[action_index];
|
|
1350
|
-
|
|
1351
|
-
// Resolve if needed
|
|
1352
|
-
let resolvedAction = codeAction;
|
|
1353
|
-
if (
|
|
1354
|
-
isCodeAction(codeAction) &&
|
|
1355
|
-
!codeAction.edit &&
|
|
1356
|
-
codeAction.data &&
|
|
1357
|
-
client.serverCapabilities?.codeActionProvider
|
|
1358
|
-
) {
|
|
1359
|
-
const provider = client.serverCapabilities.codeActionProvider;
|
|
1360
|
-
if (typeof provider === "object" && provider.resolveProvider) {
|
|
1361
|
-
resolvedAction = (await sendRequest(client, "codeAction/resolve", codeAction)) as CodeAction;
|
|
1362
|
-
}
|
|
1370
|
+
if (candidate.command) {
|
|
1371
|
+
return { command: candidate.command.command, arguments: candidate.command.arguments };
|
|
1363
1372
|
}
|
|
1373
|
+
return null;
|
|
1374
|
+
};
|
|
1364
1375
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
+
const codeAction = result[action_index];
|
|
1377
|
+
|
|
1378
|
+
// Resolve if needed
|
|
1379
|
+
let resolvedAction = codeAction;
|
|
1380
|
+
if (
|
|
1381
|
+
isCodeAction(codeAction) &&
|
|
1382
|
+
!codeAction.edit &&
|
|
1383
|
+
codeAction.data &&
|
|
1384
|
+
client.serverCapabilities?.codeActionProvider
|
|
1385
|
+
) {
|
|
1386
|
+
const provider = client.serverCapabilities.codeActionProvider;
|
|
1387
|
+
if (typeof provider === "object" && provider.resolveProvider) {
|
|
1388
|
+
resolvedAction = (await sendRequest(client, "codeAction/resolve", codeAction)) as CodeAction;
|
|
1376
1389
|
}
|
|
1377
|
-
} else {
|
|
1378
|
-
// List available actions
|
|
1379
|
-
const lines = result.map((actionItem, i) => {
|
|
1380
|
-
if ("kind" in actionItem || "isPreferred" in actionItem || "edit" in actionItem) {
|
|
1381
|
-
const actionDetails = actionItem as CodeAction;
|
|
1382
|
-
const preferred = actionDetails.isPreferred ? " (preferred)" : "";
|
|
1383
|
-
const kindInfo = actionDetails.kind ? ` [${actionDetails.kind}]` : "";
|
|
1384
|
-
return ` [${i}] ${actionDetails.title}${kindInfo}${preferred}`;
|
|
1385
|
-
}
|
|
1386
|
-
return ` [${i}] ${actionItem.title}`;
|
|
1387
|
-
});
|
|
1388
|
-
output = `Available code actions:\n${lines.join("\n")}\n\nUse action_index parameter to apply a specific action.`;
|
|
1389
|
-
}
|
|
1390
|
-
break;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
case "incoming_calls":
|
|
1394
|
-
case "outgoing_calls": {
|
|
1395
|
-
// First, prepare the call hierarchy item at the cursor position
|
|
1396
|
-
const prepareResult = (await sendRequest(client, "textDocument/prepareCallHierarchy", {
|
|
1397
|
-
textDocument: { uri },
|
|
1398
|
-
position,
|
|
1399
|
-
})) as CallHierarchyItem[] | null;
|
|
1400
|
-
|
|
1401
|
-
if (!prepareResult || prepareResult.length === 0) {
|
|
1402
|
-
output = "No callable symbol found at this position";
|
|
1403
|
-
break;
|
|
1404
1390
|
}
|
|
1405
1391
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
const calls = (await sendRequest(client, "callHierarchy/incomingCalls", { item })) as
|
|
1410
|
-
| CallHierarchyIncomingCall[]
|
|
1411
|
-
| null;
|
|
1412
|
-
|
|
1413
|
-
if (!calls || calls.length === 0) {
|
|
1414
|
-
output = `No callers found for "${item.name}"`;
|
|
1415
|
-
} else {
|
|
1416
|
-
const lines = calls.map((call) => {
|
|
1417
|
-
const loc = { uri: call.from.uri, range: call.from.selectionRange };
|
|
1418
|
-
const detail = call.from.detail ? ` (${call.from.detail})` : "";
|
|
1419
|
-
return ` ${call.from.name}${detail} @ ${formatLocation(loc, session.cwd)}`;
|
|
1420
|
-
});
|
|
1421
|
-
output = `Found ${calls.length} caller(s) of "${item.name}":\n${lines.join("\n")}`;
|
|
1422
|
-
}
|
|
1392
|
+
if (isCodeAction(resolvedAction) && resolvedAction.edit) {
|
|
1393
|
+
const applied = await applyWorkspaceEdit(resolvedAction.edit, this.session.cwd);
|
|
1394
|
+
output = `Applied "${codeAction.title}":\n${applied.map((a) => ` ${a}`).join("\n")}`;
|
|
1423
1395
|
} else {
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
if (!calls || calls.length === 0) {
|
|
1429
|
-
output = `"${item.name}" doesn't call any functions`;
|
|
1396
|
+
const commandPayload = getCommandPayload(resolvedAction);
|
|
1397
|
+
if (commandPayload) {
|
|
1398
|
+
await sendRequest(client, "workspace/executeCommand", commandPayload);
|
|
1399
|
+
output = `Executed "${codeAction.title}"`;
|
|
1430
1400
|
} else {
|
|
1431
|
-
|
|
1432
|
-
const loc = { uri: call.to.uri, range: call.to.selectionRange };
|
|
1433
|
-
const detail = call.to.detail ? ` (${call.to.detail})` : "";
|
|
1434
|
-
return ` ${call.to.name}${detail} @ ${formatLocation(loc, session.cwd)}`;
|
|
1435
|
-
});
|
|
1436
|
-
output = `"${item.name}" calls ${calls.length} function(s):\n${lines.join("\n")}`;
|
|
1401
|
+
output = `Code action "${codeAction.title}" has no edits or command to apply`;
|
|
1437
1402
|
}
|
|
1438
1403
|
}
|
|
1404
|
+
} else {
|
|
1405
|
+
// List available actions
|
|
1406
|
+
const lines = result.map((actionItem, i) => {
|
|
1407
|
+
if ("kind" in actionItem || "isPreferred" in actionItem || "edit" in actionItem) {
|
|
1408
|
+
const actionDetails = actionItem as CodeAction;
|
|
1409
|
+
const preferred = actionDetails.isPreferred ? " (preferred)" : "";
|
|
1410
|
+
const kindInfo = actionDetails.kind ? ` [${actionDetails.kind}]` : "";
|
|
1411
|
+
return ` [${i}] ${actionDetails.title}${kindInfo}${preferred}`;
|
|
1412
|
+
}
|
|
1413
|
+
return ` [${i}] ${actionItem.title}`;
|
|
1414
|
+
});
|
|
1415
|
+
output = `Available code actions:\n${lines.join("\n")}\n\nUse action_index parameter to apply a specific action.`;
|
|
1416
|
+
}
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
case "incoming_calls":
|
|
1421
|
+
case "outgoing_calls": {
|
|
1422
|
+
// First, prepare the call hierarchy item at the cursor position
|
|
1423
|
+
const prepareResult = (await sendRequest(client, "textDocument/prepareCallHierarchy", {
|
|
1424
|
+
textDocument: { uri },
|
|
1425
|
+
position,
|
|
1426
|
+
})) as CallHierarchyItem[] | null;
|
|
1427
|
+
|
|
1428
|
+
if (!prepareResult || prepareResult.length === 0) {
|
|
1429
|
+
output = "No callable symbol found at this position";
|
|
1439
1430
|
break;
|
|
1440
1431
|
}
|
|
1441
1432
|
|
|
1442
|
-
|
|
1443
|
-
// Rust-Analyzer Specific Operations
|
|
1444
|
-
// =====================================================================
|
|
1433
|
+
const item = prepareResult[0];
|
|
1445
1434
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
details: { action, serverName, success: false },
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1435
|
+
if (action === "incoming_calls") {
|
|
1436
|
+
const calls = (await sendRequest(client, "callHierarchy/incomingCalls", { item })) as
|
|
1437
|
+
| CallHierarchyIncomingCall[]
|
|
1438
|
+
| null;
|
|
1453
1439
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
const
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1440
|
+
if (!calls || calls.length === 0) {
|
|
1441
|
+
output = `No callers found for "${item.name}"`;
|
|
1442
|
+
} else {
|
|
1443
|
+
const lines = calls.map((call) => {
|
|
1444
|
+
const loc = { uri: call.from.uri, range: call.from.selectionRange };
|
|
1445
|
+
const detail = call.from.detail ? ` (${call.from.detail})` : "";
|
|
1446
|
+
return ` ${call.from.name}${detail} @ ${formatLocation(loc, this.session.cwd)}`;
|
|
1447
|
+
});
|
|
1448
|
+
output = `Found ${calls.length} caller(s) of "${item.name}":\n${lines.join("\n")}`;
|
|
1461
1449
|
}
|
|
1450
|
+
} else {
|
|
1451
|
+
const calls = (await sendRequest(client, "callHierarchy/outgoingCalls", { item })) as
|
|
1452
|
+
| CallHierarchyOutgoingCall[]
|
|
1453
|
+
| null;
|
|
1462
1454
|
|
|
1463
|
-
if (
|
|
1464
|
-
output = "
|
|
1455
|
+
if (!calls || calls.length === 0) {
|
|
1456
|
+
output = `"${item.name}" doesn't call any functions`;
|
|
1465
1457
|
} else {
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1458
|
+
const lines = calls.map((call) => {
|
|
1459
|
+
const loc = { uri: call.to.uri, range: call.to.selectionRange };
|
|
1460
|
+
const detail = call.to.detail ? ` (${call.to.detail})` : "";
|
|
1461
|
+
return ` ${call.to.name}${detail} @ ${formatLocation(loc, this.session.cwd)}`;
|
|
1462
|
+
});
|
|
1463
|
+
output = `"${item.name}" calls ${calls.length} function(s):\n${lines.join("\n")}`;
|
|
1470
1464
|
}
|
|
1471
|
-
break;
|
|
1472
1465
|
}
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1473
1468
|
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
content: [{ type: "text", text: "Error: expand_macro requires rust-analyzer" }],
|
|
1478
|
-
details: { action, serverName, success: false },
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1469
|
+
// =====================================================================
|
|
1470
|
+
// Rust-Analyzer Specific Operations
|
|
1471
|
+
// =====================================================================
|
|
1481
1472
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1473
|
+
case "flycheck": {
|
|
1474
|
+
if (!hasCapability(serverConfig, "flycheck")) {
|
|
1475
|
+
return {
|
|
1476
|
+
content: [{ type: "text", text: "Error: flycheck requires rust-analyzer" }],
|
|
1477
|
+
details: { action, serverName, success: false },
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1488
1480
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1481
|
+
await rustAnalyzer.flycheck(client, resolvedFile ?? undefined);
|
|
1482
|
+
const collected: Array<{ filePath: string; diagnostic: Diagnostic }> = [];
|
|
1483
|
+
for (const [diagUri, diags] of client.diagnostics.entries()) {
|
|
1484
|
+
const relPath = path.relative(this.session.cwd, uriToFile(diagUri));
|
|
1485
|
+
for (const diag of diags) {
|
|
1486
|
+
collected.push({ filePath: relPath, diagnostic: diag });
|
|
1494
1487
|
}
|
|
1495
|
-
break;
|
|
1496
1488
|
}
|
|
1497
1489
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
}
|
|
1490
|
+
if (collected.length === 0) {
|
|
1491
|
+
output = "Flycheck: no issues found";
|
|
1492
|
+
} else {
|
|
1493
|
+
const summary = formatDiagnosticsSummary(collected.map((d) => d.diagnostic));
|
|
1494
|
+
const formatted = collected.slice(0, 20).map((d) => formatDiagnostic(d.diagnostic, d.filePath));
|
|
1495
|
+
const more = collected.length > 20 ? `\n ... and ${collected.length - 20} more` : "";
|
|
1496
|
+
output = `Flycheck ${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}${more}`;
|
|
1497
|
+
}
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1505
1500
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1501
|
+
case "expand_macro": {
|
|
1502
|
+
if (!hasCapability(serverConfig, "expandMacro")) {
|
|
1503
|
+
return {
|
|
1504
|
+
content: [{ type: "text", text: "Error: expand_macro requires rust-analyzer" }],
|
|
1505
|
+
details: { action, serverName, success: false },
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1512
1508
|
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1509
|
+
if (!targetFile) {
|
|
1510
|
+
return {
|
|
1511
|
+
content: [{ type: "text", text: "Error: file parameter required for expand_macro" }],
|
|
1512
|
+
details: { action, serverName, success: false },
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1519
1515
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1516
|
+
const result = await rustAnalyzer.expandMacro(client, targetFile, line || 1, column || 1);
|
|
1517
|
+
if (!result) {
|
|
1518
|
+
output = "No macro expansion at this position";
|
|
1519
|
+
} else {
|
|
1520
|
+
output = `Macro: ${result.name}\n\nExpansion:\n${result.expansion}`;
|
|
1521
|
+
}
|
|
1522
|
+
break;
|
|
1523
|
+
}
|
|
1522
1524
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
} else {
|
|
1530
|
-
const preview = formatWorkspaceEdit(result, session.cwd);
|
|
1531
|
-
output =
|
|
1532
|
-
preview.length > 0
|
|
1533
|
-
? `SSR preview:\n${preview.map((p) => ` ${p}`).join("\n")}`
|
|
1534
|
-
: "SSR: no matches found";
|
|
1535
|
-
}
|
|
1536
|
-
break;
|
|
1525
|
+
case "ssr": {
|
|
1526
|
+
if (!hasCapability(serverConfig, "ssr")) {
|
|
1527
|
+
return {
|
|
1528
|
+
content: [{ type: "text", text: "Error: ssr requires rust-analyzer" }],
|
|
1529
|
+
details: { action, serverName, success: false },
|
|
1530
|
+
};
|
|
1537
1531
|
}
|
|
1538
1532
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
}
|
|
1533
|
+
if (!query) {
|
|
1534
|
+
return {
|
|
1535
|
+
content: [{ type: "text", text: "Error: query parameter (pattern) required for ssr" }],
|
|
1536
|
+
details: { action, serverName, success: false },
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1546
1539
|
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1540
|
+
if (!replacement) {
|
|
1541
|
+
return {
|
|
1542
|
+
content: [{ type: "text", text: "Error: replacement parameter required for ssr" }],
|
|
1543
|
+
details: { action, serverName, success: false },
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1553
1546
|
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1547
|
+
const shouldApply = apply === true;
|
|
1548
|
+
const result = await rustAnalyzer.ssr(client, query, replacement, !shouldApply);
|
|
1549
|
+
|
|
1550
|
+
if (shouldApply) {
|
|
1551
|
+
const applied = await applyWorkspaceEdit(result, this.session.cwd);
|
|
1552
|
+
output =
|
|
1553
|
+
applied.length > 0
|
|
1554
|
+
? `Applied SSR:\n${applied.map((a) => ` ${a}`).join("\n")}`
|
|
1555
|
+
: "SSR: no matches found";
|
|
1556
|
+
} else {
|
|
1557
|
+
const preview = formatWorkspaceEdit(result, this.session.cwd);
|
|
1558
|
+
output =
|
|
1559
|
+
preview.length > 0
|
|
1560
|
+
? `SSR preview:\n${preview.map((p) => ` ${p}`).join("\n")}`
|
|
1561
|
+
: "SSR: no matches found";
|
|
1565
1562
|
}
|
|
1563
|
+
break;
|
|
1564
|
+
}
|
|
1566
1565
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1566
|
+
case "runnables": {
|
|
1567
|
+
if (!hasCapability(serverConfig, "runnables")) {
|
|
1568
|
+
return {
|
|
1569
|
+
content: [{ type: "text", text: "Error: runnables requires rust-analyzer" }],
|
|
1570
|
+
details: { action, serverName, success: false },
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1574
1573
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1574
|
+
if (!targetFile) {
|
|
1575
|
+
return {
|
|
1576
|
+
content: [{ type: "text", text: "Error: file parameter required for runnables" }],
|
|
1577
|
+
details: { action, serverName, success: false },
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1581
1580
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1581
|
+
const result = await rustAnalyzer.runnables(client, targetFile, line);
|
|
1582
|
+
if (result.length === 0) {
|
|
1583
|
+
output = "No runnables found";
|
|
1584
|
+
} else {
|
|
1585
|
+
const lines = result.map((r) => {
|
|
1586
|
+
const args = r.args?.cargoArgs?.join(" ") || "";
|
|
1587
|
+
return ` [${r.kind}] ${r.label}${args ? ` (cargo ${args})` : ""}`;
|
|
1588
|
+
});
|
|
1589
|
+
output = `Found ${result.length} runnable(s):\n${lines.join("\n")}`;
|
|
1589
1590
|
}
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1590
1593
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1594
|
+
case "related_tests": {
|
|
1595
|
+
if (!hasCapability(serverConfig, "relatedTests")) {
|
|
1596
|
+
return {
|
|
1597
|
+
content: [{ type: "text", text: "Error: related_tests requires rust-analyzer" }],
|
|
1598
|
+
details: { action, serverName, success: false },
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (!targetFile) {
|
|
1603
|
+
return {
|
|
1604
|
+
content: [{ type: "text", text: "Error: file parameter required for related_tests" }],
|
|
1605
|
+
details: { action, serverName, success: false },
|
|
1606
|
+
};
|
|
1595
1607
|
}
|
|
1596
1608
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1609
|
+
const result = await rustAnalyzer.relatedTests(client, targetFile, line || 1, column || 1);
|
|
1610
|
+
if (result.length === 0) {
|
|
1611
|
+
output = "No related tests found";
|
|
1612
|
+
} else {
|
|
1613
|
+
output = `Found ${result.length} related test(s):\n${result.map((t) => ` ${t}`).join("\n")}`;
|
|
1614
|
+
}
|
|
1615
|
+
break;
|
|
1599
1616
|
}
|
|
1600
1617
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
details: { serverName, action, success: false },
|
|
1610
|
-
};
|
|
1618
|
+
case "reload_workspace": {
|
|
1619
|
+
await rustAnalyzer.reloadWorkspace(client);
|
|
1620
|
+
output = "Workspace reloaded successfully";
|
|
1621
|
+
break;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
default:
|
|
1625
|
+
output = `Unknown action: ${action}`;
|
|
1611
1626
|
}
|
|
1612
|
-
|
|
1613
|
-
|
|
1627
|
+
|
|
1628
|
+
return {
|
|
1629
|
+
content: [{ type: "text", text: output }],
|
|
1630
|
+
details: { serverName, action, success: true },
|
|
1631
|
+
};
|
|
1632
|
+
} catch (err) {
|
|
1633
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1634
|
+
return {
|
|
1635
|
+
content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
|
|
1636
|
+
details: { serverName, action, success: false },
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1614
1640
|
}
|