@trim21/personal-pi-extensions 0.1.513 → 0.1.514
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/lsp.ts +160 -104
package/package.json
CHANGED
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -425,14 +425,17 @@ export interface LspService {
|
|
|
425
425
|
start(): void;
|
|
426
426
|
/**
|
|
427
427
|
* 重启指定服务器:关闭其全部 client、清除对应失败记录并解除禁用;
|
|
428
|
-
*
|
|
428
|
+
* 其余服务器不受影响。配置缓存失效并立即重读盘上配置:此前运行中的该
|
|
429
|
+
* 服务器若仍存在于新配置则马上重启,未在运行的服务器保持惰性。
|
|
430
|
+
* 返回成功重启的 server id。
|
|
429
431
|
*/
|
|
430
|
-
reload(serverID: string): Promise<
|
|
432
|
+
reload(serverID: string): Promise<string[]>;
|
|
431
433
|
/**
|
|
432
434
|
* 重启全部服务器(/lsp-reload 无参):关闭所有 client、清空失败记录并
|
|
433
|
-
*
|
|
435
|
+
* 解除禁用;配置缓存失效并立即重读盘上配置,此前运行中的服务器若仍存在
|
|
436
|
+
* 于新配置则马上重启。返回成功重启的 server id。
|
|
434
437
|
*/
|
|
435
|
-
reloadAll(): Promise<
|
|
438
|
+
reloadAll(): Promise<string[]>;
|
|
436
439
|
/** 已知服务器 id(running 或 broken 的去重集合),供命令补全与提示。 */
|
|
437
440
|
serverIDs(): string[];
|
|
438
441
|
/** 注入 status 渲染回调;传入 undefined 表示不再渲染。 */
|
|
@@ -632,49 +635,115 @@ export function createLspService(
|
|
|
632
635
|
updateStatusText();
|
|
633
636
|
}
|
|
634
637
|
|
|
638
|
+
/**
|
|
639
|
+
* 记录一次启动失败:进入 broken(冷却期内跳过)、渲染 status,并按节流
|
|
640
|
+
* 间隔主动 notify。错误上报优先走会话级 sessionNotify——不依赖触发请求
|
|
641
|
+
* 恰好携带 notify(否则 Read warm-up 等静默通道会把失败吞掉);未注入
|
|
642
|
+
* 会话通知时退回请求级 notify 兜底。
|
|
643
|
+
*/
|
|
644
|
+
function reportStartupFailure(
|
|
645
|
+
key: string,
|
|
646
|
+
serverID: string,
|
|
647
|
+
root: string,
|
|
648
|
+
cause: string,
|
|
649
|
+
notify?: ExtensionUIContext["notify"],
|
|
650
|
+
): void {
|
|
651
|
+
const now = Date.now();
|
|
652
|
+
state.brokenFailAt.set(key, now);
|
|
653
|
+
state.servers.set(key, { serverID, root, state: "broken" });
|
|
654
|
+
updateStatusText();
|
|
655
|
+
const reporter = sessionNotify ?? notify;
|
|
656
|
+
const lastNotified = state.brokenNotifiedAt.get(key);
|
|
657
|
+
if (reporter && (lastNotified === undefined || now - lastNotified >= notifyIntervalMs)) {
|
|
658
|
+
state.brokenNotifiedAt.set(key, now);
|
|
659
|
+
reporter(
|
|
660
|
+
`LSP server "${serverID}" failed to start for ${root}: ${cause}. ` +
|
|
661
|
+
`Fix the issue or run /lsp-reload ${serverID} to retry now.`,
|
|
662
|
+
"error",
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* spawn 指定 adapter 并注册 client:同一 key 的 in-flight spawn 复用其结果,
|
|
669
|
+
* 失败进 broken(节流上报,返回 undefined),成功后注册 client / extensions /
|
|
670
|
+
* status 并确保 watcher 运行。供 getClients 与 reload 后的立即重启共用。
|
|
671
|
+
*/
|
|
672
|
+
async function startClient(
|
|
673
|
+
adapter: LspServerAdapter,
|
|
674
|
+
root: string,
|
|
675
|
+
cwd: string,
|
|
676
|
+
config: ResolvedLspConfig,
|
|
677
|
+
notify?: ExtensionUIContext["notify"],
|
|
678
|
+
): Promise<LspClient | undefined> {
|
|
679
|
+
const key = root + adapter.id;
|
|
680
|
+
const inflight = state.spawning.get(key);
|
|
681
|
+
if (inflight) return inflight;
|
|
682
|
+
const task = (async () => {
|
|
683
|
+
try {
|
|
684
|
+
const handle = await adapter.spawn(root, cwd);
|
|
685
|
+
if (!handle) {
|
|
686
|
+
reportStartupFailure(key, adapter.id, root, "binary not found", notify);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const client = await create({
|
|
690
|
+
serverID: adapter.id,
|
|
691
|
+
server: handle,
|
|
692
|
+
root,
|
|
693
|
+
directory: cwd,
|
|
694
|
+
diagnosticsDebounceMs: config.diagnosticsDebounceMs,
|
|
695
|
+
diagnosticsDocumentWaitTimeoutMs:
|
|
696
|
+
adapter.diagnosticsWaitMs ?? config.diagnosticsDocumentWaitTimeoutMs,
|
|
697
|
+
diagnosticsFullWaitTimeoutMs: config.diagnosticsFullWaitTimeoutMs,
|
|
698
|
+
diagnosticsRequestTimeoutMs: config.diagnosticsRequestTimeoutMs,
|
|
699
|
+
initializeTimeoutMs: adapter.startupTimeoutMs ?? config.initializeTimeoutMs,
|
|
700
|
+
maxOpenDocuments: config.maxOpenDocuments,
|
|
701
|
+
});
|
|
702
|
+
if (state.closing || state.disabled) {
|
|
703
|
+
await client.shutdown();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const duplicate = state.clients.find((c) => c.root === root && c.serverID === adapter.id);
|
|
707
|
+
if (duplicate) {
|
|
708
|
+
await client.shutdown();
|
|
709
|
+
return duplicate;
|
|
710
|
+
}
|
|
711
|
+
state.clients.push(client);
|
|
712
|
+
state.clientExtensions.set(client, adapter.extensions);
|
|
713
|
+
state.servers.set(key, { serverID: adapter.id, root, state: "running" });
|
|
714
|
+
// 启动成功:清除失败记录,之后若再次失败会立即重新上报
|
|
715
|
+
state.brokenFailAt.delete(key);
|
|
716
|
+
state.brokenNotifiedAt.delete(key);
|
|
717
|
+
updateStatusText();
|
|
718
|
+
void ensureWatcher(cwd, notify);
|
|
719
|
+
return client;
|
|
720
|
+
} catch (error) {
|
|
721
|
+
reportStartupFailure(
|
|
722
|
+
key,
|
|
723
|
+
adapter.id,
|
|
724
|
+
root,
|
|
725
|
+
error instanceof Error ? error.message : String(error),
|
|
726
|
+
notify,
|
|
727
|
+
);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
})();
|
|
731
|
+
state.spawning.set(key, task);
|
|
732
|
+
void task.finally(() => {
|
|
733
|
+
if (state.spawning.get(key) === task) state.spawning.delete(key);
|
|
734
|
+
});
|
|
735
|
+
return task;
|
|
736
|
+
}
|
|
737
|
+
|
|
635
738
|
async function getClients(
|
|
636
739
|
file: string,
|
|
637
740
|
cwd: string,
|
|
638
741
|
notify?: ExtensionUIContext["notify"],
|
|
639
742
|
adapterFilter?: (adapter: LspServerAdapter) => boolean,
|
|
640
743
|
): Promise<LspClient[]> {
|
|
641
|
-
/**
|
|
642
|
-
* 记录一次启动失败:进入 broken(冷却期内跳过)、渲染 status,并按节流
|
|
643
|
-
* 间隔主动 notify。错误上报优先走会话级 sessionNotify——不依赖触发请求
|
|
644
|
-
* 恰好携带 notify(否则 Read warm-up 等静默通道会把失败吞掉);未注入
|
|
645
|
-
* 会话通知时退回请求级 notify 兜底。
|
|
646
|
-
*/
|
|
647
|
-
function reportStartupFailure(
|
|
648
|
-
key: string,
|
|
649
|
-
serverID: string,
|
|
650
|
-
root: string,
|
|
651
|
-
cause: string,
|
|
652
|
-
): void {
|
|
653
|
-
const now = Date.now();
|
|
654
|
-
state.brokenFailAt.set(key, now);
|
|
655
|
-
state.servers.set(key, { serverID, root, state: "broken" });
|
|
656
|
-
updateStatusText();
|
|
657
|
-
const reporter = sessionNotify ?? notify;
|
|
658
|
-
const lastNotified = state.brokenNotifiedAt.get(key);
|
|
659
|
-
if (reporter && (lastNotified === undefined || now - lastNotified >= notifyIntervalMs)) {
|
|
660
|
-
state.brokenNotifiedAt.set(key, now);
|
|
661
|
-
reporter(
|
|
662
|
-
`LSP server "${serverID}" failed to start for ${root}: ${cause}. ` +
|
|
663
|
-
`Fix the issue or run /lsp-reload ${serverID} to retry now.`,
|
|
664
|
-
"error",
|
|
665
|
-
);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
744
|
if (state.closing || state.disabled) return [];
|
|
669
745
|
if (!containsPath(file, cwd)) return [];
|
|
670
746
|
const config = await currentConfig(cwd);
|
|
671
|
-
const timeout = {
|
|
672
|
-
diagnosticsDebounceMs: config.diagnosticsDebounceMs,
|
|
673
|
-
diagnosticsDocumentWaitTimeoutMs: config.diagnosticsDocumentWaitTimeoutMs,
|
|
674
|
-
diagnosticsFullWaitTimeoutMs: config.diagnosticsFullWaitTimeoutMs,
|
|
675
|
-
diagnosticsRequestTimeoutMs: config.diagnosticsRequestTimeoutMs,
|
|
676
|
-
initializeTimeoutMs: config.initializeTimeoutMs,
|
|
677
|
-
};
|
|
678
747
|
const active = filterAdapters(adapters ?? createAdapters(config.servers), config);
|
|
679
748
|
const extension = extname(file) || file;
|
|
680
749
|
const result: LspClient[] = [];
|
|
@@ -705,65 +774,7 @@ export function createLspService(
|
|
|
705
774
|
continue;
|
|
706
775
|
}
|
|
707
776
|
|
|
708
|
-
const
|
|
709
|
-
if (inflight) {
|
|
710
|
-
const client = await inflight;
|
|
711
|
-
if (client) result.push(client);
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const task = (async () => {
|
|
716
|
-
try {
|
|
717
|
-
const handle = await adapter.spawn(root, cwd);
|
|
718
|
-
if (!handle) {
|
|
719
|
-
reportStartupFailure(key, adapter.id, root, "binary not found");
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
const client = await create({
|
|
723
|
-
serverID: adapter.id,
|
|
724
|
-
server: handle,
|
|
725
|
-
root,
|
|
726
|
-
directory: cwd,
|
|
727
|
-
...timeout,
|
|
728
|
-
initializeTimeoutMs: adapter.startupTimeoutMs ?? timeout.initializeTimeoutMs,
|
|
729
|
-
diagnosticsDocumentWaitTimeoutMs:
|
|
730
|
-
adapter.diagnosticsWaitMs ?? timeout.diagnosticsDocumentWaitTimeoutMs,
|
|
731
|
-
maxOpenDocuments: config.maxOpenDocuments,
|
|
732
|
-
});
|
|
733
|
-
if (state.closing || state.disabled) {
|
|
734
|
-
await client.shutdown();
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
const duplicate = state.clients.find((c) => c.root === root && c.serverID === adapter.id);
|
|
738
|
-
if (duplicate) {
|
|
739
|
-
await client.shutdown();
|
|
740
|
-
return duplicate;
|
|
741
|
-
}
|
|
742
|
-
state.clients.push(client);
|
|
743
|
-
state.clientExtensions.set(client, adapter.extensions);
|
|
744
|
-
state.servers.set(key, { serverID: adapter.id, root, state: "running" });
|
|
745
|
-
// 启动成功:清除失败记录,之后若再次失败会立即重新上报
|
|
746
|
-
state.brokenFailAt.delete(key);
|
|
747
|
-
state.brokenNotifiedAt.delete(key);
|
|
748
|
-
updateStatusText();
|
|
749
|
-
void ensureWatcher(cwd, notify);
|
|
750
|
-
return client;
|
|
751
|
-
} catch (error) {
|
|
752
|
-
reportStartupFailure(
|
|
753
|
-
key,
|
|
754
|
-
adapter.id,
|
|
755
|
-
root,
|
|
756
|
-
error instanceof Error ? error.message : String(error),
|
|
757
|
-
);
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
})();
|
|
761
|
-
state.spawning.set(key, task);
|
|
762
|
-
void task.finally(() => {
|
|
763
|
-
if (state.spawning.get(key) === task) state.spawning.delete(key);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
const client = await task;
|
|
777
|
+
const client = await startClient(adapter, root, cwd, config, notify);
|
|
767
778
|
if (client) result.push(client);
|
|
768
779
|
}
|
|
769
780
|
|
|
@@ -979,9 +990,43 @@ export function createLspService(
|
|
|
979
990
|
updateStatusText();
|
|
980
991
|
}
|
|
981
992
|
|
|
982
|
-
|
|
993
|
+
/**
|
|
994
|
+
* reload 后立即重启此前运行中的服务器,不再等下一次工具调用。只重启新配置
|
|
995
|
+
* 中仍存在且启用的 server;无运行记录(如 /lsp-stop 之后)或配置重读失败时
|
|
996
|
+
* 不动,保持惰性 spawn。返回成功重启的 server id。
|
|
997
|
+
*/
|
|
998
|
+
async function respawnRunning(serverIDs: readonly string[]): Promise<string[]> {
|
|
999
|
+
const cwd = state.cwd;
|
|
1000
|
+
if (!cwd) return [];
|
|
1001
|
+
let config: ResolvedLspConfig;
|
|
1002
|
+
try {
|
|
1003
|
+
config = await currentConfig(cwd);
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
sessionNotify?.(
|
|
1006
|
+
`LSP reload: re-reading config failed, servers will start on the next tool call: ${
|
|
1007
|
+
error instanceof Error ? error.message : String(error)
|
|
1008
|
+
}`,
|
|
1009
|
+
"warning",
|
|
1010
|
+
);
|
|
1011
|
+
return [];
|
|
1012
|
+
}
|
|
1013
|
+
const active = filterAdapters(adapters ?? createAdapters(config.servers), config);
|
|
1014
|
+
const restarted = await Promise.all(
|
|
1015
|
+
serverIDs.map(async (serverID): Promise<string | undefined> => {
|
|
1016
|
+
const adapter = active.find((candidate) => candidate.id === serverID);
|
|
1017
|
+
if (!adapter) return;
|
|
1018
|
+
const root = serverRoot(adapter.workingDir, cwd);
|
|
1019
|
+
const client = await startClient(adapter, root, cwd, config);
|
|
1020
|
+
return client ? serverID : undefined;
|
|
1021
|
+
}),
|
|
1022
|
+
);
|
|
1023
|
+
return restarted.filter((id): id is string => id !== undefined);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function reload(serverID: string): Promise<string[]> {
|
|
1027
|
+
const wasRunning = state.clients.some((client) => client.serverID === serverID);
|
|
983
1028
|
state.closing = true;
|
|
984
|
-
//
|
|
1029
|
+
// 配置缓存失效:立即重读盘上配置,让配置修改生效
|
|
985
1030
|
state.config = undefined;
|
|
986
1031
|
state.configCwd = undefined;
|
|
987
1032
|
const targets = state.clients.filter((client) => client.serverID === serverID);
|
|
@@ -1000,9 +1045,12 @@ export function createLspService(
|
|
|
1000
1045
|
state.closing = false;
|
|
1001
1046
|
state.disabled = false;
|
|
1002
1047
|
updateStatusText();
|
|
1048
|
+
if (!wasRunning) return [];
|
|
1049
|
+
return respawnRunning([serverID]);
|
|
1003
1050
|
}
|
|
1004
1051
|
|
|
1005
|
-
async function reloadAll(): Promise<
|
|
1052
|
+
async function reloadAll(): Promise<string[]> {
|
|
1053
|
+
const runningIDs = [...new Set(state.clients.map((client) => client.serverID))];
|
|
1006
1054
|
state.closing = true;
|
|
1007
1055
|
state.config = undefined;
|
|
1008
1056
|
state.configCwd = undefined;
|
|
@@ -1018,6 +1066,7 @@ export function createLspService(
|
|
|
1018
1066
|
state.closing = false;
|
|
1019
1067
|
state.disabled = false;
|
|
1020
1068
|
updateStatusText();
|
|
1069
|
+
return respawnRunning(runningIDs);
|
|
1021
1070
|
}
|
|
1022
1071
|
|
|
1023
1072
|
function serverIDs(): string[] {
|
|
@@ -1211,13 +1260,20 @@ export function createLspManager(
|
|
|
1211
1260
|
const serverID = args.trim();
|
|
1212
1261
|
if (!serverID) {
|
|
1213
1262
|
// 无参:重读配置并重启全部服务器
|
|
1214
|
-
await service.reloadAll();
|
|
1215
|
-
ctx.ui.notify(
|
|
1263
|
+
const restarted = await service.reloadAll();
|
|
1264
|
+
ctx.ui.notify(
|
|
1265
|
+
restarted.length > 0
|
|
1266
|
+
? `LSP reloaded: ${restarted.toSorted().join(", ")} restarted`
|
|
1267
|
+
: "LSP reloaded: servers will restart on the next tool call",
|
|
1268
|
+
"info",
|
|
1269
|
+
);
|
|
1216
1270
|
return;
|
|
1217
1271
|
}
|
|
1218
|
-
await service.reload(serverID);
|
|
1272
|
+
const restarted = await service.reload(serverID);
|
|
1219
1273
|
ctx.ui.notify(
|
|
1220
|
-
|
|
1274
|
+
restarted.length > 0
|
|
1275
|
+
? `LSP server "${serverID}" reloaded`
|
|
1276
|
+
: `LSP server "${serverID}" reloaded: will restart on the next tool call`,
|
|
1221
1277
|
"info",
|
|
1222
1278
|
);
|
|
1223
1279
|
},
|