@oh-my-pi/pi-coding-agent 1.337.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 +1228 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +637 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +81 -0
- package/src/cli/args.ts +246 -0
- package/src/cli/file-processor.ts +72 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +650 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli.ts +10 -0
- package/src/commands/init.md +20 -0
- package/src/config.ts +159 -0
- package/src/core/agent-session.ts +1900 -0
- package/src/core/auth-storage.ts +236 -0
- package/src/core/bash-executor.ts +196 -0
- package/src/core/compaction/branch-summarization.ts +343 -0
- package/src/core/compaction/compaction.ts +742 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +154 -0
- package/src/core/custom-tools/index.ts +21 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +169 -0
- package/src/core/custom-tools/wrapper.ts +28 -0
- package/src/core/exec.ts +129 -0
- package/src/core/export-html/index.ts +211 -0
- package/src/core/export-html/template.css +781 -0
- package/src/core/export-html/template.html +54 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +312 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +99 -0
- package/src/core/hooks/types.ts +773 -0
- package/src/core/index.ts +52 -0
- package/src/core/mcp/client.ts +158 -0
- package/src/core/mcp/config.ts +154 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +68 -0
- package/src/core/mcp/manager.ts +181 -0
- package/src/core/mcp/tool-bridge.ts +148 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +220 -0
- package/src/core/messages.ts +189 -0
- package/src/core/model-registry.ts +317 -0
- package/src/core/model-resolver.ts +393 -0
- package/src/core/plugins/doctor.ts +59 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +338 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +32 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +760 -0
- package/src/core/session-manager.ts +1128 -0
- package/src/core/settings-manager.ts +443 -0
- package/src/core/skills.ts +437 -0
- package/src/core/slash-commands.ts +248 -0
- package/src/core/system-prompt.ts +439 -0
- package/src/core/timings.ts +25 -0
- package/src/core/tools/ask.ts +211 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +250 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +475 -0
- package/src/core/tools/edit.ts +208 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +64 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/logger.ts +56 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +196 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +337 -0
- package/src/core/tools/exa/types.ts +168 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +261 -0
- package/src/core/tools/grep.ts +555 -0
- package/src/core/tools/index.ts +202 -0
- package/src/core/tools/ls.ts +140 -0
- package/src/core/tools/lsp/client.ts +605 -0
- package/src/core/tools/lsp/config.ts +147 -0
- package/src/core/tools/lsp/edits.ts +101 -0
- package/src/core/tools/lsp/index.ts +804 -0
- package/src/core/tools/lsp/render.ts +447 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +463 -0
- package/src/core/tools/lsp/utils.ts +486 -0
- package/src/core/tools/notebook.ts +229 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +240 -0
- package/src/core/tools/renderers.ts +540 -0
- package/src/core/tools/task/agents.ts +153 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/bundled-agents/browser.md +71 -0
- package/src/core/tools/task/bundled-agents/explore.md +82 -0
- package/src/core/tools/task/bundled-agents/plan.md +54 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
- package/src/core/tools/task/bundled-agents/task.md +53 -0
- package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
- package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
- package/src/core/tools/task/bundled-commands/implement.md +11 -0
- package/src/core/tools/task/commands.ts +213 -0
- package/src/core/tools/task/discovery.ts +208 -0
- package/src/core/tools/task/executor.ts +367 -0
- package/src/core/tools/task/index.ts +388 -0
- package/src/core/tools/task/model-resolver.ts +115 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +232 -0
- package/src/core/tools/task/types.ts +99 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2370 -0
- package/src/core/tools/web-search/auth.ts +193 -0
- package/src/core/tools/web-search/index.ts +537 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +302 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +182 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +99 -0
- package/src/index.ts +176 -0
- package/src/main.ts +464 -0
- package/src/migrations.ts +135 -0
- package/src/modes/index.ts +43 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +196 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/footer.ts +381 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +247 -0
- package/src/modes/interactive/components/oauth-selector.ts +120 -0
- package/src/modes/interactive/components/plugin-settings.ts +479 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +204 -0
- package/src/modes/interactive/components/settings-selector.ts +453 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +675 -0
- package/src/modes/interactive/components/tree-selector.ts +866 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +183 -0
- package/src/modes/interactive/interactive-mode.ts +2516 -0
- package/src/modes/interactive/theme/dark.json +101 -0
- package/src/modes/interactive/theme/light.json +98 -0
- package/src/modes/interactive/theme/theme-schema.json +308 -0
- package/src/modes/interactive/theme/theme.ts +998 -0
- package/src/modes/print-mode.ts +128 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +483 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell.ts +276 -0
- package/src/utils/tools-manager.ts +274 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { Theme } from "../../../modes/interactive/theme/theme.js";
|
|
5
|
+
import { resolveToCwd } from "../path-utils.js";
|
|
6
|
+
import { ensureFileOpen, getOrCreateClient, refreshFile, sendRequest } from "./client.js";
|
|
7
|
+
import { getServerForFile, hasCapability, type LspConfig, loadConfig } from "./config.js";
|
|
8
|
+
import { applyWorkspaceEdit } from "./edits.js";
|
|
9
|
+
import { renderCall, renderResult } from "./render.js";
|
|
10
|
+
import * as rustAnalyzer from "./rust-analyzer.js";
|
|
11
|
+
import {
|
|
12
|
+
type CallHierarchyIncomingCall,
|
|
13
|
+
type CallHierarchyItem,
|
|
14
|
+
type CallHierarchyOutgoingCall,
|
|
15
|
+
type CodeAction,
|
|
16
|
+
type Command,
|
|
17
|
+
type Diagnostic,
|
|
18
|
+
type DocumentSymbol,
|
|
19
|
+
type Hover,
|
|
20
|
+
type Location,
|
|
21
|
+
type LocationLink,
|
|
22
|
+
type LspClient,
|
|
23
|
+
type LspParams,
|
|
24
|
+
type LspToolDetails,
|
|
25
|
+
lspSchema,
|
|
26
|
+
type ServerConfig,
|
|
27
|
+
type SymbolInformation,
|
|
28
|
+
type WorkspaceEdit,
|
|
29
|
+
} from "./types.js";
|
|
30
|
+
import {
|
|
31
|
+
extractHoverText,
|
|
32
|
+
fileToUri,
|
|
33
|
+
formatDiagnostic,
|
|
34
|
+
formatDiagnosticsSummary,
|
|
35
|
+
formatDocumentSymbol,
|
|
36
|
+
formatLocation,
|
|
37
|
+
formatSymbolInformation,
|
|
38
|
+
formatWorkspaceEdit,
|
|
39
|
+
sleep,
|
|
40
|
+
symbolKindToIcon,
|
|
41
|
+
uriToFile,
|
|
42
|
+
} from "./utils.js";
|
|
43
|
+
|
|
44
|
+
export type { LspToolDetails } from "./types.js";
|
|
45
|
+
|
|
46
|
+
// Cache config per cwd to avoid repeated file I/O
|
|
47
|
+
const configCache = new Map<string, LspConfig>();
|
|
48
|
+
|
|
49
|
+
function getConfig(cwd: string): LspConfig {
|
|
50
|
+
let config = configCache.get(cwd);
|
|
51
|
+
if (!config) {
|
|
52
|
+
config = loadConfig(cwd);
|
|
53
|
+
configCache.set(cwd, config);
|
|
54
|
+
}
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const FILE_SEARCH_MAX_DEPTH = 5;
|
|
59
|
+
const IGNORED_DIRS = new Set(["node_modules", "target", "dist", "build", ".git"]);
|
|
60
|
+
|
|
61
|
+
function findFileByExtensions(baseDir: string, extensions: string[], maxDepth: number): string | null {
|
|
62
|
+
const normalized = extensions.map((ext) => ext.toLowerCase());
|
|
63
|
+
const search = (dir: string, depth: number): string | null => {
|
|
64
|
+
if (depth > maxDepth) return null;
|
|
65
|
+
let entries: fs.Dirent[];
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (entry.name.startsWith(".")) continue;
|
|
74
|
+
if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
|
|
75
|
+
const fullPath = path.join(dir, entry.name);
|
|
76
|
+
|
|
77
|
+
if (entry.isFile()) {
|
|
78
|
+
const lowerName = entry.name.toLowerCase();
|
|
79
|
+
if (normalized.some((ext) => lowerName.endsWith(ext))) {
|
|
80
|
+
return fullPath;
|
|
81
|
+
}
|
|
82
|
+
} else if (entry.isDirectory()) {
|
|
83
|
+
const found = search(fullPath, depth + 1);
|
|
84
|
+
if (found) return found;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return search(baseDir, 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findFileForServer(cwd: string, serverConfig: ServerConfig): string | null {
|
|
94
|
+
return findFileByExtensions(cwd, serverConfig.fileTypes, FILE_SEARCH_MAX_DEPTH);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getRustServer(config: LspConfig): [string, ServerConfig] | null {
|
|
98
|
+
const entries = Object.entries(config.servers) as Array<[string, ServerConfig]>;
|
|
99
|
+
const byName = entries.find(([name, server]) => name === "rust-analyzer" || server.command === "rust-analyzer");
|
|
100
|
+
if (byName) return byName;
|
|
101
|
+
|
|
102
|
+
for (const [name, server] of entries) {
|
|
103
|
+
if (
|
|
104
|
+
hasCapability(server, "flycheck") ||
|
|
105
|
+
hasCapability(server, "ssr") ||
|
|
106
|
+
hasCapability(server, "runnables") ||
|
|
107
|
+
hasCapability(server, "expandMacro") ||
|
|
108
|
+
hasCapability(server, "relatedTests")
|
|
109
|
+
) {
|
|
110
|
+
return [name, server];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getServerForWorkspaceAction(config: LspConfig, action: string): [string, ServerConfig] | null {
|
|
118
|
+
const entries = Object.entries(config.servers) as Array<[string, ServerConfig]>;
|
|
119
|
+
if (entries.length === 0) return null;
|
|
120
|
+
|
|
121
|
+
if (action === "workspace_symbols") {
|
|
122
|
+
return entries[0];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (action === "flycheck" || action === "ssr" || action === "runnables" || action === "reload_workspace") {
|
|
126
|
+
return getRustServer(config);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function waitForDiagnostics(client: LspClient, uri: string, timeoutMs = 3000): Promise<Diagnostic[]> {
|
|
133
|
+
const start = Date.now();
|
|
134
|
+
while (Date.now() - start < timeoutMs) {
|
|
135
|
+
const diagnostics = client.diagnostics.get(uri);
|
|
136
|
+
if (diagnostics !== undefined) return diagnostics;
|
|
137
|
+
await sleep(100);
|
|
138
|
+
}
|
|
139
|
+
return client.diagnostics.get(uri) ?? [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolDetails, Theme> {
|
|
143
|
+
return {
|
|
144
|
+
name: "lsp",
|
|
145
|
+
label: "LSP",
|
|
146
|
+
description: `Interact with Language Server Protocol (LSP) servers to get code intelligence features.
|
|
147
|
+
|
|
148
|
+
Standard operations:
|
|
149
|
+
- diagnostics: Get errors/warnings for a file
|
|
150
|
+
- definition: Go to symbol definition
|
|
151
|
+
- references: Find all references to a symbol
|
|
152
|
+
- hover: Get type info and documentation
|
|
153
|
+
- symbols: List symbols in a file (functions, classes, etc.)
|
|
154
|
+
- workspace_symbols: Search for symbols across the project
|
|
155
|
+
- rename: Rename a symbol across the codebase
|
|
156
|
+
- actions: List and apply code actions (quick fixes, refactors)
|
|
157
|
+
- incoming_calls: Find all callers of a function
|
|
158
|
+
- outgoing_calls: Find all functions called by a function
|
|
159
|
+
- status: Show active language servers
|
|
160
|
+
|
|
161
|
+
Rust-analyzer specific (require rust-analyzer):
|
|
162
|
+
- flycheck: Run clippy/cargo check
|
|
163
|
+
- expand_macro: Show macro expansion at cursor
|
|
164
|
+
- ssr: Structural search-replace
|
|
165
|
+
- runnables: Find runnable tests/binaries
|
|
166
|
+
- related_tests: Find tests for a function
|
|
167
|
+
- reload_workspace: Reload Cargo.toml changes`,
|
|
168
|
+
parameters: lspSchema,
|
|
169
|
+
renderCall,
|
|
170
|
+
renderResult,
|
|
171
|
+
execute: async (_toolCallId, params: LspParams, _signal) => {
|
|
172
|
+
const {
|
|
173
|
+
action,
|
|
174
|
+
file,
|
|
175
|
+
files,
|
|
176
|
+
line,
|
|
177
|
+
column,
|
|
178
|
+
end_line,
|
|
179
|
+
end_character,
|
|
180
|
+
query,
|
|
181
|
+
new_name,
|
|
182
|
+
replacement,
|
|
183
|
+
kind,
|
|
184
|
+
apply,
|
|
185
|
+
action_index,
|
|
186
|
+
include_declaration,
|
|
187
|
+
} = params;
|
|
188
|
+
|
|
189
|
+
const config = getConfig(cwd);
|
|
190
|
+
|
|
191
|
+
// Status action doesn't need a file
|
|
192
|
+
if (action === "status") {
|
|
193
|
+
const servers = Object.keys(config.servers);
|
|
194
|
+
const output =
|
|
195
|
+
servers.length > 0
|
|
196
|
+
? `Active language servers: ${servers.join(", ")}`
|
|
197
|
+
: "No language servers configured for this project";
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: "text", text: output }],
|
|
200
|
+
details: { action, success: true },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Diagnostics can be batch or single-file
|
|
205
|
+
if (action === "diagnostics") {
|
|
206
|
+
const targets = files?.length ? files : file ? [file] : null;
|
|
207
|
+
if (!targets) {
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: "text", text: "Error: file or files parameter required for diagnostics" }],
|
|
210
|
+
details: { action, success: false },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const detailed = Boolean(files?.length);
|
|
215
|
+
const results: string[] = [];
|
|
216
|
+
let lastServerName: string | undefined;
|
|
217
|
+
|
|
218
|
+
for (const target of targets) {
|
|
219
|
+
const resolved = resolveToCwd(target, cwd);
|
|
220
|
+
const serverInfo = getServerForFile(config, resolved);
|
|
221
|
+
if (!serverInfo) {
|
|
222
|
+
results.push(`✗ ${target}: No language server found`);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const [serverName, serverConfig] = serverInfo;
|
|
227
|
+
lastServerName = serverName;
|
|
228
|
+
|
|
229
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
230
|
+
await refreshFile(client, resolved);
|
|
231
|
+
|
|
232
|
+
const uri = fileToUri(resolved);
|
|
233
|
+
const diagnostics = await waitForDiagnostics(client, uri);
|
|
234
|
+
const relPath = path.relative(cwd, resolved);
|
|
235
|
+
|
|
236
|
+
if (!detailed && targets.length === 1) {
|
|
237
|
+
if (diagnostics.length === 0) {
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: "No diagnostics" }],
|
|
240
|
+
details: { action, serverName, success: true },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const summary = formatDiagnosticsSummary(diagnostics);
|
|
245
|
+
const formatted = diagnostics.map((d) => formatDiagnostic(d, relPath));
|
|
246
|
+
const output = `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}`;
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: "text", text: output }],
|
|
249
|
+
details: { action, serverName, success: true },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (diagnostics.length === 0) {
|
|
254
|
+
results.push(`✓ ${relPath}: no issues`);
|
|
255
|
+
} else {
|
|
256
|
+
const summary = formatDiagnosticsSummary(diagnostics);
|
|
257
|
+
results.push(`✗ ${relPath}: ${summary}`);
|
|
258
|
+
for (const diag of diagnostics) {
|
|
259
|
+
results.push(` ${formatDiagnostic(diag, relPath)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: "text", text: results.join("\n") }],
|
|
266
|
+
details: { action, serverName: lastServerName, success: true },
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const requiresFile =
|
|
271
|
+
!file &&
|
|
272
|
+
action !== "workspace_symbols" &&
|
|
273
|
+
action !== "flycheck" &&
|
|
274
|
+
action !== "ssr" &&
|
|
275
|
+
action !== "runnables" &&
|
|
276
|
+
action !== "reload_workspace";
|
|
277
|
+
|
|
278
|
+
if (requiresFile) {
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: "Error: file parameter required for this action" }],
|
|
281
|
+
details: { action, success: false },
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const resolvedFile = file ? resolveToCwd(file, cwd) : null;
|
|
286
|
+
const serverInfo = resolvedFile
|
|
287
|
+
? getServerForFile(config, resolvedFile)
|
|
288
|
+
: getServerForWorkspaceAction(config, action);
|
|
289
|
+
|
|
290
|
+
if (!serverInfo) {
|
|
291
|
+
return {
|
|
292
|
+
content: [{ type: "text", text: "No language server found for this action" }],
|
|
293
|
+
details: { action, success: false },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const [serverName, serverConfig] = serverInfo;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const client = await getOrCreateClient(serverConfig, cwd);
|
|
301
|
+
let targetFile = resolvedFile;
|
|
302
|
+
if (action === "runnables" && !targetFile) {
|
|
303
|
+
targetFile = findFileForServer(cwd, serverConfig);
|
|
304
|
+
if (!targetFile) {
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text", text: "Error: no matching files found for runnables" }],
|
|
307
|
+
details: { action, serverName, success: false },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (targetFile) {
|
|
313
|
+
await ensureFileOpen(client, targetFile);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const uri = targetFile ? fileToUri(targetFile) : "";
|
|
317
|
+
const position = { line: (line || 1) - 1, character: (column || 1) - 1 };
|
|
318
|
+
|
|
319
|
+
let output: string;
|
|
320
|
+
|
|
321
|
+
switch (action) {
|
|
322
|
+
// =====================================================================
|
|
323
|
+
// Standard LSP Operations
|
|
324
|
+
// =====================================================================
|
|
325
|
+
|
|
326
|
+
case "definition": {
|
|
327
|
+
const result = (await sendRequest(client, "textDocument/definition", {
|
|
328
|
+
textDocument: { uri },
|
|
329
|
+
position,
|
|
330
|
+
})) as Location | Location[] | LocationLink | LocationLink[] | null;
|
|
331
|
+
|
|
332
|
+
if (!result) {
|
|
333
|
+
output = "No definition found";
|
|
334
|
+
} else {
|
|
335
|
+
const raw = Array.isArray(result) ? result : [result];
|
|
336
|
+
const locations = raw.flatMap((loc) => {
|
|
337
|
+
if ("uri" in loc) {
|
|
338
|
+
return [loc as Location];
|
|
339
|
+
}
|
|
340
|
+
if ("targetUri" in loc) {
|
|
341
|
+
// Use targetSelectionRange (the precise identifier range) with fallback to targetRange
|
|
342
|
+
const link = loc as LocationLink;
|
|
343
|
+
return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
|
|
344
|
+
}
|
|
345
|
+
return [];
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (locations.length === 0) {
|
|
349
|
+
output = "No definition found";
|
|
350
|
+
} else {
|
|
351
|
+
output = `Found ${locations.length} definition(s):\n${locations
|
|
352
|
+
.map((loc) => ` ${formatLocation(loc, cwd)}`)
|
|
353
|
+
.join("\n")}`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case "references": {
|
|
360
|
+
const result = (await sendRequest(client, "textDocument/references", {
|
|
361
|
+
textDocument: { uri },
|
|
362
|
+
position,
|
|
363
|
+
context: { includeDeclaration: include_declaration ?? true },
|
|
364
|
+
})) as Location[] | null;
|
|
365
|
+
|
|
366
|
+
if (!result || result.length === 0) {
|
|
367
|
+
output = "No references found";
|
|
368
|
+
} else {
|
|
369
|
+
const lines = result.map((loc) => ` ${formatLocation(loc, cwd)}`);
|
|
370
|
+
output = `Found ${result.length} reference(s):\n${lines.join("\n")}`;
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
case "hover": {
|
|
376
|
+
const result = (await sendRequest(client, "textDocument/hover", {
|
|
377
|
+
textDocument: { uri },
|
|
378
|
+
position,
|
|
379
|
+
})) as Hover | null;
|
|
380
|
+
|
|
381
|
+
if (!result || !result.contents) {
|
|
382
|
+
output = "No hover information";
|
|
383
|
+
} else {
|
|
384
|
+
output = extractHoverText(result.contents);
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
case "symbols": {
|
|
390
|
+
const result = (await sendRequest(client, "textDocument/documentSymbol", {
|
|
391
|
+
textDocument: { uri },
|
|
392
|
+
})) as (DocumentSymbol | SymbolInformation)[] | null;
|
|
393
|
+
|
|
394
|
+
if (!result || result.length === 0) {
|
|
395
|
+
output = "No symbols found";
|
|
396
|
+
} else if (!targetFile) {
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text: "Error: file parameter required for symbols" }],
|
|
399
|
+
details: { action, serverName, success: false },
|
|
400
|
+
};
|
|
401
|
+
} else {
|
|
402
|
+
const relPath = path.relative(cwd, targetFile);
|
|
403
|
+
// Check if hierarchical (DocumentSymbol) or flat (SymbolInformation)
|
|
404
|
+
if ("selectionRange" in result[0]) {
|
|
405
|
+
// Hierarchical
|
|
406
|
+
const lines = (result as DocumentSymbol[]).flatMap((s) => formatDocumentSymbol(s));
|
|
407
|
+
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
408
|
+
} else {
|
|
409
|
+
// Flat
|
|
410
|
+
const lines = (result as SymbolInformation[]).map((s) => {
|
|
411
|
+
const line = s.location.range.start.line + 1;
|
|
412
|
+
const icon = symbolKindToIcon(s.kind);
|
|
413
|
+
return `${icon} ${s.name} @ line ${line}`;
|
|
414
|
+
});
|
|
415
|
+
output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
case "workspace_symbols": {
|
|
422
|
+
if (!query) {
|
|
423
|
+
return {
|
|
424
|
+
content: [{ type: "text", text: "Error: query parameter required for workspace_symbols" }],
|
|
425
|
+
details: { action, serverName, success: false },
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const result = (await sendRequest(client, "workspace/symbol", { query })) as
|
|
430
|
+
| SymbolInformation[]
|
|
431
|
+
| null;
|
|
432
|
+
|
|
433
|
+
if (!result || result.length === 0) {
|
|
434
|
+
output = `No symbols matching "${query}"`;
|
|
435
|
+
} else {
|
|
436
|
+
const lines = result.map((s) => formatSymbolInformation(s, cwd));
|
|
437
|
+
output = `Found ${result.length} symbol(s) matching "${query}":\n${lines
|
|
438
|
+
.map((l) => ` ${l}`)
|
|
439
|
+
.join("\n")}`;
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
case "rename": {
|
|
445
|
+
if (!new_name) {
|
|
446
|
+
return {
|
|
447
|
+
content: [{ type: "text", text: "Error: new_name parameter required for rename" }],
|
|
448
|
+
details: { action, serverName, success: false },
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const result = (await sendRequest(client, "textDocument/rename", {
|
|
453
|
+
textDocument: { uri },
|
|
454
|
+
position,
|
|
455
|
+
newName: new_name,
|
|
456
|
+
})) as WorkspaceEdit | null;
|
|
457
|
+
|
|
458
|
+
if (!result) {
|
|
459
|
+
output = "Rename returned no edits";
|
|
460
|
+
} else {
|
|
461
|
+
const shouldApply = apply !== false;
|
|
462
|
+
if (shouldApply) {
|
|
463
|
+
const applied = await applyWorkspaceEdit(result, cwd);
|
|
464
|
+
output = `Applied rename:\n${applied.map((a) => ` ${a}`).join("\n")}`;
|
|
465
|
+
} else {
|
|
466
|
+
const preview = formatWorkspaceEdit(result, cwd);
|
|
467
|
+
output = `Rename preview:\n${preview.map((p) => ` ${p}`).join("\n")}`;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
case "actions": {
|
|
474
|
+
if (!targetFile) {
|
|
475
|
+
return {
|
|
476
|
+
content: [{ type: "text", text: "Error: file parameter required for actions" }],
|
|
477
|
+
details: { action, serverName, success: false },
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
await refreshFile(client, targetFile);
|
|
482
|
+
const diagnostics = await waitForDiagnostics(client, uri);
|
|
483
|
+
const endLine = (end_line ?? line ?? 1) - 1;
|
|
484
|
+
const endCharacter = (end_character ?? column ?? 1) - 1;
|
|
485
|
+
const range = { start: position, end: { line: endLine, character: endCharacter } };
|
|
486
|
+
const relevantDiagnostics = diagnostics.filter(
|
|
487
|
+
(d) => d.range.start.line <= range.end.line && d.range.end.line >= range.start.line,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const codeActionContext: { diagnostics: Diagnostic[]; only?: string[] } = {
|
|
491
|
+
diagnostics: relevantDiagnostics,
|
|
492
|
+
};
|
|
493
|
+
if (kind) {
|
|
494
|
+
codeActionContext.only = [kind];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const result = (await sendRequest(client, "textDocument/codeAction", {
|
|
498
|
+
textDocument: { uri },
|
|
499
|
+
range,
|
|
500
|
+
context: codeActionContext,
|
|
501
|
+
})) as Array<CodeAction | Command> | null;
|
|
502
|
+
|
|
503
|
+
if (!result || result.length === 0) {
|
|
504
|
+
output = "No code actions available";
|
|
505
|
+
} else if (action_index !== undefined) {
|
|
506
|
+
// Apply specific action
|
|
507
|
+
if (action_index < 0 || action_index >= result.length) {
|
|
508
|
+
return {
|
|
509
|
+
content: [
|
|
510
|
+
{
|
|
511
|
+
type: "text",
|
|
512
|
+
text: `Error: action_index ${action_index} out of range (0-${result.length - 1})`,
|
|
513
|
+
},
|
|
514
|
+
],
|
|
515
|
+
details: { action, serverName, success: false },
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const isCommand = (candidate: CodeAction | Command): candidate is Command =>
|
|
520
|
+
typeof (candidate as Command).command === "string";
|
|
521
|
+
const isCodeAction = (candidate: CodeAction | Command): candidate is CodeAction =>
|
|
522
|
+
!isCommand(candidate);
|
|
523
|
+
const getCommandPayload = (
|
|
524
|
+
candidate: CodeAction | Command,
|
|
525
|
+
): { command: string; arguments?: unknown[] } | null => {
|
|
526
|
+
if (isCommand(candidate)) {
|
|
527
|
+
return { command: candidate.command, arguments: candidate.arguments };
|
|
528
|
+
}
|
|
529
|
+
if (candidate.command) {
|
|
530
|
+
return { command: candidate.command.command, arguments: candidate.command.arguments };
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const codeAction = result[action_index];
|
|
536
|
+
|
|
537
|
+
// Resolve if needed
|
|
538
|
+
let resolvedAction = codeAction;
|
|
539
|
+
if (
|
|
540
|
+
isCodeAction(codeAction) &&
|
|
541
|
+
!codeAction.edit &&
|
|
542
|
+
codeAction.data &&
|
|
543
|
+
client.serverCapabilities?.codeActionProvider
|
|
544
|
+
) {
|
|
545
|
+
const provider = client.serverCapabilities.codeActionProvider;
|
|
546
|
+
if (typeof provider === "object" && provider.resolveProvider) {
|
|
547
|
+
resolvedAction = (await sendRequest(client, "codeAction/resolve", codeAction)) as CodeAction;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (isCodeAction(resolvedAction) && resolvedAction.edit) {
|
|
552
|
+
const applied = await applyWorkspaceEdit(resolvedAction.edit, cwd);
|
|
553
|
+
output = `Applied "${codeAction.title}":\n${applied.map((a) => ` ${a}`).join("\n")}`;
|
|
554
|
+
} else {
|
|
555
|
+
const commandPayload = getCommandPayload(resolvedAction);
|
|
556
|
+
if (commandPayload) {
|
|
557
|
+
await sendRequest(client, "workspace/executeCommand", commandPayload);
|
|
558
|
+
output = `Executed "${codeAction.title}"`;
|
|
559
|
+
} else {
|
|
560
|
+
output = `Code action "${codeAction.title}" has no edits or command to apply`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
// List available actions
|
|
565
|
+
const lines = result.map((actionItem, i) => {
|
|
566
|
+
if ("kind" in actionItem || "isPreferred" in actionItem || "edit" in actionItem) {
|
|
567
|
+
const actionDetails = actionItem as CodeAction;
|
|
568
|
+
const preferred = actionDetails.isPreferred ? " (preferred)" : "";
|
|
569
|
+
const kindInfo = actionDetails.kind ? ` [${actionDetails.kind}]` : "";
|
|
570
|
+
return ` [${i}] ${actionDetails.title}${kindInfo}${preferred}`;
|
|
571
|
+
}
|
|
572
|
+
return ` [${i}] ${actionItem.title}`;
|
|
573
|
+
});
|
|
574
|
+
output = `Available code actions:\n${lines.join(
|
|
575
|
+
"\n",
|
|
576
|
+
)}\n\nUse action_index parameter to apply a specific action.`;
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
case "incoming_calls":
|
|
582
|
+
case "outgoing_calls": {
|
|
583
|
+
// First, prepare the call hierarchy item at the cursor position
|
|
584
|
+
const prepareResult = (await sendRequest(client, "textDocument/prepareCallHierarchy", {
|
|
585
|
+
textDocument: { uri },
|
|
586
|
+
position,
|
|
587
|
+
})) as CallHierarchyItem[] | null;
|
|
588
|
+
|
|
589
|
+
if (!prepareResult || prepareResult.length === 0) {
|
|
590
|
+
output = "No callable symbol found at this position";
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const item = prepareResult[0];
|
|
595
|
+
|
|
596
|
+
if (action === "incoming_calls") {
|
|
597
|
+
const calls = (await sendRequest(client, "callHierarchy/incomingCalls", { item })) as
|
|
598
|
+
| CallHierarchyIncomingCall[]
|
|
599
|
+
| null;
|
|
600
|
+
|
|
601
|
+
if (!calls || calls.length === 0) {
|
|
602
|
+
output = `No callers found for "${item.name}"`;
|
|
603
|
+
} else {
|
|
604
|
+
const lines = calls.map((call) => {
|
|
605
|
+
const loc = { uri: call.from.uri, range: call.from.selectionRange };
|
|
606
|
+
const detail = call.from.detail ? ` (${call.from.detail})` : "";
|
|
607
|
+
return ` ${call.from.name}${detail} @ ${formatLocation(loc, cwd)}`;
|
|
608
|
+
});
|
|
609
|
+
output = `Found ${calls.length} caller(s) of "${item.name}":\n${lines.join("\n")}`;
|
|
610
|
+
}
|
|
611
|
+
} else {
|
|
612
|
+
const calls = (await sendRequest(client, "callHierarchy/outgoingCalls", { item })) as
|
|
613
|
+
| CallHierarchyOutgoingCall[]
|
|
614
|
+
| null;
|
|
615
|
+
|
|
616
|
+
if (!calls || calls.length === 0) {
|
|
617
|
+
output = `"${item.name}" doesn't call any functions`;
|
|
618
|
+
} else {
|
|
619
|
+
const lines = calls.map((call) => {
|
|
620
|
+
const loc = { uri: call.to.uri, range: call.to.selectionRange };
|
|
621
|
+
const detail = call.to.detail ? ` (${call.to.detail})` : "";
|
|
622
|
+
return ` ${call.to.name}${detail} @ ${formatLocation(loc, cwd)}`;
|
|
623
|
+
});
|
|
624
|
+
output = `"${item.name}" calls ${calls.length} function(s):\n${lines.join("\n")}`;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// =====================================================================
|
|
631
|
+
// Rust-Analyzer Specific Operations
|
|
632
|
+
// =====================================================================
|
|
633
|
+
|
|
634
|
+
case "flycheck": {
|
|
635
|
+
if (!hasCapability(serverConfig, "flycheck")) {
|
|
636
|
+
return {
|
|
637
|
+
content: [{ type: "text", text: "Error: flycheck requires rust-analyzer" }],
|
|
638
|
+
details: { action, serverName, success: false },
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
await rustAnalyzer.flycheck(client, resolvedFile ?? undefined);
|
|
643
|
+
const collected: Array<{ filePath: string; diagnostic: Diagnostic }> = [];
|
|
644
|
+
for (const [diagUri, diags] of client.diagnostics.entries()) {
|
|
645
|
+
const relPath = path.relative(cwd, uriToFile(diagUri));
|
|
646
|
+
for (const diag of diags) {
|
|
647
|
+
collected.push({ filePath: relPath, diagnostic: diag });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (collected.length === 0) {
|
|
652
|
+
output = "Flycheck: no issues found";
|
|
653
|
+
} else {
|
|
654
|
+
const summary = formatDiagnosticsSummary(collected.map((d) => d.diagnostic));
|
|
655
|
+
const formatted = collected.slice(0, 20).map((d) => formatDiagnostic(d.diagnostic, d.filePath));
|
|
656
|
+
const more = collected.length > 20 ? `\n ... and ${collected.length - 20} more` : "";
|
|
657
|
+
output = `Flycheck ${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}${more}`;
|
|
658
|
+
}
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
case "expand_macro": {
|
|
663
|
+
if (!hasCapability(serverConfig, "expandMacro")) {
|
|
664
|
+
return {
|
|
665
|
+
content: [{ type: "text", text: "Error: expand_macro requires rust-analyzer" }],
|
|
666
|
+
details: { action, serverName, success: false },
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (!targetFile) {
|
|
671
|
+
return {
|
|
672
|
+
content: [{ type: "text", text: "Error: file parameter required for expand_macro" }],
|
|
673
|
+
details: { action, serverName, success: false },
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const result = await rustAnalyzer.expandMacro(client, targetFile, line || 1, column || 1);
|
|
678
|
+
if (!result) {
|
|
679
|
+
output = "No macro expansion at this position";
|
|
680
|
+
} else {
|
|
681
|
+
output = `Macro: ${result.name}\n\nExpansion:\n${result.expansion}`;
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
case "ssr": {
|
|
687
|
+
if (!hasCapability(serverConfig, "ssr")) {
|
|
688
|
+
return {
|
|
689
|
+
content: [{ type: "text", text: "Error: ssr requires rust-analyzer" }],
|
|
690
|
+
details: { action, serverName, success: false },
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (!query) {
|
|
695
|
+
return {
|
|
696
|
+
content: [{ type: "text", text: "Error: query parameter (pattern) required for ssr" }],
|
|
697
|
+
details: { action, serverName, success: false },
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!replacement) {
|
|
702
|
+
return {
|
|
703
|
+
content: [{ type: "text", text: "Error: replacement parameter required for ssr" }],
|
|
704
|
+
details: { action, serverName, success: false },
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const shouldApply = apply === true;
|
|
709
|
+
const result = await rustAnalyzer.ssr(client, query, replacement, !shouldApply);
|
|
710
|
+
|
|
711
|
+
if (shouldApply) {
|
|
712
|
+
const applied = await applyWorkspaceEdit(result, cwd);
|
|
713
|
+
output =
|
|
714
|
+
applied.length > 0
|
|
715
|
+
? `Applied SSR:\n${applied.map((a) => ` ${a}`).join("\n")}`
|
|
716
|
+
: "SSR: no matches found";
|
|
717
|
+
} else {
|
|
718
|
+
const preview = formatWorkspaceEdit(result, cwd);
|
|
719
|
+
output =
|
|
720
|
+
preview.length > 0
|
|
721
|
+
? `SSR preview:\n${preview.map((p) => ` ${p}`).join("\n")}`
|
|
722
|
+
: "SSR: no matches found";
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
case "runnables": {
|
|
728
|
+
if (!hasCapability(serverConfig, "runnables")) {
|
|
729
|
+
return {
|
|
730
|
+
content: [{ type: "text", text: "Error: runnables requires rust-analyzer" }],
|
|
731
|
+
details: { action, serverName, success: false },
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (!targetFile) {
|
|
736
|
+
return {
|
|
737
|
+
content: [{ type: "text", text: "Error: file parameter required for runnables" }],
|
|
738
|
+
details: { action, serverName, success: false },
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const result = await rustAnalyzer.runnables(client, targetFile, line);
|
|
743
|
+
if (result.length === 0) {
|
|
744
|
+
output = "No runnables found";
|
|
745
|
+
} else {
|
|
746
|
+
const lines = result.map((r) => {
|
|
747
|
+
const args = r.args?.cargoArgs?.join(" ") || "";
|
|
748
|
+
return ` [${r.kind}] ${r.label}${args ? ` (cargo ${args})` : ""}`;
|
|
749
|
+
});
|
|
750
|
+
output = `Found ${result.length} runnable(s):\n${lines.join("\n")}`;
|
|
751
|
+
}
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
case "related_tests": {
|
|
756
|
+
if (!hasCapability(serverConfig, "relatedTests")) {
|
|
757
|
+
return {
|
|
758
|
+
content: [{ type: "text", text: "Error: related_tests requires rust-analyzer" }],
|
|
759
|
+
details: { action, serverName, success: false },
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (!targetFile) {
|
|
764
|
+
return {
|
|
765
|
+
content: [{ type: "text", text: "Error: file parameter required for related_tests" }],
|
|
766
|
+
details: { action, serverName, success: false },
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const result = await rustAnalyzer.relatedTests(client, targetFile, line || 1, column || 1);
|
|
771
|
+
if (result.length === 0) {
|
|
772
|
+
output = "No related tests found";
|
|
773
|
+
} else {
|
|
774
|
+
output = `Found ${result.length} related test(s):\n${result.map((t) => ` ${t}`).join("\n")}`;
|
|
775
|
+
}
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
case "reload_workspace": {
|
|
780
|
+
await rustAnalyzer.reloadWorkspace(client);
|
|
781
|
+
output = "Workspace reloaded successfully";
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
default:
|
|
786
|
+
output = `Unknown action: ${action}`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
content: [{ type: "text", text: output }],
|
|
791
|
+
details: { serverName, action, success: true },
|
|
792
|
+
};
|
|
793
|
+
} catch (err) {
|
|
794
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
795
|
+
return {
|
|
796
|
+
content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
|
|
797
|
+
details: { serverName, action, success: false },
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export const lspTool = createLspTool(process.cwd());
|