@trim21/personal-pi-extensions 0.1.612 → 0.1.619
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/aft/config.ts +9 -3
- package/src/aft/index.ts +6 -2
- package/src/aft/logger.ts +9 -3
- package/src/aft/tools.ts +3 -1
- package/src/bwrap/README.md +19 -10
- package/src/bwrap/approval-rules.ts +42 -14
- 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 +69 -24
- package/src/bwrap/runtime.ts +30 -10
- package/src/bwrap/sandbox.ts +1 -1
- 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 +48 -16
- 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 +138 -46
- 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 +24 -8
- 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 +18 -6
- package/src/opencode/todo.ts +3 -1
- package/src/session-name.ts +39 -13
- package/src/spawn-agent-agents.ts +18 -6
- package/src/spawn-agent.ts +53 -18
- 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 +6 -2
- package/src/talk/registry.ts +33 -11
- 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
|
}
|
|
@@ -288,7 +304,9 @@ function mergeWatch(
|
|
|
288
304
|
globalWatch: LspConfig["watch"],
|
|
289
305
|
localWatch: LspConfig["watch"],
|
|
290
306
|
): LspConfig["watch"] {
|
|
291
|
-
if (!globalWatch && !localWatch)
|
|
307
|
+
if (!globalWatch && !localWatch) {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
292
310
|
return {
|
|
293
311
|
...globalWatch,
|
|
294
312
|
...localWatch,
|
|
@@ -337,8 +355,12 @@ export function filterAdapters(
|
|
|
337
355
|
): LspServerAdapter[] {
|
|
338
356
|
validateConfig(config, adapters);
|
|
339
357
|
return adapters.filter((adapter) => {
|
|
340
|
-
if (config.enabled && !config.enabled.has(adapter.id))
|
|
341
|
-
|
|
358
|
+
if (config.enabled && !config.enabled.has(adapter.id)) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (config.disabled?.has(adapter.id)) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
342
364
|
return true;
|
|
343
365
|
});
|
|
344
366
|
}
|
|
@@ -532,7 +554,9 @@ export function createLspService(
|
|
|
532
554
|
|
|
533
555
|
/** 配置缓存:同一 cwd 内复用;cwd 变化或 reload 清缓存后重读。 */
|
|
534
556
|
async function currentConfig(cwd: string): Promise<ResolvedLspConfig> {
|
|
535
|
-
if (state.config && state.configCwd === cwd)
|
|
557
|
+
if (state.config && state.configCwd === cwd) {
|
|
558
|
+
return state.config;
|
|
559
|
+
}
|
|
536
560
|
const config = await loadLspConfig(cwd, globalConfigPath, (message) =>
|
|
537
561
|
sessionNotify?.(message, "warning"),
|
|
538
562
|
);
|
|
@@ -553,8 +577,12 @@ export function createLspService(
|
|
|
553
577
|
* 退化为 cwd(client 只服务 cwd 内的文件);与 cwd 无交集时返回 undefined。
|
|
554
578
|
*/
|
|
555
579
|
function watchDirFor(root: string, cwd: string): string | undefined {
|
|
556
|
-
if (containsPath(root, cwd))
|
|
557
|
-
|
|
580
|
+
if (containsPath(root, cwd)) {
|
|
581
|
+
return root;
|
|
582
|
+
}
|
|
583
|
+
if (containsPath(cwd, root)) {
|
|
584
|
+
return cwd;
|
|
585
|
+
}
|
|
558
586
|
return undefined;
|
|
559
587
|
}
|
|
560
588
|
|
|
@@ -563,7 +591,9 @@ export function createLspService(
|
|
|
563
591
|
const dirs = new Set<string>();
|
|
564
592
|
for (const client of state.clients) {
|
|
565
593
|
const dir = watchDirFor(client.root, cwd);
|
|
566
|
-
if (dir && !watcherFailed.has(dir))
|
|
594
|
+
if (dir && !watcherFailed.has(dir)) {
|
|
595
|
+
dirs.add(dir);
|
|
596
|
+
}
|
|
567
597
|
}
|
|
568
598
|
return [...dirs]
|
|
569
599
|
.filter((dir) => [...dirs].every((other) => other === dir || !containsPath(dir, other)))
|
|
@@ -646,7 +676,9 @@ export function createLspService(
|
|
|
646
676
|
cwd: string,
|
|
647
677
|
notify?: ExtensionUIContext["notify"],
|
|
648
678
|
): Promise<void> {
|
|
649
|
-
if (state.closing || state.disabled)
|
|
679
|
+
if (state.closing || state.disabled) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
650
682
|
let config: ResolvedLspConfig;
|
|
651
683
|
try {
|
|
652
684
|
config = await currentConfig(cwd);
|
|
@@ -663,23 +695,31 @@ export function createLspService(
|
|
|
663
695
|
const desired = config.watch.enabled ? desiredWatchRoots(cwd) : [];
|
|
664
696
|
// Map 迭代期间删除当前项是安全的
|
|
665
697
|
for (const [root, watcher] of watchers) {
|
|
666
|
-
if (desired.includes(root))
|
|
698
|
+
if (desired.includes(root)) {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
667
701
|
watchers.delete(root);
|
|
668
702
|
void watcher.stop().catch(() => {
|
|
669
703
|
// 停止失败不影响流程
|
|
670
704
|
});
|
|
671
705
|
}
|
|
672
706
|
for (const root of desired) {
|
|
673
|
-
if (watchers.has(root))
|
|
707
|
+
if (watchers.has(root)) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
674
710
|
const watcher = await startWatcher(root, config.watch, notify);
|
|
675
|
-
if (watcher)
|
|
711
|
+
if (watcher) {
|
|
712
|
+
watchers.set(root, watcher);
|
|
713
|
+
}
|
|
676
714
|
}
|
|
677
715
|
}
|
|
678
716
|
|
|
679
717
|
/** 事件按各 client 的 root 前缀 / 注册 pattern / 扩展名过滤后投递。 */
|
|
680
718
|
async function fanOut(changes: FileChange[]): Promise<void> {
|
|
681
719
|
const cwd = state.cwd;
|
|
682
|
-
if (!cwd)
|
|
720
|
+
if (!cwd) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
683
723
|
await Promise.all(
|
|
684
724
|
state.clients.map(async (client) => {
|
|
685
725
|
const filtered = changes.filter((change) => {
|
|
@@ -705,7 +745,9 @@ export function createLspService(
|
|
|
705
745
|
(minimatch(relativeCandidate, watcher.pattern) ||
|
|
706
746
|
minimatch(absoluteCandidate, watcher.pattern)),
|
|
707
747
|
);
|
|
708
|
-
if (!matched)
|
|
748
|
+
if (!matched) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
709
751
|
}
|
|
710
752
|
const extensions = state.clientExtensions.get(client);
|
|
711
753
|
return (
|
|
@@ -714,7 +756,9 @@ export function createLspService(
|
|
|
714
756
|
extensions.includes(extname(change.path))
|
|
715
757
|
);
|
|
716
758
|
});
|
|
717
|
-
if (filtered.length === 0)
|
|
759
|
+
if (filtered.length === 0) {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
718
762
|
try {
|
|
719
763
|
await client.notify.watchedFiles(filtered);
|
|
720
764
|
} catch {
|
|
@@ -726,7 +770,9 @@ export function createLspService(
|
|
|
726
770
|
|
|
727
771
|
/** 汇总当前所有 LSP server 状态并渲染到 footer status。 */
|
|
728
772
|
function updateStatusText(): void {
|
|
729
|
-
if (!renderStatus)
|
|
773
|
+
if (!renderStatus) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
730
776
|
if (state.disabled) {
|
|
731
777
|
renderStatus("lsp: disabled");
|
|
732
778
|
return;
|
|
@@ -792,7 +838,9 @@ export function createLspService(
|
|
|
792
838
|
): Promise<LspClient | undefined> {
|
|
793
839
|
const key = root + adapter.id;
|
|
794
840
|
const inflight = state.spawning.get(key);
|
|
795
|
-
if (inflight)
|
|
841
|
+
if (inflight) {
|
|
842
|
+
return inflight;
|
|
843
|
+
}
|
|
796
844
|
const task = (async () => {
|
|
797
845
|
try {
|
|
798
846
|
const handle = await adapter.spawn(root, cwd);
|
|
@@ -845,7 +893,9 @@ export function createLspService(
|
|
|
845
893
|
})();
|
|
846
894
|
state.spawning.set(key, task);
|
|
847
895
|
void task.finally(() => {
|
|
848
|
-
if (state.spawning.get(key) === task)
|
|
896
|
+
if (state.spawning.get(key) === task) {
|
|
897
|
+
state.spawning.delete(key);
|
|
898
|
+
}
|
|
849
899
|
});
|
|
850
900
|
return task;
|
|
851
901
|
}
|
|
@@ -856,8 +906,12 @@ export function createLspService(
|
|
|
856
906
|
notify?: ExtensionUIContext["notify"],
|
|
857
907
|
adapterFilter?: (adapter: LspServerAdapter) => boolean,
|
|
858
908
|
): Promise<LspClient[]> {
|
|
859
|
-
if (state.closing || state.disabled)
|
|
860
|
-
|
|
909
|
+
if (state.closing || state.disabled) {
|
|
910
|
+
return [];
|
|
911
|
+
}
|
|
912
|
+
if (!containsPath(file, cwd)) {
|
|
913
|
+
return [];
|
|
914
|
+
}
|
|
861
915
|
const config = await currentConfig(cwd);
|
|
862
916
|
const active = filterAdapters(adapters ?? createAdapters(config.servers), config);
|
|
863
917
|
const extension = extname(file) || file;
|
|
@@ -870,15 +924,25 @@ export function createLspService(
|
|
|
870
924
|
}
|
|
871
925
|
|
|
872
926
|
for (const adapter of active) {
|
|
873
|
-
if (adapterFilter && !adapterFilter(adapter))
|
|
874
|
-
|
|
927
|
+
if (adapterFilter && !adapterFilter(adapter)) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (adapter.extensions.length > 0 && !adapter.extensions.includes(extension)) {
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
875
933
|
const root = serverRoot(adapter, file, cwd);
|
|
876
|
-
if (!containsPath(file, root))
|
|
877
|
-
|
|
934
|
+
if (!containsPath(file, root)) {
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (!matchesInclude(adapter.include ?? [], file, root, cwd)) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
878
940
|
const key = root + adapter.id;
|
|
879
941
|
const failedAt = state.brokenFailAt.get(key);
|
|
880
942
|
if (failedAt !== undefined) {
|
|
881
|
-
if (Date.now() - failedAt < retryCooldownMs)
|
|
943
|
+
if (Date.now() - failedAt < retryCooldownMs) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
882
946
|
// 冷却已过:允许重试;重试成功后下面会清除 broken 记录
|
|
883
947
|
state.brokenFailAt.delete(key);
|
|
884
948
|
}
|
|
@@ -890,7 +954,9 @@ export function createLspService(
|
|
|
890
954
|
}
|
|
891
955
|
|
|
892
956
|
const client = await startClient(adapter, root, cwd, config, notify);
|
|
893
|
-
if (client)
|
|
957
|
+
if (client) {
|
|
958
|
+
result.push(client);
|
|
959
|
+
}
|
|
894
960
|
}
|
|
895
961
|
|
|
896
962
|
return result;
|
|
@@ -911,7 +977,9 @@ export function createLspService(
|
|
|
911
977
|
clients.map(async (client) => {
|
|
912
978
|
const after = Date.now();
|
|
913
979
|
const version = await client.notify.open({ path: file });
|
|
914
|
-
if (!diagnostics)
|
|
980
|
+
if (!diagnostics) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
915
983
|
await abortable(
|
|
916
984
|
client.waitForDiagnostics({
|
|
917
985
|
path: file,
|
|
@@ -948,7 +1016,9 @@ export function createLspService(
|
|
|
948
1016
|
cwd: string,
|
|
949
1017
|
options?: LspRequestOptions,
|
|
950
1018
|
): Promise<DiagnosticReport> {
|
|
951
|
-
if (options?.signal?.aborted)
|
|
1019
|
+
if (options?.signal?.aborted) {
|
|
1020
|
+
return EMPTY_DIAGNOSTIC_REPORT;
|
|
1021
|
+
}
|
|
952
1022
|
await touchFile(file, cwd, "document", options);
|
|
953
1023
|
const all = await diagnostics();
|
|
954
1024
|
const normalized = normalize(file);
|
|
@@ -1107,7 +1177,9 @@ export function createLspService(
|
|
|
1107
1177
|
running: readonly { serverID: string; root: string }[],
|
|
1108
1178
|
): Promise<string[]> {
|
|
1109
1179
|
const cwd = state.cwd;
|
|
1110
|
-
if (!cwd)
|
|
1180
|
+
if (!cwd) {
|
|
1181
|
+
return [];
|
|
1182
|
+
}
|
|
1111
1183
|
let config: ResolvedLspConfig;
|
|
1112
1184
|
try {
|
|
1113
1185
|
config = await currentConfig(cwd);
|
|
@@ -1125,7 +1197,9 @@ export function createLspService(
|
|
|
1125
1197
|
const restarted = await Promise.all(
|
|
1126
1198
|
[...unique.values()].map(async ({ serverID, root }): Promise<string | undefined> => {
|
|
1127
1199
|
const adapter = active.find((candidate) => candidate.id === serverID);
|
|
1128
|
-
if (!adapter)
|
|
1200
|
+
if (!adapter) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1129
1203
|
const client = await startClient(adapter, root, cwd, config);
|
|
1130
1204
|
return client ? serverID : undefined;
|
|
1131
1205
|
}),
|
|
@@ -1144,9 +1218,13 @@ export function createLspService(
|
|
|
1144
1218
|
// 个别进程退出失败不阻止清理流程
|
|
1145
1219
|
});
|
|
1146
1220
|
state.clients = state.clients.filter((client) => client.serverID !== serverID);
|
|
1147
|
-
for (const client of targets)
|
|
1221
|
+
for (const client of targets) {
|
|
1222
|
+
state.clientExtensions.delete(client);
|
|
1223
|
+
}
|
|
1148
1224
|
for (const [key, server] of state.servers) {
|
|
1149
|
-
if (server.serverID !== serverID)
|
|
1225
|
+
if (server.serverID !== serverID) {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1150
1228
|
state.brokenFailAt.delete(key);
|
|
1151
1229
|
state.brokenNotifiedAt.delete(key);
|
|
1152
1230
|
state.servers.delete(key);
|
|
@@ -1155,9 +1233,13 @@ export function createLspService(
|
|
|
1155
1233
|
state.disabled = false;
|
|
1156
1234
|
// reload 是资源耗尽后的重试入口:清掉停用记录并按新配置重算监听范围
|
|
1157
1235
|
watcherFailed.clear();
|
|
1158
|
-
if (state.cwd)
|
|
1236
|
+
if (state.cwd) {
|
|
1237
|
+
await reconcileWatchers(state.cwd);
|
|
1238
|
+
}
|
|
1159
1239
|
updateStatusText();
|
|
1160
|
-
if (running.length === 0)
|
|
1240
|
+
if (running.length === 0) {
|
|
1241
|
+
return [];
|
|
1242
|
+
}
|
|
1161
1243
|
return respawnRunning(running);
|
|
1162
1244
|
}
|
|
1163
1245
|
|
|
@@ -1180,7 +1262,9 @@ export function createLspService(
|
|
|
1180
1262
|
state.closing = false;
|
|
1181
1263
|
state.disabled = false;
|
|
1182
1264
|
watcherFailed.clear();
|
|
1183
|
-
if (state.cwd)
|
|
1265
|
+
if (state.cwd) {
|
|
1266
|
+
await reconcileWatchers(state.cwd);
|
|
1267
|
+
}
|
|
1184
1268
|
updateStatusText();
|
|
1185
1269
|
return respawnRunning(running);
|
|
1186
1270
|
}
|
|
@@ -1247,8 +1331,12 @@ function getNoopService(): LspService {
|
|
|
1247
1331
|
function enabledServerCount(config: ResolvedLspConfig, adapters?: LspServerAdapter[]): number {
|
|
1248
1332
|
const ids = adapters ? adapters.map((adapter) => adapter.id) : Object.keys(config.servers);
|
|
1249
1333
|
return ids.filter((id) => {
|
|
1250
|
-
if (config.enabled && !config.enabled.has(id))
|
|
1251
|
-
|
|
1334
|
+
if (config.enabled && !config.enabled.has(id)) {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
if (config.disabled?.has(id)) {
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1252
1340
|
return true;
|
|
1253
1341
|
}).length;
|
|
1254
1342
|
}
|
|
@@ -1286,7 +1374,9 @@ export function createLspManager(
|
|
|
1286
1374
|
}
|
|
1287
1375
|
});
|
|
1288
1376
|
validateConfig(config, options?.adapters);
|
|
1289
|
-
if (enabledServerCount(config, options?.adapters) === 0)
|
|
1377
|
+
if (enabledServerCount(config, options?.adapters) === 0) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1290
1380
|
// 下面两个闭包被 service 长期持有,可能在会话被替换或 reload 后仍触发
|
|
1291
1381
|
// (session_shutdown 清理、后台 watcher 错误、in-flight spawn 失败),
|
|
1292
1382
|
// 而旧 ctx 已被 pi 标记 stale,ctx.ui getter 会直接抛错。UI 写入是尽力
|
|
@@ -1317,7 +1407,9 @@ export function createLspManager(
|
|
|
1317
1407
|
hooks.onEnabled(pi, next);
|
|
1318
1408
|
} catch (error) {
|
|
1319
1409
|
service = undefined;
|
|
1320
|
-
if (error instanceof Error)
|
|
1410
|
+
if (error instanceof Error) {
|
|
1411
|
+
ctx.ui.notify(error.message, "error");
|
|
1412
|
+
}
|
|
1321
1413
|
}
|
|
1322
1414
|
});
|
|
1323
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
|
}
|