0%

跟着OpenCode学智能体设计和开发5:LSP集成与代码智能

LSP 集成:服务器管理与客户端通信

OpenCode 的 LSP 集成是一套高性能、可扩展的智能代码辅助系统,基于客户端-服务器架构实现,支持 35+ 编程语言的诊断、导航、符号查询等功能,核心特点是延迟生成、资源优化、多服务器协调,同时提供灵活的配置和扩展能力。

一、LSP 架构核心:三层组件与延迟生成策略

OpenCode LSP 采用关注点分离的三层架构,确保系统高效、可维护,同时通过延迟生成优化资源占用。

1. 三层核心组件

组件 职责 核心功能
服务器管理 语言服务器的生命周期控制 服务器注册、项目根目录检测、进程生成(二进制/包管理器/自动下载)
客户端通信 处理 LSP 协议交互 JSON-RPC 连接建立、文档生命周期管理、诊断接收、请求响应处理
编排层 多服务器/客户端的协调控制 状态管理、延迟客户端获取、错误处理、优雅关闭

2. 延迟生成策略(核心优化)

系统不会预启动所有语言服务器,而是遵循「按需启动」原则:

  • 只有当用户访问与服务器扩展名匹配的文件时,才会触发对应语言服务器的生成。
  • 优势:在大型多语言项目中,避免启动未使用的服务器进程,大幅降低内存和 CPU 占用。

二、服务器管理:注册、根目录检测与进程生成

服务器管理是 LSP 系统的基础,负责定义语言服务器的行为、定位项目根目录、启动服务器进程。

1. 服务器注册:统一 Info 接口

所有语言服务器都通过 LSPServer.Info 接口注册,包含 4 个核心属性:

