news 2026/10/30 5:37:56

理解 LSP 3.19 的 textDocument/didOpen:文档打开通知协议详解与客户端/服务端实现要点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
理解 LSP 3.19 的 textDocument/didOpen:文档打开通知协议详解与客户端/服务端实现要点
  • 开发工具

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

Defines a common protocol for language servers.

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

导读

textDocument/didOpen是 Language Server Protocol(LSP)中客户端向语言服务器宣告"新文本文档已被打开"的核心通知,它把文档内容的所有权从磁盘移交到客户端内存,并触发服务器初始化该文档的同步状态。本文以 LSP 3.19 规范(specification.md)中的 didOpen.md 为骨架,结合 didChange.md、didClose.md 等配套文档,讲透 didOpen 的协议语义、DidOpenTextDocumentParams与TextDocumentItem的字段细节、语言标识符规范、同步类型(Full/Incremental)以及客户端与服务端的实现约束。读完本文,你将能正确实现或调试 didOpen 的收发逻辑,并理解它与 didChange/didClose 之间的配对与版本管理规则。

1. didOpen 的协议语义:什么是"打开"一份文档

1.1 核心定义

LSP 规范对textDocument/didOpen的定义如下(原文见 didOpen.md):

文档打开通知由客户端发送给服务器,用于通知新打开的文本文档。文档内容从此由客户端管理,服务器不得再尝试通过该文档的 URI 去读取其内容。

这里有两个极易被误解的语义要点:

  • "打开"不等于"在编辑器中显示":协议层面,"打开"仅意味着文档内容由客户端托管(例如已加载进编辑器缓冲区);即使文档并未在编辑器视图中呈现,只要客户端声明对其内容负责,就应发送 didOpen。
  • 内容来源以客户端为准:服务器在收到 didOpen 后,必须使用通知参数里携带的text字段内容,而不是去磁盘(或远端)按 URI 重新读取。这一点在规范中被称为 "The document's content is now managed by the client and the server must not try to read the document's content using the document's Uri"。

1.2 与关闭通知的配对约束

规范同时强调了两条硬性规则:

  1. 开/关必须配对:在没有先发送对应的textDocument/didClose之前,不得对同一文档重复发送 didOpen("An open notification must not be sent more than once without a corresponding close notification send before")。
  2. 最大打开数为一:任意特定textDocument在同一时刻的打开计数最多为 1("open and close notification must be balanced and the max open count for a particular textDocument is one")。

换言之,didOpen → (didChange)* → didClose构成一个严格的生命周期闭环,客户端必须维护这一平衡关系,否则会造成服务器端状态混乱(重复打开可能触发状态重置或内存泄漏)。

1.3 服务器能力与文档开闭无关

规范特别注明:服务器的能力(如诊断、补全、跳转定义)与文档处于打开还是关闭状态无关("a server's ability to fulfill requests is independent of whether a text document is open or closed")。也就是说,服务器不应因文档关闭而"失忆";不过实际语言服务通常会基于内部缓存策略决定是否保留关闭文档的模型,这属于实现细节。

2. 消息结构:DidOpenTextDocumentParams 与 TextDocumentItem

2.1 通知定义

textDocument/didOpen是客户端 → 服务器方向的通知(Notification),没有响应:

  • method:textDocument/didOpen
  • params:DidOpenTextDocumentParams

其类型定义如下(源自 didOpen.md):

interface DidOpenTextDocumentParams { /** * The document that was opened. */ textDocument: TextDocumentItem; }

整个参数仅含一个textDocument字段,类型为TextDocumentItem。它是一个完整的文档快照,与didChange携带的增量补丁、didClose携带的最小标识(仅 URI)形成鲜明对比。

2.2 TextDocumentItem 的四要素

TextDocumentItem的完整定义见 textDocumentItem.md:

interface TextDocumentItem { /** * The text document's URI. */ uri: DocumentUri; /** * The text document's language identifier. */ languageId: string; /** * The version number of this document (it will increase after each * change, including undo/redo). */ version: integer; /** * The content of the opened text document. */ text: string; }
字段类型必填说明
uriDocumentUri是文档唯一标识(协议层以字符串形式传递 URI,参见 textDocumentIdentifier.md)
languageIdstring是与文档关联的语言标识符(见第 3 节)
versioninteger是文档版本号,每次变更(含撤销/重做)后递增
textstring是打开时文档的完整内容
  • uri:即DocumentUri,在协议层就是字符串 URI。此后服务器将用该 URI 作为该文档在所有请求与通知(如textDocument/completion、textDocument/didChange)中的统一主键。规范中 URI 的类型定义可参见 types/uri.md 对应的文档说明(仓库中 3.19 的通用 URI 定义见 _includes/types/uri.md)。
  • version:整数版本号。它并非只对 didOpen 有意义——它是整个同步机制的基石:服务器依赖版本号判断didChange到达顺序是否正确、内容是否过期。规范明确"it will increase after each change, including undo/redo",即撤销/重做也属于一次变更,版本必须递增。
  • text:打开瞬间的完整内容。服务器应将其作为文档同步状态的初始值,后续内容更新一律来自didChange。

3. languageId:文档的语言标识符约定

languageId用于在服务器同时处理多种语言时避免重新解析文件扩展名("to identify a document on the server side when it handles more than one language to avoid re-interpreting the file extension")。

规范在 textDocumentItem.md 中给出了推荐标识符表,客户端应优先使用这些 ID:

语言标识符语言标识符
ABAPabapLualua
Windows BatbatMakefilemakefile
BibTeXbibtexMarkdownmarkdown
ClojureclojureObjective-Cobjective-c
CoffeeScriptcoffeescriptObjective-C++objective-cpp
CcPascalpascal(@since 3.18.0)
C++cppPerlperl
C#csharpPerl 6perl6
CSScssPHPphp
Dd(@since 3.18.0)Plaintextplaintext
Delphipascal(@since 3.18.0)PowerShellpowershell
DiffdiffPugjade
DartdartPythonpython
DockerfiledockerfileRr
ElixirelixirRazor (cshtml)razor
ErlangerlangRubyruby
F#fsharpRustrust
Gitgit-commit、git-rebaseSCSSscss(花括号语法)、sass(缩进语法)
GogoScalascala
GroovygroovyShaderLabshaderlab
HandlebarshandlebarsShell Script (Bash)shellscript
HaskellhaskellSQLsql
HTMLhtmlSwiftswift
IniiniTypeScripttypescript
JavajavaTypeScript Reacttypescriptreact
JavaScriptjavascriptTeXtex
JavaScript ReactjavascriptreactText (plain)plaintext
JSONjsonVisual Basicvb
LaTeXlatexXMLxml
LesslessXSLxsl
YAMLyaml

使用要点:

  • 当文档的语言发生变化时(例如从 TypeScript 切换为 JavaScript),客户端需要按规范先发送textDocument/didClose,再以新的languageId发送textDocument/didOpen(前提是服务器同样支持新语言 ID)——见 didOpen.md 的说明。
  • pascal同时映射 Delphi 与 Pascal(@since 3.18.0),D语言标识符也是 3.18.0 起新增。

4. 完整的 JSON-RPC 示例

结合上述定义,一次典型的 didOpen 通知在 wire 上形如(基于 LSP 3.19 规范与 JSON-RPC 2.0 消息格式):

{ "jsonrpc": "2.0", "method": "textDocument/didOpen", "params": { "textDocument": { "uri": "file:///home/user/src/main.py", "languageId": "python", "version": 1, "text": "import sys\n\nprint(sys.version)\n" } } }

