Language Server Protocol WorkspaceEdit 详解:跨资源编辑、资源操作与失败处理(3.18)

发布时间:2026/10/8 19:00:01
Language Server Protocol WorkspaceEdit 详解:跨资源编辑、资源操作与失败处理(3.18)
开发工具【免费下载链接】language-server-protocolDefines a common protocol for language servers.项目地址https://gitcode.com/gh_mirrors/la/language-server-protocol点击查看免费下载导读WorkspaceEdit是 Language Server ProtocolLSP中用于表示一次跨多个资源变更的核心数据结构是代码重构、批量格式化、多文件重命名等功能在协议层的统一载体。本文以 LSP 3.18 规范中 _specifications/lsp/3.18/types/workspaceEdit.md 为骨架结合仓库内textEdit、resourceChanges、applyEdit等相关类型与消息文档完整讲解WorkspaceEdit的两种承载方式changes与documentChanges、3.13 引入的资源操作create/rename/delete、3.16 引入的变更注解change annotations以及 3.18 新增的元数据与 snippet 编辑支持。读完本文你将能准确构造符合规范的 WorkspaceEdit 载荷并为自己的语言服务器正确声明workspace.workspaceEdit客户端能力。一、什么是 WorkspaceEditWorkspaceEdit表示对工作区中多个资源的变更。一个典型的场景是用户在编辑器里执行重命名符号语言服务器计算出涉及多个文件的多处替换最终打包成一个WorkspaceEdit交给客户端一次性应用。它的核心约束是编辑载荷要么提供changes要么提供documentChanges。当客户端能够处理带版本号的文档编辑versioned document edits且documentChanges存在时客户端优先采用documentChanges而不是changes。这一偏好规则保证了新旧协议形态之间的平滑演进。1.1 接口定义3.18export 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 TextDocumentEdits 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 TextDocumentEdits 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 TextEdits * using the changes property are supported. */ documentChanges?: ( TextDocumentEdit[] | (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[] ); /** * A map of change annotations that can be referenced in * AnnotatedTextEdits 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 → TextEdit[]映射的方式直接给出对现有资源的纯文本编辑基础能力documentChanges携带版本信息的文档编辑数组可与 create/rename/delete 文件操作混合3.13 起支持混合资源操作changeAnnotations变更注解映射供AnnotatedTextEdit与文件资源操作通过annotationId引用3.16.0在仓库的机器可读元模型中metaModel.json 以结构化方式记录了WorkspaceEdit的上述三个属性及其类型约束changes为MapDocumentUri, TextEdit[]documentChanges为TextDocumentEdit | CreateFile | RenameFile | DeleteFile的联合数组与规范文档完全一致适合作为实现时的机器校验基准。1.2 两种载荷形态的选择规则选择哪种形态取决于客户端在初始化握手时声明的能力客户端支持workspace.workspaceEdit.documentChanges→ 服务器应优先发送documentChanges客户端既不支持documentChanges、也不支持workspace.workspaceEdit.resourceOperations→ 服务器只能使用纯TextEdit组成的changes映射客户端支持resourceOperations→documentChanges中可以混入CreateFile/RenameFile/DeleteFile资源操作字面量。二、承载文本编辑TextEdit 家族WorkspaceEdit的编辑内容最终落到 TextEdit 上其基本形态是范围 新文本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; }关键实践点插入文本构造一个start end的零宽Range删除文本newText传空字符串替换范围覆盖目标内容newText为替换结果。2.1 AnnotatedTextEdit带注解的文本编辑3.16从 3.16.0 起文本编辑可以携带ChangeAnnotation为变更附加人类可读的描述信息export interface ChangeAnnotation { label: string; // 在 UI 中显著展示的变更描述 needsConfirmation?: boolean; // 应用前是否需要用户确认 description?: string; // 次要展示的补充描述 }为避免在每条编辑上重复完整注解协议约定编辑或资源操作通过标识符引用注解而不是直接内联注解字面量。标识符与注解的映射统一存放在WorkspaceEdit.changeAnnotations中export type ChangeAnnotationIdentifier string; export interface AnnotatedTextEdit extends TextEdit { annotationId: ChangeAnnotationIdentifier; }这种引用而非内联的设计让服务器可以在多条编辑之间复用同一注解客户端据此将相同 label 的变更分组展示例如把所有标注为 Changes in Strings 的编辑聚合为一个树节点对应客户端能力workspace.changeAnnotationSupport.groupsOnLabel。2.2 SnippetTextEdit交互式片段编辑3.18 新增3.18.0 引入了SnippetTextEdit允许以 snippet 而非纯文本的形式插入内容export interface SnippetTextEdit { range: Range; snippet: StringValue; // 待插入的 snippet annotationId?: ChangeAnnotationIdentifier; }规范对客户端应用 snippet 编辑有三条重要约定交互式 snippet 只应用于当前活动编辑器打开的文件避免不必要的焦点切换或编辑器滚动对同一 URI只有一个 snippet 可以指定光标位置若多个 snippet 都定义了光标位置最终光标落点由客户端自行决定若 snippet 对应的文件未在活动编辑器中打开客户端应将其降级为普通的非交互文本编辑再应用确保一次工作区编辑不会擅自打开任意文件。snippet字段的类型为StringValue其语义可参考 stringValue.md 中的定义。2.3 TextDocumentEdit按文档聚合编辑TextDocumentEdit描述单个文本文档上的全部文本变更通过OptionalVersionedTextDocumentIdentifier引用文档让客户端在应用编辑前可以校验文档版本export interface TextDocumentEdit { textDocument: OptionalVersionedTextDocumentIdentifier; edits: (TextEdit | AnnotatedTextEdit | SnippetTextEdit)[]; }规范明确TextDocumentEdit描述的是从版本 Si 到 Si1 的全部变更因此服务器无需对数组内编辑排序但编辑之间不允许重叠non overlapping。OptionalVersionedTextDocumentIdentifier的version允许为integer | null——当文件未在编辑器中打开时服务器可发送null表示版本未知、以磁盘内容为准详见 versionedTextDocumentIdentifier.md。此外3.16 起AnnotatedTextEdit的发送由客户端能力workspace.workspaceEdit.changeAnnotationSupport把关3.18 起SnippetTextEdit由workspace.workspaceEdit.snippetEditSupport把关——客户端未声明相应能力时服务器不应发送对应的编辑字面量。三、资源操作create / rename / delete3.13自 3.13.0 起WorkspaceEdit可以包含资源操作创建、删除、重命名文件和文件夹定义在 resourceChanges.md 中。尽管命名是file但这些操作对文件和文件夹同样适用这与文件监视等其它 LSP 命名保持一致。三个资源操作字面量及其选项如下// 创建文件 export interface CreateFileOptions { overwrite?: boolean; // 覆盖已存在文件overwrite 优先于 ignoreIfExists ignoreIfExists?: boolean; } export interface CreateFile { kind: create; uri: DocumentUri; options?: CreateFileOptions; annotationId?: ChangeAnnotationIdentifier; // since 3.16.0 } // 重命名文件 export interface RenameFileOptions { overwrite?: boolean; // 覆盖已存在的目标优先于 ignoreIfExists ignoreIfExists?: boolean; } export interface RenameFile { kind: rename; oldUri: DocumentUri; // 旧现存位置 newUri: DocumentUri; // 新位置 options?: RenameFileOptions; annotationId?: ChangeAnnotationIdentifier; // since 3.16.0 } // 删除文件 export interface DeleteFileOptions { recursive?: boolean; // 目标为文件夹时是否递归删除内容 ignoreIfNotExists?: boolean; } export interface DeleteFile { kind: delete; uri: DocumentUri; options?: DeleteFileOptions; annotationId?: ChangeAnnotationIdentifier; // since 3.16.0 }3.1 资源操作必须按顺序执行当WorkspaceEdit包含资源操作时客户端必须按数组给定的顺序依次执行。合法与非法序列的对比是规范中最经典的例子合法(1) 创建a.txt→ (2) 对a.txt做文本编辑插入内容非法(1) 删除a.txt→ (2) 对a.txt做文本编辑插入内容——第二步必然失败。客户端如何从失败中恢复由客户端能力workspace.workspaceEdit.failureHandling描述见下一节。3.2 资源操作与文件事件通知的配合资源操作与 3.16 引入的文件事件通知didCreateFiles.md、didRenameFiles.md、didDeleteFiles.md在语义上相互补充CreateFile/RenameFile/DeleteFile是服务器驱动的变更指令而didCreateFiles/didRenameFiles/didDeleteFiles是客户端把用户发起的文件操作通知给服务器的通道二者共同构成了 LSP 中文件级变更的完整闭环。四、客户端能力声明WorkspaceEditClientCapabilitiesWorkspaceEdit的能力随协议版本不断演进客户端通过在初始化时声明workspace.workspaceEdit来描述自己的支持程度。3.18 版本的能力定义workspaceEdit.md如下export interface WorkspaceEditClientCapabilities { /** * The client supports versioned document changes in WorkspaceEdits. */ documentChanges?: boolean; /** * The resource operations the client supports. Clients should at least * support create, rename, and delete for 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?: ChangeAnnotationsSupportOptions; /** * Whether the client supports WorkspaceEditMetadata in WorkspaceEdits. * * since 3.18.0 */ metadataSupport?: boolean; /** * Whether the client supports snippets as text edits. * * since 3.18.0 */ snippetEditSupport?: boolean; }典型的能力声明 JSON客户端在 initialize 响应中发送形如{ capabilities: { workspace: { workspaceEdit: { documentChanges: true, resourceOperations: [create, rename, delete], failureHandling: textOnlyTransactional, normalizesLineEndings: true, changeAnnotationSupport: { groupsOnLabel: true }, metadataSupport: true, snippetEditSupport: true } } } }4.1 各字段语义documentChanges客户端能否处理带版本号的文档变更直接决定服务器采用changes还是documentChanges形态。resourceOperations客户端支持的资源操作种类。规范要求客户端至少支持文件与文件夹的 create、rename、delete。failureHandling应用 WorkspaceEdit 失败时的恢复策略枚举值见下文。normalizesLineEndings为true时客户端会把工作区编辑中的行结束符规范化为客户端特定的换行字符。changeAnnotationSupport是否支持变更注解。3.18 将其提取为具名类型ChangeAnnotationsSupportOptions3.17 及更早为内联对象export type ChangeAnnotationsSupportOptions { /** * 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; };metadataSupport3.18 新增客户端是否支持WorkspaceEdit携带WorkspaceEditMetadata例如标记该编辑是一次重构。snippetEditSupport3.18 新增客户端是否支持把 snippet 作为文本编辑即SnippetTextEdit。五、失败处理策略FailureHandlingKind当WorkspaceEdit应用失败时客户端的恢复行为由failureHandling决定。FailureHandlingKind有四种取值export type FailureHandlingKind abort | transactional | undo | textOnlyTransactional;取值语义abort任一变更失败即中止整个应用失败操作之前已执行的操作保持生效transactional全部操作事务化执行要么全部成功要么任何变更都不应用textOnlyTransactional若编辑只含文本文件变更则事务化执行若含资源变更create/rename/delete 文件则退化为abort策略undo尝试撤销已执行的操作但不保证撤销一定成功四种策略对应从简单粗暴到尽力保证原子性的梯度服务器应依据客户端声明选择对用户最友好的编辑构造方式。例如当客户端只声明abort时服务器应尽量把不可逆的破坏性操作放在数组末尾避免失败时留下中间状态。六、如何把 WorkspaceEdit 送到客户端workspace/applyEdit服务器构造好WorkspaceEdit后通过workspace/applyEdit请求applyEdit.md将其发送给客户端应用。该请求由服务器主动发起:arrow_right_hook:方向客户端能力路径为workspace.applyEditboolean。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; /** * Additional data about the edit. * * since 3.18.0 */ metadata?: WorkspaceEditMetadata; }其中 3.18 新增的元数据结构极为精简export interface WorkspaceEditMetadata { /** * Signal to the editor that this edit is a refactoring. */ isRefactoring?: boolean; }客户端是否接受metadata由能力workspace.workspaceEdit.metadataSupport把关isRefactoring: true可以提示编辑器将本次编辑视为一次重构操作例如影响撤销栈的呈现方式。6.1 应用结果与失败定位客户端返回ApplyWorkspaceEditResultexport 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 clients 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; }关键点applied: false表示编辑未被应用failureReason供服务器记录日志或向触发该编辑的请求如 codeAction返回合适的错误信息只有客户端声明了failureHandling能力时failedChange才可能出现用于精确指出失败的变更在documentChanges数组中的索引——这对服务器回退或向用户给出精确错误提示非常有价值。6.2 完整调用链executeCommand → applyEditWorkspaceEdit最常见的产生路径是workspace/executeCommandexecuteCommand.md客户端向服务器发送workspace/executeCommand触发服务器侧命令如提取方法重命名服务器在命令处理中构造WorkspaceEdit再通过workspace/applyEdit把变更推回客户端应用。规范原文明确说明In most cases, the server creates aWorkspaceEditstructure and applies the changes to the workspace using the requestworkspace/applyEdit, which is sent from the server to the client.此外textDocument/codeAction、textDocument/codeLens等请求返回的命令Command也会携带参数客户端随后用这些参数发起workspace/executeCommand。ExecuteCommandParams的核心字段是命令标识符command与arguments?: LSPAny[]服务器端需在executeCommandProvider.commands中列出所有可执行命令。七、综合示例一个包含资源操作与注解的完整 WorkspaceEdit下面给出一个 3.18 语义下的完整载荷示例将a.txt重命名为b.txt再对新建/重命名后的文件追加一行文本并给两步操作附上同一变更注解便于客户端按标签分组展示{ documentChanges: [ { kind: rename, oldUri: file:///workspace/a.txt, newUri: file:///workspace/b.txt, options: { overwrite: false, ignoreIfExists: false }, annotationId: rename-a-to-b }, { textDocument: { uri: file:///workspace/b.txt, version: null }, edits: [ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, newText: // refactored\n } ] } ], changeAnnotations: { rename-a-to-b: { label: Rename a.txt to b.txt and prepend header, needsConfirmation: false, description: Part of the Extract Module refactoring } } }要点回顾两个操作顺序敏感先 rename 后编辑编辑目标才存在version: null表示文件可能未在编辑器中打开以磁盘内容为准annotationId与changeAnnotations键一一对应客户端可据此分组展示若客户端声明snippetEditSupport还可把上面的TextEdit换成SnippetTextEdit以获得交互式光标跳转体验。八、版本演进速览版本WorkspaceEdit 相关新增3.13.0资源操作CreateFile/RenameFile/DeleteFile能力resourceOperations、failureHandling枚举ResourceOperationKind、FailureHandlingKind3.16.0changeAnnotations属性、AnnotatedTextEdit、ChangeAnnotation、ChangeAnnotationIdentifier能力normalizesLineEndings、changeAnnotationSupport3.18.0SnippetTextEditsnippet 作为文本编辑WorkspaceEditMetadataisRefactoring能力metadataSupport、snippetEditSupportChangeAnnotationsSupportOptions提取为具名类型总结WorkspaceEdit是 LSP 中批量、跨资源变更的统一契约其设计经历了从纯文本映射changes到版本化文档编辑documentChanges、再到文件资源操作与变更注解、直至 3.18 的元数据与 snippet 编辑的持续演进。服务器与客户端只需各自按 workspaceEdit.md 与 applyEdit.md 声明能力、遵守顺序与失败处理约定即可实现健壮的多文件重构体验。若需进一步对照机器可读定义可直接查阅 metaModel.json。赞分享开发工具【免费下载链接】language-server-protocolDefines a common protocol for language servers.项目地址https://gitcode.com/gh_mirrors/la/language-server-protocol点击查看免费下载相关推荐LSP WorkspaceEdit 深度解析Language Server Protocol 中跨文件编辑与资源操作协议详解LSP WorkspaceEdit 深度解析Language Server Protocol 中跨文件编辑与资源操作协议详解 导读 WorkspaceEdit开发工具Language Server Protocol 3.19 WorkspaceEdit 全解析从批量文本编辑到文件资源操作Language Server Protocol 3.19 WorkspaceEdit 全解析从批量文本编辑到文件资源操作 导读 WorkspaceEdit开发工具LSP linkedEditingRange 深度解析Language Server Protocol 3.18 链接编辑范围协议详解LSP linkedEditingRange 深度解析Language Server Protocol 3.18 链接编辑范围协议详解 本文以 Languag开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考