属性 类型 描述
id string 服务器唯一标识(如 typescriptrust
extensions string[] 处理的文件扩展名(如 [".ts", ".tsx"]
root RootFunction 异步函数,用于检测当前文件的项目根目录
spawn `(root: string) => Promise<Handle undefined>` 生成服务器进程的函数

注册来源:内置服务器定义 + 用户配置文件中的自定义服务器,初始化时合并到注册表。

2. 项目根目录检测:NearestRoot 辅助函数

根目录检测是 LSP 的关键能力(语言服务器需要基于项目根目录加载配置),核心实现是 NearestRoot 函数,采用向上搜索模式

  1. 从当前文件目录开始,向上遍历至实例根目录。
  2. 匹配 includePatterns 中的项目标记文件(如 package-lock.jsonCargo.toml)。
  3. 跳过 excludePatterns 中的文件(如 Deno 项目的 deno.json)。
  4. 未找到则返回实例根目录。

示例(TypeScript 服务器)

1
2
3
4
5
6
7
8
9
export const Typescript: Info = {
id: "typescript",
root: NearestRoot(
["package-lock.json", "bun.lockb"], // 匹配包管理器锁文件
["deno.json", "deno.jsonc"] // 排除 Deno 项目
),
extensions: [".ts", ".tsx", ".js"],
async spawn(root) { /* 生成逻辑 */ }
}

3. 服务器生成:三种启动策略

系统支持三种灵活的服务器启动方式,适配不同类型的语言服务器:

策略 适用场景 示例
二进制发现 已全局安装的服务器 Rust Analyzer:通过 Bun.which("rust-analyzer") 查找可执行文件
包管理器集成 项目内局部安装的服务器 如 ESLint 服务器,通过 npx eslint --lsp 启动
自动下载 无全局安装的服务器 Clangd:从 GitHub Releases 下载预编译二进制,解压并创建符号链接

受限环境适配:设置 OPENCODE_DISABLE_LSP_DOWNLOAD 标志可禁用自动下载,支持手动预安装。

4. 支持的语言服务器生态

覆盖 35+ 编程语言,涵盖主流开发场景:

类别 代表服务器 核心功能
JavaScript/TypeScript TypeScript、ESLint、Biome 类型检查、代码格式化、Linting
系统语言 Rust Analyzer、Gopls、Clangd 高性能代码分析、重构支持
Python Pyright、Ty 类型推断、静态检查
Web 框架 Vue、Svelte、Astro 组件语法分析、智能提示
DevOps 工具 Terraform、Prisma、Docker 配置文件校验、语法提示

三、客户端通信:JSON-RPC 协议交互与文档管理

客户端是 LSP 协议的执行者,负责与服务器建立连接、管理文档状态、处理诊断和请求,基于 vscode-jsonrpc 实现。

1. 连接建立:基于 Stdio 的 JSON-RPC 握手

客户端通过 LSPClient.create() 创建,核心步骤:

  1. 建立连接:通过服务器进程的 stdin/stdout 创建 JSON-RPC 消息流。
  2. 发送初始化请求:包含工作区根目录、进程 ID、客户端能力(如诊断支持、文件监听),设置 45 秒超时防止无响应。
  3. 发送初始化通知:初始化成功后,发送 initialized 通知,标志连接就绪。

2. 文档生命周期管理:版本控制与增量更新

客户端通过版本号跟踪文档状态,确保服务器获取最新内容,操作分为两种场景:

场景 操作步骤
新文件打开 1. 发送 workspace/didChangeWatchedFiles(类型:Created)
2. 发送 textDocument/didOpen(携带文件内容、语言 ID)
3. 清除该文件的历史诊断
现有文件修改 1. 发送 workspace/didChangeWatchedFiles(类型:Changed)
2. 发送 textDocument/didChange(携带增量内容、递增版本号)
3. 更新本地版本计数器

语言 ID 映射:通过 LANGUAGE_EXTENSIONS 字典将扩展名转为 LSP 标准语言 ID(如 .tstypescript)。

3. 诊断处理:接收与事件发布

诊断是 LSP 的核心功能(如语法错误、代码警告),处理流程:

  1. 服务器通过 textDocument/publishDiagnostics 通知发送诊断结果。
  2. 客户端将诊断结果存储在内存 Map(键:文件路径,值:诊断数组)。
  3. 发布诊断事件到 Bus 系统,供其他组件(如编辑器、AI Agent)消费。
  4. 特殊优化:抑制 TypeScript 服务器的初始诊断(避免早期无配置时的冗余错误)。

4. 请求/响应处理:支持 LSP 标准请求

客户端内置处理程序,响应服务器的各类请求,确保协议合规:

请求类型 用途
workspace/configuration 返回服务器初始化配置选项
client/registerCapability 确认动态能力注册(如文件监听)
window/workDoneProgress/create 支持服务器显示进度条(如索引构建)

四、LSP 编排层:多服务器协调与状态管理

编排层是 LSP 系统的「大脑」,通过集中式状态管理,协调多个服务器和客户端的生命周期,确保稳定性和资源效率。

1. 核心状态集合

初始化时创建 4 个核心集合,维护系统状态:

集合 类型 用途
servers Record<string, LSPServer.Info> 所有注册的服务器定义
clients LSPClient.Info[] 活跃的客户端连接
broken Set<string> 启动失败的服务器标识符(防止重复尝试)
spawning Map<string, Promise<...>> 进行中的服务器生成任务(去重)

2. 延迟客户端获取:按需加载优化

getClients() 函数实现延迟加载逻辑,核心步骤:

  1. 根据文件扩展名过滤匹配的服务器。
  2. 检查是否已有活跃客户端(避免重复创建)。
  3. 检查是否有进行中的生成任务(去重,防止并发生成)。
  4. 无则启动新的生成任务,并加入 spawning 映射。
  5. 启动失败则加入 broken 集合,同一会话不再重试。

3. 优雅关闭与状态报告

  • 优雅关闭:实例销毁时,自动终止所有活跃客户端的连接,释放进程资源。
  • 状态报告:通过 status() 函数返回所有连接服务器的状态(ID、根目录、连接状态),支持监控和调试。

五、工具集成:向 AI Agent 暴露代码智能功能

LSP 系统通过 lsp 工具,为 OpenCode 的 AI Agent 提供统一的代码分析接口,支持 9 种核心操作:

操作 描述 典型用例
goToDefinition 查找符号的定义位置 了解函数/类的实现来源
findReferences 查找符号的所有引用 重构前的影响范围分析
hover 获取符号的文档和类型信息 快速查看函数参数、返回值
documentSymbol 获取当前文件的符号结构 生成代码大纲、导航跳转
workspaceSymbol 跨项目搜索符号 查找全局函数、类的位置

核心适配:自动将编辑器的1 基坐标转换为 LSP 标准的0 基坐标,匹配人类使用习惯。

六、配置与性能优化:灵活定制与资源高效

1. 丰富的配置选项

用户可通过配置文件自定义 LSP 行为,满足个性化需求:

配置场景 示例
全局禁用 LSP "lsp": false
禁用特定服务器 "lsp": { "typescript": { "disabled": true } }
配置自定义服务器
"lsp": {
"my-custom-lsp": {
"command": ["my-lsp", "--stdio"],
"extensions": [".custom"],
"initialization": { "opt": "val" }
}
}
启用实验性服务器 设置 OPENCODE_EXPERIMENTAL_LSP_TY 标志,自动禁用冲突的 Pyright

2. 关键性能优化策略

LSP 系统内置多项优化,平衡功能与资源占用:

优化手段 效果
延迟生成 仅启动当前使用的语言服务器,减少内存占用
连接重用 持久化客户端-服务器连接,增量发送文件更新,避免重复进程启动
诊断防抖 150ms 防抖延迟,合并服务器连续发送的诊断更新,减少事件处理次数
生成去重 通过 spawning 映射避免同一服务器/根目录的并发生成
失败抑制 启动失败的服务器标记为 broken,同一会话不再重试,防止级联故障

七、测试与稳定性保障

系统通过虚假 LSP 服务器实现全面测试,无需依赖外部语言服务器:

  1. 虚假服务器实现最小化 LSP 握手逻辑(如响应 initialize 请求)。
  2. 测试覆盖工作区配置、能力注册、诊断发送等核心场景。
  3. 验证协议合规性和边界情况(如初始化超时、服务器崩溃)。

支持的语言服务器与配置

OpenCode 的 LSP(Language Server Protocol)集成是一套开箱即用的高级代码智能系统,支持 35+ 种编程语言,提供语法高亮、错误诊断、代码补全、符号导航等核心功能,采用延迟初始化模型优化资源占用,同时支持灵活配置和自定义扩展。

一、核心架构与关键特性

1. 延迟初始化模型(核心优化)

LSP 系统不预启动任何语言服务器,仅在用户访问对应文件类型时才按需启动,既减少了内存和 CPU 占用,又能保证代码智能功能的快速响应。

2. 三大核心组件

系统通过三个组件协同工作,实现完整的 LSP 协议支持:

  1. 服务器定义:包含每种语言服务器的启动逻辑、配置参数、根目录检测规则。
  2. LSP 客户端:基于 JSON-RPC 协议与语言服务器通信,处理消息收发、诊断接收、请求响应。
  3. 语言扩展:维护文件扩展名与语言服务器的映射关系,确保正确匹配对应服务器。

3. 多服务器并存支持

同一文件可由多个互补的语言服务器处理(例如 TypeScript 文件同时启动 typescript 语言服务器和 eslint 校验服务器),提供更全面的代码智能能力。

二、支持的语言服务器生态(35+ 种)

OpenCode 内置覆盖主流开发场景的语言服务器,按生态分类整理如下,核心信息包含自动安装支持和关键说明:

1. JavaScript/TypeScript 生态(前端核心)

Server ID 支持扩展名 自动安装 关键说明
typescript .ts、.tsx、.js、.jsx 等 基于 Bun 运行 typescript-language-server,从项目解析 tsserver.js
vue .vue PATH 中不存在时,通过 npm 下载 @vue/language-server
eslint .ts、.vue 等 从 GitHub Releases 获取并构建 VS Code ESLint 服务器
biome .ts、.json、.vue 等 部分 优先使用本地/全局二进制文件,回退到 Bun 安装

2. 其他主流生态摘要

生态分类 代表 Server ID 自动安装 关键依赖
Python pyrightty(实验性) pyright ty 需要 OPENCODE_EXPERIMENTAL_LSP_TY 标志,与 pyright 冲突
系统语言 rustgoplsclangd goplsclangd rust 需手动安装 rust-analyzerclangd 从 GitHub 下载预编译二进制
.NET/JVM csharpjdtls jdtls 需要 Java 21+,csharp 依赖 .NET SDK
Web 框架 svelteastroyaml-ls astro 依赖 TypeScript 环境

3. 专用语言(需手动安装居多)

ruby-lspelixir-lsprisma 等,其中仅 ruby-lspelixir-ls 支持自动安装,其余需手动配置二进制文件或对应 CLI 工具。

三、完整配置指南(opencode.json)

LSP 配置通过 opencode.json/opencode.jsonc 管理,支持全局控制、单个服务器自定义、自定义服务器添加。

1. 全局启用/禁用

完全禁用所有 LSP 功能,阻止任何语言服务器启动:

1
2
3
{
"lsp": false
}

2. 单个服务器配置(启用/禁用/自定义)

可针对内置服务器进行启用状态切换、命令覆盖、环境变量配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"lsp": {
"typescript": {
"disabled": false // 启用 TypeScript 服务器(默认值)
},
"eslint": {
"disabled": true // 禁用 ESLint 服务器
},
"pyright": {
"command": ["custom-pyright", "--stdio"], // 自定义启动命令
"env": {
"PYTHON_PATH": "/usr/bin/python3" // 传递环境变量
},
"initialization": {
"pythonPath": "/custom/path/to/python" // LSP 初始化参数
}
}
}
}

3. 自定义语言服务器(添加新支持)

定义全新的自定义服务器,需指定 commandextensions 核心字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"lsp": {
"my-custom-server": {
"command": ["/path/to/custom-server", "--stdio"], // 启动命令与参数
"extensions": [".custom"], // 关联的文件扩展名
"env": {
"CUSTOM_SETTING": "value" // 自定义环境变量
},
"initialization": {
"configuration": {
"enabled": true
}
}
}
}
}