注意:

  • 通知(Notification)不携带id字段,服务器无需也不应回复(参考 specification.md 中关于请求/通知对象的基础协议描述)。
  • version从 1 开始是常见实践(首次打开即为 v1),但协议本身未强制起始值,仅要求每次内容变更后递增。
  • 消息通过 LSP 基础协议传输:头部携带Content-Length(字节数)与可选的Content-Type,随后是\r\n\r\n分隔的 UTF-8 编码内容体。

5. 与同步机制的关系:openClose、TextDocumentSyncKind 与能力声明

5.1 文档同步的三个通知必须成组

规范在 specification.md 的 "Text Document Synchronization" 一节明确:

Client support fortextDocument/didOpen,textDocument/didChangeandtextDocument/didClosenotifications is mandatory in the protocol and clients can not opt out supporting them.

即协议强制客户端支持 didOpen、didChange、didClose 三者,不可选择性退出;且服务器必须要么全部实现这三个通知,要么一个都不实现。只有在客户端展示的文档都是只读的情况下,才允许关闭文档同步——否则服务器可能收到针对"内容由客户端托管"的文档请求,却拿不到内容。

5.2 客户端与服务端能力声明

  • 客户端能力(specification.md):
export interface TextDocumentSyncClientCapabilities { /** * Whether text document synchronization supports dynamic registration. */ dynamicRegistration?: boolean; /** * The client supports sending will save notifications. */ willSave?: boolean; /** * The client supports sending a will save request and * waits for a response providing text edits which will * be applied to the document before it is saved. */ willSaveWaitUntil?: boolean; /** * The client supports did save notifications. */ didSave?: boolean; }
  • 服务器能力:textDocumentSync属性,其值可为TextDocumentSyncKind或TextDocumentSyncOptions。其中TextDocumentSyncOptions的完整定义如下(specification.md):
export interface TextDocumentSyncOptions { /** * Open and close notifications are sent to the server. If omitted open * close notification should not be sent. */ openClose?: boolean; /** * Change notifications are sent to the server. See * TextDocumentSyncKind.None, TextDocumentSyncKind.Full and * TextDocumentSyncKind.Incremental. If omitted it defaults to * TextDocumentSyncKind.None. */ change?: TextDocumentSyncKind; /** * If present will save notifications are sent to the server. If omitted * the notification should not be sent. */ willSave?: boolean; /** * If present will save wait until requests are sent to the server. If * omitted the request should not be sent. */ willSaveWaitUntil?: boolean; /** * If present save notifications are sent to the server. If omitted the * notification should not be sent. */ save?: boolean | SaveOptions; }

didOpen 直接受openClose开关控制:若openClose为true(或服务器以TextDocumentSyncKind简写形式声明同步),客户端才会发送 didOpen/didClose;若省略该字段,客户端不应发送开/关通知。注意这里的默认语义与change不同——change省略时默认为TextDocumentSyncKind.None(0),而openClose省略时意味着不发送开/关通知。

5.3 TextDocumentSyncKind 三种取值

/** * Defines how the host (editor) should sync document changes to the language * server. */ export namespace TextDocumentSyncKind { /** * Documents should not be synced at all. */ export const None = 0; /** * Documents are synced by always sending the full content * of the document. */ export const Full = 1; /** * Documents are synced by sending the full content on open. * After that only incremental updates to the document are * sent. */ export const Incremental = 2; } export type TextDocumentSyncKind = 0 | 1 | 2;
取值名称含义
0None不同步文档内容
1Full每次变更都发送文档完整内容
2Incremental打开时发送完整内容(即 didOpen 的text),此后仅发送增量变更

从同步生命周期看:didOpen 是 Incremental 模式的起点——服务器以 didOpen 携带的完整text建立基线,后续靠didChange的增量补丁(range + text)持续演进文档状态。因此 didOpen 的正确实现直接决定了增量同步的基线质量:若初始内容不完整或版本号不准确,后续所有增量都会产生累积偏差。

5.4 注册选项

