@trim21/personal-pi-extensions 0.0.359 → 0.0.360
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/package.json +1 -1
- package/src/lib/lsp/client.ts +49 -19
- package/src/lib/lsp/lsp.ts +56 -1
package/package.json
CHANGED
package/src/lib/lsp/client.ts
CHANGED
|
@@ -52,6 +52,15 @@ interface DiagnosticRequestResult {
|
|
|
52
52
|
handled: boolean;
|
|
53
53
|
matched: boolean;
|
|
54
54
|
byFile: Map<string, Diagnostic[]>;
|
|
55
|
+
/** 单次请求是否超时(区别于正常失败:超时意味着服务器未响应,不应重试)。 */
|
|
56
|
+
timedOut: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 一批 pull 请求的聚合结果;timedOut 表示其中至少一个请求超时。 */
|
|
60
|
+
interface PullResult {
|
|
61
|
+
handled: boolean;
|
|
62
|
+
matched: boolean;
|
|
63
|
+
timedOut: boolean;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
interface CapabilityRegistration {
|
|
@@ -350,9 +359,16 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
350
359
|
|
|
351
360
|
// ── 诊断拉取(pull)辅助 ────────────────────────────────────────────────────
|
|
352
361
|
|
|
353
|
-
const mergeResults = (filePath: string, results: DiagnosticRequestResult[]) => {
|
|
354
|
-
if (results.every((result) => !result.handled))
|
|
362
|
+
const mergeResults = (filePath: string, results: DiagnosticRequestResult[]): PullResult => {
|
|
363
|
+
if (results.every((result) => !result.handled)) {
|
|
364
|
+
return {
|
|
365
|
+
handled: false,
|
|
366
|
+
matched: false,
|
|
367
|
+
timedOut: results.some((result) => result.timedOut),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
355
370
|
const matched = results.some((result) => result.matched);
|
|
371
|
+
const timedOut = results.some((result) => result.timedOut);
|
|
356
372
|
|
|
357
373
|
const merged = new Map<string, Diagnostic[]>();
|
|
358
374
|
for (const result of results) {
|
|
@@ -367,21 +383,27 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
367
383
|
updatePullDiagnostics(target, dedupeDiagnostics(items));
|
|
368
384
|
}
|
|
369
385
|
|
|
370
|
-
return { handled: true, matched };
|
|
386
|
+
return { handled: true, matched, timedOut };
|
|
371
387
|
};
|
|
372
388
|
|
|
373
389
|
async function requestDiagnosticReport(
|
|
374
390
|
filePath: string,
|
|
375
391
|
identifier?: string,
|
|
376
392
|
): Promise<DiagnosticRequestResult> {
|
|
393
|
+
let timedOut = false;
|
|
377
394
|
const report = await withTimeout(
|
|
378
395
|
connection.sendRequest<DocumentDiagnosticReport | null>("textDocument/diagnostic", {
|
|
379
396
|
...(identifier && { identifier }),
|
|
380
397
|
textDocument: { uri: pathToFileURL(filePath).href },
|
|
381
398
|
}),
|
|
382
399
|
diagnosticsRequestTimeoutMs,
|
|
383
|
-
).catch(() =>
|
|
384
|
-
|
|
400
|
+
).catch((error: unknown) => {
|
|
401
|
+
if (error instanceof Error && error.message.startsWith("Timeout after")) timedOut = true;
|
|
402
|
+
return null;
|
|
403
|
+
});
|
|
404
|
+
if (!report) {
|
|
405
|
+
return { handled: false, matched: false, byFile: new Map<string, Diagnostic[]>(), timedOut };
|
|
406
|
+
}
|
|
385
407
|
|
|
386
408
|
const byFile = new Map<string, Diagnostic[]>();
|
|
387
409
|
const push = (target: string, items: Diagnostic[]): void => {
|
|
@@ -404,21 +426,27 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
404
426
|
matched ||= relatedPath === filePath;
|
|
405
427
|
}
|
|
406
428
|
|
|
407
|
-
return { handled, matched, byFile };
|
|
429
|
+
return { handled, matched, byFile, timedOut };
|
|
408
430
|
}
|
|
409
431
|
|
|
410
432
|
async function requestWorkspaceDiagnosticReport(
|
|
411
433
|
filePath: string,
|
|
412
434
|
identifier?: string,
|
|
413
435
|
): Promise<DiagnosticRequestResult> {
|
|
436
|
+
let timedOut = false;
|
|
414
437
|
const report = await withTimeout(
|
|
415
438
|
connection.sendRequest<WorkspaceDiagnosticReport | null>("workspace/diagnostic", {
|
|
416
439
|
...(identifier && { identifier }),
|
|
417
440
|
previousResultIds: [],
|
|
418
441
|
}),
|
|
419
442
|
diagnosticsRequestTimeoutMs,
|
|
420
|
-
).catch(() =>
|
|
421
|
-
|
|
443
|
+
).catch((error: unknown) => {
|
|
444
|
+
if (error instanceof Error && error.message.startsWith("Timeout after")) timedOut = true;
|
|
445
|
+
return null;
|
|
446
|
+
});
|
|
447
|
+
if (!report) {
|
|
448
|
+
return { handled: false, matched: false, byFile: new Map<string, Diagnostic[]>(), timedOut };
|
|
449
|
+
}
|
|
422
450
|
|
|
423
451
|
const byFile = new Map<string, Diagnostic[]>();
|
|
424
452
|
let matched = false;
|
|
@@ -430,7 +458,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
430
458
|
matched ||= relatedPath === filePath;
|
|
431
459
|
}
|
|
432
460
|
|
|
433
|
-
return { handled: true, matched, byFile };
|
|
461
|
+
return { handled: true, matched, byFile, timedOut };
|
|
434
462
|
}
|
|
435
463
|
|
|
436
464
|
/** 是否支持文档级 pull 诊断:静态 diagnosticProvider 或动态注册的 document 诊断。 */
|
|
@@ -470,14 +498,14 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
470
498
|
filePath: string,
|
|
471
499
|
requests: Promise<DiagnosticRequestResult>[],
|
|
472
500
|
done: (results: DiagnosticRequestResult[]) => boolean,
|
|
473
|
-
): Promise<
|
|
474
|
-
if (requests.length === 0) return { handled: false, matched: false };
|
|
501
|
+
): Promise<PullResult> {
|
|
502
|
+
if (requests.length === 0) return { handled: false, matched: false, timedOut: false };
|
|
475
503
|
|
|
476
|
-
return new Promise<
|
|
504
|
+
return new Promise<PullResult>((resolve) => {
|
|
477
505
|
const results: DiagnosticRequestResult[] = [];
|
|
478
506
|
let pending = requests.length;
|
|
479
507
|
let resolved = false;
|
|
480
|
-
const finish = (merged:
|
|
508
|
+
const finish = (merged: PullResult, force = false) => {
|
|
481
509
|
if (resolved) return;
|
|
482
510
|
if (!force && !done(results)) return;
|
|
483
511
|
resolved = true;
|
|
@@ -505,9 +533,9 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
505
533
|
|
|
506
534
|
// 并发发起 identifier pull,一旦某批已产出当前文件诊断即可放行;
|
|
507
535
|
// 慢的 pull 继续在后台合并,不按 identifier 串行。见 opencode PR #23771。
|
|
508
|
-
async function requestDocumentDiagnostics(filePath: string) {
|
|
536
|
+
async function requestDocumentDiagnostics(filePath: string): Promise<PullResult> {
|
|
509
537
|
const state = documentPullState();
|
|
510
|
-
if (!state.supported) return { handled: false, matched: false };
|
|
538
|
+
if (!state.supported) return { handled: false, matched: false, timedOut: false };
|
|
511
539
|
return requestDiagnostics(
|
|
512
540
|
filePath,
|
|
513
541
|
[
|
|
@@ -520,11 +548,11 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
520
548
|
);
|
|
521
549
|
}
|
|
522
550
|
|
|
523
|
-
async function requestFullDiagnostics(filePath: string) {
|
|
551
|
+
async function requestFullDiagnostics(filePath: string): Promise<PullResult> {
|
|
524
552
|
const documentState = documentPullState();
|
|
525
553
|
const workspaceState = workspacePullState();
|
|
526
554
|
if (!documentState.supported && !workspaceState.supported) {
|
|
527
|
-
return { handled: false, matched: false };
|
|
555
|
+
return { handled: false, matched: false, timedOut: false };
|
|
528
556
|
}
|
|
529
557
|
return mergeResults(
|
|
530
558
|
filePath,
|
|
@@ -606,12 +634,13 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
606
634
|
signal?: AbortSignal;
|
|
607
635
|
}): Promise<void> {
|
|
608
636
|
const startedAt = request.after ?? Date.now();
|
|
609
|
-
// 支持 pull 的服务器:push 已被忽略,pull
|
|
610
|
-
//
|
|
637
|
+
// 支持 pull 的服务器:push 已被忽略,pull 是唯一通道。正常返回但未拿到
|
|
638
|
+
// 当前文档结果时重试;请求超时(服务器未响应)则中断,避免阻塞编辑。
|
|
611
639
|
if (supportsPullDiagnostics()) {
|
|
612
640
|
while (!connectionClosed && !request.signal?.aborted) {
|
|
613
641
|
const result = await requestDocumentDiagnostics(request.path);
|
|
614
642
|
if (result.matched) return;
|
|
643
|
+
if (result.timedOut) return;
|
|
615
644
|
await sleep(PULL_RETRY_INTERVAL_MS);
|
|
616
645
|
}
|
|
617
646
|
return;
|
|
@@ -650,6 +679,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
650
679
|
while (!connectionClosed && !request.signal?.aborted) {
|
|
651
680
|
const result = await requestFullDiagnostics(request.path);
|
|
652
681
|
if (result.handled || result.matched) return;
|
|
682
|
+
if (result.timedOut) return;
|
|
653
683
|
await sleep(PULL_RETRY_INTERVAL_MS);
|
|
654
684
|
}
|
|
655
685
|
return;
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -182,8 +182,13 @@ interface LspState {
|
|
|
182
182
|
broken: Set<string>;
|
|
183
183
|
spawning: Map<string, Promise<LspClient | undefined>>;
|
|
184
184
|
closing: boolean;
|
|
185
|
+
/** root+serverID → 服务器状态,用于 footer status 显示。 */
|
|
186
|
+
servers: Map<string, { serverID: string; root: string; state: "running" | "broken" }>;
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
/** 渲染 LSP status 文本的回调(传入 undefined 表示清除)。 */
|
|
190
|
+
export type StatusRenderer = (text: string | undefined) => void;
|
|
191
|
+
|
|
187
192
|
export interface LspRequestOptions {
|
|
188
193
|
notify?: ExtensionUIContext["notify"];
|
|
189
194
|
/** 中止时提前结束诊断等待(已中止时直接跳过诊断)。 */
|
|
@@ -212,6 +217,10 @@ export interface LspService {
|
|
|
212
217
|
diagnostics(): Promise<Record<string, Diagnostic[]>>;
|
|
213
218
|
lspDiagnosticsForFile(file: string, cwd: string, options?: LspRequestOptions): Promise<string>;
|
|
214
219
|
shutdownAll(): Promise<void>;
|
|
220
|
+
/** 注入 status 渲染回调;传入 undefined 表示不再渲染。 */
|
|
221
|
+
attachStatus(render: StatusRenderer | undefined): void;
|
|
222
|
+
/** 用当前服务器状态主动刷新一次 status(agent start/end 等生命周期边界)。 */
|
|
223
|
+
refreshStatus(): void;
|
|
215
224
|
}
|
|
216
225
|
|
|
217
226
|
/** 文件必须在工作目录内才启用 LSP(对齐 opencode 的 containsPath)。 */
|
|
@@ -241,8 +250,29 @@ export function createLspService(
|
|
|
241
250
|
broken: new Set(),
|
|
242
251
|
spawning: new Map(),
|
|
243
252
|
closing: false,
|
|
253
|
+
servers: new Map(),
|
|
244
254
|
};
|
|
245
255
|
|
|
256
|
+
let renderStatus: StatusRenderer | undefined;
|
|
257
|
+
|
|
258
|
+
/** 汇总当前所有 LSP server 状态并渲染到 footer status。 */
|
|
259
|
+
function updateStatusText(): void {
|
|
260
|
+
if (!renderStatus) return;
|
|
261
|
+
if (state.servers.size === 0) {
|
|
262
|
+
renderStatus(undefined);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const parts = Array.from(state.servers.values(), (server) => {
|
|
266
|
+
return `${server.serverID}${server.state === "broken" ? " (unavailable)" : ""}`;
|
|
267
|
+
});
|
|
268
|
+
renderStatus(`lsp: ${parts.toSorted().join(",")}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function attachStatus(render: StatusRenderer | undefined): void {
|
|
272
|
+
renderStatus = render;
|
|
273
|
+
updateStatusText();
|
|
274
|
+
}
|
|
275
|
+
|
|
246
276
|
async function getClients(
|
|
247
277
|
file: string,
|
|
248
278
|
cwd: string,
|
|
@@ -281,6 +311,8 @@ export function createLspService(
|
|
|
281
311
|
const handle = await adapter.spawn(root, cwd);
|
|
282
312
|
if (!handle) {
|
|
283
313
|
state.broken.add(key);
|
|
314
|
+
state.servers.set(key, { serverID: adapter.id, root, state: "broken" });
|
|
315
|
+
updateStatusText();
|
|
284
316
|
notify?.(
|
|
285
317
|
`LSP server "${adapter.id}" is not available for ${root} (binary not found)`,
|
|
286
318
|
"error",
|
|
@@ -307,9 +339,13 @@ export function createLspService(
|
|
|
307
339
|
return duplicate;
|
|
308
340
|
}
|
|
309
341
|
state.clients.push(client);
|
|
342
|
+
state.servers.set(key, { serverID: adapter.id, root, state: "running" });
|
|
343
|
+
updateStatusText();
|
|
310
344
|
return client;
|
|
311
345
|
} catch (error) {
|
|
312
346
|
state.broken.add(key);
|
|
347
|
+
state.servers.set(key, { serverID: adapter.id, root, state: "broken" });
|
|
348
|
+
updateStatusText();
|
|
313
349
|
notify?.(
|
|
314
350
|
`LSP server "${adapter.id}" failed to start for ${root}: ${
|
|
315
351
|
error instanceof Error ? error.message : String(error)
|
|
@@ -399,9 +435,18 @@ export function createLspService(
|
|
|
399
435
|
});
|
|
400
436
|
state.clients = [];
|
|
401
437
|
state.broken.clear();
|
|
438
|
+
state.servers.clear();
|
|
439
|
+
updateStatusText();
|
|
402
440
|
}
|
|
403
441
|
|
|
404
|
-
return {
|
|
442
|
+
return {
|
|
443
|
+
touchFile,
|
|
444
|
+
diagnostics,
|
|
445
|
+
lspDiagnosticsForFile,
|
|
446
|
+
shutdownAll,
|
|
447
|
+
attachStatus,
|
|
448
|
+
refreshStatus: updateStatusText,
|
|
449
|
+
};
|
|
405
450
|
}
|
|
406
451
|
|
|
407
452
|
export interface LspServiceOptions {
|
|
@@ -420,6 +465,16 @@ export function registerLsp(pi: ExtensionAPI, options?: LspServiceOptions): LspS
|
|
|
420
465
|
.catch((error: unknown) => {
|
|
421
466
|
if (error instanceof Error) ctx.ui.notify?.(error.message, "error");
|
|
422
467
|
});
|
|
468
|
+
// footer status 显示当前所有 LSP server 状态(无 UI 时不显示)
|
|
469
|
+
service.attachStatus(
|
|
470
|
+
ctx.ui?.setStatus
|
|
471
|
+
? (text) => ctx.ui.setStatus("lsp", text ? ctx.ui.theme.fg("accent", text) : undefined)
|
|
472
|
+
: undefined,
|
|
473
|
+
);
|
|
423
474
|
});
|
|
475
|
+
// agent 生命周期边界显式刷新 status:agent 运行中 LSP server 才被惰性
|
|
476
|
+
// spawn(首次工具调用),start/end 时保证 footer 反映当前实际状态。
|
|
477
|
+
pi.on?.("agent_start", () => service.refreshStatus());
|
|
478
|
+
pi.on?.("agent_end", () => service.refreshStatus());
|
|
424
479
|
return service;
|
|
425
480
|
}
|