注意:自定义服务器 ID 若与内置 ID 重复,将完全替换内置配置;如需互补,建议使用不同 ID。

四、关键高级特性

1. 根目录检测

语言服务器通过「向上搜索模式」确定项目工作区边界,影响文件监视、导入解析等功能,常见检测模式:

  1. 基于锁文件:package-lock.jsonCargo.lock 等。
  2. 基于配置文件:deno.jsongo.modpom.xml 等。
  3. 基于 VCS:回退到 Git 仓库根目录。
  4. 支持 Monorepo:识别 Rust 工作区、Gradle 多项目等结构。

示例(TypeScript 服务器)

1
2
3
4
root: NearestRoot(
["package-lock.json", "bun.lockb"], // 匹配的锁文件
["deno.json", "deno.jsonc"] // 排除的配置文件(Deno 项目)
)

2. 自动下载管理

多数主流服务器支持自动安装,行为由 OPENCODE_DISABLE_LSP_DOWNLOAD 环境变量控制:

  • 启用自动下载(默认):无需额外配置,缺失服务器时自动从对应源下载。
  • 禁用自动下载(受限环境):
    1
    export OPENCODE_DISABLE_LSP_DOWNLOAD=1
  • 缓存目录:自动下载的二进制文件缓存在 ~/.opencode/bin(Unix 系统)。

