news 2026/11/5 7:36:05

LSP 3.19 `textDocument/willSave` 保存前通知详解:协议语义、能力协商与参数结构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LSP 3.19 `textDocument/willSave` 保存前通知详解:协议语义、能力协商与参数结构
  • 开发工具

【免费下载链接】language-server-protocol

Defines a common protocol for language servers.

项目地址:https://gitcode.com/gh_mirrors/la/language-server-protocol
点击查看免费下载

导读

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 typeboolean

该能力为boolean类型,置为true表示客户端在每次保存操作前会向服务器广播willSave通知。它与 3.x 协议的其它同步能力一样,在 initialize.md 中定义的TextDocumentSyncClientCapabilities结构(textDocument.synchronization字段)下声明。

服务端能力(Server Capability)

服务器在initialize响应(InitializeResult.capabilities)中,通过textDocumentSync.willSave声明自己感兴趣接收textDocument/willSave通知:

属性值
property name(可选)textDocumentSync.willSave
property typeboolean

置为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;

三个取值及其典型场景如下:

取值常量名触发场景
1Manual用户按下保存快捷键、启动调试、或代码显式调用保存 API
2AfterDelay编辑器按自动保存策略在延迟后触发保存
3FocusOut编辑器失去焦点时触发的保存

TextDocumentSaveReason的类型为字面量联合1 | 2 | 3,与namespace中的常量一一对应。服务器在处理willSave时可按reason分流:例如对Manual可执行完整的收尾逻辑,对AfterDelay/FocusOut这类高频自动保存则采用更轻量的处理,避免不必要的工作。

保存事件链:willSave → willSaveWaitUntil → didSave

willSave只是保存三阶段中的第一环。当前仓库的 3.19 目录下,三个保存相关文档共同描述了一条完整的事件链:

  1. willSave.md(通知):保存前广播,服务器只能感知、不能修改。
  2. willSaveWaitUntil.md(请求):同样是保存前发送,但这是一个request(请求),服务器可以在响应中返回一个TextEdit[]数组(或null),这些编辑会被应用到文档上再执行保存。其方法名为textDocument/willSaveWaitUntil,参数同样复用WillSaveTextDocumentParams,客户端能力为textDocument.synchronization.willSaveWaitUntil,服务端能力为textDocumentSync.willSaveWaitUntil,注册选项同为TextDocumentRegistrationOptions。
  3. 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支持时应注意:

  1. 协商先行:在initialize响应中设置capabilities.textDocumentSync.willSave = true,并确认客户端在textDocument.synchronization.willSave中声明了支持;若采用动态注册,则注册选项为TextDocumentRegistrationOptions(可带文档选择器限定范围)。
  2. 保证文档已打开:只在客户端已通过didOpen打开文档后才可能收到本通知;服务器应始终维护"打开文档 → 内容快照"的映射,以便按textDocument.uri定位。
  3. 按 reason 分流:使用TextDocumentSaveReason(Manual/AfterDelay/FocusOut)决定处理深度,对自动保存场景保持轻量。
  4. 保持轻量快速:willSave是单向通知、没有响应;若需要在保存前修改内容,应改用willSaveWaitUntil请求并返回TextEdit[],同时接受客户端可能丢弃结果的行为。
  5. 与保存后通知配合:若还需感知"保存完成",应同时声明textDocumentSync.save(可携带SaveOptions.includeText)并处理textDocument/didSave,形成完整的保存生命周期闭环。

以上能力键、参数结构与枚举值均以本仓库 3.19 版规范 willSave.md 为准;该文档同时存在于 3.17、3.18 目录中,定义保持一致,适用于对应协议版本的语言服务器实现。

  • 开发工具

【免费下载链接】language-server-protocol

Defines a common protocol for language servers.

项目地址:https://gitcode.com/gh_mirrors/la/language-server-protocol
点击查看免费下载
上一篇:DDrawCompat终极指南:如何让经典DirectX游戏在现代Windows上完美运行
下一篇:让电脑静如处子:Windows最强风扇控制神器Fan Control全面指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 7:36:05

比较器仿真:从理想模型到真实环境的可靠性建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/25 13:04:26

Sunshine 入门实战:10分钟把PC游戏投到大屏

Sunshine 入门实战:10分钟把PC游戏投到大屏 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 电视大屏摆在那,游戏却全装在 PC 上。Sunshine 就是解决这件事的…

作者头像 李华