- 开发工具
【免费下载链接】language-server-protocol
Defines a common protocol for language servers.
导读
WorkspaceEdit(工作区编辑)是 Language Server Protocol(LSP)中用于描述"一次性修改多个资源"的核心数据结构,是重命名(Rename)、代码重构(Code Action)、批量格式化等能力向编辑器提交修改的"载体"。本文以 LSP 3.17 规范的 workspaceEdit.md 为主体,完整梳理WorkspaceEdit的类型定义、changes与documentChanges两种承载方式、3.13 起引入的文件/文件夹资源操作(创建、重命名、删除)、3.16 起引入的 Change Annotation 机制,以及配套的客户端能力协商(WorkspaceEditClientCapabilities)、失败处理策略(FailureHandlingKind)和workspace/applyEdit请求闭环。读完本文,你将能准确理解 LSP 工作区编辑的完整数据契约,并能为自己的语言服务端正确实现多文件批量编辑。
一、什么是 WorkspaceEdit:一个编辑,多个资源
1.1 核心定义
根据 workspaceEdit.md 的规范原文:
A workspace edit represents changes to many resources managed in the workspace.
一个WorkspaceEdit代表对工作区中多个资源的变更集合。它之所以重要,是因为语言服务端(Language Server)的很多操作天然是跨文件的:一次"重命名符号"可能同时需要修改 5 个文件中的 20 处引用;一次"提取方法"重构可能既要改原文件,又要新建一个新文件。这些改动应当作为一个整体编辑交给客户端统一应用(并可统一撤销),而不是逐条发送多个零散请求。
1.2 两条编辑通道:changes与documentChanges
WorkspaceEdit提供了两条承载编辑内容的通道,规范明确要求"二选一":
changes:一个以文档 URI 为键、以TextEdit[]为值的映射,用于描述对现有资源的文本修改;documentChanges:一个更精细的数组,既可以只包含TextDocumentEdit(面向特定文档版本的编辑),也可以在客户端支持时混入CreateFile/RenameFile/DeleteFile资源操作。
规范对两者优先级的表述是:
If the client can handle versioned document edits and if
documentChangesare present, the latter are preferred overchanges.
即:当客户端具备处理版本化文档编辑的能力(workspace.workspaceEdit.documentChanges能力)且documentChanges存在时,客户端优先使用documentChanges。
1.3 完整接口定义(LSP 3.17)
export interface WorkspaceEdit { /** * Holds changes to existing resources. */ changes?: { [uri: DocumentUri]: TextEdit[]; }; /** * Depending on the client capability * `workspace.workspaceEdit.resourceOperations` document changes are either * an array of `TextDocumentEdit`s to express changes to n different text * documents where each text document edit addresses a specific version of * a text document. Or it can contain above `TextDocumentEdit`s mixed with * create, rename and delete file / folder operations. * * Whether a client supports versioned document edits is expressed via * `workspace.workspaceEdit.documentChanges` client capability. * * If a client neither supports `documentChanges` nor * `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s * using the `changes` property are supported. */ documentChanges?: ( TextDocumentEdit[] | (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[] ); /** * A map of change annotations that can be referenced in * `AnnotatedTextEdit`s or create, rename and delete file / folder * operations. * * Whether clients honor this property depends on the client capability * `workspace.changeAnnotationSupport`. * * @since 3.16.0 */ changeAnnotations?: { [id: string /* ChangeAnnotationIdentifier */]: ChangeAnnotation; }; }三个字段的职责可以概括为一张表:
| 字段 | 类型 | 职责 | 依赖的客户端能力 |
|---|---|---|---|
changes | { [uri: DocumentUri]: TextEdit[] } | 对现有资源的纯文本编辑,按文档 URI 分组 | 最基础能力,任何客户端都需支持 |
documentChanges | TextDocumentEdit[]或(TextDocumentEdit \| CreateFile \| RenameFile \| DeleteFile)[] | 版本化文档编辑,可混入文件资源操作 | workspace.workspaceEdit.documentChanges、workspace.workspaceEdit.resourceOperations |
changeAnnotations | { [id: string]: ChangeAnnotation } | 变更注解表,供编辑与资源操作按 ID 引用(3.16+) | workspace.changeAnnotationSupport |
需要特别强调的是文档中指出的最保守的兼容情形:
If a client neither supports
documentChangesnorworkspace.workspaceEdit.resourceOperationsthen only plainTextEdits using thechangesproperty are supported.
也就是说,当客户端既不支持版本化文档编辑、也不支持资源操作时,服务端只能退回到changes属性发送纯TextEdit。
二、文件资源操作:create / rename / delete(3.13+)
2.1 操作顺序与失败语义
自3.13.0起,WorkspaceEdit可以包含资源操作(创建、删除或重命名文件/文件夹)。规范给出了两个重要的行为约束:
- 顺序即语义:客户端必须按照服务端提供的顺序执行资源操作;
- 前后依赖示例:一个编辑可以合法地由"创建文件 a.txt"和"向 a.txt 插入文本的文档编辑"组成;而像"删除 a.txt"再"向 a.txt 插入文本"这样非法的序列会导致操作失败。
关于失败后的恢复方式,规范明确由客户端能力workspace.workspaceEdit.failureHandling描述(详见本文第四节)。
2.2 三种资源操作字面量
文件资源操作的完整定义位于 resourceChanges.md。规范特别提示:命名虽然写的是"file",但这些操作同样适用于文件夹(与 LSP 中文件监视器能同时监视文件与文件夹的命名习惯一致)。
CreateFile(创建):
export interface CreateFileOptions { /** * Overwrite existing file. Overwrite wins over `ignoreIfExists` */ overwrite?: boolean; /** * Ignore if exists. */ ignoreIfExists?: boolean; } export interface CreateFile { kind: 'create'; uri: DocumentUri; options?: CreateFileOptions; annotationId?: ChangeAnnotationIdentifier; // @since 3.16.0 }RenameFile(重命名):
export interface RenameFileOptions { /** * Overwrite target if existing. Overwrite wins over `ignoreIfExists` */ overwrite?: boolean; /** * Ignores if target exists. */ ignoreIfExists?: boolean; } export interface RenameFile { kind: 'rename'; oldUri: DocumentUri; newUri: DocumentUri; options?: RenameFileOptions; annotationId?: ChangeAnnotationIdentifier; // @since 3.16.0 }DeleteFile(删除):
export interface DeleteFileOptions { /** * Delete the content recursively if a folder is denoted. */ recursive?: boolean; /** * Ignore the operation if the file doesn't exist. */ ignoreIfNotExists?: boolean; } export interface DeleteFile { kind: 'delete'; uri: DocumentUri; options?: DeleteFileOptions; annotationId?: ChangeAnnotationIdentifier; // @since 3.16.0 }三种操作的选项参数语义归纳:
| 操作 | 关键选项 | 语义 |
|---|---|---|
create | overwrite/ignoreIfExists | 目标已存在时是否覆盖(overwrite优先于ignoreIfExists)/ 已存在时是否忽略 |
rename | overwrite/ignoreIfExists | 新位置已存在时是否覆盖 / 已存在时是否忽略 |
delete | recursive/ignoreIfNotExists | 目标为文件夹时是否递归删除 / 文件不存在时是否忽略 |
每个资源操作字面量都带一个判别字段kind('create' | 'rename' | 'delete'),这正是ResourceOperationKind联合类型的取值(见本文第三节),也是 JSON-RPC 消息中判别联合类型(discriminated union)的标准写法。
三、ResourceOperationKind 与 FailureHandlingKind
3.1 ResourceOperationKind:客户端支持哪些资源操作
/** * The kind of resource operations supported by the client. */ export type ResourceOperationKind = 'create' | 'rename' | 'delete'; export namespace ResourceOperationKind { /** * Supports creating new files and folders. */ export const Create: ResourceOperationKind = 'create'; /** * Supports renaming existing files and folders. */ export const Rename: ResourceOperationKind = 'rename'; /** * Supports deleting existing files and folders. */ export const Delete: ResourceOperationKind = 'delete'; }ResourceOperationKind只有三种取值:create、rename、delete。规范对客户端的建议是:
Clients should at least support 'create', 'rename' and 'delete' files and folders.
即客户端至少应支持创建、重命名和删除文件与文件夹三种操作。
3.2 FailureHandlingKind:应用失败时的四种处理策略
export type FailureHandlingKind = 'abort' | 'transactional' | 'undo' | 'textOnlyTransactional'; export namespace FailureHandlingKind { /** * Applying the workspace change is simply aborted if one of the changes * provided fails. All operations executed before the failing operation * stay executed. */ export const Abort: FailureHandlingKind = 'abort'; /** * All operations are executed transactional. That means they either all * succeed or no changes at all are applied to the workspace. */ export const Transactional: FailureHandlingKind = 'transactional'; /** * If the workspace edit contains only textual file changes they are * executed transactional. If resource changes (create, rename or delete * file) are part of the change the failure handling strategy is abort. */ export const TextOnlyTransactional: FailureHandlingKind = 'textOnlyTransactional'; /** * The client tries to undo the operations already executed. But there is no * guarantee that this is succeeding. */ export const Undo: FailureHandlingKind = 'undo'; }四种失败处理策略对比如下:
| 取值 | 策略描述 | 适用场景 |
|---|---|---|
abort | 某个变更失败则整体中止;失败之前已执行的操作保留 | 轻量客户端,无事务保障 |
transactional | 所有操作按事务执行:要么全部成功,要么一个都不应用 | 强一致性要求的客户端 |
textOnlyTransactional | 仅包含文本编辑时按事务执行;一旦混入资源操作则退化为 abort | 能保证文本事务、但无法回滚文件系统操作的客户端 |
undo | 尽力回滚已执行的操作,不保证一定成功 | 提供撤销栈的客户端 |
这四种策略是服务端判断"一次跨文件编辑提交后能获得什么保证"的关键依据:例如重命名符号时若混入文件重命名,遇到textOnlyTransactional客户端就必须意识到资源操作部分并不具备事务保证。
四、WorkspaceEditClientCapabilities:能力协商
4.1 能力字段的演化
规范指出,工作区编辑的能力"随着时间不断演化",客户端通过workspace.workspaceEdit属性路径上报自身支持程度。3.13 版本新增了ResourceOperationKind、FailureHandlingKind以及resourceOperations、failureHandling能力;3.16 又新增了normalizesLineEndings与changeAnnotationSupport。
export interface WorkspaceEditClientCapabilities { /** * The client supports versioned document changes in `WorkspaceEdit`s */ documentChanges?: boolean; /** * The resource operations the client supports. Clients should at least * support 'create', 'rename' and 'delete' files and folders. * * @since 3.13.0 */ resourceOperations?: ResourceOperationKind[]; /** * The failure handling strategy of a client if applying the workspace edit * fails. * * @since 3.13.0 */ failureHandling?: FailureHandlingKind; /** * Whether the client normalizes line endings to the client specific * setting. * If set to `true` the client will normalize line ending characters * in a workspace edit to the client specific new line character(s). * * @since 3.16.0 */ normalizesLineEndings?: boolean; /** * Whether the client in general supports change annotations on text edits, * create file, rename file and delete file changes. * * @since 3.16.0 */ changeAnnotationSupport?: { /** * Whether the client groups edits with equal labels into tree nodes, * for instance all edits labelled with "Changes in Strings" would * be a tree node. */ groupsOnLabel?: boolean; }; }各字段含义汇总:
| 能力字段 | 类型 | 含义 |
|---|---|---|
documentChanges | boolean | 是否支持WorkspaceEdit中的版本化文档变更 |
resourceOperations | ResourceOperationKind[] | 支持的资源操作列表(create/rename/delete) |
failureHandling | FailureHandlingKind | 应用编辑失败时采用的恢复策略 |
normalizesLineEndings | boolean | 为true时客户端会把编辑中的换行符归一化为客户端特有的换行字符 |
changeAnnotationSupport | { groupsOnLabel?: boolean } | 是否支持变更注解;groupsOnLabel表示是否将相同标签的编辑分组为树节点(例如所有标记为 "Changes in Strings" 的编辑聚合成一个树节点) |
这些能力定义在 workspaceEdit.md,并作为Workspace能力的一部分在初始化握手阶段由客户端上报(参见 initialize.md 中WorkspaceClientCapabilities的workspaceEdit?: WorkspaceEditClientCapabilities字段)。服务端应依据这些能力裁剪自己的行为:
- 未上报
documentChanges时,不要发送版本化文档编辑; resourceOperations为空时,不要发送文件资源操作;- 未上报
changeAnnotationSupport时,不要发送AnnotatedTextEdit字面量(见 textDocumentEdit.md 的明确要求)。
4.2 端到端应用:workspace/applyEdit 请求
WorkspaceEdit本身只是数据结构,真正"动手改文件"发生在服务端向客户端发送workspace/applyEdit请求时(定义见 applyEdit.md):
- 方法名:
'workspace/applyEdit'(服务端 → 客户端,箭头符号:arrow_right_hook:表示由服务端发起的请求); - 能力字段:
workspace.applyEdit(boolean); - 请求参数
ApplyWorkspaceEditParams:
export interface ApplyWorkspaceEditParams { /** * An optional label of the workspace edit. This label is * presented in the user interface for example on an undo * stack to undo the workspace edit. */ label?: string; /** * The edits to apply. */ edit: WorkspaceEdit; }- 响应结果
ApplyWorkspaceEditResult:
export interface ApplyWorkspaceEditResult { /** * Indicates whether the edit was applied or not. */ applied: boolean; /** * An optional textual description for why the edit was not applied. * This may be used by the server for diagnostic logging or to provide * a suitable error for a request that triggered the edit. */ failureReason?: string; /** * Depending on the client's failure handling strategy `failedChange` * might contain the index of the change that failed. This property is * only available if the client signals a `failureHandling` strategy * in its client capabilities. */ failedChange?: uinteger; }请求中的label会展示在用户界面中(例如撤销栈上的名称),这是给用户"这条编辑做了什么"的可读标识;响应中的applied是编辑是否被应用的硬性结果,failureReason供服务端记录诊断日志,而failedChange(仅在客户端上报了failureHandling能力时可用)给出失败变更的索引,服务端可据此精确定位是哪一步出了问题——这与第四节failureHandling的语义直接挂钩。
在 LSP 3.17 仓库中,WorkspaceEdit及相关类型还被 rename.md、executeCommand.md、codeAction.md 以及willCreateFiles/willRenameFiles/willDeleteFiles(见 workspace/willCreateFiles.md)等文档反复引用,可见它是 LSP 中横跨重构、命令执行、文件事件等模块的通用"修改载体"。
五、TextDocumentEdit 与版本化文档编辑
5.1 为什么需要版本化
changes通道以 URI 为键,但不知道文档版本。若文档在服务端计算编辑之后、客户端应用之前又被用户改动,直接套用旧文本位置可能产生冲突。为此,documentChanges通道引入TextDocumentEdit——它通过OptionalVersionedTextDocumentIdentifier携带文档版本信息,让客户端在应用编辑前可以校验版本是否匹配。定义见 textDocumentEdit.md:
export interface TextDocumentEdit { /** * The text document to change. */ textDocument: OptionalVersionedTextDocumentIdentifier; /** * The edits to be applied. * * @since 3.16.0 - support for AnnotatedTextEdit. This is guarded by the * client capability `workspace.workspaceEdit.changeAnnotationSupport` */ edits: (TextEdit | AnnotatedTextEdit)[]; }5.2 OptionalVersionedTextDocumentIdentifier 的版本语义
OptionalVersionedTextDocumentIdentifier定义于 versionedTextDocumentIdentifier.md,其version字段类型为integer | null:
- 当服务端 → 客户端传递该标识符、而目标文件并未在编辑器中打开(服务端未收到 open 通知)时,服务端可发送
null,表示"版本已知、磁盘内容为准"(依据文档内容所有权规则); - 版本号在每次变更(含 undo/redo)后递增,不必连续。
5.3 无需排序但不得重叠
TextDocumentEdit有一个独特的便利性:它描述了对某个文档从版本 Si 到 Si+1 的所有变更,因此:
the creator of a
TextDocumentEditdoesn't need to sort the array of edits or do any kind of ordering. However the edits must be non overlapping.
即服务端无需对编辑数组排序(客户端自会按版本推进应用),但编辑区间之间不得重叠——这是服务端实现时必须自己保证的不变量。
六、Change Annotation:3.16 起的"变更分组与说明"机制
6.1 动机:给编辑"贴上可读标签"
跨文件重构往往产生几十个编辑,用户希望知道每个编辑属于哪类改动、能否安全应用。LSP 3.16 引入了Change Annotation(变更注解)机制,让编辑可以被标注"人类可读的描述 + 是否需要确认"。相关定义位于 textEdit.md:
/** * Additional information that describes document changes. * * @since 3.16.0 */ export interface ChangeAnnotation { /** * A human-readable string describing the actual change. The string * is rendered prominent in the user interface. */ label: string; /** * A flag which indicates that user confirmation is needed * before applying the change. */ needsConfirmation?: boolean; /** * A human-readable string which is rendered less prominent in * the user interface. */ description?: string; }label是必须的,且会在 UI 中醒目渲染;needsConfirmation标记该变更应用前需要用户确认;description是次要渲染的可选补充说明。
6.2 为什么用"标识符"而不是内联对象
协议设计上,编辑或资源操作引用的是注解标识符(ChangeAnnotationIdentifier,即string),而不是注解字面量本身:
This allows servers to use the identical annotation across multiple edits or resource operations which then allows clients to group the operations under that change annotation.
好处显而易见:同一个注解可被多个编辑/资源操作共享引用,客户端从而能把这些操作按注解分组(例如把标记为 "Changes in Strings" 的所有编辑聚合为一个树节点展示,对应能力groupsOnLabel)。
export type ChangeAnnotationIdentifier = string; /** * A special text edit with an additional change annotation. * * @since 3.16.0. */ export interface AnnotatedTextEdit extends TextEdit { /** * The actual annotation identifier. */ annotationId: ChangeAnnotationIdentifier; }6.3 注解的存放位置与守卫条件
注解本体存放在WorkspaceEdit.changeAnnotations映射中(见第一节接口),编辑与资源操作只通过annotationId引用。两个关键守卫条件:
- 能力守卫:
ChangeAnnotation、AnnotatedTextEdit以及CreateFile/RenameFile/DeleteFile上的annotationId都受workspace.workspaceEdit.changeAnnotationSupport能力保护——客户端未上报该能力时,服务端不应发送AnnotatedTextEdit字面量(textDocumentEdit.md 对此有明确说明); - 版本边界:该机制自 3.16.0 引入,3.15 及更早版本的客户端无法理解,服务端必须结合初始化握手时协商的协议版本与能力做降级处理。
七、实践建议:服务端如何构造一个"正确"的 WorkspaceEdit
综合以上规范要点,服务端构造WorkspaceEdit时可遵循以下检查清单:
- 先看能力,再选通道:读初始化时客户端上报的
workspace.workspaceEdit。支持documentChanges就优先用TextDocumentEdit[];支持resourceOperations才能混入CreateFile/RenameFile/DeleteFile;两者皆不支持时退回到纯changes映射。 - 版本化编辑要带上版本:
TextDocumentEdit.textDocument使用OptionalVersionedTextDocumentIdentifier,文档未打开时可传version: null;编辑区间不得重叠(无需排序)。 - 资源操作讲究顺序:混入文件操作时,按"先创建、后引用、再删除"的依赖顺序排列,例如先
create新文件,再TextDocumentEdit向新文件写入内容;删除后再写同一文件属于非法序列。 - 善用标签与注解:需要让用户看清改动来源时,使用
label+changeAnnotations+annotationId(受changeAnnotationSupport守卫),对需要确认的破坏性改动设置needsConfirmation: true。 - 关注失败反馈:通过
workspace/applyEdit的响应检查applied,结合客户端failureHandling策略理解failedChange的语义,并利用failureReason做诊断日志。
这些类型与请求在 LSP 3.17 规范仓库中形成了完整的闭环:定义侧在 types/workspaceEdit.md 与 types/resourceChanges.md、types/textDocumentEdit.md、types/textEdit.md,应用侧在 workspace/applyEdit.md,能力协商入口在 general/initialize.md,并被 language/rename.md 等请求文档作为结果类型引用。服务端开发者按这一链路实现,即可交付可靠、可回退、可分组展示的跨文件编辑体验。
- 开发工具
【免费下载链接】language-server-protocol
Defines a common protocol for language servers.
相关推荐
微服务的事件驱动数据管理:从分布式数据一致性问题到事件源架构(doocs/advanced-java)
微服务的事件驱动数据管理:从分布式数据一致性问题到事件源架构(doocs/advanced java) 本篇技术指南围绕微服务架构下"分布式数据管理"这一核心痛
开发工具Jira Python 库测试策略:单元测试与集成测试最佳实践
Jira Python 库测试策略:单元测试与集成测试最佳实践 Jira Python 库是一个功能强大的工具,为开发者提供了与 Jira 系统交互的便捷接口。
后端Language Server Protocol (LSP) 开源项目教程
Language Server Protocol LSP 开源项目教程 项目的目录结构及介绍 Language Server Protocol LSP 项目的目
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考