decorated-pi 0.3.0 → 0.4.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/README.md +58 -34
- package/extensions/file-times.ts +60 -2
- package/extensions/guidance.ts +5 -3
- package/extensions/index.ts +2 -0
- package/extensions/io.ts +210 -29
- package/extensions/lsp/client.ts +181 -428
- package/extensions/lsp/env.ts +45 -12
- package/extensions/lsp/format.ts +102 -237
- package/extensions/lsp/index.ts +8 -11
- package/extensions/lsp/manager.ts +249 -0
- package/extensions/lsp/prompt.ts +3 -42
- package/extensions/lsp/protocol.ts +219 -0
- package/extensions/lsp/servers.ts +80 -160
- package/extensions/lsp/tools.ts +160 -553
- package/extensions/lsp/types.ts +42 -0
- package/extensions/mcp/builtin.ts +126 -0
- package/extensions/mcp/client.ts +106 -0
- package/extensions/mcp/index.ts +123 -0
- package/extensions/patch.ts +291 -73
- package/extensions/providers/ark-coding.ts +2 -0
- package/extensions/safety/detect.ts +20 -744
- package/extensions/safety/entropy.ts +226 -0
- package/extensions/safety/index.ts +1 -93
- package/extensions/safety/patterns.ts +155 -0
- package/extensions/safety/types.ts +50 -0
- package/extensions/settings.ts +8 -0
- package/extensions/slash.ts +161 -7
- package/extensions/smart-at.ts +5 -5
- package/extensions/subdir-agents.ts +43 -13
- package/package.json +2 -3
- package/tsconfig.json +16 -0
- package/extensions/lsp/server-manager.ts +0 -309
- package/extensions/lsp/trust.ts +0 -45
package/extensions/lsp/env.ts
CHANGED
|
@@ -1,18 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LSP Child Process Environment —
|
|
2
|
+
* LSP Child Process Environment — whitelist-only for maximum security.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* LSP servers communicate over stdio, not HTTP. They only need a minimal
|
|
5
|
+
* set of environment variables to locate binaries, read user config, and
|
|
6
|
+
* respect locale settings.
|
|
7
|
+
*
|
|
8
|
+
* Proxy variables are explicitly excluded — LSP over stdio has no use for
|
|
9
|
+
* them, and passing them through could cause hangs if the server tries
|
|
10
|
+
* to make outbound HTTP requests.
|
|
6
11
|
*/
|
|
7
|
-
import { create_child_process_env as create_shared_child_process_env } from "@spences10/pi-child-env";
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const WHITELIST = new Set([
|
|
14
|
+
// Core — needed to find binaries and user config
|
|
15
|
+
"PATH", "HOME", "USER", "SHELL",
|
|
16
|
+
// Terminal compatibility
|
|
17
|
+
"TERM", "COLORTERM",
|
|
18
|
+
// Locale
|
|
19
|
+
"LANG",
|
|
20
|
+
// Pi-specific
|
|
21
|
+
"PI_CODING_AGENT_DIR",
|
|
22
|
+
// Node.js
|
|
23
|
+
"NODE_PATH", "NODE_OPTIONS",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a safe environment for spawning an LSP server child process.
|
|
28
|
+
*
|
|
29
|
+
* Strategy: whitelist. Only variables explicitly listed are passed through.
|
|
30
|
+
* Proxy, API keys, tokens, and all other env vars are silently stripped.
|
|
31
|
+
*/
|
|
32
|
+
export function createChildProcessEnv(
|
|
33
|
+
extras: Record<string, string> = {},
|
|
34
|
+
source: NodeJS.ProcessEnv = process.env,
|
|
12
35
|
): NodeJS.ProcessEnv {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
36
|
+
const env: Record<string, string> = {};
|
|
37
|
+
|
|
38
|
+
for (const [key, value] of Object.entries(source)) {
|
|
39
|
+
if (typeof value !== "string") continue;
|
|
40
|
+
if (WHITELIST.has(key)) env[key] = value;
|
|
41
|
+
// Also pass through LC_* variables (locale category vars)
|
|
42
|
+
if (key.startsWith("LC_")) env[key] = value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Explicit overrides always win
|
|
46
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
47
|
+
if (typeof value === "string") env[key] = value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return env;
|
|
18
51
|
}
|
package/extensions/lsp/format.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LSP Output Formatting —
|
|
2
|
+
* LSP Output Formatting — test-compatible standalone module.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Core formatting logic duplicated here so tests can import directly.
|
|
5
|
+
* Runtime tools.ts has its own inline copies.
|
|
6
6
|
*/
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import type { LspDiagnostic, LspHover, LspLocation, LspDocumentSymbol } from "./
|
|
8
|
+
import type { LspDiagnostic, LspHover, LspLocation, LspDocumentSymbol } from "./types.js";
|
|
9
9
|
import { LspClientStartError } from "./client.js";
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
export type { LspDiagnostic, LspHover, LspLocation, LspDocumentSymbol } from "./types.js";
|
|
12
|
+
export { LspClientStartError } from "./client.js";
|
|
13
|
+
|
|
14
|
+
// ─── Error formatting ────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export class LspToolError extends Error {
|
|
17
|
+
constructor(public readonly details: LspToolErrorDetail) {
|
|
18
|
+
super(details.message);
|
|
19
|
+
this.name = "LspToolError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
11
22
|
|
|
12
23
|
export interface LspToolErrorDetail {
|
|
13
24
|
kind: string;
|
|
@@ -20,126 +31,31 @@ export interface LspToolErrorDetail {
|
|
|
20
31
|
code?: string;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
export class LspToolError extends Error {
|
|
24
|
-
details: LspToolErrorDetail;
|
|
25
|
-
constructor(details: LspToolErrorDetail) {
|
|
26
|
-
super(details.message);
|
|
27
|
-
this.name = "LspToolError";
|
|
28
|
-
this.details = details;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const SYMBOL_KIND_LABELS: Record<number, string> = {
|
|
33
|
-
2: "module",
|
|
34
|
-
3: "namespace",
|
|
35
|
-
5: "class",
|
|
36
|
-
6: "method",
|
|
37
|
-
7: "property",
|
|
38
|
-
8: "field",
|
|
39
|
-
9: "constructor",
|
|
40
|
-
11: "interface",
|
|
41
|
-
12: "function",
|
|
42
|
-
13: "variable",
|
|
43
|
-
14: "constant",
|
|
44
|
-
23: "struct",
|
|
45
|
-
24: "event",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const SYMBOL_KIND_NAMES = Object.values(SYMBOL_KIND_LABELS);
|
|
49
|
-
|
|
50
|
-
export function format_status_lines(
|
|
51
|
-
cwd: string,
|
|
52
|
-
clients_by_server: Map<string, any>,
|
|
53
|
-
failed_servers: Map<string, any>
|
|
54
|
-
): string[] {
|
|
55
|
-
const lines: string[] = [];
|
|
56
|
-
const active_languages = new Set<string>();
|
|
57
|
-
|
|
58
|
-
const running_states = Array.from(clients_by_server.values()).sort(
|
|
59
|
-
(a: any, b: any) =>
|
|
60
|
-
a.language.localeCompare(b.language) ||
|
|
61
|
-
a.workspace_root.localeCompare(b.workspace_root)
|
|
62
|
-
);
|
|
63
|
-
for (const running of running_states) {
|
|
64
|
-
active_languages.add(running.language);
|
|
65
|
-
lines.push(
|
|
66
|
-
`${running.language}: running (ready=${running.client.is_ready()}) — ${running.command} [workspace ${running.workspace_root}]`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const failures = Array.from(failed_servers.values()).sort(
|
|
71
|
-
(a: any, b: any) =>
|
|
72
|
-
(a.language ?? "").localeCompare(b.language ?? "") ||
|
|
73
|
-
(a.workspace_root ?? "").localeCompare(b.workspace_root ?? "")
|
|
74
|
-
);
|
|
75
|
-
for (const failure of failures) {
|
|
76
|
-
if (failure.language) active_languages.add(failure.language);
|
|
77
|
-
const workspace = failure.workspace_root
|
|
78
|
-
? ` [workspace ${failure.workspace_root}]`
|
|
79
|
-
: "";
|
|
80
|
-
const language = failure.language ?? "unknown";
|
|
81
|
-
lines.push(`${language}: failed — ${failure.message}${workspace}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
for (const language of list_supported_languages()) {
|
|
85
|
-
if (active_languages.has(language)) continue;
|
|
86
|
-
const config = get_server_config(language, cwd);
|
|
87
|
-
if (config) {
|
|
88
|
-
lines.push(`${language}: idle — ${config.command}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return lines.length > 0 ? lines : ["No language servers configured for this project."];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
34
|
export function to_lsp_tool_error(
|
|
96
|
-
file: string,
|
|
97
|
-
|
|
98
|
-
workspace_root: string | undefined,
|
|
99
|
-
command: string,
|
|
100
|
-
install_hint: string | undefined,
|
|
101
|
-
error: unknown
|
|
35
|
+
file: string, language: string, workspaceRoot: string | undefined,
|
|
36
|
+
command: string, installHint: string | undefined, error: unknown,
|
|
102
37
|
): LspToolErrorDetail {
|
|
103
|
-
if (error instanceof LspToolError)
|
|
104
|
-
return error.details;
|
|
105
|
-
}
|
|
38
|
+
if (error instanceof LspToolError) return error.details;
|
|
106
39
|
if (error instanceof LspClientStartError) {
|
|
107
|
-
const missing_binary = error.code === "ENOENT";
|
|
108
40
|
return {
|
|
109
|
-
kind: "server_start_failed",
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
workspace_root,
|
|
113
|
-
command,
|
|
114
|
-
install_hint,
|
|
115
|
-
code: error.code,
|
|
116
|
-
message: missing_binary
|
|
117
|
-
? `command "${command}" not found`
|
|
118
|
-
: error.message,
|
|
41
|
+
kind: "server_start_failed", file, language, workspace_root: workspaceRoot,
|
|
42
|
+
command, install_hint: installHint, code: error.code,
|
|
43
|
+
message: error.code === "ENOENT" ? `command "${command}" not found` : error.message,
|
|
119
44
|
};
|
|
120
45
|
}
|
|
121
46
|
const err = error as Record<string, unknown> | undefined;
|
|
122
47
|
return {
|
|
123
|
-
kind: "tool_execution_failed",
|
|
124
|
-
|
|
125
|
-
language,
|
|
126
|
-
workspace_root,
|
|
127
|
-
command,
|
|
128
|
-
install_hint,
|
|
48
|
+
kind: "tool_execution_failed", file, language, workspace_root: workspaceRoot,
|
|
49
|
+
command, install_hint: installHint,
|
|
129
50
|
message: error instanceof Error ? error.message : String(error),
|
|
130
|
-
code:
|
|
131
|
-
err && typeof err.code === "string" ? err.code : undefined,
|
|
51
|
+
code: err?.code as string | undefined,
|
|
132
52
|
};
|
|
133
53
|
}
|
|
134
54
|
|
|
135
55
|
export function format_tool_error(details: LspToolErrorDetail): string {
|
|
136
|
-
if (details.kind === "unsupported_language")
|
|
137
|
-
return details.message;
|
|
138
|
-
}
|
|
56
|
+
if (details.kind === "unsupported_language") return details.message;
|
|
139
57
|
const lines = [
|
|
140
|
-
details.language
|
|
141
|
-
? `${details.language} LSP unavailable for ${details.file}`
|
|
142
|
-
: `LSP request failed for ${details.file}`,
|
|
58
|
+
details.language ? `${details.language} LSP unavailable for ${details.file}` : `LSP request failed for ${details.file}`,
|
|
143
59
|
`Reason: ${details.message}`,
|
|
144
60
|
];
|
|
145
61
|
if (details.command) lines.push(`Command: ${details.command}`);
|
|
@@ -148,90 +64,84 @@ export function format_tool_error(details: LspToolErrorDetail): string {
|
|
|
148
64
|
return lines.join("\n");
|
|
149
65
|
}
|
|
150
66
|
|
|
151
|
-
|
|
152
|
-
switch (severity) {
|
|
153
|
-
case 1: return "error";
|
|
154
|
-
case 2: return "warning";
|
|
155
|
-
case 3: return "info";
|
|
156
|
-
case 4: return "hint";
|
|
157
|
-
default: return "info";
|
|
158
|
-
}
|
|
159
|
-
}
|
|
67
|
+
// ─── Severity ─────────────────────────────────────────────────────────────
|
|
160
68
|
|
|
161
69
|
export type SeverityFilter = "error" | "warning" | "info" | "hint";
|
|
70
|
+
const SEVERITY_MAP: Record<SeverityFilter, number> = { error: 1, warning: 2, info: 3, hint: 4 };
|
|
162
71
|
|
|
163
|
-
|
|
164
|
-
error:
|
|
165
|
-
|
|
166
|
-
info: 3,
|
|
167
|
-
hint: 4,
|
|
168
|
-
};
|
|
72
|
+
function severityLabel(s: number): string {
|
|
73
|
+
return s === 1 ? "error" : s === 2 ? "warning" : s === 3 ? "info" : "hint";
|
|
74
|
+
}
|
|
169
75
|
|
|
170
|
-
export function filter_diagnostics(
|
|
171
|
-
diagnostics
|
|
172
|
-
severities
|
|
173
|
-
)
|
|
174
|
-
if (!severities || severities.length === 0) return diagnostics;
|
|
175
|
-
// 取最小的 severity 值(error=1 < warning=2 < info=3 < hint=4)
|
|
176
|
-
const minSeverity = Math.min(...severities.map((s) => SEVERITY_MAP[s]));
|
|
177
|
-
// 显示 severity <= minSeverity(更严重 + 自身)
|
|
178
|
-
return diagnostics.filter((d) => (d.severity ?? 1) <= minSeverity);
|
|
76
|
+
export function filter_diagnostics(diagnostics: LspDiagnostic[], severities?: SeverityFilter[]): LspDiagnostic[] {
|
|
77
|
+
if (!severities?.length) return diagnostics;
|
|
78
|
+
const min = Math.min(...severities.map((s) => SEVERITY_MAP[s]));
|
|
79
|
+
return diagnostics.filter((d) => (d.severity ?? 1) <= min);
|
|
179
80
|
}
|
|
180
81
|
|
|
181
|
-
export function format_diagnostics(
|
|
182
|
-
file: string,
|
|
183
|
-
diagnostics: LspDiagnostic[],
|
|
184
|
-
severities?: SeverityFilter[]
|
|
185
|
-
): string {
|
|
82
|
+
export function format_diagnostics(file: string, diagnostics: LspDiagnostic[], severities?: SeverityFilter[]): string {
|
|
186
83
|
const filtered = filter_diagnostics(diagnostics, severities);
|
|
187
84
|
if (filtered.length === 0) return `${file}: no diagnostics`;
|
|
188
85
|
const lines = [`${file}: ${filtered.length} diagnostic(s)`];
|
|
189
86
|
for (const d of filtered) {
|
|
190
|
-
const
|
|
87
|
+
const pos = `${d.range.start.line + 1}:${d.range.start.character + 1}`;
|
|
191
88
|
const source = d.source ? ` [${d.source}]` : "";
|
|
192
89
|
const code = d.code != null ? ` (${d.code})` : "";
|
|
193
|
-
lines.push(
|
|
194
|
-
` ${position} ${severity_label(d.severity ?? 1)}${source}${code}: ${d.message}`
|
|
195
|
-
);
|
|
90
|
+
lines.push(` ${pos} ${severityLabel(d.severity ?? 1)}${source}${code}: ${d.message}`);
|
|
196
91
|
}
|
|
197
92
|
return lines.join("\n");
|
|
198
93
|
}
|
|
199
94
|
|
|
95
|
+
// ─── Hover ────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
200
97
|
export function format_hover(hover: LspHover | null): string {
|
|
201
98
|
if (!hover) return "No hover info.";
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (Array.isArray(contents)) {
|
|
206
|
-
return contents.map(extract).join("\n\n").trim() || "No hover info.";
|
|
207
|
-
}
|
|
208
|
-
return extract(contents).trim() || "No hover info.";
|
|
99
|
+
const extract = (item: unknown): string => typeof item === "string" ? item : ((item as any)?.value ?? "");
|
|
100
|
+
if (Array.isArray(hover.contents)) return hover.contents.map(extract).join("\n\n").trim() || "No hover info.";
|
|
101
|
+
return extract(hover.contents).trim() || "No hover info.";
|
|
209
102
|
}
|
|
210
103
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
104
|
+
// ─── Locations ────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export function format_locations(locations: LspLocation[], emptyMessage: string): string {
|
|
107
|
+
if (locations.length === 0) return emptyMessage;
|
|
108
|
+
return locations.map((loc) => `${fileUrlToPath(loc.uri)}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`).join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function fileUrlToPath(uri: string): string {
|
|
112
|
+
try { return uri.startsWith("file:") ? fileURLToPath(uri) : uri; } catch { return uri; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Symbols ──────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
const SYMBOL_KIND_LABELS: Record<number, string> = {
|
|
118
|
+
2: "module", 3: "namespace", 5: "class", 6: "method", 7: "property",
|
|
119
|
+
8: "field", 9: "constructor", 11: "interface", 12: "function",
|
|
120
|
+
13: "variable", 14: "constant", 23: "struct", 24: "event",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export function symbol_kind_label(kind: number): string {
|
|
124
|
+
return SYMBOL_KIND_LABELS[kind] ?? "symbol";
|
|
222
125
|
}
|
|
223
126
|
|
|
224
|
-
export function format_document_symbols(
|
|
225
|
-
file: string,
|
|
226
|
-
symbols: LspDocumentSymbol[]
|
|
227
|
-
): string {
|
|
127
|
+
export function format_document_symbols(file: string, symbols: LspDocumentSymbol[]): string {
|
|
228
128
|
if (symbols.length === 0) return `${file}: no symbols`;
|
|
229
129
|
const lines = [`${file}: ${symbols.length} top-level symbol(s)`];
|
|
230
|
-
|
|
130
|
+
appendSymbols(lines, symbols, 1);
|
|
231
131
|
return lines.join("\n");
|
|
232
132
|
}
|
|
233
133
|
|
|
234
|
-
|
|
134
|
+
function appendSymbols(lines: string[], symbols: LspDocumentSymbol[], depth: number) {
|
|
135
|
+
for (const s of symbols) {
|
|
136
|
+
const indent = " ".repeat(depth);
|
|
137
|
+
const detail = s.detail ? ` — ${s.detail}` : "";
|
|
138
|
+
const range = `${s.range.start.line + 1}:${s.range.start.character + 1}`;
|
|
139
|
+
lines.push(`${indent}${symbol_kind_label(s.kind)} ${s.name}${detail} @ ${range}`);
|
|
140
|
+
if (s.children?.length) appendSymbols(lines, s.children, depth + 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface SymbolMatchOptions {
|
|
235
145
|
max_results: number;
|
|
236
146
|
top_level_only: boolean;
|
|
237
147
|
exact_match: boolean;
|
|
@@ -239,57 +149,41 @@ export interface SymbolMatchOptions {
|
|
|
239
149
|
language: string;
|
|
240
150
|
}
|
|
241
151
|
|
|
242
|
-
export interface SymbolMatch {
|
|
243
|
-
symbol: LspDocumentSymbol;
|
|
244
|
-
depth: number;
|
|
245
|
-
}
|
|
152
|
+
export interface SymbolMatch { symbol: LspDocumentSymbol; depth: number }
|
|
246
153
|
|
|
247
154
|
export function find_symbol_matches(
|
|
248
155
|
symbols: LspDocumentSymbol[],
|
|
249
156
|
query: string,
|
|
250
|
-
options: SymbolMatchOptions
|
|
157
|
+
options: SymbolMatchOptions,
|
|
251
158
|
): SymbolMatch[] {
|
|
252
159
|
const normalized = query.trim().toLowerCase();
|
|
253
160
|
if (!normalized) return [];
|
|
254
|
-
|
|
255
161
|
const matches: SymbolMatch[] = [];
|
|
256
|
-
|
|
257
|
-
const expand_exact_name_values = (name: string): string[] => {
|
|
162
|
+
const expandName = (name: string): string[] => {
|
|
258
163
|
const trimmed = name.trim().toLowerCase();
|
|
259
164
|
if (!trimmed) return [];
|
|
260
165
|
const expanded = new Set([trimmed]);
|
|
261
166
|
if (options.language === "cpp" && trimmed.includes("::")) {
|
|
262
|
-
const parts = trimmed
|
|
263
|
-
.split("::")
|
|
264
|
-
.map((part) => part.trim())
|
|
265
|
-
.filter(Boolean);
|
|
167
|
+
const parts = trimmed.split("::").map(p => p.trim()).filter(Boolean);
|
|
266
168
|
if (parts.length > 0) expanded.add(parts[parts.length - 1]!);
|
|
267
169
|
}
|
|
268
|
-
return
|
|
170
|
+
return [...expanded];
|
|
269
171
|
};
|
|
270
172
|
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
const
|
|
173
|
+
const matchesQuery = (s: LspDocumentSymbol): boolean => {
|
|
174
|
+
const name = s.name.trim().toLowerCase();
|
|
175
|
+
const detail = (s.detail ?? "").trim().toLowerCase();
|
|
274
176
|
if (options.exact_match) {
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
...(raw_detail ? [raw_detail] : []),
|
|
278
|
-
];
|
|
279
|
-
return exact_values.some((value) => value === normalized);
|
|
177
|
+
const exactValues = [...expandName(s.name), ...(detail ? [detail] : [])];
|
|
178
|
+
return exactValues.some(v => v === normalized);
|
|
280
179
|
}
|
|
281
|
-
|
|
282
|
-
return fuzzy_values.some((value) => value.includes(normalized));
|
|
180
|
+
return name.includes(normalized) || detail.includes(normalized);
|
|
283
181
|
};
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return options.kinds.has(symbol_kind_label(symbol.kind));
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const visit = (entries: LspDocumentSymbol[], depth: number): void => {
|
|
182
|
+
const matchesKind = (s: LspDocumentSymbol): boolean =>
|
|
183
|
+
options.kinds.size === 0 || options.kinds.has(symbol_kind_label(s.kind));
|
|
184
|
+
const visit = (entries: LspDocumentSymbol[], depth: number) => {
|
|
291
185
|
for (const symbol of entries) {
|
|
292
|
-
if (
|
|
186
|
+
if (matchesKind(symbol) && matchesQuery(symbol)) {
|
|
293
187
|
matches.push({ symbol, depth });
|
|
294
188
|
if (matches.length >= options.max_results) return;
|
|
295
189
|
}
|
|
@@ -299,57 +193,28 @@ export function find_symbol_matches(
|
|
|
299
193
|
}
|
|
300
194
|
}
|
|
301
195
|
};
|
|
302
|
-
|
|
303
196
|
visit(symbols, 1);
|
|
304
197
|
return matches;
|
|
305
198
|
}
|
|
306
199
|
|
|
307
|
-
export function format_symbol_matches(
|
|
308
|
-
file:
|
|
309
|
-
query: string,
|
|
310
|
-
matches: SymbolMatch[]
|
|
311
|
-
): string {
|
|
312
|
-
if (matches.length === 0) {
|
|
313
|
-
return `${file}: no symbols matching "${query}"`;
|
|
314
|
-
}
|
|
200
|
+
export function format_symbol_matches(file: string, query: string, matches: SymbolMatch[]): string {
|
|
201
|
+
if (matches.length === 0) return `${file}: no symbols matching "${query}"`;
|
|
315
202
|
const lines = [`${file}: ${matches.length} symbol match(es) for "${query}"`];
|
|
316
203
|
for (const { symbol, depth } of matches) {
|
|
317
204
|
const indent = " ".repeat(depth);
|
|
318
205
|
const detail = symbol.detail ? ` — ${symbol.detail}` : "";
|
|
319
206
|
const range = `${symbol.range.start.line + 1}:${symbol.range.start.character + 1}`;
|
|
320
|
-
lines.push(
|
|
321
|
-
`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`
|
|
322
|
-
);
|
|
207
|
+
lines.push(`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`);
|
|
323
208
|
}
|
|
324
209
|
return lines.join("\n");
|
|
325
210
|
}
|
|
326
211
|
|
|
327
|
-
|
|
328
|
-
lines: string[],
|
|
329
|
-
symbols: LspDocumentSymbol[],
|
|
330
|
-
depth: number
|
|
331
|
-
): void {
|
|
332
|
-
for (const symbol of symbols) {
|
|
333
|
-
const indent = " ".repeat(depth);
|
|
334
|
-
const detail = symbol.detail ? ` — ${symbol.detail}` : "";
|
|
335
|
-
const range = `${symbol.range.start.line + 1}:${symbol.range.start.character + 1}`;
|
|
336
|
-
lines.push(
|
|
337
|
-
`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`
|
|
338
|
-
);
|
|
339
|
-
if (symbol.children?.length) {
|
|
340
|
-
append_symbol_lines(lines, symbol.children, depth + 1);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
212
|
+
// ─── Collapse (for tools test) ────────────────────────────────────────────
|
|
344
213
|
|
|
345
|
-
export function
|
|
346
|
-
|
|
214
|
+
export function collapse_lsp_text(text: string, maxLines = 20) {
|
|
215
|
+
const lines = text.split("\n").reverse().reduce((acc, l) => l === "" && acc.length === 0 ? acc : [l, ...acc], [] as string[]);
|
|
216
|
+
const totalLines = lines.length;
|
|
217
|
+
return { totalLines, displayLines: lines.slice(0, maxLines), remainingLines: Math.max(0, totalLines - maxLines) };
|
|
347
218
|
}
|
|
348
219
|
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
return uri.startsWith("file:") ? fileURLToPath(uri) : uri;
|
|
352
|
-
} catch {
|
|
353
|
-
return uri;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
220
|
+
export const __lspFormatTest = { collapse_lsp_text: collapse_lsp_text };
|
package/extensions/lsp/index.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LSP Extension
|
|
2
|
+
* LSP Extension — language server integration for Pi.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
|
|
4
|
+
* Provides: lsp_diagnostics, lsp_document_symbols.
|
|
6
5
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { LspServerManager } from "./manager.js";
|
|
8
|
+
import { registerLspTools } from "./tools.js";
|
|
10
9
|
|
|
11
|
-
export function setupLsp(pi:
|
|
10
|
+
export function setupLsp(pi: ExtensionAPI) {
|
|
12
11
|
const manager = new LspServerManager();
|
|
13
|
-
|
|
14
|
-
setup_lsp_prompt(pi);
|
|
15
|
-
register_lsp_tools(pi, manager);
|
|
12
|
+
registerLspTools(pi, manager);
|
|
16
13
|
|
|
17
14
|
pi.on("session_shutdown", async () => {
|
|
18
|
-
await manager.
|
|
15
|
+
await manager.clearLanguageState();
|
|
19
16
|
});
|
|
20
17
|
}
|