3. 实验性功能

部分服务器标记为实验性,需设置对应环境标志才能启用:

  • 代表功能:Ty LSP(Python 替代 Pyright)
  • 启用命令:export OPENCODE_EXPERIMENTAL_LSP_TY=1
  • 行为:启用 Ty 同时自动禁用冲突的 Pyright 服务器。

4. 防抖诊断系统

为避免频繁处理诊断更新,客户端实现 150ms 防抖逻辑,处理流程:

  1. 文件打开/修改时,发送 didOpen/didChange 通知。
  2. 服务器返回 publishDiagnostics 诊断信息。
  3. 客户端防抖 150ms,合并连续诊断更新(如语法检查→语义分析)。
  4. 发布诊断事件供下游组件消费,严重性映射:1=ERROR、2=WARN、3=INFO、4=HINT。

五、核心 LSP 操作(供 AI Agent 与工具调用)

LSP 系统通过 lsp 工具暴露 9 种核心操作,支持代码分析与导航:

操作 描述 核心参数
goToDefinition 跳转到符号定义位置 file、line、character
findReferences 查找符号的所有引用 file、line、character
hover 显示符号的悬停文档与类型信息 file、line、character
documentSymbol 列出当前文件的所有符号(大纲) uri
workspaceSymbol 跨项目搜索全局符号 query
goToImplementation 跳转到接口/抽象方法的实现 file、line、character

这些操作自动对 AI Agent 开放,无需额外配置即可用于代码分析与重构。

六、故障排除与性能注意事项

1. 常见问题排查

(1)服务器无法初始化

  1. 查看控制台输出,获取服务器启动日志。
  2. 验证依赖环境(如 jdtls 需 Java 21+、gopls 需 Go SDK)。
  3. 确认未设置 OPENCODE_DISABLE_LSP_DOWNLOAD(如需自动安装)。

