☰
LSP 3.17 Pull Diagnostics 诊断拉取机制详解:从服务器推送通知到客户端驱动的诊断计算
2026/10/6 7:30:29 网站建设 项目流程
  • 开发工具

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

Defines a common protocol for language servers.

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

本文基于当前仓库 Language Server Protocol 3.17 规范 中的 pullDiagnostics.md 整理而成。该文档自 3.17.0 起引入诊断拉取(Diagnostic Pull)模型:客户端通过textDocument/diagnostic与workspace/diagnostic两个请求主动向服务器索取文档级与工作区级诊断,并配套workspace/diagnostic/refresh刷新机制。读完本文,你将掌握拉取式诊断的完整协议设计——包括能力协商、resultId增量报告、full/unchanged报告类型、部分结果与ServerCancelled错误处理,以及客户端实现方应遵循的拉取策略。

为什么需要 Pull Diagnostics:推送模型的局限

在 3.17 之前,诊断(Diagnostics)由服务器通过textDocument/publishDiagnostics通知推送给客户端(见 publishDiagnostics.md)。这一模型有明确的优点:对于工作区范围的诊断,服务器可以在自己偏好的时间点自由计算,无需与客户端 UI 交互同步。

但推送模型同样存在结构性缺陷:

  • 服务器无法优先计算用户正在输入或当前可见文件的诊断;
  • 若服务器试图从textDocument/didOpen与textDocument/didChange通知中推断客户端的 UI 状态,会得出误判(false positives)——因为这些通知本质上是**所有权转移(ownership transfer)**通知,文件在编辑器中是否可见与是否已打开/已同步是两个独立维度。

因此规范引入了诊断拉取请求(diagnostic pull requests)的概念,把主动权交还给客户端:由客户端决定为哪些文档计算诊断以及在什么时间点计算。这一设计同时为增量更新(resultId)、工作区级全量拉取和部分结果(partial results)提供了协议层面的支撑。

能力协商:客户端与服务器如何声明诊断拉取支持

与 LSP 中其他功能一样,诊断拉取通过initialize握手(见 initialize.md)和动态注册(client/registerCapability)完成能力协商。

客户端能力:textDocument.diagnostic

属性名(可选):textDocument.diagnostic,类型为DiagnosticClientCapabilities:

/** * Client capabilities specific to diagnostic pull requests. * * @since 3.17.0 */ export interface DiagnosticClientCapabilities { /** * Whether implementation supports dynamic registration. If this is set to * `true` the client supports the new * `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` * return value for the corresponding server capability as well. */ dynamicRegistration?: boolean; /** * Whether the clients supports related documents for document diagnostic * pulls. */ relatedDocumentSupport?: boolean; /** * Whether the clients accepts diagnostics with related information. */ relatedInformation?: boolean; /** * Client supports the tag property to provide meta data about a diagnostic. * Clients supporting tags have to handle unknown tags gracefully. */ tagSupport?: ClientDiagnosticsTagOptions; /** * Client supports a codeDescription property */ codeDescriptionSupport?: boolean; /** * Whether code action supports the `data` property which is * preserved between a `textDocument/publishDiagnostics` and * `textDocument/codeAction` request. */ dataSupport?: boolean; }

各字段含义如下:

字段含义注意事项
dynamicRegistration是否支持动态注册为true时,客户端也支持服务器能力返回(TextDocumentRegistrationOptions & StaticRegistrationOptions)
relatedDocumentSupport是否支持文档诊断拉取中的相关文档决定服务器能否返回relatedDocuments字段
relatedInformation是否接受带相关信息的诊断对应Diagnostic.relatedInformation
tagSupport是否支持诊断标签元数据通过valueSet: DiagnosticTag[]声明支持哪些标签,且客户端必须优雅处理未知标签
codeDescriptionSupport是否支持codeDescription属性对应Diagnostic.codeDescription(3.16 引入)
dataSupport是否支持data属性该数据在textDocument/publishDiagnostics与textDocument/codeAction请求之间被保留

服务器能力:diagnosticProvider

属性名(可选):diagnosticProvider,类型为DiagnosticOptions:

