@trim21/personal-pi-extensions 0.1.611 → 0.1.613
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 +2 -2
- package/src/aft/config.ts +9 -3
- package/src/aft/index.ts +6 -2
- package/src/aft/logger.ts +9 -3
- package/src/aft/tools.ts +5 -3
- package/src/bwrap/approval-rules.ts +51 -18
- package/src/bwrap/approval-suggest.ts +12 -4
- package/src/bwrap/core.ts +39 -13
- package/src/bwrap/dcg-scan.ts +16 -6
- package/src/bwrap/holder.ts +3 -1
- package/src/bwrap/mihomo-config.ts +18 -6
- package/src/bwrap/network-stack.ts +30 -10
- package/src/bwrap/runtime.ts +36 -14
- package/src/claude-code/common.ts +6 -2
- package/src/claude-code/edit-utils.ts +3 -1
- package/src/claude-code/files.ts +40 -14
- package/src/claude-code/glob.ts +12 -4
- package/src/claude-code/grep.ts +41 -14
- package/src/claude-code/session-tools.ts +18 -6
- package/src/claude-code/shell.ts +9 -3
- package/src/gh/base.ts +72 -24
- package/src/gh/tools/download-release-assets.ts +12 -4
- package/src/gh/tools/list-releases.ts +3 -1
- package/src/gh/tools/list-workflow-runs.ts +9 -3
- package/src/gh/tools/read-ci-logs.ts +21 -7
- package/src/gh/tools/read-repo.ts +3 -1
- package/src/lib/cli-args.ts +6 -2
- package/src/lib/cli.ts +55 -22
- package/src/lib/file-reads.ts +24 -8
- package/src/lib/github.ts +35 -12
- package/src/lib/lsp/adapter.ts +6 -2
- package/src/lib/lsp/bin.ts +12 -4
- package/src/lib/lsp/client.ts +205 -69
- package/src/lib/lsp/diagnostic.ts +6 -2
- package/src/lib/lsp/inspect-tool.ts +25 -9
- package/src/lib/lsp/lsp.ts +154 -58
- package/src/lib/lsp/rename-tool.ts +11 -4
- package/src/lib/lsp/rename.ts +25 -8
- package/src/lib/lsp/server-config.ts +41 -14
- package/src/lib/lsp/watcher.ts +51 -17
- package/src/lib/path.ts +18 -6
- package/src/lib/proc.ts +3 -1
- package/src/lib/proxy.ts +9 -3
- package/src/lib/request-policy.ts +3 -1
- package/src/lib/seq-state.ts +3 -1
- package/src/lib/ui.ts +23 -8
- package/src/lib/write-guard.ts +21 -7
- package/src/openai-cost/config.ts +3 -1
- package/src/openai-cost/cost.ts +54 -18
- package/src/openai-cost/index.ts +3 -1
- package/src/openai-cost/provider.ts +6 -2
- package/src/opencode/bash.ts +9 -3
- package/src/opencode/edit-engine.ts +63 -41
- package/src/opencode/files.ts +30 -10
- package/src/opencode/glob.ts +3 -1
- package/src/opencode/grep.ts +12 -4
- package/src/opencode/question.ts +3 -1
- package/src/opencode/ripgrep.ts +23 -9
- package/src/opencode/todo.ts +3 -1
- package/src/session-name.ts +40 -16
- package/src/spawn-agent-agents.ts +18 -6
- package/src/spawn-agent.ts +58 -21
- package/src/system-prompt/index.ts +9 -3
- package/src/talk/core.ts +143 -48
- package/src/talk/format.ts +12 -4
- package/src/talk/group.ts +9 -3
- package/src/talk/index.ts +30 -10
- package/src/talk/mailbox.ts +30 -10
- package/src/talk/policy.ts +10 -5
- package/src/talk/registry.ts +35 -12
- package/src/talk/storage.ts +3 -1
- package/src/vision-agent.ts +41 -14
- package/src/web/config.ts +3 -1
- package/src/web/fetch.ts +75 -25
- package/src/web/search.ts +30 -10
|
@@ -138,14 +138,19 @@ async function formatReferenceLocations(
|
|
|
138
138
|
const byPath = new Map<string, InspectLocation[]>();
|
|
139
139
|
for (const location of locations) {
|
|
140
140
|
const existing = byPath.get(location.path);
|
|
141
|
-
if (existing)
|
|
142
|
-
|
|
141
|
+
if (existing) {
|
|
142
|
+
existing.push(location);
|
|
143
|
+
} else {
|
|
144
|
+
byPath.set(location.path, [location]);
|
|
145
|
+
}
|
|
143
146
|
}
|
|
144
147
|
const paths = [...byPath.keys()];
|
|
145
148
|
const sections = [`Found ${locations.length} reference(s) in ${paths.length} file(s):`];
|
|
146
149
|
for (const path of paths.slice(0, MAX_FILES_LISTED)) {
|
|
147
150
|
const entries = byPath.get(path);
|
|
148
|
-
if (!entries)
|
|
151
|
+
if (!entries) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
149
154
|
const shown = entries.slice(0, MAX_ENTRIES_PER_FILE);
|
|
150
155
|
sections.push(`### ${path} (${entries.length})`);
|
|
151
156
|
for (const entry of shown) {
|
|
@@ -177,11 +182,15 @@ function formatMarkedString(marked: MarkedString): string {
|
|
|
177
182
|
|
|
178
183
|
/** hover contents 原样透传:仅做结构格式化(MarkedString → code fence),不改写内容。 */
|
|
179
184
|
function formatHoverContents(contents: Hover["contents"]): string {
|
|
180
|
-
if (typeof contents === "string")
|
|
185
|
+
if (typeof contents === "string") {
|
|
186
|
+
return contents;
|
|
187
|
+
}
|
|
181
188
|
if (Array.isArray(contents)) {
|
|
182
189
|
return contents.map((marked) => formatMarkedString(marked)).join("\n\n");
|
|
183
190
|
}
|
|
184
|
-
if ("language" in contents)
|
|
191
|
+
if ("language" in contents) {
|
|
192
|
+
return formatMarkedString(contents);
|
|
193
|
+
}
|
|
185
194
|
return contents.value;
|
|
186
195
|
}
|
|
187
196
|
|
|
@@ -217,10 +226,15 @@ async function probeSymbolCandidates(options: {
|
|
|
217
226
|
const groups = new Map<string, { output: InspectOutput; candidates: LspPosition[] }>();
|
|
218
227
|
for (const [index, candidate] of candidates.entries()) {
|
|
219
228
|
const output = outputs.at(index);
|
|
220
|
-
if (!output)
|
|
229
|
+
if (!output) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
221
232
|
const group = groups.get(output.text);
|
|
222
|
-
if (group)
|
|
223
|
-
|
|
233
|
+
if (group) {
|
|
234
|
+
group.candidates.push(candidate);
|
|
235
|
+
} else {
|
|
236
|
+
groups.set(output.text, { output, candidates: [candidate] });
|
|
237
|
+
}
|
|
224
238
|
}
|
|
225
239
|
if (groups.size > 1) {
|
|
226
240
|
const listing = [...groups.values()]
|
|
@@ -235,7 +249,9 @@ async function probeSymbolCandidates(options: {
|
|
|
235
249
|
);
|
|
236
250
|
}
|
|
237
251
|
const first = [...groups.values()].at(0);
|
|
238
|
-
if (!first)
|
|
252
|
+
if (!first) {
|
|
253
|
+
throw new Error("LSP inspect returned no result");
|
|
254
|
+
}
|
|
239
255
|
return first.output;
|
|
240
256
|
}
|
|
241
257
|
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -152,7 +152,9 @@ export interface ResolvedLspConfig {
|
|
|
152
152
|
function parseTimeoutString(value: string): number {
|
|
153
153
|
// 单位组永远参与匹配(缺省为空串),避免"可选捕获组在类型上不可空"的歧义
|
|
154
154
|
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|)\s*$/.exec(value.trim());
|
|
155
|
-
if (!match)
|
|
155
|
+
if (!match) {
|
|
156
|
+
return NaN;
|
|
157
|
+
}
|
|
156
158
|
const amount = Number(match[1]);
|
|
157
159
|
const factors: Record<string, number | undefined> = {
|
|
158
160
|
"": 1,
|
|
@@ -167,7 +169,9 @@ function parseTimeoutString(value: string): number {
|
|
|
167
169
|
|
|
168
170
|
/** 时长字段换算为 ms:number 原样、字符串按 parseTimeoutString;无效值返回 undefined。 */
|
|
169
171
|
function toMs(value: number | string | undefined): number | undefined {
|
|
170
|
-
if (value === undefined)
|
|
172
|
+
if (value === undefined) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
171
175
|
const ms = typeof value === "number" ? value : parseTimeoutString(value);
|
|
172
176
|
return Number.isFinite(ms) && ms > 0 ? ms : undefined;
|
|
173
177
|
}
|
|
@@ -224,11 +228,15 @@ function unknownFieldWarnings(config: LspConfig, filePath: string): string[] {
|
|
|
224
228
|
const warnings: string[] = [];
|
|
225
229
|
const check = (record: object, known: Record<string, unknown>, scope: string): void => {
|
|
226
230
|
for (const key of Object.keys(record)) {
|
|
227
|
-
if (!(key in known))
|
|
231
|
+
if (!(key in known)) {
|
|
232
|
+
warnings.push(`${filePath}${scope}: unknown field "${key}" ignored`);
|
|
233
|
+
}
|
|
228
234
|
}
|
|
229
235
|
};
|
|
230
236
|
check(config, lspConfigSchema.properties, "");
|
|
231
|
-
if (config.watch)
|
|
237
|
+
if (config.watch) {
|
|
238
|
+
check(config.watch, watchConfigSchema.properties, " watch");
|
|
239
|
+
}
|
|
232
240
|
for (const [id, server] of Object.entries(config.servers ?? {})) {
|
|
233
241
|
check(server, serverConfigSchema.properties, ` (server "${id}")`);
|
|
234
242
|
}
|
|
@@ -243,10 +251,14 @@ async function readConfigFile(
|
|
|
243
251
|
try {
|
|
244
252
|
const raw = await readFile(filePath, "utf8");
|
|
245
253
|
const config = Value.Parse(lspConfigSchema, JSON.parse(raw) as unknown);
|
|
246
|
-
for (const message of unknownFieldWarnings(config, filePath))
|
|
254
|
+
for (const message of unknownFieldWarnings(config, filePath)) {
|
|
255
|
+
onWarning?.(message);
|
|
256
|
+
}
|
|
247
257
|
return config;
|
|
248
258
|
} catch (error) {
|
|
249
|
-
if (isMissingFile(error))
|
|
259
|
+
if (isMissingFile(error)) {
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
250
262
|
throw error;
|
|
251
263
|
}
|
|
252
264
|
}
|
|
@@ -256,10 +268,14 @@ function readConfigFileSync(filePath: string, onWarning?: (message: string) => v
|
|
|
256
268
|
try {
|
|
257
269
|
const raw = readFileSync(filePath, "utf8");
|
|
258
270
|
const config = Value.Parse(lspConfigSchema, JSON.parse(raw) as unknown);
|
|
259
|
-
for (const message of unknownFieldWarnings(config, filePath))
|
|
271
|
+
for (const message of unknownFieldWarnings(config, filePath)) {
|
|
272
|
+
onWarning?.(message);
|
|
273
|
+
}
|
|
260
274
|
return config;
|
|
261
275
|
} catch (error) {
|
|
262
|
-
if (isMissingFile(error))
|
|
276
|
+
if (isMissingFile(error)) {
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
263
279
|
throw error;
|
|
264
280
|
}
|
|
265
281
|
}
|
|
@@ -274,12 +290,13 @@ function validateConfig(config: ResolvedLspConfig, adapters?: LspServerAdapter[]
|
|
|
274
290
|
adapters ? adapters.map((adapter) => adapter.id) : Object.keys(config.servers),
|
|
275
291
|
);
|
|
276
292
|
const unknown = config.enabled === undefined ? [] : [...config.enabled.difference(available)];
|
|
277
|
-
if (unknown.length
|
|
278
|
-
|
|
279
|
-
throw new Error(
|
|
280
|
-
`lsp.json: unknown server id in enabled: ${unknown.join(", ")} (available: ${list})`,
|
|
281
|
-
);
|
|
293
|
+
if (unknown.length === 0) {
|
|
294
|
+
return;
|
|
282
295
|
}
|
|
296
|
+
const list = [...available].toSorted().join(", ") || "none";
|
|
297
|
+
throw new Error(
|
|
298
|
+
`lsp.json: unknown server id in enabled: ${unknown.join(", ")} (available: ${list})`,
|
|
299
|
+
);
|
|
283
300
|
}
|
|
284
301
|
|
|
285
302
|
/** 合并 watch 段:全局为基底、本地逐字段覆盖;ignore 取并集去重(全局在前)。两边都未配置时返回 undefined,调用方据此省略 watch 键。 */
|
|
@@ -287,7 +304,9 @@ function mergeWatch(
|
|
|
287
304
|
globalWatch: LspConfig["watch"],
|
|
288
305
|
localWatch: LspConfig["watch"],
|
|
289
306
|
): LspConfig["watch"] {
|
|
290
|
-
if (!globalWatch && !localWatch)
|
|
307
|
+
if (!globalWatch && !localWatch) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
291
310
|
return {
|
|
292
311
|
...globalWatch,
|
|
293
312
|
...localWatch,
|
|
@@ -336,8 +355,12 @@ export function filterAdapters(
|
|
|
336
355
|
): LspServerAdapter[] {
|
|
337
356
|
validateConfig(config, adapters);
|
|
338
357
|
return adapters.filter((adapter) => {
|
|
339
|
-
if (config.enabled && !config.enabled.has(adapter.id))
|
|
340
|
-
|
|
358
|
+
if (config.enabled && !config.enabled.has(adapter.id)) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (config.disabled?.has(adapter.id)) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
341
364
|
return true;
|
|
342
365
|
});
|
|
343
366
|
}
|
|
@@ -531,7 +554,9 @@ export function createLspService(
|
|
|
531
554
|
|
|
532
555
|
/** 配置缓存:同一 cwd 内复用;cwd 变化或 reload 清缓存后重读。 */
|
|
533
556
|
async function currentConfig(cwd: string): Promise<ResolvedLspConfig> {
|
|
534
|
-
if (state.config && state.configCwd === cwd)
|
|
557
|
+
if (state.config && state.configCwd === cwd) {
|
|
558
|
+
return state.config;
|
|
559
|
+
}
|
|
535
560
|
const config = await loadLspConfig(cwd, globalConfigPath, (message) =>
|
|
536
561
|
sessionNotify?.(message, "warning"),
|
|
537
562
|
);
|
|
@@ -552,8 +577,12 @@ export function createLspService(
|
|
|
552
577
|
* 退化为 cwd(client 只服务 cwd 内的文件);与 cwd 无交集时返回 undefined。
|
|
553
578
|
*/
|
|
554
579
|
function watchDirFor(root: string, cwd: string): string | undefined {
|
|
555
|
-
if (containsPath(root, cwd))
|
|
556
|
-
|
|
580
|
+
if (containsPath(root, cwd)) {
|
|
581
|
+
return root;
|
|
582
|
+
}
|
|
583
|
+
if (containsPath(cwd, root)) {
|
|
584
|
+
return cwd;
|
|
585
|
+
}
|
|
557
586
|
return undefined;
|
|
558
587
|
}
|
|
559
588
|
|
|
@@ -562,7 +591,9 @@ export function createLspService(
|
|
|
562
591
|
const dirs = new Set<string>();
|
|
563
592
|
for (const client of state.clients) {
|
|
564
593
|
const dir = watchDirFor(client.root, cwd);
|
|
565
|
-
if (dir && !watcherFailed.has(dir))
|
|
594
|
+
if (dir && !watcherFailed.has(dir)) {
|
|
595
|
+
dirs.add(dir);
|
|
596
|
+
}
|
|
566
597
|
}
|
|
567
598
|
return [...dirs]
|
|
568
599
|
.filter((dir) => [...dirs].every((other) => other === dir || !containsPath(dir, other)))
|
|
@@ -645,7 +676,9 @@ export function createLspService(
|
|
|
645
676
|
cwd: string,
|
|
646
677
|
notify?: ExtensionUIContext["notify"],
|
|
647
678
|
): Promise<void> {
|
|
648
|
-
if (state.closing || state.disabled)
|
|
679
|
+
if (state.closing || state.disabled) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
649
682
|
let config: ResolvedLspConfig;
|
|
650
683
|
try {
|
|
651
684
|
config = await currentConfig(cwd);
|
|
@@ -662,23 +695,31 @@ export function createLspService(
|
|
|
662
695
|
const desired = config.watch.enabled ? desiredWatchRoots(cwd) : [];
|
|
663
696
|
// Map 迭代期间删除当前项是安全的
|
|
664
697
|
for (const [root, watcher] of watchers) {
|
|
665
|
-
if (desired.includes(root))
|
|
698
|
+
if (desired.includes(root)) {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
666
701
|
watchers.delete(root);
|
|
667
702
|
void watcher.stop().catch(() => {
|
|
668
703
|
// 停止失败不影响流程
|
|
669
704
|
});
|
|
670
705
|
}
|
|
671
706
|
for (const root of desired) {
|
|
672
|
-
if (watchers.has(root))
|
|
707
|
+
if (watchers.has(root)) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
673
710
|
const watcher = await startWatcher(root, config.watch, notify);
|
|
674
|
-
if (watcher)
|
|
711
|
+
if (watcher) {
|
|
712
|
+
watchers.set(root, watcher);
|
|
713
|
+
}
|
|
675
714
|
}
|
|
676
715
|
}
|
|
677
716
|
|
|
678
717
|
/** 事件按各 client 的 root 前缀 / 注册 pattern / 扩展名过滤后投递。 */
|
|
679
718
|
async function fanOut(changes: FileChange[]): Promise<void> {
|
|
680
719
|
const cwd = state.cwd;
|
|
681
|
-
if (!cwd)
|
|
720
|
+
if (!cwd) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
682
723
|
await Promise.all(
|
|
683
724
|
state.clients.map(async (client) => {
|
|
684
725
|
const filtered = changes.filter((change) => {
|
|
@@ -704,7 +745,9 @@ export function createLspService(
|
|
|
704
745
|
(minimatch(relativeCandidate, watcher.pattern) ||
|
|
705
746
|
minimatch(absoluteCandidate, watcher.pattern)),
|
|
706
747
|
);
|
|
707
|
-
if (!matched)
|
|
748
|
+
if (!matched) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
708
751
|
}
|
|
709
752
|
const extensions = state.clientExtensions.get(client);
|
|
710
753
|
return (
|
|
@@ -713,7 +756,9 @@ export function createLspService(
|
|
|
713
756
|
extensions.includes(extname(change.path))
|
|
714
757
|
);
|
|
715
758
|
});
|
|
716
|
-
if (filtered.length === 0)
|
|
759
|
+
if (filtered.length === 0) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
717
762
|
try {
|
|
718
763
|
await client.notify.watchedFiles(filtered);
|
|
719
764
|
} catch {
|
|
@@ -725,7 +770,9 @@ export function createLspService(
|
|
|
725
770
|
|
|
726
771
|
/** 汇总当前所有 LSP server 状态并渲染到 footer status。 */
|
|
727
772
|
function updateStatusText(): void {
|
|
728
|
-
if (!renderStatus)
|
|
773
|
+
if (!renderStatus) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
729
776
|
if (state.disabled) {
|
|
730
777
|
renderStatus("lsp: disabled");
|
|
731
778
|
return;
|
|
@@ -764,14 +811,17 @@ export function createLspService(
|
|
|
764
811
|
updateStatusText();
|
|
765
812
|
const reporter = sessionNotify ?? notify;
|
|
766
813
|
const lastNotified = state.brokenNotifiedAt.get(key);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
`LSP server "${serverID}" failed to start for ${root}: ${cause}. ` +
|
|
771
|
-
`Fix the issue or run /lsp-reload ${serverID} to retry now.`,
|
|
772
|
-
"error",
|
|
773
|
-
);
|
|
814
|
+
const due = lastNotified === undefined || now - lastNotified >= notifyIntervalMs;
|
|
815
|
+
if (!reporter || !due) {
|
|
816
|
+
return;
|
|
774
817
|
}
|
|
818
|
+
|
|
819
|
+
state.brokenNotifiedAt.set(key, now);
|
|
820
|
+
reporter(
|
|
821
|
+
`LSP server "${serverID}" failed to start for ${root}: ${cause}. ` +
|
|
822
|
+
`Fix the issue or run /lsp-reload ${serverID} to retry now.`,
|
|
823
|
+
"error",
|
|
824
|
+
);
|
|
775
825
|
}
|
|
776
826
|
|
|
777
827
|
/**
|
|
@@ -788,7 +838,9 @@ export function createLspService(
|
|
|
788
838
|
): Promise<LspClient | undefined> {
|
|
789
839
|
const key = root + adapter.id;
|
|
790
840
|
const inflight = state.spawning.get(key);
|
|
791
|
-
if (inflight)
|
|
841
|
+
if (inflight) {
|
|
842
|
+
return inflight;
|
|
843
|
+
}
|
|
792
844
|
const task = (async () => {
|
|
793
845
|
try {
|
|
794
846
|
const handle = await adapter.spawn(root, cwd);
|
|
@@ -841,7 +893,9 @@ export function createLspService(
|
|
|
841
893
|
})();
|
|
842
894
|
state.spawning.set(key, task);
|
|
843
895
|
void task.finally(() => {
|
|
844
|
-
if (state.spawning.get(key) === task)
|
|
896
|
+
if (state.spawning.get(key) === task) {
|
|
897
|
+
state.spawning.delete(key);
|
|
898
|
+
}
|
|
845
899
|
});
|
|
846
900
|
return task;
|
|
847
901
|
}
|
|
@@ -852,8 +906,12 @@ export function createLspService(
|
|
|
852
906
|
notify?: ExtensionUIContext["notify"],
|
|
853
907
|
adapterFilter?: (adapter: LspServerAdapter) => boolean,
|
|
854
908
|
): Promise<LspClient[]> {
|
|
855
|
-
if (state.closing || state.disabled)
|
|
856
|
-
|
|
909
|
+
if (state.closing || state.disabled) {
|
|
910
|
+
return [];
|
|
911
|
+
}
|
|
912
|
+
if (!containsPath(file, cwd)) {
|
|
913
|
+
return [];
|
|
914
|
+
}
|
|
857
915
|
const config = await currentConfig(cwd);
|
|
858
916
|
const active = filterAdapters(adapters ?? createAdapters(config.servers), config);
|
|
859
917
|
const extension = extname(file) || file;
|
|
@@ -866,15 +924,25 @@ export function createLspService(
|
|
|
866
924
|
}
|
|
867
925
|
|
|
868
926
|
for (const adapter of active) {
|
|
869
|
-
if (adapterFilter && !adapterFilter(adapter))
|
|
870
|
-
|
|
927
|
+
if (adapterFilter && !adapterFilter(adapter)) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (adapter.extensions.length > 0 && !adapter.extensions.includes(extension)) {
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
871
933
|
const root = serverRoot(adapter, file, cwd);
|
|
872
|
-
if (!containsPath(file, root))
|
|
873
|
-
|
|
934
|
+
if (!containsPath(file, root)) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (!matchesInclude(adapter.include ?? [], file, root, cwd)) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
874
940
|
const key = root + adapter.id;
|
|
875
941
|
const failedAt = state.brokenFailAt.get(key);
|
|
876
942
|
if (failedAt !== undefined) {
|
|
877
|
-
if (Date.now() - failedAt < retryCooldownMs)
|
|
943
|
+
if (Date.now() - failedAt < retryCooldownMs) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
878
946
|
// 冷却已过:允许重试;重试成功后下面会清除 broken 记录
|
|
879
947
|
state.brokenFailAt.delete(key);
|
|
880
948
|
}
|
|
@@ -886,7 +954,9 @@ export function createLspService(
|
|
|
886
954
|
}
|
|
887
955
|
|
|
888
956
|
const client = await startClient(adapter, root, cwd, config, notify);
|
|
889
|
-
if (client)
|
|
957
|
+
if (client) {
|
|
958
|
+
result.push(client);
|
|
959
|
+
}
|
|
890
960
|
}
|
|
891
961
|
|
|
892
962
|
return result;
|
|
@@ -907,7 +977,9 @@ export function createLspService(
|
|
|
907
977
|
clients.map(async (client) => {
|
|
908
978
|
const after = Date.now();
|
|
909
979
|
const version = await client.notify.open({ path: file });
|
|
910
|
-
if (!diagnostics)
|
|
980
|
+
if (!diagnostics) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
911
983
|
await abortable(
|
|
912
984
|
client.waitForDiagnostics({
|
|
913
985
|
path: file,
|
|
@@ -944,7 +1016,9 @@ export function createLspService(
|
|
|
944
1016
|
cwd: string,
|
|
945
1017
|
options?: LspRequestOptions,
|
|
946
1018
|
): Promise<DiagnosticReport> {
|
|
947
|
-
if (options?.signal?.aborted)
|
|
1019
|
+
if (options?.signal?.aborted) {
|
|
1020
|
+
return EMPTY_DIAGNOSTIC_REPORT;
|
|
1021
|
+
}
|
|
948
1022
|
await touchFile(file, cwd, "document", options);
|
|
949
1023
|
const all = await diagnostics();
|
|
950
1024
|
const normalized = normalize(file);
|
|
@@ -1103,7 +1177,9 @@ export function createLspService(
|
|
|
1103
1177
|
running: readonly { serverID: string; root: string }[],
|
|
1104
1178
|
): Promise<string[]> {
|
|
1105
1179
|
const cwd = state.cwd;
|
|
1106
|
-
if (!cwd)
|
|
1180
|
+
if (!cwd) {
|
|
1181
|
+
return [];
|
|
1182
|
+
}
|
|
1107
1183
|
let config: ResolvedLspConfig;
|
|
1108
1184
|
try {
|
|
1109
1185
|
config = await currentConfig(cwd);
|
|
@@ -1121,7 +1197,9 @@ export function createLspService(
|
|
|
1121
1197
|
const restarted = await Promise.all(
|
|
1122
1198
|
[...unique.values()].map(async ({ serverID, root }): Promise<string | undefined> => {
|
|
1123
1199
|
const adapter = active.find((candidate) => candidate.id === serverID);
|
|
1124
|
-
if (!adapter)
|
|
1200
|
+
if (!adapter) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1125
1203
|
const client = await startClient(adapter, root, cwd, config);
|
|
1126
1204
|
return client ? serverID : undefined;
|
|
1127
1205
|
}),
|
|
@@ -1140,9 +1218,13 @@ export function createLspService(
|
|
|
1140
1218
|
// 个别进程退出失败不阻止清理流程
|
|
1141
1219
|
});
|
|
1142
1220
|
state.clients = state.clients.filter((client) => client.serverID !== serverID);
|
|
1143
|
-
for (const client of targets)
|
|
1221
|
+
for (const client of targets) {
|
|
1222
|
+
state.clientExtensions.delete(client);
|
|
1223
|
+
}
|
|
1144
1224
|
for (const [key, server] of state.servers) {
|
|
1145
|
-
if (server.serverID !== serverID)
|
|
1225
|
+
if (server.serverID !== serverID) {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1146
1228
|
state.brokenFailAt.delete(key);
|
|
1147
1229
|
state.brokenNotifiedAt.delete(key);
|
|
1148
1230
|
state.servers.delete(key);
|
|
@@ -1151,9 +1233,13 @@ export function createLspService(
|
|
|
1151
1233
|
state.disabled = false;
|
|
1152
1234
|
// reload 是资源耗尽后的重试入口:清掉停用记录并按新配置重算监听范围
|
|
1153
1235
|
watcherFailed.clear();
|
|
1154
|
-
if (state.cwd)
|
|
1236
|
+
if (state.cwd) {
|
|
1237
|
+
await reconcileWatchers(state.cwd);
|
|
1238
|
+
}
|
|
1155
1239
|
updateStatusText();
|
|
1156
|
-
if (running.length === 0)
|
|
1240
|
+
if (running.length === 0) {
|
|
1241
|
+
return [];
|
|
1242
|
+
}
|
|
1157
1243
|
return respawnRunning(running);
|
|
1158
1244
|
}
|
|
1159
1245
|
|
|
@@ -1176,7 +1262,9 @@ export function createLspService(
|
|
|
1176
1262
|
state.closing = false;
|
|
1177
1263
|
state.disabled = false;
|
|
1178
1264
|
watcherFailed.clear();
|
|
1179
|
-
if (state.cwd)
|
|
1265
|
+
if (state.cwd) {
|
|
1266
|
+
await reconcileWatchers(state.cwd);
|
|
1267
|
+
}
|
|
1180
1268
|
updateStatusText();
|
|
1181
1269
|
return respawnRunning(running);
|
|
1182
1270
|
}
|
|
@@ -1243,8 +1331,12 @@ function getNoopService(): LspService {
|
|
|
1243
1331
|
function enabledServerCount(config: ResolvedLspConfig, adapters?: LspServerAdapter[]): number {
|
|
1244
1332
|
const ids = adapters ? adapters.map((adapter) => adapter.id) : Object.keys(config.servers);
|
|
1245
1333
|
return ids.filter((id) => {
|
|
1246
|
-
if (config.enabled && !config.enabled.has(id))
|
|
1247
|
-
|
|
1334
|
+
if (config.enabled && !config.enabled.has(id)) {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
if (config.disabled?.has(id)) {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1248
1340
|
return true;
|
|
1249
1341
|
}).length;
|
|
1250
1342
|
}
|
|
@@ -1282,7 +1374,9 @@ export function createLspManager(
|
|
|
1282
1374
|
}
|
|
1283
1375
|
});
|
|
1284
1376
|
validateConfig(config, options?.adapters);
|
|
1285
|
-
if (enabledServerCount(config, options?.adapters) === 0)
|
|
1377
|
+
if (enabledServerCount(config, options?.adapters) === 0) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1286
1380
|
// 下面两个闭包被 service 长期持有,可能在会话被替换或 reload 后仍触发
|
|
1287
1381
|
// (session_shutdown 清理、后台 watcher 错误、in-flight spawn 失败),
|
|
1288
1382
|
// 而旧 ctx 已被 pi 标记 stale,ctx.ui getter 会直接抛错。UI 写入是尽力
|
|
@@ -1313,7 +1407,9 @@ export function createLspManager(
|
|
|
1313
1407
|
hooks.onEnabled(pi, next);
|
|
1314
1408
|
} catch (error) {
|
|
1315
1409
|
service = undefined;
|
|
1316
|
-
if (error instanceof Error)
|
|
1410
|
+
if (error instanceof Error) {
|
|
1411
|
+
ctx.ui.notify(error.message, "error");
|
|
1412
|
+
}
|
|
1317
1413
|
}
|
|
1318
1414
|
});
|
|
1319
1415
|
|
|
@@ -161,8 +161,11 @@ export function registerLspRenameTool(
|
|
|
161
161
|
for (const { candidate, result: probe } of successes) {
|
|
162
162
|
const key = canonicalizeEdit(probe.edit);
|
|
163
163
|
const group = groups.get(key);
|
|
164
|
-
if (group)
|
|
165
|
-
|
|
164
|
+
if (group) {
|
|
165
|
+
group.candidates.push(candidate);
|
|
166
|
+
} else {
|
|
167
|
+
groups.set(key, { result: probe, candidates: [candidate] });
|
|
168
|
+
}
|
|
166
169
|
}
|
|
167
170
|
if (groups.size > 1) {
|
|
168
171
|
const listing = [...groups.values()]
|
|
@@ -180,7 +183,9 @@ export function registerLspRenameTool(
|
|
|
180
183
|
);
|
|
181
184
|
}
|
|
182
185
|
const firstGroup = [...groups.values()].at(0);
|
|
183
|
-
if (!firstGroup)
|
|
186
|
+
if (!firstGroup) {
|
|
187
|
+
throw new Error("LSP rename returned no target");
|
|
188
|
+
}
|
|
184
189
|
const { result } = firstGroup;
|
|
185
190
|
|
|
186
191
|
// ── 内存展开 → 审批 → 写盘 → 诊断 ────────────────────────────────────
|
|
@@ -218,7 +223,9 @@ export function registerLspRenameTool(
|
|
|
218
223
|
notify,
|
|
219
224
|
signal,
|
|
220
225
|
});
|
|
221
|
-
if (diagnostics.text !== "")
|
|
226
|
+
if (diagnostics.text !== "") {
|
|
227
|
+
diagnosticText += `${diagnostics.text}\n`;
|
|
228
|
+
}
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
const renamed =
|
package/src/lib/lsp/rename.ts
CHANGED
|
@@ -55,7 +55,9 @@ function toPath(uri: string): string {
|
|
|
55
55
|
function lineStartOffsets(text: string): number[] {
|
|
56
56
|
const starts = [0];
|
|
57
57
|
for (let i = 0; i < text.length; i++) {
|
|
58
|
-
if (text.codePointAt(i) === 10)
|
|
58
|
+
if (text.codePointAt(i) === 10) {
|
|
59
|
+
starts.push(i + 1);
|
|
60
|
+
}
|
|
59
61
|
}
|
|
60
62
|
return starts;
|
|
61
63
|
}
|
|
@@ -64,7 +66,9 @@ function lineStartOffsets(text: string): number[] {
|
|
|
64
66
|
function lineContentLength(text: string, starts: readonly number[], line: number): number {
|
|
65
67
|
const base = starts[line];
|
|
66
68
|
let end = line + 1 < starts.length ? starts[line + 1] - 1 : text.length;
|
|
67
|
-
if (end > base && text.codePointAt(end - 1) === 13)
|
|
69
|
+
if (end > base && text.codePointAt(end - 1) === 13) {
|
|
70
|
+
end--;
|
|
71
|
+
}
|
|
68
72
|
return end - base;
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -117,8 +121,11 @@ function collectTextEdits(edit: WorkspaceEdit): Map<string, RangeLike[]> {
|
|
|
117
121
|
const byPath = new Map<string, RangeLike[]>();
|
|
118
122
|
const push = (path: string, edits: readonly RangeLike[]): void => {
|
|
119
123
|
const existing = byPath.get(path);
|
|
120
|
-
if (existing)
|
|
121
|
-
|
|
124
|
+
if (existing) {
|
|
125
|
+
existing.push(...edits);
|
|
126
|
+
} else {
|
|
127
|
+
byPath.set(path, [...edits]);
|
|
128
|
+
}
|
|
122
129
|
};
|
|
123
130
|
for (const [uri, edits] of Object.entries(edit.changes ?? {})) {
|
|
124
131
|
push(toPath(uri), edits.filter(isRangeLike));
|
|
@@ -190,8 +197,14 @@ const WORD_PATTERN = /[\p{L}\p{N}_$]+/gu;
|
|
|
190
197
|
|
|
191
198
|
/** 两个路径集合是否一致(用于判断 references 结果是否收敛)。 */
|
|
192
199
|
export function samePathSet(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
|
193
|
-
if (a.size !== b.size)
|
|
194
|
-
|
|
200
|
+
if (a.size !== b.size) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
for (const path of a) {
|
|
204
|
+
if (!b.has(path)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
195
208
|
return true;
|
|
196
209
|
}
|
|
197
210
|
|
|
@@ -244,7 +257,9 @@ export function symbolCandidates(
|
|
|
244
257
|
character?: number,
|
|
245
258
|
): LspPosition[] {
|
|
246
259
|
const starts = lineStartOffsets(text);
|
|
247
|
-
if (line < 0 || line >= starts.length)
|
|
260
|
+
if (line < 0 || line >= starts.length) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
248
263
|
const base = starts[line];
|
|
249
264
|
const length = lineContentLength(text, starts, line);
|
|
250
265
|
const matches = [...text.slice(base, base + length).matchAll(WORD_PATTERN)];
|
|
@@ -264,7 +279,9 @@ export function symbolCandidates(
|
|
|
264
279
|
}
|
|
265
280
|
const candidates: LspPosition[] = [];
|
|
266
281
|
for (const match of matches) {
|
|
267
|
-
if (match[0] === symbol)
|
|
282
|
+
if (match[0] === symbol) {
|
|
283
|
+
candidates.push({ line, character: match.index });
|
|
284
|
+
}
|
|
268
285
|
}
|
|
269
286
|
return candidates;
|
|
270
287
|
}
|