LSP 只读导航
模型读代码最常用的四种语义问题——定义、引用、实现、悬停——收敛成一条只读 seam,
语言服务器的启动与协议细节全部留在 provider 里。对应实现:src/Tether.Lsp/(seam)、
src/Tether.Lsp.Stdio/(stdio provider)与 src/Tether.Lsp.Tools/(工具 consumer)。
seam
public interface ILspService
{
IDisposable RegisterServer(LspServerSpec spec);
ValueTask<LspResult> QueryAsync(LspOperation operation, LspPosition position, CancellationToken ct = default);
}
LspOperation 只有四个取值:Definition、References、Implementation、Hover。
没有写操作——seam 的定位是导航而不是编辑,重命名、code action 之类有意不在内。
LspServerSpec 把服务器绑定到一组扩展名(不是语言 id):注册时按 branded id 与扩展名原子登记,
重复 id 或重复扩展名整体失败,不会留下半个注册。查询按文件扩展名路由到认领它的服务器。
线协议纪律
- UTF-16 零基坐标。
LspPosition的 line / character 按 LSP 规范走 UTF-16 code unit, provider 负责与 .NET 字符串(同为 UTF-16,但索引语义要对齐)之间的换算。 - 瞬态文档生命周期。 每次查询临时
textDocument/didOpen,查询结束后didClose—— 不把整个 workspace 的打开状态留在服务器侧,模型改过的文件下次查询总是以盘上内容为准。 - 取消与重试。 查询是可取消的;协议层的瞬态失败有界重试,终态失败以稳定码上抛。
stdio provider 与 framing
StdioLspProvider 用手写的 Content-Length JSON-RPC framing 与服务器通信。
LSP 的 framing 足够简单(Content-Length: n\r\n\r\n + JSON 体),手写实现换来零额外依赖与完全可控的缓冲上限,
行为回归由测试守住(见 issue #26 的 intentional deviation 说明)。
LspTransport 负责把响应按 id 配回请求、处理服务器主动发来的通知,并在进程退出时保证不悬挂。
稳定失败码
| 码 | 含义 |
|---|---|
LSP_NO_SERVER | 没有服务器认领这个扩展名 |
LSP_DUPLICATE | 注册时 id 或扩展名重复 |
LSP_OUT_OF_RANGE | 位置超出文档范围 |
LSP_UNSUPPORTED | 服务器声明不支持该操作 |
LSP_PROTOCOL | 协议级错误(含服务器返回的 error response) |
LSP_START | 服务器进程启动失败 |
工具暴露
LspToolsPlugin 把四种操作暴露为模型工具。模型给出文件路径与行列,拿回一组 LspLocation 或悬停文本;
大型结果集交给 溢写 机制落到盘上的 spill store,模型看到的是定位符而非超长正文。