/** * Diagnostic options. * * @since 3.17.0 */ export interface DiagnosticOptions extends WorkDoneProgressOptions { /** * An optional identifier under which the diagnostics are * managed by the client. */ identifier?: string; /** * Whether the language has inter file dependencies meaning that * editing code in one file can result in a different diagnostic * set in another file. Inter file dependencies are common for * most programming languages and typically uncommon for linters. */ interFileDependencies: boolean; /** * The server provides support for workspace diagnostics as well. */ workspaceDiagnostics: boolean; }

三个关键配置项:

  • identifier(可选):客户端在管理诊断时使用的额外标识符。当同一服务器为不同文档集合提供多套诊断时,客户端与服务器通过该标识符对齐上下文;
  • interFileDependencies(必填):声明语言是否具有跨文件依赖,即编辑文件 A 会导致文件 B 的诊断集合变化。对多数编程语言(编译/类型系统)该值为true,而对典型的 linter 通常为false。该值直接决定客户端的拉取策略(见下文"实现建议");
  • workspaceDiagnostics(必填):服务器是否额外支持工作区级诊断拉取。

DiagnosticOptions还继承自WorkDoneProgressOptions(参见 workDoneProgress.md),即服务器可声明是否支持工作进度报告。

注册选项:DiagnosticRegistrationOptions

/** * Diagnostic registration options. * * @since 3.17.0 */ export interface DiagnosticRegistrationOptions extends TextDocumentRegistrationOptions, DiagnosticOptions, StaticRegistrationOptions { }

注册选项同时继承TextDocumentRegistrationOptions(文档选择器)与StaticRegistrationOptions(静态注册时的id),既支持initialize时的静态声明,也支持运行期通过client/registerCapability动态注册。

文档级诊断拉取:textDocument/diagnostic

请求语义

textDocument/diagnostic请求由客户端发送给服务器,要求服务器计算指定文档的诊断。与其他拉取请求一致,服务器针对的是文档当前已同步(synced)的版本——这与 LSP 的文档同步模型(textDocument/didChange,见 didChange.md)紧密相关,客户端保证请求时携带的版本状态与服务端一致。

请求参数:DocumentDiagnosticParams

/** * Parameters of the document diagnostic request. * * @since 3.17.0 */ export interface DocumentDiagnosticParams extends WorkDoneProgressParams, PartialResultParams { /** * The text document. */ textDocument: TextDocumentIdentifier; /** * The additional identifier provided during registration. */ identifier?: string; /** * The result id of a previous response if provided. */ previousResultId?: string; }
  • textDocument:目标文档标识(TextDocumentIdentifier,见 textDocumentIdentifier.md);
  • identifier:注册时提供的标识符,用于匹配对应的服务器诊断上下文;
  • previousResultId:上一次响应的resultId。这是增量机制的关键——服务器据此判断能否返回unchanged报告,避免重复传输未变化的诊断集合。

DocumentDiagnosticParams同时继承WorkDoneProgressParams与PartialResultParams(见 partialResultParams.md),说明该请求支持工作进度令牌与部分结果上报。

响应:DocumentDiagnosticReport

/** * The result of a document diagnostic pull request. A report can * either be a full report containing all diagnostics for the * requested document or a unchanged report indicating that nothing * has changed in terms of diagnostics in comparison to the last * pull request. * * @since 3.17.0 */ export type DocumentDiagnosticReport = RelatedFullDocumentDiagnosticReport | RelatedUnchangedDocumentDiagnosticReport;

响应是一个可辨识联合(discriminated union),通过kind字段区分两种形态:

/** * The document diagnostic report kinds. * * @since 3.17.0 */ export namespace DocumentDiagnosticReportKind { /** * A diagnostic report with a full * set of problems. */ export const Full = 'full'; /** * A report indicating that the last * returned report is still accurate. */ export const Unchanged = 'unchanged'; } export type DocumentDiagnosticReportKind = 'full' | 'unchanged';

full报告——FullDocumentDiagnosticReport:

/** * A diagnostic report with a full set of problems. * * @since 3.17.0 */ export interface FullDocumentDiagnosticReport { /** * A full document diagnostic report. */ kind: DocumentDiagnosticReportKind.Full; /** * An optional result id. If provided it will * be sent on the next diagnostic request for the * same document. */ resultId?: string; /** * The actual items. */ items: Diagnostic[]; }

