- 开发工具
【免费下载链接】language-server-protocol
Defines a common protocol for language servers.
导读
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 与关闭通知的配对约束
规范同时强调了两条硬性规则:
- 开/关必须配对:在没有先发送对应的
textDocument/didClose之前,不得对同一文档重复发送 didOpen("An open notification must not be sent more than once without a corresponding close notification send before")。 - 最大打开数为一:任意特定
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; }| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
uri | DocumentUri | 是 | 文档唯一标识(协议层以字符串形式传递 URI,参见 textDocumentIdentifier.md) |
languageId | string | 是 | 与文档关联的语言标识符(见第 3 节) |
version | integer | 是 | 文档版本号,每次变更(含撤销/重做)后递增 |
text | string | 是 | 打开时文档的完整内容 |
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:
| 语言 | 标识符 | 语言 | 标识符 |
|---|---|---|---|
| ABAP | abap | Lua | lua |
| Windows Bat | bat | Makefile | makefile |
| BibTeX | bibtex | Markdown | markdown |
| Clojure | clojure | Objective-C | objective-c |
| CoffeeScript | coffeescript | Objective-C++ | objective-cpp |
| C | c | Pascal | pascal(@since 3.18.0) |
| C++ | cpp | Perl | perl |
| C# | csharp | Perl 6 | perl6 |
| CSS | css | PHP | php |
| D | d(@since 3.18.0) | Plaintext | plaintext |
| Delphi | pascal(@since 3.18.0) | PowerShell | powershell |
| Diff | diff | Pug | jade |
| Dart | dart | Python | python |
| Dockerfile | dockerfile | R | r |
| Elixir | elixir | Razor (cshtml) | razor |
| Erlang | erlang | Ruby | ruby |
| F# | fsharp | Rust | rust |
| Git | git-commit、git-rebase | SCSS | scss(花括号语法)、sass(缩进语法) |
| Go | go | Scala | scala |
| Groovy | groovy | ShaderLab | shaderlab |
| Handlebars | handlebars | Shell Script (Bash) | shellscript |
| Haskell | haskell | SQL | sql |
| HTML | html | Swift | swift |
| Ini | ini | TypeScript | typescript |
| Java | java | TypeScript React | typescriptreact |
| JavaScript | javascript | TeX | tex |
| JavaScript React | javascriptreact | Text (plain) | plaintext |
| JSON | json | Visual Basic | vb |
| LaTeX | latex | XML | xml |
| Less | less | XSL | xsl |
| YAML | yaml |
使用要点:
- 当文档的语言发生变化时(例如从 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 for
textDocument/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;| 取值 | 名称 | 含义 |
|---|---|---|
0 | None | 不同步文档内容 |
1 | Full | 每次变更都发送文档完整内容 |
2 | Incremental | 打开时发送完整内容(即 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 the
textDocument/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 客户端实现清单
- 配对维护:为每个 URI 维护打开状态,未关闭前禁止重复发送 didOpen;关闭后禁止再次发送 didChange。
- 快照完整性:didOpen 必须携带打开瞬间的完整
text,不能发送空内容或截断内容(Incremental 模式的基线依赖它)。 - 语言切换:languageId 变化时先 didClose 再 didOpen,且确认服务器支持新语言 ID。
- 版本递增:version 在每次变更(含撤销/重做)后 +1;didChange 中的版本号指向变更应用后的版本(见 didChange.md 中
VersionedTextDocumentIdentifier的注释)。 - 能力协商:仅当服务器在
initialize响应中声明textDocumentSync(且 openClose 开启)时才发送 didOpen;若服务器声明TextDocumentSyncKind.None,则不做任何同步。
7.2 服务器实现清单
- 不得按 URI 回读:didOpen 到达后,以参数中的
text为权威内容建立文档模型,禁止自行访问磁盘。 - 三合一实现:要么同时处理 didOpen/didChange/didClose,要么全部不处理(规范强制)。
- 版本校验:记录每份文档的版本,检测乱序/重复的 didChange;如遇缺失的 didOpen(先收到 didChange),应视为协议违规并做防御处理。
- 动态注册:若支持
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.
相关推荐
LSP Type Definition Request(textDocument/typeDefinition)协议详解:从客户端能力到服务端实现
LSP Type Definition Request(textDocument/typeDefinition)协议详解:从客户端能力到服务端实现 本文基于 l
开发工具深入解析 LSP 3.18 的 textDocument/didOpen:文档打开通知与文本同步机制
深入解析 LSP 3.18 的 textDocument/didOpen:文档打开通知与文本同步机制 导读 textDocument/didOpen 是 Lan
开发工具LSP 3.19 中的 Go to Implementation 请求(textDocument/implementation):协议详解与实现指南
LSP 3.19 中的 Go to Implementation 请求(textDocument/implementation):协议详解与实现指南 导读 本文
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考