0-1. 学习目标与一句话
LSP 模块是代码语义反馈层:它按文件懒启动 language server,编辑后 touchFile 等待 diagnostics,把错误追加到 tool output;同时 lsp tool 提供 definition、references、hover、symbols 等查询。
来源:packages/opencode/src/lsp/lsp.ts:211-299、packages/opencode/src/lsp/lsp.ts:346-379、packages/opencode/src/tool/lsp.ts:37-110。
2-5. 它的位置与最小源码路径
源码路径卡片
lsp/lsp.ts:123-138:LSP service interface。lsp/lsp.ts:211-299:按文件懒启动 client。lsp/lsp.ts:346-379:touchFile 和 diagnostics 聚合。lsp/client.ts:141-305:JSON-RPC connection 和 initialize。lsp/client.ts:594-692:didOpen/didChange 和等待 diagnostics。tool/lsp.ts:37-110:LSP tool。tool/edit.ts:192-207、tool/write.ts:80-99:编辑后诊断回填。
6. 用户输入到 agent 行动的整体链路
edit/write tool
-> modify file
-> lsp.touchFile(file, "document" or "full")
-> getClients(file)
-> client.notify.open(didOpen/didChange)
-> waitForDiagnostics
-> lsp.diagnostics()
-> Diagnostic.report(...)
-> tool output includes errors
-> next LLM round
Java 类比
LSP.Service 像 LanguageIntelligenceService;LSPServer.Info 像 server factory;touchFile 像 IDE 的 documentOpened/documentChanged;Diagnostic.report 像编译错误格式化器。
7. 核心源码逐段讲解
LSP Interface
export interface Interface {
readonly touchFile: (input: string, diagnostics?: "document" | "full") => Effect.Effect<void>
readonly diagnostics: () => Effect.Effect<Record<string, LSPClient.Diagnostic[]>>
readonly hover: (input: LocInput) => Effect.Effect<any>
readonly definition: (input: LocInput) => Effect.Effect<any[]>
readonly references: (input: LocInput) => Effect.Effect<any[]>
readonly documentSymbol: (uri: string) => Effect.Effect<(DocumentSymbol | Symbol)[]>
}
路径:packages/opencode/src/lsp/lsp.ts:123-138。
懒启动 client
if (!containsPath(file, ctx)) return []
const extension = path.parse(file).ext || file
for (const server of Object.values(s.servers)) {
if (server.extensions.length && !server.extensions.includes(extension)) continue
const root = await server.root(file, ctx)
if (!root) continue
if (s.broken.has(root + server.id)) continue
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
if (match) { result.push(match); continue }
const task = schedule(server, root, root + server.id)
s.spawning.set(root + server.id, task)
const client = await task
if (client) result.push(client)
}
路径:packages/opencode/src/lsp/lsp.ts:211-299。
touchFile
const clients = yield* getClients(input)
yield* Effect.promise(() =>
Promise.all(
clients.map(async (client) => {
const after = Date.now()
const version = await client.notify.open({ path: input })
if (!diagnostics) return
return client.waitForDiagnostics({ path: input, version, mode: diagnostics, after })
}),
).catch((err) => {
log.error("failed to touch file", { err, file: input })
}),
)
路径:packages/opencode/src/lsp/lsp.ts:346-366。
client open/change
const document = files[request.path]
if (document !== undefined) {
const next = document.version + 1
files[request.path] = { version: next, text }
await connection.sendNotification("textDocument/didChange", {
textDocument: { uri: pathToFileURL(request.path).href, version: next },
contentChanges: [{ text }],
})
return next
}
await connection.sendNotification("textDocument/didOpen", {
textDocument: { uri: pathToFileURL(request.path).href, languageId, version: 0, text },
})
路径:packages/opencode/src/lsp/client.ts:594-669。
LSP tool
yield* ctx.ask({
permission: "lsp",
patterns: ["*"],
always: ["*"],
metadata: meta,
})
yield* lsp.touchFile(file, "document")
switch (args.operation) {
case "goToDefinition":
return lsp.definition(position)
case "findReferences":
return lsp.references(position)
case "hover":
return lsp.hover(position)
}
路径:packages/opencode/src/tool/lsp.ts:56-103。
8. 关键 TypeScript 语法复习
as const:让 LSP operation 数组变成 literal union。interface:编译期接口,运行时不存在。diagnostics?: "document" | "full":可选参数 + literal union。function* <T>:泛型函数,返回类型由 callback 决定。get diagnostics():JS getter,调用像属性。results.flat().filter(Boolean):类似 Java Stream flatMap + nonNull。
9-10. 架构思想与模块协作
- Lazy initialization:按文件需要启动 language server。
- Factory/Registry:server info 和 client cache 分离。
- JSON-RPC adapter:stdin/stdout 封装成 LSP connection。
- Feedback loop:diagnostics 写回 tool output,进入下一轮 LLM。
- Best-effort enhancement:LSP 故障记录日志,不让 edit/write 整体崩掉。
11. mini agent 对应代码
async function afterEdit(file, ctx) {
await lsp.touchFile(file, "document")
const diagnostics = await lsp.diagnostics()
const block = reportErrors(file, diagnostics[file] ?? [])
return block
? "Edit applied successfully.\n\nDiagnostics detected:\n" + block
: "Edit applied successfully."
}
12. 费曼复述区
- 为什么 edit/write 后要调用 LSP?
touchFile做了哪两件事?- push diagnostics 和 pull diagnostics 有什么差异?
- LSP tool 和自动 diagnostics 的关系是什么?
13-15. 练习与自测
- 入门:把
LSP.Interface方法分成 lifecycle/diagnostics/query。 - 进阶:解释
clients、spawning、broken三个状态。 - 源码追踪:从
EditTool追到client.notify.open。 - 小实现:写一个 mini diagnostics service,并在 edit tool 后调用。
- 自测:diagnostics 最终如何进入下一轮模型推理?