items是完整的诊断列表(Diagnostic类型定义见 diagnostic.md,包含range、severity(1=Error、2=Warning、3=Information、4=Hint)、code、codeDescription、source、message、tags(1=Unnecessary、2=Deprecated)、relatedInformation与data等字段)。resultId可选:一旦提供,客户端会在下一次对该文档的诊断请求中原样带回。

unchanged报告——UnchangedDocumentDiagnosticReport:

/** * A diagnostic report indicating that the last returned * report is still accurate. * * @since 3.17.0 */ export interface UnchangedDocumentDiagnosticReport { /** * A document diagnostic report indicating * no changes to the last result. A server can * only return `unchanged` if result ids are * provided. */ kind: DocumentDiagnosticReportKind.Unchanged; /** * A result id which will be sent on the next * diagnostic request for the same document. */ resultId: string; }

注意约束:服务器只有在客户端提供了previousResultId时才有资格返回unchanged,且unchanged报告中的resultId是必填的,用于下一轮请求。

相关文档报告(relatedDocuments)

两种文档报告都可通过扩展携带相关文档的诊断——这是跨文件依赖语言(如 C/C++)的关键能力:

/** * A full diagnostic report with a set of related documents. * * @since 3.17.0 */ export interface RelatedFullDocumentDiagnosticReport extends FullDocumentDiagnosticReport { /** * Diagnostics of related documents. This information is useful * in programming languages where code in a file A can generate * diagnostics in a file B which A depends on. An example of * such a language is C/C++ where macro definitions in a file * a.cpp and result in errors in a header file b.hpp. * * @since 3.17.0 */ relatedDocuments?: { [uri: string /** DocumentUri */]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; }; }
/** * An unchanged diagnostic report with a set of related documents. * * @since 3.17.0 */ export interface RelatedUnchangedDocumentDiagnosticReport extends UnchangedDocumentDiagnosticReport { /** * Diagnostics of related documents. This information is useful * in programming languages where code in a file A can generate * diagnostics in a file B which A depends on. An example of * such a language is C/C++ where macro definitions in a file * a.cpp and result in errors in a header file b.hpp. * * @since 3.17.0 */ relatedDocuments?: { [uri: string /** DocumentUri */]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; }; }

relatedDocuments以DocumentUri → 报告的映射形式给出:FullDocumentDiagnosticReport与UnchangedDocumentDiagnosticReport均可作为相关文档的报告值。规范的典型场景示例是 C/C++:a.cpp中的宏定义可能导致其依赖的头文件b.hpp中产生错误——这类"编辑 A 文件、报错在 B 文件"的场景正是relatedDocuments存在的原因。

部分结果(Partial Result)

支持部分结果时,协议要求第一个发送的字面量必须是DocumentDiagnosticReport,随后可跟随 n 个DocumentDiagnosticReportPartialResult字面量:

/** * A partial result for a document diagnostic report. * * @since 3.17.0 */ export interface DocumentDiagnosticReportPartialResult { relatedDocuments: { [uri: string /** DocumentUri */]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport; }; }

即:第一个分片必须是合法、完整的DocumentDiagnosticReport(保证请求可被整体消费),之后的每个分片以relatedDocuments形式增量补充相关文档的诊断。

错误处理:ServerCancelled与重触发

当请求执行期间发生异常时,服务器返回带code和message的错误。特殊地,服务器被允许返回错误码ServerCancelled,表示"当前无法计算该结果"。此时可附带DiagnosticServerCancellationData数据指示客户端是否应重新触发请求:

/** * Cancellation data returned from a diagnostic request. * * @since 3.17.0 */ export interface DiagnosticServerCancellationData { retriggerRequest: boolean; }

关键默认值:如果未提供数据,默认为{ retriggerRequest: true }——即客户端应当重新发起诊断拉取。这一机制为服务器提供了"此刻忙/资源不足,稍后再来"的协商出口,避免了诊断拉取模型下服务器被请求淹没或被迫返回过时结果。

工作区级诊断拉取:workspace/diagnostic

请求语义与流式行为

