@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,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP Tool TUI Rendering
|
|
3
|
+
*
|
|
4
|
+
* Renders LSP tool calls and results in the TUI with:
|
|
5
|
+
* - Syntax-highlighted hover information
|
|
6
|
+
* - Color-coded diagnostics by severity
|
|
7
|
+
* - Grouped references and symbols
|
|
8
|
+
* - Collapsible/expandable views
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AgentToolResult, RenderResultOptions } from "@oh-my-pi/pi-agent-core";
|
|
12
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
13
|
+
import { highlight, supportsLanguage } from "cli-highlight";
|
|
14
|
+
import type { Theme } from "../../../modes/interactive/theme/theme.js";
|
|
15
|
+
import type { LspParams, LspToolDetails } from "./types.js";
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Tree Drawing Characters
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
const TREE_MID = "├─";
|
|
22
|
+
const TREE_END = "└─";
|
|
23
|
+
const TREE_PIPE = "│";
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Call Rendering
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Render the LSP tool call in the TUI.
|
|
31
|
+
* Shows: "lsp <operation> <file/filecount>"
|
|
32
|
+
*/
|
|
33
|
+
export function renderCall(args: unknown, theme: Theme): Text {
|
|
34
|
+
const p = args as LspParams & { file?: string; files?: string[] };
|
|
35
|
+
|
|
36
|
+
let text = theme.fg("toolTitle", theme.bold("LSP "));
|
|
37
|
+
text += theme.fg("accent", p.action || "?");
|
|
38
|
+
|
|
39
|
+
if (p.file) {
|
|
40
|
+
text += ` ${theme.fg("muted", p.file)}`;
|
|
41
|
+
} else if (p.files?.length) {
|
|
42
|
+
text += ` ${theme.fg("muted", `${p.files.length} file(s)`)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new Text(text, 0, 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Result Rendering
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Render LSP tool result with intelligent formatting based on result type.
|
|
54
|
+
* Detects hover, diagnostics, references, symbols, etc. and formats accordingly.
|
|
55
|
+
*/
|
|
56
|
+
export function renderResult(
|
|
57
|
+
result: AgentToolResult<LspToolDetails>,
|
|
58
|
+
options: RenderResultOptions,
|
|
59
|
+
theme: Theme,
|
|
60
|
+
): Text {
|
|
61
|
+
const content = result.content?.[0];
|
|
62
|
+
if (!content || content.type !== "text" || !("text" in content) || !content.text) {
|
|
63
|
+
return new Text(theme.fg("error", "No result"), 0, 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const text = content.text;
|
|
67
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
68
|
+
const expanded = options.expanded;
|
|
69
|
+
|
|
70
|
+
// Detect result type and render accordingly
|
|
71
|
+
const codeBlockMatch = text.match(/```(\w*)\n([\s\S]*?)```/);
|
|
72
|
+
if (codeBlockMatch) {
|
|
73
|
+
return renderHover(codeBlockMatch, text, lines, expanded, theme);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const errorMatch = text.match(/(\d+)\s+error\(s\)/);
|
|
77
|
+
const warningMatch = text.match(/(\d+)\s+warning\(s\)/);
|
|
78
|
+
if (errorMatch || warningMatch || text.includes("✗")) {
|
|
79
|
+
return renderDiagnostics(errorMatch, warningMatch, lines, expanded, theme);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const refMatch = text.match(/(\d+)\s+reference\(s\)/);
|
|
83
|
+
if (refMatch) {
|
|
84
|
+
return renderReferences(refMatch, lines, expanded, theme);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const symbolsMatch = text.match(/Symbols in (.+):/);
|
|
88
|
+
if (symbolsMatch) {
|
|
89
|
+
return renderSymbols(symbolsMatch, lines, expanded, theme);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default fallback rendering
|
|
93
|
+
return renderGeneric(text, lines, expanded, theme);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// Hover Rendering
|
|
98
|
+
// =============================================================================
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Render hover information with syntax-highlighted code blocks.
|
|
102
|
+
*/
|
|
103
|
+
function renderHover(
|
|
104
|
+
codeBlockMatch: RegExpMatchArray,
|
|
105
|
+
fullText: string,
|
|
106
|
+
_lines: string[],
|
|
107
|
+
expanded: boolean,
|
|
108
|
+
theme: Theme,
|
|
109
|
+
): Text {
|
|
110
|
+
const lang = codeBlockMatch[1] || "";
|
|
111
|
+
const code = codeBlockMatch[2].trim();
|
|
112
|
+
const afterCode = fullText.slice(fullText.indexOf("```", 3) + 3).trim();
|
|
113
|
+
|
|
114
|
+
const codeLines = highlightCode(code, lang, theme);
|
|
115
|
+
const icon = theme.fg("accent", "●");
|
|
116
|
+
const langLabel = lang ? theme.fg("mdCodeBlockBorder", ` ${lang}`) : "";
|
|
117
|
+
|
|
118
|
+
if (expanded) {
|
|
119
|
+
let output = `${icon} ${theme.fg("toolTitle", "Hover")}${langLabel}`;
|
|
120
|
+
output += `\n ${theme.fg("mdCodeBlockBorder", "┌───")}`;
|
|
121
|
+
for (const line of codeLines) {
|
|
122
|
+
output += `\n ${theme.fg("mdCodeBlockBorder", "│")} ${line}`;
|
|
123
|
+
}
|
|
124
|
+
output += `\n ${theme.fg("mdCodeBlockBorder", "└───")}`;
|
|
125
|
+
if (afterCode) {
|
|
126
|
+
output += `\n ${theme.fg("muted", afterCode)}`;
|
|
127
|
+
}
|
|
128
|
+
return new Text(output, 0, 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Collapsed view
|
|
132
|
+
const firstCodeLine = codeLines[0] || "";
|
|
133
|
+
const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
|
|
134
|
+
|
|
135
|
+
let output = `${icon} ${theme.fg("toolTitle", "Hover")}${langLabel}${expandHint}`;
|
|
136
|
+
output += `\n ${theme.fg("mdCodeBlockBorder", "│")} ${firstCodeLine}`;
|
|
137
|
+
|
|
138
|
+
if (codeLines.length > 1) {
|
|
139
|
+
output += `\n ${theme.fg("mdCodeBlockBorder", "│")} ${theme.fg("muted", `… ${codeLines.length - 1} more lines`)}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (afterCode) {
|
|
143
|
+
const docPreview = afterCode.length > 60 ? `${afterCode.slice(0, 60)}…` : afterCode;
|
|
144
|
+
output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", docPreview)}`;
|
|
145
|
+
} else {
|
|
146
|
+
output += `\n ${theme.fg("mdCodeBlockBorder", "└───")}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return new Text(output, 0, 0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Syntax highlight code using highlight.ts.
|
|
154
|
+
*/
|
|
155
|
+
function highlightCode(codeText: string, language: string, theme: Theme): string[] {
|
|
156
|
+
const validLang = language && supportsLanguage(language) ? language : undefined;
|
|
157
|
+
try {
|
|
158
|
+
const cliTheme = {
|
|
159
|
+
keyword: (s: string) => theme.fg("syntaxKeyword", s),
|
|
160
|
+
built_in: (s: string) => theme.fg("syntaxType", s),
|
|
161
|
+
literal: (s: string) => theme.fg("syntaxNumber", s),
|
|
162
|
+
number: (s: string) => theme.fg("syntaxNumber", s),
|
|
163
|
+
string: (s: string) => theme.fg("syntaxString", s),
|
|
164
|
+
comment: (s: string) => theme.fg("syntaxComment", s),
|
|
165
|
+
function: (s: string) => theme.fg("syntaxFunction", s),
|
|
166
|
+
title: (s: string) => theme.fg("syntaxFunction", s),
|
|
167
|
+
class: (s: string) => theme.fg("syntaxType", s),
|
|
168
|
+
type: (s: string) => theme.fg("syntaxType", s),
|
|
169
|
+
attr: (s: string) => theme.fg("syntaxVariable", s),
|
|
170
|
+
variable: (s: string) => theme.fg("syntaxVariable", s),
|
|
171
|
+
params: (s: string) => theme.fg("syntaxVariable", s),
|
|
172
|
+
operator: (s: string) => theme.fg("syntaxOperator", s),
|
|
173
|
+
punctuation: (s: string) => theme.fg("syntaxPunctuation", s),
|
|
174
|
+
};
|
|
175
|
+
return highlight(codeText, { language: validLang, ignoreIllegals: true, theme: cliTheme }).split("\n");
|
|
176
|
+
} catch {
|
|
177
|
+
return codeText.split("\n");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// Diagnostics Rendering
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Render diagnostics with color-coded severity.
|
|
187
|
+
*/
|
|
188
|
+
function renderDiagnostics(
|
|
189
|
+
errorMatch: RegExpMatchArray | null,
|
|
190
|
+
warningMatch: RegExpMatchArray | null,
|
|
191
|
+
lines: string[],
|
|
192
|
+
expanded: boolean,
|
|
193
|
+
theme: Theme,
|
|
194
|
+
): Text {
|
|
195
|
+
const errorCount = errorMatch ? Number.parseInt(errorMatch[1], 10) : 0;
|
|
196
|
+
const warnCount = warningMatch ? Number.parseInt(warningMatch[1], 10) : 0;
|
|
197
|
+
|
|
198
|
+
const icon =
|
|
199
|
+
errorCount > 0 ? theme.fg("error", "●") : warnCount > 0 ? theme.fg("warning", "●") : theme.fg("success", "●");
|
|
200
|
+
|
|
201
|
+
const meta: string[] = [];
|
|
202
|
+
if (errorCount > 0) meta.push(`${errorCount} error${errorCount !== 1 ? "s" : ""}`);
|
|
203
|
+
if (warnCount > 0) meta.push(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`);
|
|
204
|
+
if (meta.length === 0) meta.push("No issues");
|
|
205
|
+
|
|
206
|
+
const diagLines = lines.filter((l) => l.includes("✗") || /:\d+:\d+/.test(l));
|
|
207
|
+
|
|
208
|
+
if (expanded) {
|
|
209
|
+
let output = `${icon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", meta.join(", "))}`;
|
|
210
|
+
for (let i = 0; i < diagLines.length; i++) {
|
|
211
|
+
const isLast = i === diagLines.length - 1;
|
|
212
|
+
const branch = isLast ? TREE_END : TREE_MID;
|
|
213
|
+
const line = diagLines[i].trim();
|
|
214
|
+
const color = line.includes("[error]") ? "error" : line.includes("[warning]") ? "warning" : "dim";
|
|
215
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg(color, line)}`;
|
|
216
|
+
}
|
|
217
|
+
return new Text(output, 0, 0);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Collapsed view
|
|
221
|
+
const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
|
|
222
|
+
let output = `${icon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", meta.join(", "))}${expandHint}`;
|
|
223
|
+
|
|
224
|
+
const previewLines = diagLines.length > 0 ? diagLines.slice(0, 4) : lines.slice(0, 4);
|
|
225
|
+
for (let i = 0; i < previewLines.length; i++) {
|
|
226
|
+
const isLast = i === previewLines.length - 1 && diagLines.length <= 4;
|
|
227
|
+
const branch = isLast ? TREE_END : TREE_MID;
|
|
228
|
+
output += `\n ${theme.fg("dim", branch)} ${previewLines[i].trim()}`;
|
|
229
|
+
}
|
|
230
|
+
if (diagLines.length > 4) {
|
|
231
|
+
output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${diagLines.length - 4} more`)}`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return new Text(output, 0, 0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// =============================================================================
|
|
238
|
+
// References Rendering
|
|
239
|
+
// =============================================================================
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Render references grouped by file.
|
|
243
|
+
*/
|
|
244
|
+
function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded: boolean, theme: Theme): Text {
|
|
245
|
+
const refCount = Number.parseInt(refMatch[1], 10);
|
|
246
|
+
const icon = refCount > 0 ? theme.fg("success", "●") : theme.fg("warning", "●");
|
|
247
|
+
|
|
248
|
+
const locLines = lines.filter((l) => /^\s*\S+:\d+:\d+/.test(l));
|
|
249
|
+
|
|
250
|
+
// Group by file
|
|
251
|
+
const byFile = new Map<string, Array<[string, string]>>();
|
|
252
|
+
for (const loc of locLines) {
|
|
253
|
+
const match = loc.trim().match(/^(.+):(\d+):(\d+)$/);
|
|
254
|
+
if (match) {
|
|
255
|
+
const [, file, line, col] = match;
|
|
256
|
+
if (!byFile.has(file)) byFile.set(file, []);
|
|
257
|
+
byFile.get(file)!.push([line, col]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const files = Array.from(byFile.keys());
|
|
262
|
+
|
|
263
|
+
const renderGrouped = (maxFiles: number, maxLocsPerFile: number, showHint: boolean): string => {
|
|
264
|
+
const expandHint = showHint ? theme.fg("dim", " (Ctrl+O to expand)") : "";
|
|
265
|
+
let output = `${icon} ${theme.fg("toolTitle", "References")} ${theme.fg("dim", `${refCount} found`)}${expandHint}`;
|
|
266
|
+
|
|
267
|
+
const filesToShow = files.slice(0, maxFiles);
|
|
268
|
+
for (let fi = 0; fi < filesToShow.length; fi++) {
|
|
269
|
+
const file = filesToShow[fi];
|
|
270
|
+
const locs = byFile.get(file)!;
|
|
271
|
+
const isLastFile = fi === filesToShow.length - 1 && files.length <= maxFiles;
|
|
272
|
+
const fileBranch = isLastFile ? TREE_END : TREE_MID;
|
|
273
|
+
const fileCont = isLastFile ? " " : `${TREE_PIPE} `;
|
|
274
|
+
|
|
275
|
+
if (locs.length === 1) {
|
|
276
|
+
output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("accent", file)}:${theme.fg(
|
|
277
|
+
"muted",
|
|
278
|
+
`${locs[0][0]}:${locs[0][1]}`,
|
|
279
|
+
)}`;
|
|
280
|
+
} else {
|
|
281
|
+
output += `\n ${theme.fg("dim", fileBranch)} ${theme.fg("accent", file)}`;
|
|
282
|
+
|
|
283
|
+
const locsToShow = locs.slice(0, maxLocsPerFile);
|
|
284
|
+
const locStrs = locsToShow.map(([l, c]) => `${l}:${c}`);
|
|
285
|
+
const locsText = locStrs.join(", ");
|
|
286
|
+
const hasMore = locs.length > maxLocsPerFile;
|
|
287
|
+
|
|
288
|
+
output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", TREE_END)} ${theme.fg("muted", locsText)}`;
|
|
289
|
+
if (hasMore) {
|
|
290
|
+
output += theme.fg("dim", ` … +${locs.length - maxLocsPerFile} more`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (files.length > maxFiles) {
|
|
296
|
+
output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${files.length - maxFiles} more files`)}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return output;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (expanded) {
|
|
303
|
+
return new Text(renderGrouped(files.length, 30, false), 0, 0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return new Text(renderGrouped(4, 10, true), 0, 0);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// =============================================================================
|
|
310
|
+
// Symbols Rendering
|
|
311
|
+
// =============================================================================
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Render document symbols in a hierarchical tree.
|
|
315
|
+
*/
|
|
316
|
+
function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded: boolean, theme: Theme): Text {
|
|
317
|
+
const fileName = symbolsMatch[1];
|
|
318
|
+
const icon = theme.fg("accent", "●");
|
|
319
|
+
|
|
320
|
+
interface SymbolInfo {
|
|
321
|
+
name: string;
|
|
322
|
+
line: string;
|
|
323
|
+
indent: number;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const symbolLines = lines.filter((l) => l.includes("@") && l.includes("line"));
|
|
327
|
+
const symbols: SymbolInfo[] = [];
|
|
328
|
+
|
|
329
|
+
for (const line of symbolLines) {
|
|
330
|
+
const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
331
|
+
const symMatch = line.trim().match(/^(.+?)\s*@\s*line\s*(\d+)/);
|
|
332
|
+
if (symMatch) {
|
|
333
|
+
symbols.push({ name: symMatch[1], line: symMatch[2], indent });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const isLastSibling = (i: number): boolean => {
|
|
338
|
+
const myIndent = symbols[i].indent;
|
|
339
|
+
for (let j = i + 1; j < symbols.length; j++) {
|
|
340
|
+
const nextIndent = symbols[j].indent;
|
|
341
|
+
if (nextIndent === myIndent) return false;
|
|
342
|
+
if (nextIndent < myIndent) return true;
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const getPrefix = (i: number): string => {
|
|
348
|
+
const myIndent = symbols[i].indent;
|
|
349
|
+
if (myIndent === 0) return " ";
|
|
350
|
+
|
|
351
|
+
let prefix = " ";
|
|
352
|
+
for (let level = 2; level <= myIndent; level += 2) {
|
|
353
|
+
let ancestorIdx = -1;
|
|
354
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
355
|
+
if (symbols[j].indent === level - 2) {
|
|
356
|
+
ancestorIdx = j;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (ancestorIdx >= 0 && isLastSibling(ancestorIdx)) {
|
|
361
|
+
prefix += " ";
|
|
362
|
+
} else {
|
|
363
|
+
prefix += `${TREE_PIPE} `;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return prefix;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const topLevelCount = symbols.filter((s) => s.indent === 0).length;
|
|
370
|
+
|
|
371
|
+
if (expanded) {
|
|
372
|
+
let output = `${icon} ${theme.fg("toolTitle", "Symbols")} ${theme.fg("dim", `in ${fileName}`)}`;
|
|
373
|
+
|
|
374
|
+
for (let i = 0; i < symbols.length; i++) {
|
|
375
|
+
const sym = symbols[i];
|
|
376
|
+
const prefix = getPrefix(i);
|
|
377
|
+
const branch = isLastSibling(i) ? TREE_END : TREE_MID;
|
|
378
|
+
output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("accent", sym.name)} ${theme.fg(
|
|
379
|
+
"muted",
|
|
380
|
+
`@${sym.line}`,
|
|
381
|
+
)}`;
|
|
382
|
+
}
|
|
383
|
+
return new Text(output, 0, 0);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Collapsed: show first 4 top-level symbols
|
|
387
|
+
const expandHint = theme.fg("dim", " (Ctrl+O to expand)");
|
|
388
|
+
let output = `${icon} ${theme.fg("toolTitle", "Symbols")} ${theme.fg("dim", `in ${fileName}`)}${expandHint}`;
|
|
389
|
+
|
|
390
|
+
const topLevel = symbols.filter((s) => s.indent === 0).slice(0, 4);
|
|
391
|
+
for (let i = 0; i < topLevel.length; i++) {
|
|
392
|
+
const sym = topLevel[i];
|
|
393
|
+
const isLast = i === topLevel.length - 1 && topLevelCount <= 4;
|
|
394
|
+
const branch = isLast ? TREE_END : TREE_MID;
|
|
395
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg("accent", sym.name)} ${theme.fg("muted", `@${sym.line}`)}`;
|
|
396
|
+
}
|
|
397
|
+
if (topLevelCount > 4) {
|
|
398
|
+
output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${topLevelCount - 4} more`)}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return new Text(output, 0, 0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// =============================================================================
|
|
405
|
+
// Generic Rendering
|
|
406
|
+
// =============================================================================
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Generic fallback rendering for unknown result types.
|
|
410
|
+
*/
|
|
411
|
+
function renderGeneric(text: string, lines: string[], expanded: boolean, theme: Theme): Text {
|
|
412
|
+
const hasError = text.includes("Error:") || text.includes("✗");
|
|
413
|
+
const hasSuccess = text.includes("✓") || text.includes("Applied");
|
|
414
|
+
|
|
415
|
+
const icon =
|
|
416
|
+
hasError && !hasSuccess
|
|
417
|
+
? theme.fg("error", "●")
|
|
418
|
+
: hasSuccess && !hasError
|
|
419
|
+
? theme.fg("success", "●")
|
|
420
|
+
: theme.fg("accent", "●");
|
|
421
|
+
|
|
422
|
+
if (expanded) {
|
|
423
|
+
let output = `${icon} ${theme.fg("toolTitle", "LSP")}`;
|
|
424
|
+
for (const line of lines) {
|
|
425
|
+
output += `\n ${line}`;
|
|
426
|
+
}
|
|
427
|
+
return new Text(output, 0, 0);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const firstLine = lines[0] || "No output";
|
|
431
|
+
const expandHint = lines.length > 1 ? theme.fg("dim", " (Ctrl+O to expand)") : "";
|
|
432
|
+
let output = `${icon} ${theme.fg("toolTitle", "LSP")} ${theme.fg("dim", firstLine.slice(0, 60))}${expandHint}`;
|
|
433
|
+
|
|
434
|
+
if (lines.length > 1) {
|
|
435
|
+
const previewLines = lines.slice(1, 4);
|
|
436
|
+
for (let i = 0; i < previewLines.length; i++) {
|
|
437
|
+
const isLast = i === previewLines.length - 1 && lines.length <= 4;
|
|
438
|
+
const branch = isLast ? TREE_END : TREE_MID;
|
|
439
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg("dim", previewLines[i].trim().slice(0, 80))}`;
|
|
440
|
+
}
|
|
441
|
+
if (lines.length > 4) {
|
|
442
|
+
output += `\n ${theme.fg("dim", TREE_END)} ${theme.fg("muted", `… ${lines.length - 4} more lines`)}`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return new Text(output, 0, 0);
|
|
447
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { sendNotification, sendRequest } from "./client.js";
|
|
2
|
+
import type { Diagnostic, ExpandMacroResult, LspClient, RelatedTest, Runnable, WorkspaceEdit } from "./types.js";
|
|
3
|
+
import { fileToUri } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wait for specified milliseconds.
|
|
7
|
+
*/
|
|
8
|
+
async function sleep(ms: number): Promise<void> {
|
|
9
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run flycheck (cargo check) and collect diagnostics.
|
|
14
|
+
* Sends rust-analyzer/runFlycheck notification and waits for diagnostics to accumulate.
|
|
15
|
+
*
|
|
16
|
+
* @param client - LSP client instance
|
|
17
|
+
* @param file - Optional file path to check (if not provided, checks entire workspace)
|
|
18
|
+
* @returns Array of all collected diagnostics
|
|
19
|
+
*/
|
|
20
|
+
export async function flycheck(client: LspClient, file?: string): Promise<Diagnostic[]> {
|
|
21
|
+
const textDocument = file ? { uri: fileToUri(file) } : null;
|
|
22
|
+
await sendNotification(client, "rust-analyzer/runFlycheck", { textDocument });
|
|
23
|
+
|
|
24
|
+
// Wait for diagnostics to accumulate (2 seconds as per reference)
|
|
25
|
+
await sleep(2000);
|
|
26
|
+
|
|
27
|
+
// Collect all diagnostics from client
|
|
28
|
+
const allDiags: Diagnostic[] = [];
|
|
29
|
+
for (const diags of Array.from(client.diagnostics.values())) {
|
|
30
|
+
allDiags.push(...diags);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return allDiags;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Expand macro at the given position.
|
|
38
|
+
*
|
|
39
|
+
* @param client - LSP client instance
|
|
40
|
+
* @param file - File path containing the macro
|
|
41
|
+
* @param line - 1-based line number
|
|
42
|
+
* @param character - 1-based character offset
|
|
43
|
+
* @returns ExpandMacroResult with macro name and expansion, or null if no macro at position
|
|
44
|
+
*/
|
|
45
|
+
export async function expandMacro(
|
|
46
|
+
client: LspClient,
|
|
47
|
+
file: string,
|
|
48
|
+
line: number,
|
|
49
|
+
character: number,
|
|
50
|
+
): Promise<ExpandMacroResult | null> {
|
|
51
|
+
const result = (await sendRequest(client, "rust-analyzer/expandMacro", {
|
|
52
|
+
textDocument: { uri: fileToUri(file) },
|
|
53
|
+
position: { line: line - 1, character: character - 1 },
|
|
54
|
+
})) as ExpandMacroResult | null;
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Perform structural search and replace (SSR).
|
|
61
|
+
*
|
|
62
|
+
* @param client - LSP client instance
|
|
63
|
+
* @param pattern - Search pattern
|
|
64
|
+
* @param replacement - Replacement pattern
|
|
65
|
+
* @param parseOnly - If true, returns matches only; if false, returns WorkspaceEdit to apply
|
|
66
|
+
* @returns WorkspaceEdit containing matches or changes to apply
|
|
67
|
+
*/
|
|
68
|
+
export async function ssr(
|
|
69
|
+
client: LspClient,
|
|
70
|
+
pattern: string,
|
|
71
|
+
replacement: string,
|
|
72
|
+
parseOnly = true,
|
|
73
|
+
): Promise<WorkspaceEdit> {
|
|
74
|
+
const result = (await sendRequest(client, "experimental/ssr", {
|
|
75
|
+
query: `${pattern} ==>> ${replacement}`,
|
|
76
|
+
parseOnly,
|
|
77
|
+
textDocument: { uri: "" }, // SSR searches workspace-wide
|
|
78
|
+
position: { line: 0, character: 0 },
|
|
79
|
+
selections: [],
|
|
80
|
+
})) as WorkspaceEdit;
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get runnables (tests, binaries, examples) for a file.
|
|
87
|
+
*
|
|
88
|
+
* @param client - LSP client instance
|
|
89
|
+
* @param file - File path to query
|
|
90
|
+
* @param line - Optional 1-based line number to get runnables at specific position
|
|
91
|
+
* @returns Array of Runnable items
|
|
92
|
+
*/
|
|
93
|
+
export async function runnables(client: LspClient, file: string, line?: number): Promise<Runnable[]> {
|
|
94
|
+
const params: { textDocument: { uri: string }; position?: { line: number; character: number } } = {
|
|
95
|
+
textDocument: { uri: fileToUri(file) },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (line !== undefined) {
|
|
99
|
+
params.position = { line: line - 1, character: 0 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = (await sendRequest(client, "experimental/runnables", params)) as Runnable[];
|
|
103
|
+
return result ?? [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get related tests for a position (e.g., tests for a function).
|
|
108
|
+
*
|
|
109
|
+
* @param client - LSP client instance
|
|
110
|
+
* @param file - File path
|
|
111
|
+
* @param line - 1-based line number
|
|
112
|
+
* @param character - 1-based character offset
|
|
113
|
+
* @returns Array of test runnable labels
|
|
114
|
+
*/
|
|
115
|
+
export async function relatedTests(
|
|
116
|
+
client: LspClient,
|
|
117
|
+
file: string,
|
|
118
|
+
line: number,
|
|
119
|
+
character: number,
|
|
120
|
+
): Promise<string[]> {
|
|
121
|
+
const tests = (await sendRequest(client, "rust-analyzer/relatedTests", {
|
|
122
|
+
textDocument: { uri: fileToUri(file) },
|
|
123
|
+
position: { line: line - 1, character: character - 1 },
|
|
124
|
+
})) as RelatedTest[];
|
|
125
|
+
|
|
126
|
+
if (!tests?.length) return [];
|
|
127
|
+
|
|
128
|
+
const labels: string[] = [];
|
|
129
|
+
for (const t of tests) {
|
|
130
|
+
if (t.runnable?.label) {
|
|
131
|
+
labels.push(t.runnable.label);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return labels;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reload workspace (re-index Cargo projects).
|
|
140
|
+
*
|
|
141
|
+
* @param client - LSP client instance
|
|
142
|
+
*/
|
|
143
|
+
export async function reloadWorkspace(client: LspClient): Promise<void> {
|
|
144
|
+
await sendRequest(client, "rust-analyzer/reloadWorkspace", null);
|
|
145
|
+
}
|