@oh-my-pi/pi-coding-agent 6.1.0 → 6.7.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 +56 -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 +34 -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 +25 -25
- 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 +62 -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 +824 -639
- 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 +49 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +228 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +237 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +1 -1
- 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 +89 -41
- 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 +24 -0
- package/src/modes/interactive/components/tool-execution.ts +34 -6
- 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 -326
|
@@ -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" };
|
|
@@ -667,6 +667,7 @@ export type WritethroughCallback = (
|
|
|
667
667
|
content: string,
|
|
668
668
|
signal?: AbortSignal,
|
|
669
669
|
file?: BunFile,
|
|
670
|
+
batch?: LspWritethroughBatchRequest,
|
|
670
671
|
) => Promise<FileDiagnosticsResult | undefined>;
|
|
671
672
|
|
|
672
673
|
/** No-op writethrough callback */
|
|
@@ -684,772 +685,956 @@ export async function writethroughNoop(
|
|
|
684
685
|
return undefined;
|
|
685
686
|
}
|
|
686
687
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
688
|
+
interface PendingWritethrough {
|
|
689
|
+
dst: string;
|
|
690
|
+
content: string;
|
|
691
|
+
file?: BunFile;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
interface LspWritethroughBatchRequest {
|
|
695
|
+
id: string;
|
|
696
|
+
flush: boolean;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
interface LspWritethroughBatchState {
|
|
700
|
+
entries: Map<string, PendingWritethrough>;
|
|
701
|
+
options: Required<WritethroughOptions>;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const writethroughBatches = new Map<string, LspWritethroughBatchState>();
|
|
705
|
+
|
|
706
|
+
function getOrCreateWritethroughBatch(id: string, options: Required<WritethroughOptions>): LspWritethroughBatchState {
|
|
707
|
+
const existing = writethroughBatches.get(id);
|
|
708
|
+
if (existing) {
|
|
709
|
+
existing.options.enableFormat ||= options.enableFormat;
|
|
710
|
+
existing.options.enableDiagnostics ||= options.enableDiagnostics;
|
|
711
|
+
return existing;
|
|
692
712
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
713
|
+
const batch: LspWritethroughBatchState = {
|
|
714
|
+
entries: new Map<string, PendingWritethrough>(),
|
|
715
|
+
options: { ...options },
|
|
716
|
+
};
|
|
717
|
+
writethroughBatches.set(id, batch);
|
|
718
|
+
return batch;
|
|
719
|
+
}
|
|
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
|
+
|
|
734
|
+
function summarizeDiagnosticMessages(messages: string[]): { summary: string; errored: boolean } {
|
|
735
|
+
const counts = { error: 0, warning: 0, info: 0, hint: 0 };
|
|
736
|
+
for (const message of messages) {
|
|
737
|
+
const match = message.match(/\[(error|warning|info|hint)\]/i);
|
|
738
|
+
if (!match) continue;
|
|
739
|
+
const key = match[1].toLowerCase() as keyof typeof counts;
|
|
740
|
+
counts[key] += 1;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const parts: string[] = [];
|
|
744
|
+
if (counts.error > 0) parts.push(`${counts.error} error(s)`);
|
|
745
|
+
if (counts.warning > 0) parts.push(`${counts.warning} warning(s)`);
|
|
746
|
+
if (counts.info > 0) parts.push(`${counts.info} info(s)`);
|
|
747
|
+
if (counts.hint > 0) parts.push(`${counts.hint} hint(s)`);
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
summary: parts.length > 0 ? parts.join(", ") : "no issues",
|
|
751
|
+
errored: counts.error > 0,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function mergeDiagnostics(
|
|
756
|
+
results: Array<FileDiagnosticsResult | undefined>,
|
|
757
|
+
options: Required<WritethroughOptions>,
|
|
758
|
+
): FileDiagnosticsResult | undefined {
|
|
759
|
+
const messages: string[] = [];
|
|
760
|
+
const servers = new Set<string>();
|
|
761
|
+
let hasResults = false;
|
|
762
|
+
let hasFormatter = false;
|
|
763
|
+
let formatted = false;
|
|
764
|
+
|
|
765
|
+
for (const result of results) {
|
|
766
|
+
if (!result) continue;
|
|
767
|
+
hasResults = true;
|
|
768
|
+
if (result.server) {
|
|
769
|
+
for (const server of result.server.split(",")) {
|
|
770
|
+
const trimmed = server.trim();
|
|
771
|
+
if (trimmed) {
|
|
772
|
+
servers.add(trimmed);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
698
775
|
}
|
|
699
|
-
|
|
776
|
+
if (result.messages.length > 0) {
|
|
777
|
+
messages.push(...result.messages);
|
|
778
|
+
}
|
|
779
|
+
if (result.formatter !== undefined) {
|
|
780
|
+
hasFormatter = true;
|
|
781
|
+
if (result.formatter === FileFormatResult.FORMATTED) {
|
|
782
|
+
formatted = true;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (!hasResults && !hasFormatter) {
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
700
790
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
791
|
+
let summary = options.enableDiagnostics ? "no issues" : "OK";
|
|
792
|
+
let errored = false;
|
|
793
|
+
if (messages.length > 0) {
|
|
794
|
+
const summaryInfo = summarizeDiagnosticMessages(messages);
|
|
795
|
+
summary = summaryInfo.summary;
|
|
796
|
+
errored = summaryInfo.errored;
|
|
797
|
+
}
|
|
798
|
+
const formatter = hasFormatter ? (formatted ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED) : undefined;
|
|
705
799
|
|
|
706
|
-
|
|
707
|
-
|
|
800
|
+
return {
|
|
801
|
+
server: servers.size > 0 ? Array.from(servers).join(", ") : undefined,
|
|
802
|
+
messages,
|
|
803
|
+
summary,
|
|
804
|
+
errored,
|
|
805
|
+
formatter,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
708
808
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
809
|
+
async function runLspWritethrough(
|
|
810
|
+
dst: string,
|
|
811
|
+
content: string,
|
|
812
|
+
cwd: string,
|
|
813
|
+
options: Required<WritethroughOptions>,
|
|
814
|
+
signal?: AbortSignal,
|
|
815
|
+
file?: BunFile,
|
|
816
|
+
): Promise<FileDiagnosticsResult | undefined> {
|
|
817
|
+
const { enableFormat, enableDiagnostics } = options;
|
|
818
|
+
const config = await getConfig(cwd);
|
|
819
|
+
const servers = getServersForFile(config, dst);
|
|
820
|
+
if (servers.length === 0) {
|
|
821
|
+
return writethroughNoop(dst, content, signal, file);
|
|
822
|
+
}
|
|
823
|
+
const { lspServers, customLinterServers } = splitServers(servers);
|
|
824
|
+
|
|
825
|
+
let finalContent = content;
|
|
826
|
+
const writeContent = async (value: string) => (file ? file.write(value) : Bun.write(dst, value));
|
|
827
|
+
const getWritePromise = once(() => writeContent(finalContent));
|
|
828
|
+
const useCustomFormatter = enableFormat && customLinterServers.length > 0;
|
|
829
|
+
|
|
830
|
+
// Capture diagnostic versions BEFORE syncing to detect stale diagnostics
|
|
831
|
+
const minVersions = enableDiagnostics ? await captureDiagnosticVersions(cwd, servers) : undefined;
|
|
832
|
+
|
|
833
|
+
let formatter: FileFormatResult | undefined;
|
|
834
|
+
let diagnostics: FileDiagnosticsResult | undefined;
|
|
835
|
+
try {
|
|
836
|
+
const timeoutSignal = AbortSignal.timeout(10_000);
|
|
837
|
+
const operationSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
838
|
+
await untilAborted(operationSignal, async () => {
|
|
839
|
+
if (useCustomFormatter) {
|
|
840
|
+
// Custom linters (e.g. Biome CLI) require on-disk input.
|
|
841
|
+
await writeContent(content);
|
|
842
|
+
finalContent = await formatContent(dst, content, cwd, customLinterServers, operationSignal);
|
|
843
|
+
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
844
|
+
await writeContent(finalContent);
|
|
845
|
+
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal);
|
|
846
|
+
} else {
|
|
847
|
+
// 1. Sync original content to LSP servers
|
|
848
|
+
await syncFileContent(dst, content, cwd, lspServers, operationSignal);
|
|
849
|
+
|
|
850
|
+
// 2. Format in-memory via LSP
|
|
851
|
+
if (enableFormat) {
|
|
852
|
+
finalContent = await formatContent(dst, content, cwd, lspServers, operationSignal);
|
|
719
853
|
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
720
|
-
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// 3. If formatted, sync formatted content to LSP servers
|
|
857
|
+
if (finalContent !== content) {
|
|
721
858
|
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal);
|
|
722
|
-
}
|
|
723
|
-
// 1. Sync original content to LSP servers
|
|
724
|
-
await syncFileContent(dst, content, cwd, lspServers, operationSignal);
|
|
859
|
+
}
|
|
725
860
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
formatter = finalContent !== content ? FileFormatResult.FORMATTED : FileFormatResult.UNCHANGED;
|
|
730
|
-
}
|
|
861
|
+
// 4. Write to disk
|
|
862
|
+
await getWritePromise();
|
|
863
|
+
}
|
|
731
864
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
await syncFileContent(dst, finalContent, cwd, lspServers, operationSignal);
|
|
735
|
-
}
|
|
865
|
+
// 5. Notify saved to LSP servers
|
|
866
|
+
await notifyFileSaved(dst, cwd, lspServers, operationSignal);
|
|
736
867
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
868
|
+
// 6. Get diagnostics from all servers (wait for fresh results)
|
|
869
|
+
if (enableDiagnostics) {
|
|
870
|
+
diagnostics = await getDiagnosticsForFile(dst, cwd, servers, operationSignal, minVersions);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
} catch {
|
|
874
|
+
await getWritePromise();
|
|
875
|
+
}
|
|
740
876
|
|
|
741
|
-
|
|
742
|
-
|
|
877
|
+
if (formatter !== undefined) {
|
|
878
|
+
diagnostics ??= {
|
|
879
|
+
server: servers.map(([name]) => name).join(", "),
|
|
880
|
+
messages: [],
|
|
881
|
+
summary: "OK",
|
|
882
|
+
errored: false,
|
|
883
|
+
};
|
|
884
|
+
diagnostics.formatter = formatter;
|
|
885
|
+
}
|
|
743
886
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
887
|
+
return diagnostics;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async function flushWritethroughBatch(
|
|
891
|
+
batch: PendingWritethrough[],
|
|
892
|
+
cwd: string,
|
|
893
|
+
options: Required<WritethroughOptions>,
|
|
894
|
+
signal?: AbortSignal,
|
|
895
|
+
): Promise<FileDiagnosticsResult | undefined> {
|
|
896
|
+
if (batch.length === 0) {
|
|
897
|
+
return undefined;
|
|
898
|
+
}
|
|
899
|
+
const results: Array<FileDiagnosticsResult | undefined> = [];
|
|
900
|
+
for (const entry of batch) {
|
|
901
|
+
results.push(await runLspWritethrough(entry.dst, entry.content, cwd, options, signal, entry.file));
|
|
902
|
+
}
|
|
903
|
+
return mergeDiagnostics(results, options);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** Create a writethrough callback for LSP aware write operations */
|
|
907
|
+
export function createLspWritethrough(cwd: string, options?: WritethroughOptions): WritethroughCallback {
|
|
908
|
+
const resolvedOptions: Required<WritethroughOptions> = {
|
|
909
|
+
enableFormat: options?.enableFormat ?? false,
|
|
910
|
+
enableDiagnostics: options?.enableDiagnostics ?? false,
|
|
911
|
+
};
|
|
912
|
+
if (!resolvedOptions.enableFormat && !resolvedOptions.enableDiagnostics) {
|
|
913
|
+
return writethroughNoop;
|
|
914
|
+
}
|
|
915
|
+
return async (
|
|
916
|
+
dst: string,
|
|
917
|
+
content: string,
|
|
918
|
+
signal?: AbortSignal,
|
|
919
|
+
file?: BunFile,
|
|
920
|
+
batch?: LspWritethroughBatchRequest,
|
|
921
|
+
) => {
|
|
922
|
+
if (!batch) {
|
|
923
|
+
return runLspWritethrough(dst, content, cwd, resolvedOptions, signal, file);
|
|
751
924
|
}
|
|
752
925
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
};
|
|
760
|
-
diagnostics.formatter = formatter;
|
|
926
|
+
const state = getOrCreateWritethroughBatch(batch.id, resolvedOptions);
|
|
927
|
+
state.entries.set(dst, { dst, content, file });
|
|
928
|
+
|
|
929
|
+
if (!batch.flush) {
|
|
930
|
+
await writethroughNoop(dst, content, signal, file);
|
|
931
|
+
return undefined;
|
|
761
932
|
}
|
|
762
933
|
|
|
763
|
-
|
|
934
|
+
writethroughBatches.delete(batch.id);
|
|
935
|
+
return flushWritethroughBatch(Array.from(state.entries.values()), cwd, state.options, signal);
|
|
764
936
|
};
|
|
765
937
|
}
|
|
766
938
|
|
|
767
|
-
/**
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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);
|
|
771
955
|
}
|
|
772
|
-
return {
|
|
773
|
-
name: "lsp",
|
|
774
|
-
label: "LSP",
|
|
775
|
-
description: renderPromptTemplate(lspDescription),
|
|
776
|
-
parameters: lspSchema,
|
|
777
|
-
renderCall,
|
|
778
|
-
renderResult,
|
|
779
|
-
execute: async (_toolCallId, params: LspParams, _signal) => {
|
|
780
|
-
const {
|
|
781
|
-
action,
|
|
782
|
-
file,
|
|
783
|
-
files,
|
|
784
|
-
line,
|
|
785
|
-
column,
|
|
786
|
-
end_line,
|
|
787
|
-
end_character,
|
|
788
|
-
query,
|
|
789
|
-
new_name,
|
|
790
|
-
replacement,
|
|
791
|
-
kind,
|
|
792
|
-
apply,
|
|
793
|
-
action_index,
|
|
794
|
-
include_declaration,
|
|
795
|
-
} = params;
|
|
796
|
-
|
|
797
|
-
const config = await getConfig(session.cwd);
|
|
798
|
-
|
|
799
|
-
// Status action doesn't need a file
|
|
800
|
-
if (action === "status") {
|
|
801
|
-
const servers = Object.keys(config.servers);
|
|
802
|
-
const lspmuxState = await detectLspmux();
|
|
803
|
-
const lspmuxStatus = lspmuxState.available
|
|
804
|
-
? lspmuxState.running
|
|
805
|
-
? "lspmux: active (multiplexing enabled)"
|
|
806
|
-
: "lspmux: installed but server not running"
|
|
807
|
-
: "";
|
|
808
|
-
|
|
809
|
-
const serverStatus =
|
|
810
|
-
servers.length > 0
|
|
811
|
-
? `Active language servers: ${servers.join(", ")}`
|
|
812
|
-
: "No language servers configured for this project";
|
|
813
|
-
|
|
814
|
-
const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
|
|
815
|
-
return {
|
|
816
|
-
content: [{ type: "text", text: output }],
|
|
817
|
-
details: { action, success: true },
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
956
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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) {
|
|
824
1027
|
return {
|
|
825
|
-
content: [
|
|
826
|
-
|
|
827
|
-
type: "text",
|
|
828
|
-
text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
|
|
829
|
-
},
|
|
830
|
-
],
|
|
831
|
-
details: { action, success: true },
|
|
1028
|
+
content: [{ type: "text", text: "Error: file or files parameter required for diagnostics" }],
|
|
1029
|
+
details: { action, success: false },
|
|
832
1030
|
};
|
|
833
1031
|
}
|
|
834
1032
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (!targets) {
|
|
839
|
-
return {
|
|
840
|
-
content: [{ type: "text", text: "Error: file or files parameter required for diagnostics" }],
|
|
841
|
-
details: { action, success: false },
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
const detailed = Boolean(files?.length);
|
|
846
|
-
const results: string[] = [];
|
|
847
|
-
const allServerNames = new Set<string>();
|
|
1033
|
+
const detailed = Boolean(files?.length);
|
|
1034
|
+
const results: string[] = [];
|
|
1035
|
+
const allServerNames = new Set<string>();
|
|
848
1036
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
+
}
|
|
856
1044
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
allDiagnostics.push(...diagnostics);
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
const client = await getOrCreateClient(serverConfig, session.cwd);
|
|
872
|
-
const minVersion = client.diagnosticsVersion;
|
|
873
|
-
await refreshFile(client, resolved);
|
|
874
|
-
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);
|
|
875
1056
|
allDiagnostics.push(...diagnostics);
|
|
876
|
-
|
|
877
|
-
// Server failed, continue with others
|
|
1057
|
+
continue;
|
|
878
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
|
|
879
1066
|
}
|
|
1067
|
+
}
|
|
880
1068
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
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);
|
|
890
1077
|
}
|
|
1078
|
+
}
|
|
891
1079
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
return {
|
|
895
|
-
content: [{ type: "text", text: "No diagnostics" }],
|
|
896
|
-
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const summary = formatDiagnosticsSummary(uniqueDiagnostics);
|
|
901
|
-
const formatted = uniqueDiagnostics.map((d) => formatDiagnostic(d, relPath));
|
|
902
|
-
const output = `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}`;
|
|
1080
|
+
if (!detailed && targets.length === 1) {
|
|
1081
|
+
if (uniqueDiagnostics.length === 0) {
|
|
903
1082
|
return {
|
|
904
|
-
content: [{ type: "text", text:
|
|
1083
|
+
content: [{ type: "text", text: "No diagnostics" }],
|
|
905
1084
|
details: { action, serverName: Array.from(allServerNames).join(", "), success: true },
|
|
906
1085
|
};
|
|
907
1086
|
}
|
|
908
1087
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
917
|
-
}
|
|
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
|
+
};
|
|
918
1095
|
}
|
|
919
1096
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
+
}
|
|
924
1106
|
}
|
|
925
1107
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
action
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
action !== "runnables" &&
|
|
932
|
-
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
|
+
}
|
|
933
1113
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
+
}
|
|
940
1128
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1129
|
+
const resolvedFile = file ? resolveToCwd(file, this.session.cwd) : null;
|
|
1130
|
+
const serverInfo = resolvedFile
|
|
1131
|
+
? getLspServerForFile(config, resolvedFile)
|
|
1132
|
+
: getServerForWorkspaceAction(config, action);
|
|
945
1133
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1134
|
+
if (!serverInfo) {
|
|
1135
|
+
return {
|
|
1136
|
+
content: [{ type: "text", text: "No language server found for this action" }],
|
|
1137
|
+
details: { action, success: false },
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
952
1140
|
|
|
953
|
-
|
|
1141
|
+
const [serverName, serverConfig] = serverInfo;
|
|
954
1142
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
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
|
+
};
|
|
966
1153
|
}
|
|
1154
|
+
}
|
|
967
1155
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1156
|
+
if (targetFile) {
|
|
1157
|
+
await ensureFileOpen(client, targetFile);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const uri = targetFile ? fileToUri(targetFile) : "";
|
|
1161
|
+
const position = { line: (line || 1) - 1, character: (column || 1) - 1 };
|
|
971
1162
|
|
|
972
|
-
|
|
973
|
-
const position = { line: (line || 1) - 1, character: (column || 1) - 1 };
|
|
1163
|
+
let output: string;
|
|
974
1164
|
|
|
975
|
-
|
|
1165
|
+
switch (action) {
|
|
1166
|
+
// =====================================================================
|
|
1167
|
+
// Standard LSP Operations
|
|
1168
|
+
// =====================================================================
|
|
976
1169
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1170
|
+
case "definition": {
|
|
1171
|
+
const result = (await sendRequest(client, "textDocument/definition", {
|
|
1172
|
+
textDocument: { uri },
|
|
1173
|
+
position,
|
|
1174
|
+
})) as Location | Location[] | LocationLink | LocationLink[] | null;
|
|
981
1175
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
+
});
|
|
987
1191
|
|
|
988
|
-
if (
|
|
1192
|
+
if (locations.length === 0) {
|
|
989
1193
|
output = "No definition found";
|
|
990
1194
|
} else {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
return [loc as Location];
|
|
995
|
-
}
|
|
996
|
-
if ("targetUri" in loc) {
|
|
997
|
-
// Use targetSelectionRange (the precise identifier range) with fallback to targetRange
|
|
998
|
-
const link = loc as LocationLink;
|
|
999
|
-
return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
|
|
1000
|
-
}
|
|
1001
|
-
return [];
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
if (locations.length === 0) {
|
|
1005
|
-
output = "No definition found";
|
|
1006
|
-
} else {
|
|
1007
|
-
output = `Found ${locations.length} definition(s):\n${locations
|
|
1008
|
-
.map((loc) => ` ${formatLocation(loc, session.cwd)}`)
|
|
1009
|
-
.join("\n")}`;
|
|
1010
|
-
}
|
|
1195
|
+
output = `Found ${locations.length} definition(s):\n${locations
|
|
1196
|
+
.map((loc) => ` ${formatLocation(loc, this.session.cwd)}`)
|
|
1197
|
+
.join("\n")}`;
|
|
1011
1198
|
}
|
|
1012
|
-
break;
|
|
1013
1199
|
}
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1014
1202
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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;
|
|
1021
1209
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
}
|
|
1028
|
-
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")}`;
|
|
1029
1215
|
}
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1030
1218
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1219
|
+
case "hover": {
|
|
1220
|
+
const result = (await sendRequest(client, "textDocument/hover", {
|
|
1221
|
+
textDocument: { uri },
|
|
1222
|
+
position,
|
|
1223
|
+
})) as Hover | null;
|
|
1036
1224
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
}
|
|
1042
|
-
break;
|
|
1225
|
+
if (!result || !result.contents) {
|
|
1226
|
+
output = "No hover information";
|
|
1227
|
+
} else {
|
|
1228
|
+
output = extractHoverText(result.contents);
|
|
1043
1229
|
}
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1044
1232
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1233
|
+
case "symbols": {
|
|
1234
|
+
const result = (await sendRequest(client, "textDocument/documentSymbol", {
|
|
1235
|
+
textDocument: { uri },
|
|
1236
|
+
})) as (DocumentSymbol | SymbolInformation)[] | null;
|
|
1049
1237
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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")}`;
|
|
1057
1252
|
} else {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
// Flat
|
|
1066
|
-
const lines = (result as SymbolInformation[]).map((s) => {
|
|
1067
|
-
const line = s.location.range.start.line + 1;
|
|
1068
|
-
const icon = symbolKindToIcon(s.kind);
|
|
1069
|
-
return `${icon} ${s.name} @ line ${line}`;
|
|
1070
|
-
});
|
|
1071
|
-
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
1072
|
-
}
|
|
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")}`;
|
|
1073
1260
|
}
|
|
1074
|
-
break;
|
|
1075
1261
|
}
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1076
1264
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
+
}
|
|
1084
1272
|
|
|
1085
|
-
|
|
1086
|
-
| SymbolInformation[]
|
|
1087
|
-
| null;
|
|
1273
|
+
const result = (await sendRequest(client, "workspace/symbol", { query })) as SymbolInformation[] | null;
|
|
1088
1274
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}
|
|
1095
|
-
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")}`;
|
|
1096
1280
|
}
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
1097
1283
|
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
+
}
|
|
1105
1291
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1292
|
+
const result = (await sendRequest(client, "textDocument/rename", {
|
|
1293
|
+
textDocument: { uri },
|
|
1294
|
+
position,
|
|
1295
|
+
newName: new_name,
|
|
1296
|
+
})) as WorkspaceEdit | null;
|
|
1111
1297
|
|
|
1112
|
-
|
|
1113
|
-
|
|
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")}`;
|
|
1114
1305
|
} else {
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
const applied = await applyWorkspaceEdit(result, session.cwd);
|
|
1118
|
-
output = `Applied rename:\n${applied.map((a) => ` ${a}`).join("\n")}`;
|
|
1119
|
-
} else {
|
|
1120
|
-
const preview = formatWorkspaceEdit(result, session.cwd);
|
|
1121
|
-
output = `Rename preview:\n${preview.map((p) => ` ${p}`).join("\n")}`;
|
|
1122
|
-
}
|
|
1306
|
+
const preview = formatWorkspaceEdit(result, this.session.cwd);
|
|
1307
|
+
output = `Rename preview:\n${preview.map((p) => ` ${p}`).join("\n")}`;
|
|
1123
1308
|
}
|
|
1124
|
-
|
|
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];
|
|
1125
1336
|
}
|
|
1126
1337
|
|
|
1127
|
-
|
|
1128
|
-
|
|
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) {
|
|
1129
1349
|
return {
|
|
1130
|
-
content: [
|
|
1350
|
+
content: [
|
|
1351
|
+
{
|
|
1352
|
+
type: "text",
|
|
1353
|
+
text: `Error: action_index ${action_index} out of range (0-${result.length - 1})`,
|
|
1354
|
+
},
|
|
1355
|
+
],
|
|
1131
1356
|
details: { action, serverName, success: false },
|
|
1132
1357
|
};
|
|
1133
1358
|
}
|
|
1134
1359
|
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
(
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const codeActionContext: { diagnostics: Diagnostic[]; only?: string[] } = {
|
|
1146
|
-
diagnostics: relevantDiagnostics,
|
|
1147
|
-
};
|
|
1148
|
-
if (kind) {
|
|
1149
|
-
codeActionContext.only = [kind];
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
const result = (await sendRequest(client, "textDocument/codeAction", {
|
|
1153
|
-
textDocument: { uri },
|
|
1154
|
-
range,
|
|
1155
|
-
context: codeActionContext,
|
|
1156
|
-
})) as Array<CodeAction | Command> | null;
|
|
1157
|
-
|
|
1158
|
-
if (!result || result.length === 0) {
|
|
1159
|
-
output = "No code actions available";
|
|
1160
|
-
} else if (action_index !== undefined) {
|
|
1161
|
-
// Apply specific action
|
|
1162
|
-
if (action_index < 0 || action_index >= result.length) {
|
|
1163
|
-
return {
|
|
1164
|
-
content: [
|
|
1165
|
-
{
|
|
1166
|
-
type: "text",
|
|
1167
|
-
text: `Error: action_index ${action_index} out of range (0-${result.length - 1})`,
|
|
1168
|
-
},
|
|
1169
|
-
],
|
|
1170
|
-
details: { action, serverName, success: false },
|
|
1171
|
-
};
|
|
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 };
|
|
1172
1369
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
typeof (candidate as Command).command === "string";
|
|
1176
|
-
const isCodeAction = (candidate: CodeAction | Command): candidate is CodeAction =>
|
|
1177
|
-
!isCommand(candidate);
|
|
1178
|
-
const getCommandPayload = (
|
|
1179
|
-
candidate: CodeAction | Command,
|
|
1180
|
-
): { command: string; arguments?: unknown[] } | null => {
|
|
1181
|
-
if (isCommand(candidate)) {
|
|
1182
|
-
return { command: candidate.command, arguments: candidate.arguments };
|
|
1183
|
-
}
|
|
1184
|
-
if (candidate.command) {
|
|
1185
|
-
return { command: candidate.command.command, arguments: candidate.command.arguments };
|
|
1186
|
-
}
|
|
1187
|
-
return null;
|
|
1188
|
-
};
|
|
1189
|
-
|
|
1190
|
-
const codeAction = result[action_index];
|
|
1191
|
-
|
|
1192
|
-
// Resolve if needed
|
|
1193
|
-
let resolvedAction = codeAction;
|
|
1194
|
-
if (
|
|
1195
|
-
isCodeAction(codeAction) &&
|
|
1196
|
-
!codeAction.edit &&
|
|
1197
|
-
codeAction.data &&
|
|
1198
|
-
client.serverCapabilities?.codeActionProvider
|
|
1199
|
-
) {
|
|
1200
|
-
const provider = client.serverCapabilities.codeActionProvider;
|
|
1201
|
-
if (typeof provider === "object" && provider.resolveProvider) {
|
|
1202
|
-
resolvedAction = (await sendRequest(client, "codeAction/resolve", codeAction)) as CodeAction;
|
|
1203
|
-
}
|
|
1370
|
+
if (candidate.command) {
|
|
1371
|
+
return { command: candidate.command.command, arguments: candidate.command.arguments };
|
|
1204
1372
|
}
|
|
1373
|
+
return null;
|
|
1374
|
+
};
|
|
1205
1375
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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;
|
|
1217
1389
|
}
|
|
1218
|
-
} else {
|
|
1219
|
-
// List available actions
|
|
1220
|
-
const lines = result.map((actionItem, i) => {
|
|
1221
|
-
if ("kind" in actionItem || "isPreferred" in actionItem || "edit" in actionItem) {
|
|
1222
|
-
const actionDetails = actionItem as CodeAction;
|
|
1223
|
-
const preferred = actionDetails.isPreferred ? " (preferred)" : "";
|
|
1224
|
-
const kindInfo = actionDetails.kind ? ` [${actionDetails.kind}]` : "";
|
|
1225
|
-
return ` [${i}] ${actionDetails.title}${kindInfo}${preferred}`;
|
|
1226
|
-
}
|
|
1227
|
-
return ` [${i}] ${actionItem.title}`;
|
|
1228
|
-
});
|
|
1229
|
-
output = `Available code actions:\n${lines.join("\n")}\n\nUse action_index parameter to apply a specific action.`;
|
|
1230
1390
|
}
|
|
1231
|
-
break;
|
|
1232
|
-
}
|
|
1233
1391
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
const prepareResult = (await sendRequest(client, "textDocument/prepareCallHierarchy", {
|
|
1238
|
-
textDocument: { uri },
|
|
1239
|
-
position,
|
|
1240
|
-
})) as CallHierarchyItem[] | null;
|
|
1241
|
-
|
|
1242
|
-
if (!prepareResult || prepareResult.length === 0) {
|
|
1243
|
-
output = "No callable symbol found at this position";
|
|
1244
|
-
break;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
const item = prepareResult[0];
|
|
1248
|
-
|
|
1249
|
-
if (action === "incoming_calls") {
|
|
1250
|
-
const calls = (await sendRequest(client, "callHierarchy/incomingCalls", { item })) as
|
|
1251
|
-
| CallHierarchyIncomingCall[]
|
|
1252
|
-
| null;
|
|
1253
|
-
|
|
1254
|
-
if (!calls || calls.length === 0) {
|
|
1255
|
-
output = `No callers found for "${item.name}"`;
|
|
1256
|
-
} else {
|
|
1257
|
-
const lines = calls.map((call) => {
|
|
1258
|
-
const loc = { uri: call.from.uri, range: call.from.selectionRange };
|
|
1259
|
-
const detail = call.from.detail ? ` (${call.from.detail})` : "";
|
|
1260
|
-
return ` ${call.from.name}${detail} @ ${formatLocation(loc, session.cwd)}`;
|
|
1261
|
-
});
|
|
1262
|
-
output = `Found ${calls.length} caller(s) of "${item.name}":\n${lines.join("\n")}`;
|
|
1263
|
-
}
|
|
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")}`;
|
|
1264
1395
|
} else {
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
if (!calls || calls.length === 0) {
|
|
1270
|
-
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}"`;
|
|
1271
1400
|
} else {
|
|
1272
|
-
|
|
1273
|
-
const loc = { uri: call.to.uri, range: call.to.selectionRange };
|
|
1274
|
-
const detail = call.to.detail ? ` (${call.to.detail})` : "";
|
|
1275
|
-
return ` ${call.to.name}${detail} @ ${formatLocation(loc, session.cwd)}`;
|
|
1276
|
-
});
|
|
1277
|
-
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`;
|
|
1278
1402
|
}
|
|
1279
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";
|
|
1280
1430
|
break;
|
|
1281
1431
|
}
|
|
1282
1432
|
|
|
1283
|
-
|
|
1284
|
-
// Rust-Analyzer Specific Operations
|
|
1285
|
-
// =====================================================================
|
|
1433
|
+
const item = prepareResult[0];
|
|
1286
1434
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
details: { action, serverName, success: false },
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1435
|
+
if (action === "incoming_calls") {
|
|
1436
|
+
const calls = (await sendRequest(client, "callHierarchy/incomingCalls", { item })) as
|
|
1437
|
+
| CallHierarchyIncomingCall[]
|
|
1438
|
+
| null;
|
|
1294
1439
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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")}`;
|
|
1302
1449
|
}
|
|
1450
|
+
} else {
|
|
1451
|
+
const calls = (await sendRequest(client, "callHierarchy/outgoingCalls", { item })) as
|
|
1452
|
+
| CallHierarchyOutgoingCall[]
|
|
1453
|
+
| null;
|
|
1303
1454
|
|
|
1304
|
-
if (
|
|
1305
|
-
output = "
|
|
1455
|
+
if (!calls || calls.length === 0) {
|
|
1456
|
+
output = `"${item.name}" doesn't call any functions`;
|
|
1306
1457
|
} else {
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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")}`;
|
|
1311
1464
|
}
|
|
1312
|
-
break;
|
|
1313
1465
|
}
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1314
1468
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
content: [{ type: "text", text: "Error: expand_macro requires rust-analyzer" }],
|
|
1319
|
-
details: { action, serverName, success: false },
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1469
|
+
// =====================================================================
|
|
1470
|
+
// Rust-Analyzer Specific Operations
|
|
1471
|
+
// =====================================================================
|
|
1322
1472
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
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
|
+
}
|
|
1329
1480
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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 });
|
|
1335
1487
|
}
|
|
1336
|
-
break;
|
|
1337
1488
|
}
|
|
1338
1489
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}
|
|
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
|
+
}
|
|
1346
1500
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
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
|
+
}
|
|
1353
1508
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
+
}
|
|
1360
1515
|
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
+
}
|
|
1363
1524
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
} else {
|
|
1371
|
-
const preview = formatWorkspaceEdit(result, session.cwd);
|
|
1372
|
-
output =
|
|
1373
|
-
preview.length > 0
|
|
1374
|
-
? `SSR preview:\n${preview.map((p) => ` ${p}`).join("\n")}`
|
|
1375
|
-
: "SSR: no matches found";
|
|
1376
|
-
}
|
|
1377
|
-
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
|
+
};
|
|
1378
1531
|
}
|
|
1379
1532
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
}
|
|
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
|
+
}
|
|
1387
1539
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1540
|
+
if (!replacement) {
|
|
1541
|
+
return {
|
|
1542
|
+
content: [{ type: "text", text: "Error: replacement parameter required for ssr" }],
|
|
1543
|
+
details: { action, serverName, success: false },
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1394
1546
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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";
|
|
1406
1562
|
}
|
|
1563
|
+
break;
|
|
1564
|
+
}
|
|
1407
1565
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
+
}
|
|
1415
1573
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1574
|
+
if (!targetFile) {
|
|
1575
|
+
return {
|
|
1576
|
+
content: [{ type: "text", text: "Error: file parameter required for runnables" }],
|
|
1577
|
+
details: { action, serverName, success: false },
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1422
1580
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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")}`;
|
|
1430
1590
|
}
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1431
1593
|
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
+
};
|
|
1436
1607
|
}
|
|
1437
1608
|
|
|
1438
|
-
|
|
1439
|
-
|
|
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;
|
|
1440
1616
|
}
|
|
1441
1617
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
details: { serverName, action, success: false },
|
|
1451
|
-
};
|
|
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}`;
|
|
1452
1626
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
+
}
|
|
1455
1640
|
}
|