workspace/diagnostic请求同样由客户端发起,目标是工作区范围内的诊断——这些诊断在推送模型下原本由服务器主动推送。与文档级请求的关键差异在于:

  • 该请求可以长时间运行(long running),且不绑定到某个具体的工作区或文档状态;
  • 如果客户端支持工作区诊断拉取的流式(streaming)输出,协议允许对同一个文档 URI 多次提供诊断报告,最后一次上报者胜出(the last one reported will win over previous reports)。

文档拉取与工作区拉取的冲突裁决

客户端可能同时发起两类拉取:对某文档既收到工作区诊断报告,又单独发起文档级诊断拉取。此时客户端必须决定展示哪一份,规范给出两条裁决原则:

  1. 更高文档版本(version)的诊断胜出——注意文档版本号是持续递增的;
  2. 文档拉取(document pull)的诊断胜出于工作区拉取(workspace pull)的诊断。

请求参数:WorkspaceDiagnosticParams

/** * Parameters of the workspace diagnostic request. * * @since 3.17.0 */ export interface WorkspaceDiagnosticParams extends WorkDoneProgressParams, PartialResultParams { /** * The additional identifier provided during registration. */ identifier?: string; /** * The currently known diagnostic reports with their * previous result ids. */ previousResultIds: PreviousResultId[]; }

与文档级不同,工作区请求通过previousResultIds批量携带客户端已知的所有文档 resultId,让服务器只返回发生变化的部分:

/** * A previous result id in a workspace pull request. * * @since 3.17.0 */ export interface PreviousResultId { /** * The URI for which the client knows a * result id. */ uri: DocumentUri; /** * The value of the previous result id. */ value: string; }

响应:WorkspaceDiagnosticReport

/** * A workspace diagnostic report. * * @since 3.17.0 */ export interface WorkspaceDiagnosticReport { items: WorkspaceDocumentDiagnosticReport[]; }

items中的每个元素是WorkspaceDocumentDiagnosticReport,它是以下两种形态的联合:

/** * A full document diagnostic report for a workspace diagnostic result. * * @since 3.17.0 */ export interface WorkspaceFullDocumentDiagnosticReport extends FullDocumentDiagnosticReport { /** * The URI for which diagnostic information is reported. */ uri: DocumentUri; /** * The version number for which the diagnostics are reported. * If the document is not marked as open `null` can be provided. */ version: integer | null; }
/** * An unchanged document diagnostic report for a workspace diagnostic result. * * @since 3.17.0 */ export interface WorkspaceUnchangedDocumentDiagnosticReport extends UnchangedDocumentDiagnosticReport { /** * The URI for which diagnostic information is reported. */ uri: DocumentUri; /** * The version number for which the diagnostics are reported. * If the document is not marked as open `null` can be provided. */ version: integer | null; };
/** * A workspace diagnostic document report. * * @since 3.17.0 */ export type WorkspaceDocumentDiagnosticReport = WorkspaceFullDocumentDiagnosticReport | WorkspaceUnchangedDocumentDiagnosticReport;

工作区级报告在文档级报告之上额外增加了两个字段:

  • uri:报告所属文档的 URI;
  • version:诊断对应的文档版本号;若文档未标记为打开(open)状态,可提供null。

version字段直接服务于前面提到的冲突裁决规则——客户端正是依据版本号比较不同来源诊断的新旧程度。

部分结果(Partial Result)

与文档级一致,工作区请求的部分结果要求第一个字面量必须是WorkspaceDiagnosticReport,随后可跟随 n 个WorkspaceDiagnosticReportPartialResult:

/** * A partial result for a workspace diagnostic report. * * @since 3.17.0 */ export interface WorkspaceDiagnosticReportPartialResult { items: WorkspaceDocumentDiagnosticReport[]; }

错误处理

工作区请求的错误语义与文档级完全相同:可返回ServerCancelled错误码,并携带DiagnosticServerCancellationData决定是否让客户端重触发;未提供数据时默认{ retriggerRequest: true }。

诊断刷新:workspace/diagnostic/refresh

客户端能力:workspace.diagnostics

workspace/diagnostic/refresh是从服务器发送到客户端的请求。服务器用它要求客户端刷新所有需要的文档与工作区诊断。客户端能力声明如下:

属性名(可选):workspace.diagnostics,类型为DiagnosticWorkspaceClientCapabilities:

/** * Workspace client capabilities specific to diagnostic pull requests. * * @since 3.17.0 */ export interface DiagnosticWorkspaceClientCapabilities { /** * Whether the client implementation supports a refresh request sent from * the server to the client. * * Note that this event is global and will force the client to refresh all * pulled diagnostics currently shown. It should be used with absolute care * and is useful for situation where a server for example detects a project * wide change that requires such a calculation. */ refreshSupport?: boolean; }

请求与响应

  • method:workspace/diagnostic/refresh
  • params:无
  • result:void
  • error:请求执行期间发生异常时返回code与message

规范特别强调该事件的全局性:一旦发出,客户端必须刷新当前展示的所有已拉取诊断。因此它必须被极其谨慎地使用——典型场景是服务器检测到项目级配置变更(如tsconfig.json、pyproject.toml等被修改)需要全量重算诊断时。

客户端实现建议(Implementation Considerations)

LSP 规范不强制任何具体的客户端实现方式(这通常取决于客户端 UI 的行为),但针对诊断同时存在于文档级与工作区级这一特点,给出了三条可落地的建议:

  • 客户端应当对用户正在输入(typing)的文档进行主动、频繁的拉取——这是拉取模型相比推送模型最直接的优势:把计算资源优先投向用户当前关注的文档;
  • 若服务器声明了interFileDependencies(跨文件依赖),客户端还应拉取可见文档(visible documents)的诊断以确保准确性,但此类拉取应降低频率——可见文档的优先级低于正在输入文档,避免过度占用服务器资源;
  • 若服务器声明了工作区拉取支持,客户端还应拉取工作区诊断。建议客户端为工作区拉取实现部分结果进度(partial result progress),以便服务器将请求保持打开很长时间;当服务器关闭一个工作区诊断拉取请求时,客户端应重新触发该请求。

元模型视角:协议定义的一手证据

上述三个方法在仓库的元模型文件中均有精确定义,可作为实现方核对协议结构的权威依据。在 metaModel.json 中:

  • textDocument/diagnostic(约 L932 起):messageDirection为clientToServer,params引用DocumentDiagnosticParams,result引用DocumentDiagnosticReport,partialResult引用DocumentDiagnosticReportPartialResult,errorData引用DiagnosticServerCancellationData,registrationOptions引用DiagnosticRegistrationOptions;
  • workspace/diagnostic(约 L958 起):同为clientToServer方向,params为WorkspaceDiagnosticParams,result为WorkspaceDiagnosticReport,并同样声明partialResult与errorData;
  • workspace/diagnostic/refresh(约 L980 起):messageDirection为serverToClient,result为null(void),params为空——与文档中的描述完全一致。

这与 3.17 规范主文档 specification.md 的组织方式相互印证:该文件在第 651 行通过{% include_relative language/pullDiagnostics.md %}将本主题嵌入完整的语言特性章节,紧随 publishDiagnostics.md 之后,构成"推送 → 拉取"的对照叙述。

结语:从"服务器决定何时报"到"客户端决定何时取"

Pull Diagnostics 是 LSP 3.17 中对诊断模型的一次方向性调整:计算时机与计算范围的决定权从服务器移交给了客户端。服务器通过DiagnosticOptions.interFileDependencies与workspaceDiagnostics声明自身能力,客户端据此决定按文档、按可见性、按工作区三个粒度组织拉取节奏;resultId与full/unchanged报告类型让增量更新变得廉价;relatedDocuments覆盖了跨文件依赖语言的诊断传播;workspace/diagnostic/refresh则保留了服务器在项目级变更时的主动干预通道。对于 LSP 服务器与客户端实现者而言,理解这一整套协议结构(建议结合 metaModel.json 与 diagnostic.md 对照阅读),是构建低延迟、高准确度诊断体验的基础。

  • 开发工具

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

Defines a common protocol for language servers.

项目地址:https://gitcode.com/gh_mirrors/la/language-server-protocol
点击查看免费下载
上一篇:Ruby on Rails数据统计最佳实践:descriptive_statistics gem集成教程
下一篇:终极自动化测试学习指南:程序员必知的10个高效测试网站

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询