@trim21/personal-pi-extensions 0.1.489 → 0.1.491
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/bwrap/approval-suggest.ts +1 -1
- package/src/bwrap/core.ts +6 -6
- package/src/bwrap/mihomo-config.ts +3 -1
- package/src/bwrap/network-stack.ts +2 -2
- package/src/claude-code/edit-utils.ts +4 -4
- package/src/claude-code/files.ts +41 -26
- package/src/claude-code/glob.ts +1 -1
- package/src/gh-readonly.ts +12 -12
- package/src/lib/cli.ts +3 -6
- package/src/lib/lsp/client.ts +139 -9
- package/src/lib/lsp/inspect-tool.ts +376 -0
- package/src/lib/lsp/language.ts +1 -1
- package/src/lib/lsp/lsp-find-definition.md +8 -0
- package/src/lib/lsp/lsp-find-reference.md +8 -0
- package/src/lib/lsp/lsp-inspect.md +8 -0
- package/src/lib/lsp/lsp.ts +200 -34
- package/src/lib/lsp/rename-tool.ts +2 -2
- package/src/lib/ui.ts +2 -2
- package/src/lib/write-guard.ts +1 -1
- package/src/openai-cost/config.ts +1 -1
- package/src/openai-cost/cost.ts +1 -1
- package/src/opencode/edit-engine.ts +1 -1
- package/src/opencode/files.ts +34 -23
- package/src/spawn-agent.ts +2 -2
- package/src/talk/index.ts +2 -8
- package/src/talk/registry.ts +1 -1
- package/src/vision-agent.ts +2 -2
package/package.json
CHANGED
package/src/bwrap/core.ts
CHANGED
|
@@ -105,11 +105,11 @@ export function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
|
105
105
|
const base = {
|
|
106
106
|
mode: config.mode,
|
|
107
107
|
bwrapPath: config.bwrapPath,
|
|
108
|
-
writablePaths: config.writablePaths
|
|
108
|
+
writablePaths: config.writablePaths,
|
|
109
109
|
extraWritablePaths: config.extraWritablePaths,
|
|
110
|
-
denyPaths: config.denyPaths
|
|
111
|
-
extraArgs: config.extraArgs
|
|
112
|
-
networkAllowlist: config.networkAllowlist
|
|
110
|
+
denyPaths: config.denyPaths,
|
|
111
|
+
extraArgs: config.extraArgs,
|
|
112
|
+
networkAllowlist: config.networkAllowlist,
|
|
113
113
|
mihomoPath: config.mihomoPath,
|
|
114
114
|
slirp4netnsPath: config.slirp4netnsPath,
|
|
115
115
|
approvalRules: config.approvalRules ?? [],
|
|
@@ -529,8 +529,8 @@ export function createBwrapBashOperations(
|
|
|
529
529
|
}, timeout * 1000)
|
|
530
530
|
: undefined;
|
|
531
531
|
const onAbort = () => killChild(child);
|
|
532
|
-
child.stdout
|
|
533
|
-
child.stderr
|
|
532
|
+
child.stdout.on("data", onData);
|
|
533
|
+
child.stderr.on("data", onData);
|
|
534
534
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
535
535
|
child.once("error", reject);
|
|
536
536
|
child.once("close", (exitCode) => {
|
|
@@ -81,7 +81,9 @@ function parseAllowlistEntry(entry: string): AllowlistEntry {
|
|
|
81
81
|
const match = /^\[(.+)\](?::(\d+))?$/.exec(entry);
|
|
82
82
|
if (match?.[1] === undefined) throw new Error(`Invalid allowlist entry "${entry}"`);
|
|
83
83
|
host = match[1];
|
|
84
|
-
|
|
84
|
+
// 端口组 (?::(\d+))? 可选:无端口时 match[2] 在运行时是 undefined
|
|
85
|
+
const portPart = match[2] as string | undefined;
|
|
86
|
+
port = portPart === undefined ? undefined : parsePort(portPart);
|
|
85
87
|
} else {
|
|
86
88
|
const colon = entry.lastIndexOf(":");
|
|
87
89
|
if (colon === -1) {
|
|
@@ -332,8 +332,8 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
332
332
|
killChild(child.pid);
|
|
333
333
|
};
|
|
334
334
|
|
|
335
|
-
child.stdout
|
|
336
|
-
child.stderr
|
|
335
|
+
child.stdout.on("data", execOptions.onData);
|
|
336
|
+
child.stderr.on("data", execOptions.onData);
|
|
337
337
|
execOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
338
338
|
|
|
339
339
|
child.once("error", (error) => {
|
|
@@ -73,10 +73,10 @@ function applyCurlySingleQuotes(str: string): string {
|
|
|
73
73
|
for (let i = 0; i < chars.length; i++) {
|
|
74
74
|
if (chars[i] === "'") {
|
|
75
75
|
// 缩写中的撇号(如 don't)用右花引号,不做开/闭判断
|
|
76
|
-
const prev = chars[i - 1];
|
|
77
|
-
const next = chars[i + 1];
|
|
78
|
-
const prevIsLetter =
|
|
79
|
-
const nextIsLetter =
|
|
76
|
+
const prev = i > 0 ? chars[i - 1] : "";
|
|
77
|
+
const next = i + 1 < chars.length ? chars[i + 1] : "";
|
|
78
|
+
const prevIsLetter = /\p{L}/u.test(prev);
|
|
79
|
+
const nextIsLetter = /\p{L}/u.test(next);
|
|
80
80
|
if (prevIsLetter && nextIsLetter) {
|
|
81
81
|
result.push(RIGHT_SINGLE_CURLY_QUOTE);
|
|
82
82
|
} else {
|
package/src/claude-code/files.ts
CHANGED
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
import { Type } from "typebox";
|
|
16
16
|
|
|
17
17
|
import { appendLspDiagnosticText } from "../lib/lsp/diagnostic.js";
|
|
18
|
-
import {
|
|
18
|
+
import { registerLspInspectTools } from "../lib/lsp/inspect-tool.js";
|
|
19
|
+
import { createLspManager, type LspService, type LspServiceOptions } from "../lib/lsp/lsp.js";
|
|
19
20
|
import { registerLspRenameTool } from "../lib/lsp/rename-tool.js";
|
|
20
21
|
import { formatSubtitlePath } from "../lib/path.js";
|
|
21
22
|
import type { ToolPendant } from "../lib/pendant.ts";
|
|
@@ -228,7 +229,7 @@ function requireCurrentRead(
|
|
|
228
229
|
export function registerFileTools(
|
|
229
230
|
pi: ExtensionAPI,
|
|
230
231
|
state: ClaudeCodeState,
|
|
231
|
-
|
|
232
|
+
getService: () => LspService,
|
|
232
233
|
): void {
|
|
233
234
|
pi.registerTool({
|
|
234
235
|
name: "Read",
|
|
@@ -351,9 +352,11 @@ export function registerFileTools(
|
|
|
351
352
|
const snapshot = { ...snapshotOf(buffer), offset, limit };
|
|
352
353
|
state.reads.set(key, snapshot);
|
|
353
354
|
// LSP 文件事件通知是后台任务,失败不影响读取(read 不驻留文档)
|
|
354
|
-
void
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
void getService()
|
|
356
|
+
.notifyFile(filePath, ctx.cwd)
|
|
357
|
+
.catch(() => {
|
|
358
|
+
// 后台通知失败不影响读取
|
|
359
|
+
});
|
|
357
360
|
return {
|
|
358
361
|
content: [{ type: "text", text: formatted.text }],
|
|
359
362
|
details: {
|
|
@@ -443,7 +446,7 @@ export function registerFileTools(
|
|
|
443
446
|
text: diagnosticText,
|
|
444
447
|
errorCount,
|
|
445
448
|
warningCount,
|
|
446
|
-
} = await
|
|
449
|
+
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
447
450
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
448
451
|
signal,
|
|
449
452
|
});
|
|
@@ -546,7 +549,7 @@ export function registerFileTools(
|
|
|
546
549
|
text: diagnosticText,
|
|
547
550
|
errorCount,
|
|
548
551
|
warningCount,
|
|
549
|
-
} = await
|
|
552
|
+
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
550
553
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
551
554
|
});
|
|
552
555
|
return [
|
|
@@ -645,7 +648,7 @@ export function registerFileTools(
|
|
|
645
648
|
text: diagnosticText,
|
|
646
649
|
errorCount,
|
|
647
650
|
warningCount,
|
|
648
|
-
} = await
|
|
651
|
+
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
649
652
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
650
653
|
});
|
|
651
654
|
return [
|
|
@@ -674,21 +677,8 @@ export function registerFileTools(
|
|
|
674
677
|
},
|
|
675
678
|
});
|
|
676
679
|
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
// 持久化(FILE_TOOL_NAMES 的 restoreFileReads 依赖 details.reads)。
|
|
680
|
-
registerLspRenameTool(pi, service, {
|
|
681
|
-
recordReads: async (applied) => {
|
|
682
|
-
const reads: Record<string, FileSnapshot> = {};
|
|
683
|
-
for (const fileEdit of applied) {
|
|
684
|
-
const key = await readStateKey(fileEdit.path);
|
|
685
|
-
const snapshot = snapshotOf(fileEdit.newText);
|
|
686
|
-
state.reads.set(key, snapshot);
|
|
687
|
-
reads[key] = snapshot;
|
|
688
|
-
}
|
|
689
|
-
return reads;
|
|
690
|
-
},
|
|
691
|
-
});
|
|
680
|
+
// 只读符号查询工具(find-definition / find-reference / inspect)与 opencode 共享;
|
|
681
|
+
// 它们由 manager 的 onEnabled 回调注册,不在这里注册。
|
|
692
682
|
}
|
|
693
683
|
|
|
694
684
|
/** 会更新 reads state 并随 details 持久化快照的工具名。 */
|
|
@@ -720,10 +710,34 @@ function restoreFileReads(
|
|
|
720
710
|
* state 归本文件所有:扩展实例内创建,并随 session 事件从历史分支恢复,
|
|
721
711
|
* 与主进程 index.ts 聚合加载时的行为一致。
|
|
722
712
|
*/
|
|
723
|
-
export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
724
|
-
const service = registerLsp(pi);
|
|
713
|
+
export default function claudeCodeFileTools(pi: ExtensionAPI, options?: LspServiceOptions): void {
|
|
725
714
|
const state = createClaudeCodeState();
|
|
726
715
|
|
|
716
|
+
// LSP 专属工具(lsp-rename / inspect 族)仅在 lsp.json 存在 enabled 服务器时
|
|
717
|
+
// 注册(session_start 校验后);本工具集跟踪 read-before-write 状态,rename
|
|
718
|
+
// 落盘的文件要标记为已读并随 details 持久化(restoreFileReads 依赖 details.reads)。
|
|
719
|
+
const manager = createLspManager(
|
|
720
|
+
pi,
|
|
721
|
+
{
|
|
722
|
+
onEnabled: (pi, service) => {
|
|
723
|
+
registerLspRenameTool(pi, service, {
|
|
724
|
+
recordReads: async (applied) => {
|
|
725
|
+
const reads: Record<string, FileSnapshot> = {};
|
|
726
|
+
for (const fileEdit of applied) {
|
|
727
|
+
const key = await readStateKey(fileEdit.path);
|
|
728
|
+
const snapshot = snapshotOf(fileEdit.newText);
|
|
729
|
+
state.reads.set(key, snapshot);
|
|
730
|
+
reads[key] = snapshot;
|
|
731
|
+
}
|
|
732
|
+
return reads;
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
registerLspInspectTools(pi, service);
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
options,
|
|
739
|
+
);
|
|
740
|
+
|
|
727
741
|
// 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
|
|
728
742
|
// 已读记账随之丢失。这里从当前分支的历史工具结果里恢复:digest 是当时的值,
|
|
729
743
|
// 若文件在此期间被外部修改,Edit/Write 时的指纹对比仍会要求重新 Read,
|
|
@@ -739,5 +753,6 @@ export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
|
739
753
|
restoreFileReads(state, ctx.sessionManager);
|
|
740
754
|
});
|
|
741
755
|
|
|
742
|
-
|
|
756
|
+
// 文件工具无条件注册;service 惰性获取,disabled 时为 no-op。
|
|
757
|
+
registerFileTools(pi, state, () => manager.mustLazyGetService());
|
|
743
758
|
}
|
package/src/claude-code/glob.ts
CHANGED
|
@@ -34,7 +34,7 @@ function extractGlobBaseDirectory(pattern: string): {
|
|
|
34
34
|
} {
|
|
35
35
|
const globChars = /[*?[{]/;
|
|
36
36
|
const match = globChars.exec(pattern);
|
|
37
|
-
if (!match
|
|
37
|
+
if (!match) {
|
|
38
38
|
// 无 glob 特殊字符:字面路径,目录部分作为 baseDir
|
|
39
39
|
return { baseDir: dirname(pattern), relativePattern: basename(pattern) };
|
|
40
40
|
}
|
package/src/gh-readonly.ts
CHANGED
|
@@ -122,12 +122,12 @@ export function runGh(
|
|
|
122
122
|
timeoutId = setTimeout(() => killProcess("timeout"), timeout);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
proc.stdout
|
|
125
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
126
126
|
const text = data.toString();
|
|
127
127
|
stdout += text;
|
|
128
128
|
combined.push(text);
|
|
129
129
|
});
|
|
130
|
-
proc.stderr
|
|
130
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
131
131
|
const text = data.toString();
|
|
132
132
|
stderr += text;
|
|
133
133
|
combined.push(text);
|
|
@@ -793,7 +793,7 @@ export async function renderStepLog(
|
|
|
793
793
|
const totalLines = clean.split("\n").length;
|
|
794
794
|
let logToShow = clean;
|
|
795
795
|
let appliedOffset = false;
|
|
796
|
-
if (offset !== undefined && offset
|
|
796
|
+
if (offset !== undefined && offset > 1) {
|
|
797
797
|
if (offset > totalLines) {
|
|
798
798
|
return {
|
|
799
799
|
content: [
|
|
@@ -867,7 +867,7 @@ export async function renderJobLogs(
|
|
|
867
867
|
): Promise<CiLogsResult> {
|
|
868
868
|
const { job, offset, limit, full } = params;
|
|
869
869
|
|
|
870
|
-
if (
|
|
870
|
+
if (jobs.length === 0) {
|
|
871
871
|
return {
|
|
872
872
|
content: [{ type: "text", text: `No jobs found for run ${params.runId}` }],
|
|
873
873
|
details: {},
|
|
@@ -923,7 +923,7 @@ export async function renderJobLogs(
|
|
|
923
923
|
|
|
924
924
|
// Apply offset on the cleaned text, then truncate.
|
|
925
925
|
let logToShow = clean;
|
|
926
|
-
if (offset !== undefined && offset
|
|
926
|
+
if (offset !== undefined && offset > 1) {
|
|
927
927
|
if (offset > totalLines) {
|
|
928
928
|
steps.push({ name: s.name });
|
|
929
929
|
continue;
|
|
@@ -1037,9 +1037,12 @@ export async function writeLogFile(
|
|
|
1037
1037
|
|
|
1038
1038
|
let content: string;
|
|
1039
1039
|
let what: string;
|
|
1040
|
-
if (step
|
|
1040
|
+
if (step === undefined) {
|
|
1041
|
+
content = stripAnsi(rawLog);
|
|
1042
|
+
what = `job "${targetJob.name}" (id: ${targetJob.id})`;
|
|
1043
|
+
} else {
|
|
1041
1044
|
const found = targetJob.steps.find((s) => s.name.toLowerCase() === step.toLowerCase());
|
|
1042
|
-
if (
|
|
1045
|
+
if (found === undefined) {
|
|
1043
1046
|
return {
|
|
1044
1047
|
content: [
|
|
1045
1048
|
{
|
|
@@ -1064,9 +1067,6 @@ export async function writeLogFile(
|
|
|
1064
1067
|
}
|
|
1065
1068
|
content = cleanStepOutput(stepLog);
|
|
1066
1069
|
what = `step ${found.number} ("${found.name}") of job "${targetJob.name}"`;
|
|
1067
|
-
} else {
|
|
1068
|
-
content = stripAnsi(rawLog);
|
|
1069
|
-
what = `job "${targetJob.name}" (id: ${targetJob.id})`;
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
1072
|
const target = resolve(cwd ?? process.cwd(), outputFile);
|
|
@@ -1480,7 +1480,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1480
1480
|
getJobLog(String(run_id), jobId, effectiveRepo, signal, ctx.cwd, params);
|
|
1481
1481
|
|
|
1482
1482
|
// ── Write the complete log to a file ───────────────────────────────
|
|
1483
|
-
if (output_file !== undefined && output_file !==
|
|
1483
|
+
if (output_file !== undefined && output_file !== "") {
|
|
1484
1484
|
const result = await writeLogFile(
|
|
1485
1485
|
{ runId: String(run_id), job, step, outputFile: output_file },
|
|
1486
1486
|
jobs,
|
|
@@ -1492,7 +1492,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1492
1492
|
}
|
|
1493
1493
|
|
|
1494
1494
|
// ── Fetch a specific step's logs (requires `job`) ─────────────────
|
|
1495
|
-
if (step !== undefined
|
|
1495
|
+
if (step !== undefined) {
|
|
1496
1496
|
onUpdate?.({
|
|
1497
1497
|
content: [{ type: "text", text: `Fetching job list...` }],
|
|
1498
1498
|
details: {},
|
package/src/lib/cli.ts
CHANGED
|
@@ -238,7 +238,7 @@ export function parseCommand<TFlags extends TObject>(
|
|
|
238
238
|
} else if (info.kind === "boolean") {
|
|
239
239
|
rawFlags[info.key] = true;
|
|
240
240
|
} else {
|
|
241
|
-
const next = tokens
|
|
241
|
+
const next = tokens.at(i + 1);
|
|
242
242
|
if (next !== undefined && !looksLikeFlag(next)) {
|
|
243
243
|
rawFlags[info.key] = next;
|
|
244
244
|
i++;
|
|
@@ -266,7 +266,7 @@ export function parseCommand<TFlags extends TObject>(
|
|
|
266
266
|
} else if (j + 1 < rest.length) {
|
|
267
267
|
rawFlags[info.key] = rest.slice(j + 1);
|
|
268
268
|
} else {
|
|
269
|
-
const next = tokens
|
|
269
|
+
const next = tokens.at(i + 1);
|
|
270
270
|
if (next !== undefined && !looksLikeFlag(next)) {
|
|
271
271
|
rawFlags[info.key] = next;
|
|
272
272
|
i++;
|
|
@@ -327,10 +327,7 @@ export function parseCommand<TFlags extends TObject>(
|
|
|
327
327
|
parsed = Value.Convert(spec.flags, Value.Clone(parsed));
|
|
328
328
|
if (!Value.Check(spec.flags, parsed)) {
|
|
329
329
|
const [first] = [...Value.Errors(spec.flags, parsed)];
|
|
330
|
-
return errorResult(
|
|
331
|
-
spec,
|
|
332
|
-
`Invalid arguments: ${first?.message ?? "value does not match flags"}`,
|
|
333
|
-
);
|
|
330
|
+
return errorResult(spec, `Invalid arguments: ${first.message}`);
|
|
334
331
|
}
|
|
335
332
|
} catch {
|
|
336
333
|
return errorResult(spec, "Invalid arguments");
|
package/src/lib/lsp/client.ts
CHANGED
|
@@ -20,7 +20,14 @@ import {
|
|
|
20
20
|
StreamMessageReader,
|
|
21
21
|
StreamMessageWriter,
|
|
22
22
|
} from "vscode-jsonrpc/node";
|
|
23
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
Diagnostic as VSCodeDiagnostic,
|
|
25
|
+
Hover,
|
|
26
|
+
Location as LspLocation,
|
|
27
|
+
LocationLink,
|
|
28
|
+
Range,
|
|
29
|
+
WorkspaceEdit,
|
|
30
|
+
} from "vscode-languageserver-types";
|
|
24
31
|
|
|
25
32
|
import type { LspServerHandle } from "./adapter.js";
|
|
26
33
|
import { LANGUAGE_EXTENSIONS } from "./language.js";
|
|
@@ -72,6 +79,20 @@ const LSP_CONTENT_MODIFIED = -32801;
|
|
|
72
79
|
*/
|
|
73
80
|
export class RenameNotPossibleError extends Error {}
|
|
74
81
|
|
|
82
|
+
/**
|
|
83
|
+
* 服务器不支持某 LSP 方法(MethodNotFound)。与传输失败区分,供服务层跳过
|
|
84
|
+
* 该服务器尝试下一个,而不是把整个操作当作失败。
|
|
85
|
+
*/
|
|
86
|
+
export class LspMethodNotSupportedError extends Error {
|
|
87
|
+
readonly serverID: string;
|
|
88
|
+
readonly method: string;
|
|
89
|
+
constructor(serverID: string, method: string) {
|
|
90
|
+
super(`LSP server "${serverID}" does not support ${method}`);
|
|
91
|
+
this.serverID = serverID;
|
|
92
|
+
this.method = method;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
/**
|
|
76
97
|
* rename edit 未覆盖 references 看到的全部文件:服务器索引可能仍在后台加载。
|
|
77
98
|
* 抛出时发生在写盘之前,整个 rename 无副作用,可稍后重试。
|
|
@@ -148,6 +169,50 @@ export interface RenameSymbolResult {
|
|
|
148
169
|
placeholder?: string;
|
|
149
170
|
}
|
|
150
171
|
|
|
172
|
+
/** definition / references / hover 请求的输入(line / character 为 0-based LSP position)。 */
|
|
173
|
+
export interface InspectPositionRequest {
|
|
174
|
+
path: string;
|
|
175
|
+
line: number;
|
|
176
|
+
character: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** definition / references 归一化后的位置(0-based;1-based 格式化由工具层负责)。 */
|
|
180
|
+
export interface InspectLocation {
|
|
181
|
+
path: string;
|
|
182
|
+
line: number;
|
|
183
|
+
character: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type DefinitionResult = LspLocation | LspLocation[] | LocationLink | LocationLink[] | null;
|
|
187
|
+
|
|
188
|
+
/** Location / LocationLink → path + 0-based 坐标;非 file: URI 是服务器的意外行为,跳过。 */
|
|
189
|
+
function toInspectLocations(result: DefinitionResult): InspectLocation[] {
|
|
190
|
+
if (result === null) return [];
|
|
191
|
+
const items = Array.isArray(result) ? result : [result];
|
|
192
|
+
const locations: InspectLocation[] = [];
|
|
193
|
+
for (const item of items) {
|
|
194
|
+
if ("targetUri" in item) {
|
|
195
|
+
if (!item.targetUri.startsWith("file:")) continue;
|
|
196
|
+
const range =
|
|
197
|
+
(item as Omit<LocationLink, "targetSelectionRange"> & { targetSelectionRange?: Range })
|
|
198
|
+
.targetSelectionRange ?? item.targetRange;
|
|
199
|
+
locations.push({
|
|
200
|
+
path: normalize(fileURLToPath(item.targetUri)),
|
|
201
|
+
line: range.start.line,
|
|
202
|
+
character: range.start.character,
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
if (!item.uri.startsWith("file:")) continue;
|
|
206
|
+
locations.push({
|
|
207
|
+
path: normalize(fileURLToPath(item.uri)),
|
|
208
|
+
line: item.range.start.line,
|
|
209
|
+
character: item.range.start.character,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return locations;
|
|
214
|
+
}
|
|
215
|
+
|
|
151
216
|
export class InitializeError extends Error {
|
|
152
217
|
readonly serverID: string;
|
|
153
218
|
constructor(serverID: string, cause: unknown) {
|
|
@@ -241,6 +306,21 @@ export interface LspClient {
|
|
|
241
306
|
* 位置不在符号上 / 服务器不支持 rename 时抛 RenameNotPossibleError。
|
|
242
307
|
*/
|
|
243
308
|
renameSymbol(request: RenameSymbolRequest): Promise<RenameSymbolResult>;
|
|
309
|
+
/**
|
|
310
|
+
* textDocument/definition:归一化后的定义位置列表(Location / LocationLink
|
|
311
|
+
* 统一转 path + 0-based 坐标;无结果返回空数组)。
|
|
312
|
+
*/
|
|
313
|
+
definition(request: InspectPositionRequest): Promise<InspectLocation[]>;
|
|
314
|
+
/**
|
|
315
|
+
* textDocument/references:归一化后的引用位置列表(是否含声明处由服务器
|
|
316
|
+
* 按 includeDeclaration 决定,此处固定包含,对齐 rename 覆盖校验口径)。
|
|
317
|
+
*/
|
|
318
|
+
references(request: InspectPositionRequest): Promise<InspectLocation[]>;
|
|
319
|
+
/**
|
|
320
|
+
* textDocument/hover:服务器返回的 contents 原样透传,不做内容归一化;
|
|
321
|
+
* 服务器无信息时返回 null(合法应答,非错误)。
|
|
322
|
+
*/
|
|
323
|
+
hover(request: InspectPositionRequest): Promise<Hover | null>;
|
|
244
324
|
shutdown(): Promise<void>;
|
|
245
325
|
}
|
|
246
326
|
|
|
@@ -347,7 +427,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
347
427
|
new StreamMessageReader(input.server.process.stdout),
|
|
348
428
|
new StreamMessageWriter(input.server.process.stdin),
|
|
349
429
|
);
|
|
350
|
-
input.server.process.stderr
|
|
430
|
+
input.server.process.stderr.resume();
|
|
351
431
|
/** 连接或服务器进程已关闭;pull 重试循环以此终止,避免无界等待。 */
|
|
352
432
|
let connectionClosed = false;
|
|
353
433
|
input.server.process.once("exit", () => {
|
|
@@ -504,7 +584,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
504
584
|
await connection.sendNotification("workspace/didChangeConfiguration", { settings });
|
|
505
585
|
}
|
|
506
586
|
|
|
507
|
-
const files: Record<string, { version: number; text: string }> = {};
|
|
587
|
+
const files: Record<string, { version: number; text: string } | undefined> = {};
|
|
508
588
|
|
|
509
589
|
// ── 驻留 LRU ────────────────────────────────────────────────────────────────
|
|
510
590
|
|
|
@@ -746,16 +826,16 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
746
826
|
if (timeout <= 0) return Promise.resolve(false);
|
|
747
827
|
return new Promise<boolean>((resolve) => {
|
|
748
828
|
let finished = false;
|
|
829
|
+
const timer = setTimeout(() => finish(false), timeout);
|
|
749
830
|
const finish = (result: boolean) => {
|
|
750
831
|
if (finished) return;
|
|
751
832
|
finished = true;
|
|
752
|
-
|
|
833
|
+
clearTimeout(timer);
|
|
753
834
|
registrationListeners.delete(listener);
|
|
754
835
|
resolve(result);
|
|
755
836
|
};
|
|
756
837
|
const listener = () => finish(true);
|
|
757
838
|
registrationListeners.add(listener);
|
|
758
|
-
const timer = setTimeout(() => finish(false), timeout);
|
|
759
839
|
});
|
|
760
840
|
}
|
|
761
841
|
|
|
@@ -769,12 +849,14 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
769
849
|
return new Promise<boolean>((resolve) => {
|
|
770
850
|
let finished = false;
|
|
771
851
|
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
852
|
+
const timeoutTimer = setTimeout(() => finish(false), request.timeout);
|
|
853
|
+
const unsub = () => diagnosticListeners.delete(listener);
|
|
772
854
|
const finish = (result: boolean) => {
|
|
773
855
|
if (finished) return;
|
|
774
856
|
finished = true;
|
|
775
857
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
776
|
-
|
|
777
|
-
unsub
|
|
858
|
+
clearTimeout(timeoutTimer);
|
|
859
|
+
unsub();
|
|
778
860
|
resolve(result);
|
|
779
861
|
};
|
|
780
862
|
const schedule = () => {
|
|
@@ -789,13 +871,11 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
789
871
|
);
|
|
790
872
|
};
|
|
791
873
|
|
|
792
|
-
const timeoutTimer = setTimeout(() => finish(false), request.timeout);
|
|
793
874
|
const listener = (event: { path: string; serverID: string }) => {
|
|
794
875
|
if (event.path !== request.path || event.serverID !== input.serverID) return;
|
|
795
876
|
schedule();
|
|
796
877
|
};
|
|
797
878
|
diagnosticListeners.add(listener);
|
|
798
|
-
const unsub = () => diagnosticListeners.delete(listener);
|
|
799
879
|
schedule();
|
|
800
880
|
});
|
|
801
881
|
}
|
|
@@ -922,6 +1002,31 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
922
1002
|
return 0;
|
|
923
1003
|
};
|
|
924
1004
|
|
|
1005
|
+
// ── 只读符号查询(definition / references / hover)──────────────────────────
|
|
1006
|
+
|
|
1007
|
+
/** 请求前先同步磁盘内容(didOpen/didChange),保证服务器基于最新文本应答。 */
|
|
1008
|
+
const preparePositionRequest = async (request: InspectPositionRequest) => {
|
|
1009
|
+
const resolvedPath = normalize(
|
|
1010
|
+
isAbsolute(request.path) ? request.path : resolve(input.directory, request.path),
|
|
1011
|
+
);
|
|
1012
|
+
await openDocument({ path: resolvedPath });
|
|
1013
|
+
return {
|
|
1014
|
+
uri: pathToFileURL(resolvedPath).href,
|
|
1015
|
+
position: { line: request.line, character: request.character },
|
|
1016
|
+
};
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
const sendInspectRequest = async <T>(method: string, message: object): Promise<T> => {
|
|
1020
|
+
try {
|
|
1021
|
+
return await retryOnContentModified(() => connection.sendRequest<T>(method, message));
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
if (error instanceof ResponseError && error.code === LSP_METHOD_NOT_FOUND) {
|
|
1024
|
+
throw new LspMethodNotSupportedError(input.serverID, method);
|
|
1025
|
+
}
|
|
1026
|
+
throw error;
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
925
1030
|
return {
|
|
926
1031
|
root: input.root,
|
|
927
1032
|
get serverID() {
|
|
@@ -1101,6 +1206,31 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1101
1206
|
current = toPaths(await referencesRequest());
|
|
1102
1207
|
}
|
|
1103
1208
|
},
|
|
1209
|
+
async definition(request: InspectPositionRequest): Promise<InspectLocation[]> {
|
|
1210
|
+
const { uri, position } = await preparePositionRequest(request);
|
|
1211
|
+
return toInspectLocations(
|
|
1212
|
+
await sendInspectRequest<DefinitionResult>("textDocument/definition", {
|
|
1213
|
+
textDocument: { uri },
|
|
1214
|
+
position,
|
|
1215
|
+
}),
|
|
1216
|
+
);
|
|
1217
|
+
},
|
|
1218
|
+
async references(request: InspectPositionRequest): Promise<InspectLocation[]> {
|
|
1219
|
+
const { uri, position } = await preparePositionRequest(request);
|
|
1220
|
+
const locations = await sendInspectRequest<LspLocation[] | null>("textDocument/references", {
|
|
1221
|
+
textDocument: { uri },
|
|
1222
|
+
position,
|
|
1223
|
+
context: { includeDeclaration: true },
|
|
1224
|
+
});
|
|
1225
|
+
return toInspectLocations(locations);
|
|
1226
|
+
},
|
|
1227
|
+
async hover(request: InspectPositionRequest): Promise<Hover | null> {
|
|
1228
|
+
const { uri, position } = await preparePositionRequest(request);
|
|
1229
|
+
return await sendInspectRequest<Hover | null>("textDocument/hover", {
|
|
1230
|
+
textDocument: { uri },
|
|
1231
|
+
position,
|
|
1232
|
+
});
|
|
1233
|
+
},
|
|
1104
1234
|
get diagnostics() {
|
|
1105
1235
|
const result = new Map<string, Diagnostic[]>();
|
|
1106
1236
|
for (const key of new Set([...pushDiagnostics.keys(), ...pullDiagnostics.keys()])) {
|