0-1. 学习目标与一句话

LSP 模块是代码语义反馈层:它按文件懒启动 language server,编辑后 touchFile 等待 diagnostics,把错误追加到 tool output;同时 lsp tool 提供 definition、references、hover、symbols 等查询。

来源:packages/opencode/src/lsp/lsp.ts:211-299packages/opencode/src/lsp/lsp.ts:346-379packages/opencode/src/tool/lsp.ts:37-110

2-5. 它的位置与最小源码路径

源码路径卡片

  1. lsp/lsp.ts:123-138:LSP service interface。
  2. lsp/lsp.ts:211-299:按文件懒启动 client。
  3. lsp/lsp.ts:346-379:touchFile 和 diagnostics 聚合。
  4. lsp/client.ts:141-305:JSON-RPC connection 和 initialize。
  5. lsp/client.ts:594-692:didOpen/didChange 和等待 diagnostics。
  6. tool/lsp.ts:37-110:LSP tool。
  7. tool/edit.ts:192-207tool/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.ServiceLanguageIntelligenceServiceLSPServer.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. 费曼复述区

  1. 为什么 edit/write 后要调用 LSP?
  2. touchFile 做了哪两件事?
  3. push diagnostics 和 pull diagnostics 有什么差异?
  4. LSP tool 和自动 diagnostics 的关系是什么?

13-15. 练习与自测

  • 入门:把 LSP.Interface 方法分成 lifecycle/diagnostics/query。
  • 进阶:解释 clientsspawningbroken 三个状态。
  • 源码追踪:从 EditTool 追到 client.notify.open
  • 小实现:写一个 mini diagnostics service,并在 edit tool 后调用。
  • 自测:diagnostics 最终如何进入下一轮模型推理?

16. 下一步阅读建议

继续读 “UI / TUI / Desktop / IDE”。LSP 和权限都通过事件、tool output 和 session 状态对外呈现,UI 章会看到这些状态如何被不同前端消费。