- 开发工具
【免费下载链接】language-server-protocol
Defines a common protocol for language servers.
导读
textDocument/willSave是 Language Server Protocol 中"保存前回调"机制的核心通知:客户端在实际写入磁盘之前向语言服务器广播一条clientToServer方向的单向通知,让服务器有机会感知"即将发生一次保存"这一事件。本文基于当前仓库中 3.19 版规范文档,完整讲解该通知的触发时机、客户端与服务端的双向能力声明、WillSaveTextDocumentParams参数结构与TextDocumentSaveReason保存原因枚举,并结合仓库中的 metaModel 结构化定义与配套的willSaveWaitUntil、didSave文档,厘清"保存前通知 → 保存前编辑请求 → 保存后通知"的完整事件链。读完本文,你将能够在自己的语言服务器与客户端中正确声明并处理 willSave 能力,并根据保存原因(手动、延时、失焦)实现差异化的保存前逻辑。
通知语义:在真实保存发生之前触发
根据 willSave.md 的定义,textDocument/willSave是一条notification(通知),由客户端(编辑器/工具)在文档尚未真正保存时发送给服务器。它属于clientToServer方向的消息,方法名为textDocument/willSave。
该通知带有两个重要的隐含约束:
- 文档必须处于打开状态:如果服务器已注册 open / close 事件(即声明了
textDocument/didOpen、textDocument/didClose同步能力),那么客户端在发送willSave之前必须确保目标文档已通过didOpen打开。原因是客户端在未获得文件所有权转移(ownership transferal)的情况下,不能改变文件内容——只有处于打开状态且内容由客户端托管的文档,才适合触发保存前回调。 - 通知不携带返回值:作为 notification,
willSave只能"通知",不能向客户端返回修改结果。如果服务器需要在保存前修改文档内容,应使用配套的请求类型textDocument/willSaveWaitUntil(详见后文"保存事件链"一节)。
从协议方向上看,willSave通知的触发时机严格介于"用户/编辑器发起保存"与"磁盘写入完成"之间,因此它适合执行无副作用或只读性的收尾动作,例如:
- 更新服务器内部的内存状态(索引、符号表、诊断缓存);
- 记录"该文档将于 reason 原因被保存"的日志;
- 触发与保存相关的统计分析。
能力协商:客户端与服务端的双向声明
willSave不是强制的协议特性,而是通过**能力协商(capability negotiation)**在initialize阶段开启的。双方都需要显式声明,且任何一方缺失都会导致该特性不生效。
客户端能力(Client Capability)
客户端在initialize请求的ClientCapabilities中,通过textDocument.synchronization.willSave声明自己支持发送textDocument/willSave通知:
| 属性 | 值 |
|---|---|
| property name(可选) | textDocument.synchronization.willSave |
| property type | boolean |
该能力为boolean类型,置为true表示客户端在每次保存操作前会向服务器广播willSave通知。它与 3.x 协议的其它同步能力一样,在 initialize.md 中定义的TextDocumentSyncClientCapabilities结构(textDocument.synchronization字段)下声明。
服务端能力(Server Capability)
服务器在initialize响应(InitializeResult.capabilities)中,通过textDocumentSync.willSave声明自己感兴趣接收textDocument/willSave通知:
| 属性 | 值 |
|---|---|
| property name(可选) | textDocumentSync.willSave |
| property type | boolean |
置为true表示服务器希望接收该通知。注意,textDocumentSync字段既可以是一个详细结构(TextDocumentSyncOptions),也可以为了向后兼容直接使用TextDocumentSyncKind数字;willSave布尔开关属于前者——只有当服务器以结构化的TextDocumentSyncOptions声明同步方式时,willSave子字段才有意义。若textDocumentSync省略,协议默认取TextDocumentSyncKind.None(见 initialize.md 中ServerCapabilities的定义)。
一个完整的协商结果是"双向都开启":客户端声明textDocument.synchronization.willSave: true,服务器声明textDocumentSync.willSave: true。只有这样,保存前通知才会真正发送。
注册选项:TextDocumentRegistrationOptions
willSave通知的动态注册使用TextDocumentRegistrationOptions作为注册选项。该选项用于约束通知适用的文档选择器(document selector),使服务器可以仅对特定语言/模式的文档开启保存前回调,而不是全量接收。它同样被textDocument/willSaveWaitUntil请求使用(见 willSaveWaitUntil.md)。
参数结构:WillSaveTextDocumentParams
textDocument/willSave通知的参数类型为WillSaveTextDocumentParams,在 willSave.md 中定义如下:
/** * The parameters send in a will save text document notification. */ export interface WillSaveTextDocumentParams { /** * The document that will be saved. */ textDocument: TextDocumentIdentifier; /** * The 'TextDocumentSaveReason'. */ reason: TextDocumentSaveReason; }该接口包含两个字段:
textDocument: TextDocumentIdentifier:即将被保存的文档标识。TextDocumentIdentifier在 textDocumentIdentifier.md 中定义,仅含一个uri: DocumentUri字段——协议层面的 URI 一律以字符串传递:interface TextDocumentIdentifier { /** * The text document's URI. */ uri: DocumentUri; }服务器可据此定位到自身内存中对应的文档快照(前提是已通过
didOpen打开)。reason: TextDocumentSaveReason:本次保存的触发原因,用于让服务器区分保存场景,详下一节。
一个实际的 JSON-RPC 通知负载示例如下:
{ "jsonrpc": "2.0", "method": "textDocument/willSave", "params": { "textDocument": { "uri": "file:///home/user/project/src/main.py" }, "reason": 1 } }保存原因枚举:TextDocumentSaveReason
TextDocumentSaveReason用于描述"为什么文档会被保存",帮助服务器按场景差异化处理。它在 willSave.md 中定义为一个常量命名空间与联合类型的组合:
/** * Represents reasons why a text document is saved. */ export namespace TextDocumentSaveReason { /** * Manually triggered, e.g. by the user pressing save, by starting * debugging, or by an API call. */ export const Manual = 1; /** * Automatic after a delay. */ export const AfterDelay = 2; /** * When the editor lost focus. */ export const FocusOut = 3; } export type TextDocumentSaveReason = 1 | 2 | 3;三个取值及其典型场景如下:
| 取值 | 常量名 | 触发场景 |
|---|---|---|
1 | Manual | 用户按下保存快捷键、启动调试、或代码显式调用保存 API |
2 | AfterDelay | 编辑器按自动保存策略在延迟后触发保存 |
3 | FocusOut | 编辑器失去焦点时触发的保存 |
TextDocumentSaveReason的类型为字面量联合1 | 2 | 3,与namespace中的常量一一对应。服务器在处理willSave时可按reason分流:例如对Manual可执行完整的收尾逻辑,对AfterDelay/FocusOut这类高频自动保存则采用更轻量的处理,避免不必要的工作。
保存事件链:willSave → willSaveWaitUntil → didSave
willSave只是保存三阶段中的第一环。当前仓库的 3.19 目录下,三个保存相关文档共同描述了一条完整的事件链:
- willSave.md(通知):保存前广播,服务器只能感知、不能修改。
- willSaveWaitUntil.md(请求):同样是保存前发送,但这是一个request(请求),服务器可以在响应中返回一个
TextEdit[]数组(或null),这些编辑会被应用到文档上再执行保存。其方法名为textDocument/willSaveWaitUntil,参数同样复用WillSaveTextDocumentParams,客户端能力为textDocument.synchronization.willSaveWaitUntil,服务端能力为textDocumentSync.willSaveWaitUntil,注册选项同为TextDocumentRegistrationOptions。 - didSave.md(通知):保存完成之后客户端再发送
textDocument/didSave通知,参数为DidSaveTextDocumentParams,其中text?: string字段是否携带完整内容取决于服务端声明textDocumentSync.save时是否将SaveOptions.includeText置为true。
对willSaveWaitUntil的响应,TextEdit结构在 textEdit.md 中定义:
interface TextEdit { /** * The range of the text document to be manipulated. To insert * text into a document, create a range where start === end. */ range: Range; /** * The string to be inserted. For delete operations, use an * empty string. */ newText: string; }需要特别注意的是 willSaveWaitUntil.md 中给出的客户端实现约束:如果计算编辑耗时过长,或服务器在该请求上持续失败,客户端可能会丢弃其结果——这是为了保持保存操作"快速且可靠"。因此,服务器不应把关键、不可逆的逻辑寄托于willSaveWaitUntil,而应将它视为"尽力而为"的保存前修正机会。这一约束同样隐含地提醒我们:willSave通知(以及willSaveWaitUntil请求)都必须保持轻量、快速,避免阻塞用户保存操作。
元模型佐证:metaModel 中的结构化定义
仓库 metaModel.json(3.19 版元模型)以机器可读的 JSON 形式对textDocument/willSave通知做了与规范文档一致的结构化描述,可作为实现语言绑定(如 LSP 客户端/服务器 SDK)时的权威依据。其核心记录(约位于第 2333-2347 行)如下:
{ "method": "textDocument/willSave", "typeName": "WillSaveTextDocumentNotification", "messageDirection": "clientToServer", "clientCapability": "textDocument.synchronization.willSave", "serverCapability": "textDocumentSync.willSave", "params": { "kind": "reference", "name": "WillSaveTextDocumentParams" }, "registrationOptions": { "kind": "reference", "name": "TextDocumentRegistrationOptions" }, "documentation": "A document will save notification is sent from the client to the server before\nthe document is actually saved." }从该记录可以确认几项实现事实:
- 方向:
messageDirection为clientToServer,与文档中"客户端发送、服务器接收"的语义一致; - 能力键:
clientCapability与serverCapability分别对应textDocument.synchronization.willSave与textDocumentSync.willSave,与规范文档中的属性名完全一致; - 参数与注册选项:分别引用
WillSaveTextDocumentParams与TextDocumentRegistrationOptions,与规范文档一一对应; - 同一文件中还定义了配套的
textDocument/willSaveWaitUntil记录(clientCapability: textDocument.synchronization.willSaveWaitUntil、serverCapability: textDocumentSync.willSaveWaitUntil),以及TextDocumentSyncClientCapabilities结构中willSave、willSaveWaitUntil两个能力字段的元模型声明。
也就是说,规范文档(人类可读)与 metaModel(机器可读)互为印证,任何实现语言服务器 SDK 的工具链(如代码生成器)都可以直接消费 metaModel.json 生成WillSaveTextDocumentParams、TextDocumentSaveReason等类型与消息分发的样板代码。
服务器实现要点小结
综合规范文档与元模型,实现textDocument/willSave支持时应注意:
- 协商先行:在
initialize响应中设置capabilities.textDocumentSync.willSave = true,并确认客户端在textDocument.synchronization.willSave中声明了支持;若采用动态注册,则注册选项为TextDocumentRegistrationOptions(可带文档选择器限定范围)。 - 保证文档已打开:只在客户端已通过
didOpen打开文档后才可能收到本通知;服务器应始终维护"打开文档 → 内容快照"的映射,以便按textDocument.uri定位。 - 按 reason 分流:使用
TextDocumentSaveReason(Manual/AfterDelay/FocusOut)决定处理深度,对自动保存场景保持轻量。 - 保持轻量快速:
willSave是单向通知、没有响应;若需要在保存前修改内容,应改用willSaveWaitUntil请求并返回TextEdit[],同时接受客户端可能丢弃结果的行为。 - 与保存后通知配合:若还需感知"保存完成",应同时声明
textDocumentSync.save(可携带SaveOptions.includeText)并处理textDocument/didSave,形成完整的保存生命周期闭环。
以上能力键、参数结构与枚举值均以本仓库 3.19 版规范 willSave.md 为准;该文档同时存在于 3.17、3.18 目录中,定义保持一致,适用于对应协议版本的语言服务器实现。
- 开发工具
【免费下载链接】language-server-protocol
Defines a common protocol for language servers.
相关推荐
Language Server Protocol 3.17 保存前通知 `textDocument/willSave` 全解析:协议定义、参数结构与能力协商
Language Server Protocol 3.17 保存前通知 textDocument/willSave 全解析:协议定义、参数结构与能力协商 导读
开发工具LSP 3.19 Hover 请求(textDocument/hover)协议详解:从能力协商到响应构造
LSP 3.19 Hover 请求(textDocument/hover)协议详解:从能力协商到响应构造 hover(悬停)是 LSP 中最常用、用户感知最直观
开发工具LSP 文本格式化请求(textDocument/formatting)协议详解:从能力协商到 FormattingOptions
LSP 文本格式化请求(textDocument/formatting)协议详解:从能力协商到 FormattingOptions 本篇指南聚焦语言服务器协议(
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考