Turborepo LSP 语言服务器深度解析:turbo.json 的补全、诊断与引用跳转实现
【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo
本文围绕 Turbo 仓库中 crates/turborepo-lsp/readme.md 所定义的语言服务器(Language Server Protocol, LSP)模块展开,结合 src/lib.rs、src/main.rs 与 packages/turbo-vsc/src/extension.ts 的源码实现,讲解 Turbo 如何为turbo.json提供补全、诊断、引用查找、Code Lens 等 IDE 能力。读完本文,你将理解 turborepo-lsp 的架构分层、与 daemon 的协作方式、每类 LSP 特性的实现路径,以及 VS Code 扩展端如何启动和消费这个服务器。
一、turborepo-lsp 的定位与用途
根据 crates/turborepo-lsp/readme.md,turborepo-lsp是 Turborepo 的Language Server Protocol 实现,目标是为turbo.json文件提供 IDE 特性,包括:
- 补全(Completion):任务名(task names)、包名(package names);
- 悬停信息(Hover information);
- 诊断(Diagnostics):对配置文件做校验并报告错误;
- 跳转到定义 / 引用查找(Go to definition / references)。
它专门为turbo-vsc(VS Code 扩展)设计,通过stdio与编辑器通信,并在 daemon 可用时借助 daemon 实现高效的包发现(package discovery)。
二、整体架构:基于 tower-lsp 的服务器
readme 中给出了模块的架构图:
turborepo-lsp └── tower-lsp server ├── Completions (task names, package names) ├── Hover information ├── Diagnostics (validation errors) └── Go to definition从源码看,这一架构落在 src/lib.rs 中:Backend结构体实现tower_lsp::LanguageServertrait,由run_lsp_server()创建多线程 Tokio runtime,然后通过Server::new(stdin, stdout, socket)建立基于 stdio 的 LSP 服务(lib.rs#L322-L343)。
2.1 两个核心集成对象
readme 明确列出模块的两大集成依赖:
- Daemon(后台守护进程):用于高效的包发现(package discovery)。
Backend中持有tokio::sync::watch通道(initializer/daemon),初始化成功后把DaemonClient<DaemonConnector>发送给后台任务;需要包信息时,package_discovery()会等待该通道就绪,然后调用discover_repository_blocking()获取RepositoryDiscoverySnapshot(lib.rs#L899-L932)。 - Repository analysis(仓库分析):用于构建包图(package graph)。
turborepo_repository提供的RepoState::infer、PackageGraph等负责确认仓库根目录、包身份与脚本任务,LSP 在此基础上组织LspPackages。
在initialize阶段,服务器会对编辑器传来的root_uri做多步处理(lib.rs#L347-L399):
- 校验 URI 是本地
file协议、路径为绝对路径; - 调用
RepoState::infer从子目录向上推断真正的 monorepo 根(多根 VS Code 工作区可能只传入子目录,若直接用子目录启动 daemon,会把 cookie 文件写到错误位置); - 构造
DaemonPaths::from_repo_root,得到 socket 文件、pid 文件等路径; - 用
tokio_retry以 100ms 固定间隔重试 5 次连接 daemon(DaemonConnector::new(can_start_server, can_kill_server, &repo_root, None)); - 连接成功后,通过
pidlock获取一个独占锁(用于“turbo optimize”等独占特性;若被其他 VS Code 窗口持有,则降级为不带独占功能继续运行,lib.rs#L450-L486)。
值得一提的错误处理:如果 daemon 握手时返回VersionMismatch(版本不匹配),服务器会向用户弹窗提示 “Pre-2.0 versions of turborepo are not compatible with 2.0 or later of the extension”,并返回空的InitializeResult表示不支持任何特性(lib.rs#L402-L425)。
2.2 声明的能力集合
initialize返回的ServerCapabilities(lib.rs#L489-L539)包括:
| 能力 | 配置 |
|---|---|
text_document_sync | INCREMENTAL(增量同步) |
completion_provider | resolve_provider: false,触发字符"." |
code_lens_provider | 启用 |
code_action_provider | 仅QUICKFIX |
references_provider | 启用 |
workspace | 支持 workspace folders 与变更通知 |
注意:readme 中规划的Hover能力在当前 lib.rs 的LanguageServer实现中并未看到显式覆盖(tower-lsp对该类方法提供默认空实现),当前实现重点落在 completion、references、code lens、code action 与 diagnostics 上。这一点从Backend实现的 trait 方法集合可以确认。
三、四大核心能力的源码实现
3.1 补全(Completion):任务名与包名
补全逻辑位于 lib.rs#L855-L879,注释清晰描述了算法:
- 获取所有包(package discovery);
- 读取所有
package.json; - 汇总所有去重后的脚本名(task names);
- flatMap 生成所有
package#script组合; - 将两类标签(限定形式 + 裸任务名)串联返回。
生成标签的核心在LspPackages::completion_labels()(lib.rs#L204-L219):
- 限定形式:
{package}#{task},例如@repo/ui#test、//#lint(//表示根包); - 裸任务名:所有包脚本名去重后的结果,例如
lint、build。
每个补全项的类型为CompletionItemKind::FIELD。任务索引task_index()用OnceLock缓存、按包快照只构建一次,并通过HashSet<(script, identity)>去重,保证“P 个包共享 S 个脚本”时身份判定是 O(1) 的(lib.rs#L182-L202)。
3.2 诊断(Diagnostics):完整的配置校验规则
每次文件打开或编辑时,handle_file_update(lib.rs#L935-L1129)会先用jsonc_parser解析文件内容(支持 JSONC 注释),再执行一系列校验,最后通过publish_diagnostics推送给编辑器。可识别的诊断规则如下:
a.dependsOn中的^前缀提示(HINT)
对"^build"这类写法,提示:The '^' means "run thebuildtask in the package's dependencies before this one"(lib.rs#L1042-L1055)。
b. 任务自依赖(ERROR,turbo:self-dependency)
若任务的dependsOn中出现了不带^的自身名字,报错A task cannot depend on itself.(lib.rs#L1058-L1070)。
c. 弃用的$环境变量语法(ERROR,deprecated:env-var)
对"$FOO"这类旧式写法,报错The $ syntax is deprecated. Please apply the codemod.,并携带代码deprecated:env-var供 Code Action 消费(lib.rs#L1075-L1093)。注意$TURBO_EXTENDS$哨兵值会被is_turbo_extends_sentinel识别并跳过(lib.rs#L1167-L1169)。
d. 包与任务存在性校验(ERROR)
report_invalid_packages_and_tasks(lib.rs#L1198-L1274)按package#task拆解后匹配:
| 场景 | 诊断代码 | 示例消息 |
|---|---|---|
| 指定了包但包不存在 | turbo:no-such-package | The packagefoodoes not exist in [...] |
| 包存在但该包无此任务 | turbo:no-such-task-in-package | The taskbuilddoes not exist in the packageapp. |
| 任务在任何地方都不存在 | turbo:no-such-task | The taskbuilddoes not exist. |
e. 中转节点豁免(transit node)
collect_transit_node_tasks会扫描所有dependsOn中含"^{task}"的任务(即只作为依赖关系存在、并不一定有对应脚本的“拓扑”任务),这类任务不报“任务不存在”(lib.rs#L1171-L1188)。
f. Glob 语法校验(ERROR)
对globalDependencies、每个任务的inputs/outputs中的字符串,用wax::Glob解析,失败时报Invalid glob: ...(lib.rs#L1110-L1123)。这对应 VS Code 扩展 README 中的“配置帮助”特性(packages/turbo-vsc/README.md)。
3.3 引用查找(References):从 pipeline 反查 package.json
references处理器(lib.rs#L542-L630)的工作流:
- 从内存中的
crop::Rope取回当前文件文本,解析出tasks对象; - 遍历每个任务 key 的 AST 范围,判断光标是否落在某个任务名上;
- 若命中,则调用
LspPackages::references(task)(lib.rs#L221-L263):支持package#task限定(通过rsplit_once('#')拆解),遍历所有包源码,找出所有出现"{task}"的位置,利用crop::Rope把字节偏移换算成 LSP 的{line, character}坐标,返回Location列表。
配合//#task形式,根包的脚本也能被精确限定;测试completion_references_and_file_update_index_include_root_and_packages验证了//#lint只返回 1 处引用(lib.rs#L1709-L1735)。
3.4 Code Lens 与 Code Action:一键运行与一键修复
- Code Lens(lib.rs#L633-L696):对每个任务 key 生成
Run {task}的 CodeLens,命令为turbo.run,参数为任务名。扩展端收到后会在集成终端中执行turbo run <task>(packages/turbo-vsc/src/extension.ts#L201-L220)。 - Code Action(lib.rs#L700-L730):对
deprecated:env-var诊断提供 Quick FixApply codemod,执行turbo.codemod命令并携带migrate-env-var-dependencies参数;扩展端将其转译为npx --yes @turbo/codemod migrate-env-var-dependencies(extension.ts#L222-L232)。
四、turbo-vsc 扩展如何消费该服务器
readme 指出该模块“专为turbo-vsc扩展设计”。扩展端的关键配置(packages/turbo-vsc/src/extension.ts):
- 文档选择器:
**/turbo.json、**/turbo.jsonc、**/package.json(extension.ts#L294-L301); - 服务器二进制:优先使用随扩展打包的
out/turborepo-lsp-{platform}-{arch};若检测到已安装的 turbo 二进制,则以其为服务器并追加__internal_lsp参数(extension.ts#L72-L93、extension.ts#L273-L291); - 安全边界:工作区不受信任(untrusted)时扩展直接禁用(extension.ts#L37-L43);
- 配套命令:
turbo.daemon.start、turbo.daemon.stop、turbo.daemon.status、turbo.run、turbo.codemod、turbo.install,以及状态栏上的 daemon 开关(extension.ts#L124-L253)。
五、main.rs:daemon 生命周期 CLI
turborepo-lsp/src/main.rs 是二进制入口:main()先检查命令行参数中是否包含daemon,包含则走 daemon 子命令分发,否则启动 LSP 服务器(main.rs#L16-L22)。
daemon 子命令及其行为:
| 子命令 | 行为 | 输出 |
|---|---|---|
start | 启动 daemon | ✓ daemon is running |
stop | 停止 daemon | ✓ stopped daemon |
restart | 重启 daemon | ✓ restarted daemon |
status | 查询状态(--json输出结构化信息) | ✓ daemon is running+ log/pid/socket 文件路径与 uptime |
clean | 清理(默认同时清理日志) | Done |
logs | 跟踪 daemon 日志 | 跟随日志输出 |
关键参数(main.rs#L161-L212):
--idle-time:空闲超时,默认4h0m0s;--cwd:指定工作目录(默认取当前目录);--root-turbo-json/--turbo-json-path:指定 turbo.json 路径;--dangerously-disable-package-manager-check:跳过包管理器检查;--verbosity/-v:日志级别(0/1 为 INFO,2 为 DEBUG,更高为 TRACE)。
daemon 未运行时执行status会提示:daemon is not running, runturbo daemon startto start it。这些行为都由 crates/turborepo-lsp/tests/daemon_lifecycle.rs 集成测试逐条断言,例如校验status --json输出中包含uptime_ms、pid_file、sock_file且log_file真实存在。
六、缓存、失效与包快照
为了让编辑过程中的补全与诊断保持低延迟,实现采用了两级缓存:
LspPackageCache(lib.rs#L266-L281):Arc<LspPackages>的原子缓存,did_save、did_change_workspace_folders、did_change_configuration、did_change_watched_files都会使其失效,而普通的did_change(打字过程中的增量编辑)不会触发重建,直到保存;task_index(OnceLock):在包快照内部只构建一次,之后每个文档变更都复用。
LspPackages内部还定义了稳定的排序键:根包(//)优先、具名包其次、未命名/聚合包最后(package_sort_key,lib.rs#L302-L308),保证补全与引用结果不受源码插入顺序影响(有对应测试source_insertion_order_does_not_affect_lsp_results验证,lib.rs#L1673-L1707)。
从仓库发现(daemon 路径)构建包快照时,from_repository_discovery直接消费RepositoryDiscoverySnapshot的 scope 列表——测试还验证了它可以同时索引 JavaScript(package.json)与 Rust(Cargo.toml)两种 toolchain 的任务(lib.rs#L1737-L1777),说明该 LSP 并不局限于纯 JS 仓库。
七、依赖与工程细节
Cargo.toml 揭示了技术选型:
tower-lsp 0.20:LSP 服务器框架;jsonc-parser 0.23:JSONC/JSON AST 解析(容忍注释与尾逗号,适配turbo.json与turbo.jsonc);crop 0.4:rope 文本结构,用于增量编辑同步与字节↔行列坐标换算;wax:glob 校验;turborepo-daemon/turborepo-repository:包发现与包图;clap(derive):daemon CLI 解析;pidlock:LSP 独占锁(路径来自crates/turborepo-pidlock)。
值得注意的工程细节:默认 feature 为rustls-tls,但注释明确说明语言服务器本身不进行任何 TLS I/O(它只通过本地 socket 与 daemon 通信),native-tls/rustls-tls两个 feature 仅是保留的“惰性别名”,用于让工作区与turborepocrate 的 feature 引用继续解析。
总结
turborepo-lsp是 Turbo 2.x 体系中连接“编辑器”与“构建核心”的关键桥梁:它用 Rust + tower-lsp 实现了 LSP 服务端,通过 stdio 与 VS Code 扩展通信,借助 daemon 的仓库发现能力与turborepo-repository的包图分析,为turbo.json提供任务/包名补全、覆盖十余类校验规则的实时诊断、从 pipeline 反查package.json的引用跳转,以及“一键运行任务”“一键应用 codemod”的 Code Lens / Code Action。readme 中规划的“悬停信息”等能力,可从 lib.rs 的扩展点继续演进。对于想深入理解 Turbo 开发者体验层的读者,建议沿着 crates/turborepo-lsp/src/lib.rs → crates/turborepo-lsp/src/main.rs → packages/turbo-vsc/src/extension.ts 这条链路逐层阅读。
【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考