@trim21/personal-pi-extensions 0.0.317 → 0.0.320
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 +2 -2
- package/package.json +1 -1
- package/src/lib/lsp/client.ts +19 -3
- package/src/lib/lsp/lsp.ts +71 -7
- package/src/lib/lsp/server-config.ts +15 -3
package/README.md
CHANGED
|
@@ -348,7 +348,7 @@ pi -e ./src/talk/index.ts
|
|
|
348
348
|
|
|
349
349
|
read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级诊断)。LSP protocol 是统一的,因此服务器不需要为每个语言写 adapter:用一份 JSON 配置声明如何启动即可。
|
|
350
350
|
|
|
351
|
-
|
|
351
|
+
配置文件(项目优先于全局):顶层字段(`enabled`/`disabled`、超时等)本地覆盖全局;`servers` 按服务器 id 合并——同名 id 本地整体覆盖、新增 id,全局其余服务器保留。
|
|
352
352
|
|
|
353
353
|
- `~/.pi/agent/lsp.json`(全局)
|
|
354
354
|
- `.pi/lsp.json`(项目)
|
|
@@ -382,7 +382,7 @@ read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级
|
|
|
382
382
|
- `startupTimeoutMs` / `diagnosticsWaitMs`:per-server 超时,覆盖全局配置与默认值
|
|
383
383
|
- `initializationOptions` 与 `settings` 按 LSP 语义分离:前者进 initialize 请求,后者进 didChangeConfiguration / workspace/configuration 请求
|
|
384
384
|
|
|
385
|
-
内置默认服务器(typescript / pyright / ruff / clangd)始终存在;`servers` 以 key 为服务器 id 与默认合并——同 key
|
|
385
|
+
内置默认服务器(typescript / pyright / ruff / clangd)始终存在;`servers` 以 key 为服务器 id 与默认合并——同 key 整体覆盖(整个配置替换默认)、新 key 新增、`"enabled": false` 移除(如 `"clangd": { "enabled": false }`)。executable 的发现逻辑(如 tsserver 路径、venv 里的 python)不内置,需要时用 `bin` / `args` / `settings` 自行表达。
|
|
386
386
|
|
|
387
387
|
旧的 `enabled`(白名单)/ `disabled`(排除)与全局超时字段(`initializeTimeoutMs` 等)继续可用。
|
|
388
388
|
|
package/package.json
CHANGED
package/src/lib/lsp/client.ts
CHANGED
|
@@ -170,10 +170,22 @@ async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
|
170
170
|
|
|
171
171
|
function stopProcess(process: LspServerHandle["process"]): Promise<void> {
|
|
172
172
|
if (process.exitCode !== null) return Promise.resolve();
|
|
173
|
-
|
|
173
|
+
try {
|
|
174
|
+
process.kill();
|
|
175
|
+
} catch {
|
|
176
|
+
// Windows 上对已退出/从未成功启动的进程 kill 会抛 EINVAL
|
|
177
|
+
return Promise.resolve();
|
|
178
|
+
}
|
|
174
179
|
return new Promise((resolve) => {
|
|
175
180
|
process.once("exit", () => resolve());
|
|
176
|
-
|
|
181
|
+
process.once("error", () => resolve());
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
try {
|
|
184
|
+
process.kill("SIGKILL");
|
|
185
|
+
} catch {
|
|
186
|
+
// 进程已退出,忽略
|
|
187
|
+
}
|
|
188
|
+
}, 1_000).unref();
|
|
177
189
|
});
|
|
178
190
|
}
|
|
179
191
|
|
|
@@ -289,7 +301,11 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
289
301
|
},
|
|
290
302
|
}),
|
|
291
303
|
initializeTimeoutMs,
|
|
292
|
-
).catch((error: unknown) => {
|
|
304
|
+
).catch(async (error: unknown) => {
|
|
305
|
+
// 握手失败(超时/拒绝)时清理连接并终止已 spawn 的子进程,避免进程泄漏
|
|
306
|
+
connection.end();
|
|
307
|
+
connection.dispose();
|
|
308
|
+
await stopProcess(input.server.process);
|
|
293
309
|
throw new InitializeError(input.serverID, error);
|
|
294
310
|
});
|
|
295
311
|
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
* - state(client 缓存、broken 集合、spawning 去重)是 service 工厂的
|
|
5
5
|
* 闭包变量,不做成模块级全局;
|
|
6
6
|
* - 配置来源:全局 `~/.pi/agent/lsp.json` + 本地 `<cwd>/.pi/lsp.json`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* (本地覆盖全局):`servers` 按 id 合并(同名 id 整体覆盖、新增 id,全局
|
|
8
|
+
* 其余服务器保留),`enabled`/`disabled` 白名单与各超时参数继续生效;
|
|
9
|
+
* 配置在每个工具的调用 cwd 下惰性读取;enabled/disabled 引用不存在的
|
|
10
|
+
* 服务器 id 是配置错误:全局配置在扩展加载(createLspService)时抛错,
|
|
11
|
+
* 本地配置在 session 开始预加载时通知,工具调用时校验抛错兜底;
|
|
10
12
|
* - client 按 (root, serverID) 缓存,并发 spawn 去重,启动失败记入 broken
|
|
11
13
|
* 集合(服务实例生命周期内不再重试);
|
|
12
14
|
* - 工具只与 touchFile / diagnostics / lspDiagnosticsForFile 三个方法打交道;通知回调按请求传入。
|
|
13
15
|
*/
|
|
14
16
|
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
15
18
|
import { readFile } from "node:fs/promises";
|
|
16
19
|
import { homedir } from "node:os";
|
|
17
20
|
import { extname, join, normalize, sep } from "node:path";
|
|
@@ -23,7 +26,13 @@ import { Value } from "typebox/value";
|
|
|
23
26
|
import { type LspServerAdapter } from "./adapter.js";
|
|
24
27
|
import { create, type CreateInput, type Diagnostic, type Info as LspClient } from "./client.js";
|
|
25
28
|
import { report } from "./diagnostic.js";
|
|
26
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createAdapters,
|
|
31
|
+
defaultServers,
|
|
32
|
+
mergeServerConfigs,
|
|
33
|
+
mergeServerRecords,
|
|
34
|
+
serverConfigSchema,
|
|
35
|
+
} from "./server-config.js";
|
|
27
36
|
|
|
28
37
|
/** 超时值:number(毫秒,>=1)或字符串("500"、"5s"、"1m"),Parse 后由 toMs 统一换算。 */
|
|
29
38
|
const timeoutValue = Type.Union([Type.Number({ minimum: 1 }), Type.String()]);
|
|
@@ -99,9 +108,42 @@ async function readConfigFile(filePath: string): Promise<LspConfig> {
|
|
|
99
108
|
}
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
/** 同步版 readConfigFile(扩展加载时校验全局配置用)。 */
|
|
112
|
+
function readConfigFileSync(filePath: string): LspConfig {
|
|
113
|
+
try {
|
|
114
|
+
const raw = readFileSync(filePath, "utf8");
|
|
115
|
+
return Value.Parse(lspConfigSchema, JSON.parse(raw) as unknown);
|
|
116
|
+
} catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* enabled/disabled 引用的 id 必须存在于实际生效的服务器集合(注入的 adapters
|
|
123
|
+
* 或默认 + 配置 servers),否则抛配置错误(避免静默失效)。
|
|
124
|
+
*/
|
|
125
|
+
function validateConfig(config: LspConfig, adapters?: LspServerAdapter[]): void {
|
|
126
|
+
const available = new Set(
|
|
127
|
+
adapters
|
|
128
|
+
? adapters.map((adapter) => adapter.id)
|
|
129
|
+
: Object.keys(mergeServerConfigs(defaultServers, config.servers)),
|
|
130
|
+
);
|
|
131
|
+
const unknown = [...(config.enabled ?? []), ...(config.disabled ?? [])].filter(
|
|
132
|
+
(id) => !available.has(id),
|
|
133
|
+
);
|
|
134
|
+
if (unknown.length > 0) {
|
|
135
|
+
const list = [...available].toSorted().join(", ") || "none";
|
|
136
|
+
throw new Error(
|
|
137
|
+
`lsp.json: unknown server id in enabled/disabled: ${unknown.join(", ")} (available: ${list})`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
102
142
|
/**
|
|
103
143
|
* 合并后的生效配置:全局 `~/.pi/agent/lsp.json` 为基底,本地
|
|
104
|
-
* `<cwd>/.pi/lsp.json`
|
|
144
|
+
* `<cwd>/.pi/lsp.json` 覆盖——顶层标量字段(enabled/disabled、超时等)本地
|
|
145
|
+
* 直接替换;`servers` 按 id 合并(同名 id 整体覆盖、新增 id,全局其余服务器
|
|
146
|
+
* 保留)。
|
|
105
147
|
*/
|
|
106
148
|
export async function loadLspConfig(
|
|
107
149
|
cwd: string,
|
|
@@ -111,14 +153,23 @@ export async function loadLspConfig(
|
|
|
111
153
|
readConfigFile(globalConfigPath),
|
|
112
154
|
readConfigFile(join(cwd, ".pi", "lsp.json")),
|
|
113
155
|
]);
|
|
114
|
-
|
|
156
|
+
const servers = mergeServerRecords(globalConfig.servers, localConfig.servers);
|
|
157
|
+
return {
|
|
158
|
+
...globalConfig,
|
|
159
|
+
...localConfig,
|
|
160
|
+
...(servers && { servers }),
|
|
161
|
+
};
|
|
115
162
|
}
|
|
116
163
|
|
|
117
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* 按配置过滤 adapter 列表。enabled/disabled 引用了实际生效集合中不存在的
|
|
166
|
+
* 服务器 id 时抛错(配置错误,避免静默失效)。
|
|
167
|
+
*/
|
|
118
168
|
export function filterAdapters(
|
|
119
169
|
adapters: LspServerAdapter[],
|
|
120
170
|
config: LspConfig,
|
|
121
171
|
): LspServerAdapter[] {
|
|
172
|
+
validateConfig(config, adapters);
|
|
122
173
|
return adapters.filter((adapter) => {
|
|
123
174
|
if (config.enabled && !config.enabled.includes(adapter.id)) return false;
|
|
124
175
|
if (config.disabled?.includes(adapter.id)) return false;
|
|
@@ -166,6 +217,11 @@ export function createLspService(
|
|
|
166
217
|
adapters?: LspServerAdapter[],
|
|
167
218
|
globalConfigPath?: string,
|
|
168
219
|
): LspService {
|
|
220
|
+
// 扩展加载时校验全局配置(本地配置在 session_start 预加载时校验)
|
|
221
|
+
validateConfig(
|
|
222
|
+
readConfigFileSync(globalConfigPath ?? join(homedir(), ".pi", "agent", "lsp.json")),
|
|
223
|
+
adapters,
|
|
224
|
+
);
|
|
169
225
|
const state: LspState = {
|
|
170
226
|
clients: [],
|
|
171
227
|
broken: new Set(),
|
|
@@ -333,5 +389,13 @@ export interface LspServiceOptions {
|
|
|
333
389
|
export function registerLsp(pi: ExtensionAPI, options?: LspServiceOptions): LspService {
|
|
334
390
|
const service = createLspService(options?.adapters, options?.globalConfigPath);
|
|
335
391
|
pi.on?.("session_shutdown", () => service.shutdownAll());
|
|
392
|
+
// session 开始是最早能拿到本地配置 cwd 的时机:预加载并校验,配置错误立即通知
|
|
393
|
+
pi.on?.("session_start", (_event, ctx) => {
|
|
394
|
+
void loadLspConfig(ctx.cwd, options?.globalConfigPath)
|
|
395
|
+
.then((config) => validateConfig(config, options?.adapters))
|
|
396
|
+
.catch((error: unknown) => {
|
|
397
|
+
if (error instanceof Error) ctx.ui.notify?.(error.message, "error");
|
|
398
|
+
});
|
|
399
|
+
});
|
|
336
400
|
return service;
|
|
337
401
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 配置文件沿用 lsp.json(全局 ~/.pi/agent/lsp.json + 本地 <cwd>/.pi/lsp.json):
|
|
6
6
|
* 顶层 `servers` 是 id → 配置的 record,按 id 与内置默认服务器合并
|
|
7
|
-
*
|
|
8
|
-
* 白名单过滤。
|
|
7
|
+
* (同名 id 整体覆盖、新增 id、enabled:false 禁用),之后仍受现有
|
|
8
|
+
* enabled/disabled 白名单过滤。
|
|
9
9
|
*
|
|
10
10
|
* executable 发现统一由用户配置:bin 支持绝对路径 / 项目工作区
|
|
11
11
|
* (node_modules/.bin、.venv/bin、venv/bin)/ PATH,不再内置各语言的
|
|
@@ -111,7 +111,19 @@ export const defaultServers: Record<string, ServerConfig> = {
|
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
/**
|
|
114
|
+
/** 按 id 合并 servers record:同名 id 整体覆盖(不做逐字段 merge),其余保留;返回 undefined 表示没有任何 servers 定义。 */
|
|
115
|
+
export function mergeServerRecords(
|
|
116
|
+
...records: (Readonly<Record<string, ServerConfig>> | undefined)[]
|
|
117
|
+
): Record<string, ServerConfig> | undefined {
|
|
118
|
+
const merged: Record<string, ServerConfig> = {};
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
if (!record) continue;
|
|
121
|
+
Object.assign(merged, record);
|
|
122
|
+
}
|
|
123
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 用户 servers(id → 配置)与默认配置合并:同名 id 整体覆盖、新增 id、enabled:false 移除。 */
|
|
115
127
|
export function mergeServerConfigs(
|
|
116
128
|
defaults: Readonly<Record<string, ServerConfig>>,
|
|
117
129
|
user: Readonly<Record<string, ServerConfig>> | undefined,
|