(2)根目录检测异常

  1. 验证项目根目录是否存在预期的配置/锁文件(如 package-lock.json)。
  2. 全局服务器(如 bash)会回退到 Instance.directory 作为工作区。

(3)OAuth 回调/诊断失败

  1. 检查服务器配置是否正确,扩展是否匹配。
  2. 损坏的服务器会被缓存,重启 OpenCode 可重试初始化。

2. 性能优化建议

  1. 禁用未使用的服务器,减少内存与 CPU 占用,提升启动速度。
  2. 手动安装大型服务器(如 clangd),避免自动下载的网络延迟。
  3. 对于 Monorepo,确保根目录检测正确,避免多服务器重复启动。

总结

  1. OpenCode LSP 集成的核心优势是延迟初始化、开箱即用、灵活扩展,支持 35+ 语言的全量代码智能功能。
  2. 配置核心是 opencode.jsonlsp 字段,支持全局、单个服务器、自定义服务器三层配置。
  3. 高级特性(根目录检测、自动下载、防抖诊断)保障了系统的稳定性与高效性。
  4. 实验性功能需通过环境标志启用,故障排查优先查看控制台日志与依赖环境。

符号导航与文档符号

符号导航和文档符号功能通过语言服务器协议(LSP)集成提供了强大的代码智能能力,使 Agent 能够理解代码结构、定位定义并高效导航代码库。

架构概览

符号导航系统通过多层架构运行,在抽象 LSP 服务器复杂性的同时,为符号查询提供统一接口。该架构支持 30 多种语言服务器,每种服务器都具备专门的符号提取功能。

文档符号

文档符号提供特定文件内所有符号的分层视图,包括类、函数、方法、接口、变量、常量、结构和枚举。系统利用 LSP textDocument/documentSymbol 请求从语言服务器提取结构信息。

符号类型分类

系统实现了 LSP SymbolKind 规范,包含 26 个不同类别,但结果会过滤为与导航最相关的类型:

Symbol Kind Value Description
Class 5 类定义和声明
Function 12 函数定义和声明
Method 6 类/对象内的方法定义
Interface 11 接口定义
Variable 13 变量声明
Constant 14 常量声明
Struct 23 结构定义 (Rust, Go)
Enum 10 枚举定义

系统过滤工作区符号查询,仅包含这八种主要符号类型,以减少噪音并提高相关性 [来源: packages/opencode/src/lsp/index.ts#L319-L358]。

DocumentSymbol Schema

文档符号包含详细的元数据,用于精确导航和代码理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
name: string, // 符号名称
detail?: string, // 附加详细信息 (例如:签名)
kind: number, // SymbolKind 枚举值
range: { // 符号的完整范围
start: { line: number, character: number },
end: { line: number, character: number }
},
selectionRange: { // 标识符高亮范围
start: { line: number, character: number },
end: { line: number, character: number }
}
}