didOpen 的注册选项为TextDocumentRegistrationOptions(见 didOpen.md),该选项贯穿几乎所有基于文本文档的功能(hover、completion 等),用于声明该功能适用的文档选择器(document selector)。结合仓库中 documentFilter.md 等文档,TextDocumentRegistrationOptions的形态大致如下:

export interface TextDocumentRegistrationOptions { /** * A document selector to identify the scope of the registration. */ documentSelector: DocumentSelector | null; }

documentSelector为null表示"不作为服务端静态注册目标"(常配合动态注册)。当服务器通过client/registerCapability动态注册 didOpen 处理时,会用该选项限定只接收匹配选择器的文档的 didOpen 通知。

6. didOpen 在文档生命周期中的位置

6.1 从打开到关闭的完整流程

结合 LSP 3.19 的同步章节(specification.md),一份文档在客户端中的生命周期如下:

阶段客户端行为消息携带内容
用户打开文档声明内容所有权textDocument/didOpen完整TextDocumentItem(uri、languageId、version、text)
用户编辑文档同步内容变更textDocument/didChange(0 次或多次)VersionedTextDocumentIdentifier+ 增量/全量变更
用户保存文档通知保存textDocument/didSave(可选)TextDocumentIdentifier(+ 可选 text)
用户关闭文档释放内容所有权textDocument/didClose仅TextDocumentIdentifier

仓库中的时序图 language-server-sequence.png 直观展示了这一流程:客户端在用户打开文档时发送textDocument/didOpen,随后在编辑时发送textDocument/didChange,服务器回发textDocument/publishDiagnostics诊断,用户执行跳转定义时走textDocument/definition请求/响应,最后文档关闭时发送textDocument/didClose。其中 didOpen 正是整个同步链条的第一环。

6.2 为什么必须先用 didOpen 再发 didChange

didChange.md 明确规定:

Before a client can change a text document it must claim ownership of its content using thetextDocument/didOpennotification.

即客户端必须先通过 didOpen 声明对文档内容的所有权,才能发送 didChange。这是内容所有权模型的核心约束:didOpen 把"权威内容"从磁盘迁移到客户端,didChange 才有合法的变更依据;否则服务器无从得知变更前的基线内容,也无法正确应用增量。

6.3 版本号与同步时序示例

didChange.md 给出了一个连续输入场景下的同步示例(假设用户输入触发了textDocument/completion):

文档版本用户输入客户端行为请求
5文档变更一将文档 v5 同步给服务器textDocument/didChange
5-基于文档 v5 向服务器发起请求textDocument/completion
6文档变更二将文档 v6 同步给服务器textDocument/didChange

该表的要点:在向服务器发起语言功能请求之前,客户端必须确保文档状态已同步到服务器("the client must ensure that the document's state is synchronized with the server to guarantee reliable results")。didOpen 为这个版本递增链条提供了初始的 version 基线——首个版本通常在打开时确立(如 v1),此后每次 didChange 携带VersionedTextDocumentIdentifier递增版本号。开发语言功能(如补全、签名帮助)时,应遵循"先同步、后请求"的时序,保证服务器计算所用的文档状态与客户端一致。

7. 实现要点与常见陷阱

7.1 客户端实现清单

  1. 配对维护:为每个 URI 维护打开状态,未关闭前禁止重复发送 didOpen;关闭后禁止再次发送 didChange。
  2. 快照完整性:didOpen 必须携带打开瞬间的完整text,不能发送空内容或截断内容(Incremental 模式的基线依赖它)。
  3. 语言切换:languageId 变化时先 didClose 再 didOpen,且确认服务器支持新语言 ID。
  4. 版本递增:version 在每次变更(含撤销/重做)后 +1;didChange 中的版本号指向变更应用后的版本(见 didChange.md 中VersionedTextDocumentIdentifier的注释)。
  5. 能力协商:仅当服务器在initialize响应中声明textDocumentSync(且 openClose 开启)时才发送 didOpen;若服务器声明TextDocumentSyncKind.None,则不做任何同步。