range 和 selectionRange 的区别实现了精确的符号定位——前者覆盖整个符号声明,后者隔离标识符以便准确的光标定位 [来源: packages/opencode/src/lsp/index.ts#L51-L62]。

文档符号通过递归的 children 属性支持分层嵌套,使 Agent 能够理解类结构、嵌套函数和模块组织,而无需额外查询。

通过 LSP Tool 使用

Agent 通过 lsp 工具与文档符号交互,该工具需要指定文件路径和位置上下文:

1
2
3
4
5
6
{
"operation": "documentSymbol",
"filePath": "src/components/Button.tsx",
"line": 1,
"character": 1
}

虽然 line 和 character 参数是验证所必需的,但 documentSymbol 查询对整个文件进行操作,而不管指定的位置如何。这种设计在所有 LSP 操作中保持 API 一致性 [来源: packages/opencode/src/tool/lsp.ts#L10-L30]。

工作区符号

工作区符号通过项目所有索引文件的模糊搜索实现跨文件符号发现。此功能对于理解大型代码库和在不知道确切文件位置的情况下定位符号至关重要。

查询处理

workspaceSymbol 函数接受查询字符串并对所有已注册的语言服务器执行模式匹配:

1
2
3
4
5
6
7
8
9
async function workspaceSymbol(query: string) {
return runAll((client) =>
client.connection
.sendRequest("workspace/symbol", { query })
.then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
.then((result: any) => result.slice(0, 10))
.catch(() => []),
).then((result) => result.flat() as LSP.Symbol[])
}

来自多个服务器的结果被聚合、过滤为相关的符号类型,并限制为每个服务器 10 个结果,以管理响应大小 [来源: packages/opencode/src/lsp/index.ts#L359-L369]。

符号位置 Schema

工作区符号包含支持直接导航的位置元数据:

1
2
3
4
5
6
7
8
9
10
11
{
name: string, // 符号名称
kind: number, // SymbolKind 枚举值
location: {
uri: string, // 文件 URI
range: { // 文件中的符号位置
start: { line: number, character: number },
end: { line: number, character: number }
}
}
}

URI 使用 file:// 协议,需要转换为文件系统路径才能进行文件操作 [来源: packages/opencode/src/lsp/index.ts#L37-L49]。

使用示例

1
2
3
4
5
6
7
{
"operation": "workspaceSymbol",
"filePath": "src/index.ts",
"line": 1,
"character": 1,
"query": "Button"
}

这会在工作区的所有 TypeScript 和 JavaScript 文件中搜索匹配 “Button” 的符号,并返回来自多个文件的定义 [来源: packages/opencode/src/tool/lsp.ts#L10-L30]。

服务器特定能力

不同的语言服务器提供不同级别的符号支持。下表概述了主要服务器的文档符号能力:

Language Server Document Symbols Workspace Symbols Hierarchical Support
Typescript ✓ (嵌套类、方法)
Pyright ✓ (类、嵌套函数)
rust-analyzer ✓ (模块、impl)
gopls ✓ (结构、接口)
clangd ✓ (命名空间、类)
lua-language-server ✓ (表、模块)
dart ✓ (类、mixin)

某些语言服务器(例如 lua-language-server、bash-language-server)如果 PATH 中不存在,会在首次使用时自动下载和安装,而其他服务器(例如 rust-analyzer、gopls)需要手动安装。

与 Agent 工作流的集成

符号导航与其他 LSP 操作无缝集成,提供全面的代码智能:

这使得 Agent 能够首先探索文档结构,然后导航到特定定义、查找引用或分析调用层次结构——所有这些都通过一致的 API 完成 [来源: packages/opencode/src/tool/lsp.txt#L1-L20]。

错误处理和回退

系统为 LSP 通信失败实现了强大的错误处理:

  1. 服务器不可用:如果未为文件类型配置 LSP 服务器,工具将返回错误:”No LSP server available for this file type” [来源: packages/opencode/src/tool/lsp.ts#L45-L47]。
  2. 通信失败:捕获并记录单个服务器故障,返回空数组而不是传播错误。这允许工作区查询即使某些服务器无响应也能成功 [来源: packages/opencode/src/lsp/index.ts#L365-L367]。
  3. 初始化超时:服务器初始化具有 45 秒超时,并具有用于跟踪的特定错误类型 [来源: packages/opencode/src/lsp/client.ts#L82-L125]。

诊断信息通过 150ms 的防抖处理,以防止当 LSP 服务器快速连续发送语法和语义诊断信息时出现重复通知。

性能考虑

几项优化确保了高效的符号导航:

  1. 延迟服务器生成:LSP 服务器在首次访问特定文件类型和工作区根目录时按需生成 [来源: packages/opencode/src/lsp/index.ts#L177-L262]。
  2. 客户端缓存:活动客户端在同一工作区根目录内的多个请求中被重用,避免重复的服务器初始化 [来源: packages/opencode/src/lsp/index.ts#L231-L234]。
  3. 结果限制:工作区符号查询限制为每个服务器 10 个结果,以防止常见查询的响应过大 [来源: packages/opencode/src/lsp/index.ts#L366]。
  4. 并行查询:对多个服务器的请求并行执行,使用 Promise.all 聚合结果 [来源: packages/opencode/src/lsp/index.ts#L457-L461]。

支持的语言服务器

系统内置支持 30 多种语言服务器,具有自动配置和初始化功能:

  • TypeScript/JavaScript: typescript, deno
  • Python: pyright, ty (实验性)
  • Rust: rust-analyzer
  • Go: gopls
  • C/C++: clangd
  • Java: jdtls
  • Kotlin: kotlin-ls
  • C#/F#: csharp, fsharp
  • Web: vue, svelte, astro, eslint, oxlint, biome
  • Ruby: ruby-lsp
  • Lua: lua-ls
  • PHP: intelephense
  • Terraform: terraform
  • Docker: dockerfile
  • 以及更多…

每个服务器都包含专门的根目录检测逻辑(例如,为 Rust 查找 Cargo.toml,为 Node.js 项目查找 package.json),以确保正确的工作区上下文 [来源: packages/opencode/src/lsp/server.ts#L53-L2032]。

代码格式化集成

OpenCode 提供自动代码格式化集成功能,该功能在文件保存时运行,支持独立格式化工具和基于 LSP 的格式化能力。此系统可确保你的项目代码保持一致性,而无需手动干预。

架构概述

格式化系统采用事件驱动架构运行,响应文件编辑事件,并根据文件扩展名和项目配置自动调用相应的格式化工具。

该系统利用项目状态管理来维护格式化工具配置,并支持多种格式化工具来源,包括独立 CLI 工具和 LSP 服务器。

来源:format/index.ts

内置格式化工具

OpenCode 内置了对众多流行代码格式化工具的支持,这些工具可在文件保存时自动调用。

JavaScript/TypeScript 格式化工具

Prettier - 最受欢迎的 JavaScript/TypeScript 格式化工具,支持多种文件类型,包括 HTML、CSS、JSON 和 Markdown。当 package.json 中包含 prettier 作为依赖项或 devDependency 时,会自动启用。

1
2
3
4
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", 
".html", ".htm", ".css", ".scss", ".sass", ".less", ".vue", ".svelte",
".json", ".jsonc", ".yaml", ".yml", ".toml", ".xml", ".md", ".mdx",
".graphql", ".gql"]

来源:format/formatter.ts

Biome - 一款快速的 JavaScript/TypeScript 格式化工具和 Linter。当在项目层级中检测到 biome.json 或 biome.jsonc 配置文件时,会自动启用。

来源:format/formatter.ts

oxfmt - 一款实验性格式化工具,需要启用 OPENCODE_EXPERIMENTAL_OXFMT 标志。当 package.json 中包含 oxfmt 作为依赖项时激活。

来源:format/formatter.ts

特定语言格式化工具

Formatter Language(s) Configuration Detection
gofmt Go Binary availability check
mix Elixir Binary availability check
zig Zig Binary availability check
clang-format C/C++ .clang-format file presence
ktlint Kotlin Binary availability check
ruff Python Config files (pyproject.toml, ruff.toml, .ruff.toml) or dependency detection
rustfmt Rust Binary availability check
rubocop Ruby Binary availability check
standardrb Ruby Binary availability check
htmlbeautifier HTML/Ruby Binary availability check
dart Dart Binary availability check
ocamlformat OCaml Binary availability check
terraform HCL Binary availability check
latexindent LaTeX Binary availability check
gleam Gleam Binary availability check
shfmt Shell scripts Binary availability check
nixfmt Nix Binary availability check

来源:format/formatter.ts

基于 LSP 的格式化

除了独立的格式化工具外,OpenCode 还可以利用语言服务器协议 (LSP) 服务器来实现格式化功能。许多现代 LSP 服务器将格式化作为核心功能提供,而 OpenCode 的 LSP 集成支持这一功能。

支持 LSP 格式化的服务器

以下 LSP 服务器提供格式化功能并受 OpenCode 支持:

  • TypeScript - typescript-language-server
  • Biome - 内置支持格式化的 LSP
  • Deno - 原生 Deno LSP,支持格式化
  • Gopls - Go 语言服务器,集成 gofmt
  • Rust Analyzer - Rust 语言服务器,集成 rustfmt
  • Pyright - Python 语言服务器
  • Clangd - C/C++ 语言服务器,集成 clang-format
  • Zls - Zig 语言服务器
  • 以及更多

来源:lsp/server.ts

当相应的 LSP 服务器对某个文件处于活动状态时,会自动使用基于 LSP 的格式化工具。这提供了语言感知的格式化,能够遵守你项目的特定配置和约定,通常优于独立的格式化工具。

配置

格式化工具可以通过项目级别或全局级别的 opencode.json 或 opencode.jsonc 配置文件进行配置。

禁用所有格式化工具

要禁用所有自动格式化功能:

1
2
3
{
"formatter": false
}

来源:config/config.ts

禁用特定格式化工具

要在保持其他格式化工具启用的同时禁用单个格式化工具:

1
2
3
4
5
6
7
8
9
10
{
"formatter": {
"prettier": {
"disabled": true
},
"ruff": {
"disabled": true
}
}
}

自定义格式化工具配置

你可以通过覆盖命令、扩展名或环境变量来自定义格式化工具:

1
2
3
4
5
6
7
8
9
10
11
12
{
"formatter": {
"prettier": {
"command": ["bun", "x", "prettier", "--write", "$FILE", "--config", ".prettierrc.custom"],
"environment": {
"BUN_BE_BUN": "1",
"PRETTIER_CONFIG": ".prettierrc.custom"
},
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
}

添加自定义格式化工具

定义全新的格式化工具:

1
2
3
4
5
6
7
8
9
{
"formatter": {
"my-custom-formatter": {
"command": ["my-formatter", "--format", "$FILE"],
"extensions": [".custom"],
"disabled": false
}
}
}

来源:config/config.ts

工作原理

格式化工具检测与激活

格式化工具系统遵循复杂的检测流程:

  1. 配置加载 - 系统按优先顺序从多个来源加载格式化工具配置:远程配置 → 全局配置 → 项目配置 → 内联配置
  2. 状态初始化 - 格式化工具状态通过合并可用格式化工具与自定义配置进行初始化
  3. 扩展名匹配 - 编辑文件时,提取其扩展名并与已注册的格式化工具进行匹配
  4. 启用检查 - 调用每个匹配到的格式化工具的 enabled() 函数,以验证其是否应处于活动状态
  5. 命令执行 - 如果已启用,则执行格式化工具的命令,并将 $FILE 占位符替换为实际文件路径

来源:format/index.ts

事件驱动的格式化

格式化系统通过全局事件总线订阅文件编辑事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bus.subscribe(File.Event.Edited, async (payload) => {
const file = payload.properties.file
const ext = path.extname(file)

for (const item of await getFormatter(ext)) {
// Execute formatter
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: Instance.directory,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
})
// Handle exit codes
}
})

来源:format/index.ts

格式化工具状态 API

你可以通过 API 查询所有已配置格式化工具的状态:

1
2
3
4
5
6
7
const status = await Format.status()
// Returns array of:
// {
// name: string
// extensions: string[]
// enabled: boolean
// }

这有助于了解项目中哪些格式化工具是可用且处于活动状态的。

来源:format/index.ts

最佳实践

项目级配置

在项目级别配置格式化工具以确保团队范围内的一致性。将 opencode.json 放置在项目根目录,并包含格式化工具设置:

1
2
3
4
5
6
7
8
9
10
{
"formatter": {
"prettier": {
"disabled": false
},
"ruff": {
"disabled": false
}
}
}

格式化工具优先级

当多个格式化工具支持同一文件扩展名时,所有已启用的格式化工具都将执行。在配置功能重叠的工具时需考虑到这一点:

  1. Prettier 和 Biome 都支持 JavaScript/TypeScript 文件
  2. 为每种文件类型选择一个主格式化工具以避免冲突
  3. 使用 disabled 字段防止不需要的格式化工具运行

环境变量

格式化工具可以利用环境变量进行配置。系统会自动包含 process.env 并合并特定格式化工具的环境变量:

1
2
3
4
env: {
...process.env,
...item.environment
}

来源:format/index.ts

性能考量

格式化工具异步执行,且忽略 stdout 和 stderr 输出以防止干扰 UI。退出代码会被记录以便调试。该系统设计为非阻塞的,即使格式化工具失败也允许继续操作。

来源:format/index.ts

故障排除

格式化工具未运行

  1. 检查格式化工具二进制文件是否在你的 PATH 中可用
  2. 验证配置文件是否被检测到(例如 .prettierrc、biome.json)
  3. 确保格式化工具未在 opencode.json 中被禁用
  4. 查看日志以查找执行错误

格式化工具冲突

如果多个格式化工具修改同一文件,最后完成执行的将决定最终状态。使用 disabled 配置来控制针对特定文件扩展名运行哪些格式化工具。

缺少依赖项

某些格式化工具需要安装依赖项。例如:

  1. Prettier 需要列在 package.json 依赖项中
  2. Ruff 需要 ruff.toml 或包含在 requirements.txt 中

enabled() 函数会在尝试运行格式化工具之前自动执行这些检查。

来源:format/formatter.ts