7.2 服务器实现清单

  1. 不得按 URI 回读:didOpen 到达后,以参数中的text为权威内容建立文档模型,禁止自行访问磁盘。
  2. 三合一实现:要么同时处理 didOpen/didChange/didClose,要么全部不处理(规范强制)。
  3. 版本校验:记录每份文档的版本,检测乱序/重复的 didChange;如遇缺失的 didOpen(先收到 didChange),应视为协议违规并做防御处理。
  4. 动态注册:若支持dynamicRegistration,可通过client/registerCapability注册 didOpen,并用TextDocumentRegistrationOptions.documentSelector限定文档范围。

7.3 常见陷阱

  • 重复打开不配对:同一 URI 未关闭又发 didOpen,违反"max open count is one",会导致服务器状态被意外重置。
  • 把 didOpen 当请求处理:didOpen 是通知,不应带id,服务器不应回复响应消息。
  • 忽略 languageId 变更:直接把新 languageId 悄悄改掉而不走 didClose/didOpen,服务器端语言上下文不会更新。
  • 省略 version:didOpen 的version不是可选字段;缺失或恒为常量的版本号会让增量同步的乱序检测失效。

8. 相关文档与进一步阅读

  • 本通知的权威定义:didOpen.md
  • 变更与版本同步规则:didChange.md
  • 关闭配对通知:didClose.md
  • 保存通知(可选环节):didSave.md
  • 文档快照类型与语言标识符表:textDocumentItem.md
  • 文档最小标识:textDocumentIdentifier.md
  • 同步总览与能力定义(TextDocumentSyncKind、TextDocumentSyncOptions、TextDocumentSyncClientCapabilities):specification.md 的 "Text Document Synchronization" 一节
  • 注册选项使用的文档过滤器:documentFilter.md
  • 完整客户端/服务器时序:language-server-sequence.png

结语

textDocument/didOpen看似简单(一个通知、一个参数对象),却是 LSP 文档同步模型的地基:它确立内容所有权、建立增量同步基线、声明语言与版本信息,并与 didChange/didClose 构成严格配对的完整生命周期。理解并正确实现它,是构建可靠语言服务器(尤其是依赖增量同步、需要精确诊断与补全的服务)的前提。以本文梳理的协议语义、类型细节与实现清单为准,结合上述规范原文逐项核对,即可避免绝大多数同步类集成问题。

  • 开发工具

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

Defines a common protocol for language servers.

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

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

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

串口通信可靠性:延时为何致命,平台开发守则与高速选型实战

前一阵在调一块多协议接入板,主控是STM32F407VET6,跑着三个串口、一路RS485和一路TTL屏显。本来以为好好的,结果联调时问题一个接一个:串口发送乱码、上位机超时、编译报重定义……最离谱的是有人为了“让发送稳一点”&#xff0c…

作者头像 李华
网站建设 2026/10/16 0:54:56

公鸡母鸡目标检测识别数据集:1,300张图像 | 目标检测分类识别

公鸡母鸡目标检测识别数据集:1,300张图像 | 目标检测分类识别 源码数据分享 通过网盘分享的文件:公鸡母鸡分类识别数据集 链接: https://pan.baidu.com/s/1oHTa3XsWyQXw81e8D5Phfw?pwd46vq 提取码: 46vq 一、引言:从人工辨识到AI智能识别的…

作者头像 李华
网站建设 2026/10/22 22:29:07

从六款开源RAG产品逆向工程到自研架构蓝图

做了几年RAG项目,从早期拿开源框架拼demo,到后来给企业做私有化知识库,最大的体会是:RAG看起来简单,做到生产可用很难。检索精度、解析质量、响应延迟、可维护性,每一个环节都会在真实场景里给你找麻烦。去…

作者头像 李华
网站建设 2026/10/14 6:38:12

GPT-6 开放 API 后,Codex 的 auth.json 怎么改到 TaoToken

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

作者头像 李华