☰
SemanticTokens 完整指南:LSP 语义标记协议解析
2026/10/7 2:22:30 网站建设 项目流程
  • 开发工具

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

Defines a common protocol for language servers.

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

本文基于开源仓库 language-server-protocol(LSP 规范仓库)中的 semanticTokens.md(3.19 版本规范)展开,深入讲解 LSP 语义标记(Semantic Tokens)的完整技术细节。通过本文,读者将掌握语义标记的 token 类型/修饰符体系、legend 整数编码机制、相对位置编码算法、增量(delta)传输机制,以及 full / range / refresh 三类请求与客户端-服务端能力协商的完整实战方案。


一、语义标记概览:为什么需要它

语义标记(Semantic Tokens)是自LSP 3.16.0起引入的一项请求能力。它由客户端发送给服务端,用于为给定文件解析语义标记。与传统基于正则表达式的语法高亮不同,语义标记依赖语言特有的符号信息——例如一个标识符是class、function还是variable,从而提供更准确、更丰富的着色信息。

由于一次语义标记请求通常会产生非常大的结果集,协议因此支持将 token 用数字进行编码以压缩传输体积;此外还提供增量(delta)可选支持,让服务端在文件编辑后只发送变化部分而非全量数据。

该特性在仓库中的正式定义位于 _specifications/lsp/3.19/language/semanticTokens.md,并被 3.19 规范总文档 第 649 行通过{% include_relative language/semanticTokens.md %}引用,是 3.19 规范主体的组成部分。本文的源码证据均来自该仓库的 3.19 目录。

二、核心概念:Token 类型与修饰符

一个 token 由**一种 token 类型(token type)与一组 token 修饰符(token modifiers)**共同表示。token 类型类似于class或function,token 修饰符类似于static或async。

协议预定义了一套 token 类型和修饰符,但允许客户端扩展,并在对应的客户端能力(client capability)中宣告其支持的值。

2.1 预定义 Token 类型(SemanticTokenTypes)

export enum SemanticTokenTypes { namespace = 'namespace', /** * Represents a generic type. Acts as a fallback for types which * can't be mapped to a specific type like class or enum. */ type = 'type', class = 'class', enum = 'enum', interface = 'interface', struct = 'struct', typeParameter = 'typeParameter', parameter = 'parameter', variable = 'variable', property = 'property', enumMember = 'enumMember', event = 'event', function = 'function', method = 'method', macro = 'macro', keyword = 'keyword', modifier = 'modifier', comment = 'comment', string = 'string', number = 'number', regexp = 'regexp', operator = 'operator', /** * @since 3.17.0 */ decorator = 'decorator', /** * @since 3.18.0 */ label = 'label' }

值得注意的是,type是通用类型的兜底(fallback):当类型无法映射到class、enum等具体类型时使用。decorator自 3.17.0 加入,label自 3.18.0 加入——这两点可以从仓库 3.18/3.19 的 metaModel.json 中枚举SemanticTokenTypes的取值序列得到印证(含decorator与label)。

2.2 预定义 Token 修饰符(SemanticTokenModifiers)

export enum SemanticTokenModifiers { declaration = 'declaration', definition = 'definition', readonly = 'readonly', static = 'static', deprecated = 'deprecated', abstract = 'abstract', async = 'async', modification = 'modification', documentation = 'documentation', defaultLibrary = 'defaultLibrary' }

2.3 Token 格式(TokenFormat)

协议额外定义了一个 token 格式能力,以便未来扩展编码格式。目前唯一指定的格式是relative,表示 token 使用相对位置描述(详见下文"整数编码"):

export namespace TokenFormat { export const Relative: 'relative' = 'relative'; } export type TokenFormat = 'relative';

三、整数编码与 Legend 机制

3.1 为什么需要 Legend

在能力协商层面,类型和修饰符使用字符串定义;但真实的编码发生在整数层面。因此服务端必须让客户端知道:它使用哪个数字对应哪种类型/修饰符。这一映射通过legend实现:

export interface SemanticTokensLegend { /** * The token types a server uses. */ tokenTypes: string[]; /** * The token modifiers a server uses. */ tokenModifiers: string[]; }

3.2 类型查表与修饰符位标志

编码规则有两条核心约定:

  • 类型按索引查表:tokenType值为1即代表tokenTypes[1]。
  • 修饰符用位标志(bit flags):一个 token 类型可以携带多个修饰符,因此tokenModifier值为3时,先视为二进制0b00000011,表示第 0、1 位被置位,即[tokenModifiers[0], tokenModifiers[1]]。

3.3 相对位置的 5 整数元组

文件中的 token 位置有两种表达方式:绝对位置与相对位置。relative格式采用相对位置,因为文件编辑时大部分 token 彼此间保持相对稳定,这简化了服务端计算 delta的过程。

每个 token 用5 个整数表示。设文件中的第i个 token,其数组索引含义如下:

数组索引字段含义
5*ideltaLinetoken 所在行号,相对前一个 token 的行号
5*i+1deltaStarttoken 起始字符,相对前一个 token 的起始字符(同行时为相对差值,否则相对 0)
5*i+2lengthtoken 的长度
5*i+3tokenType在SemanticTokensLegend.tokenTypes中查表;规范要求tokenType < 65536
5*i+4tokenModifiers每个置位 bit 在SemanticTokensLegend.tokenModifiers中查表

3.4 位置编码(Position Encoding)约束

deltaStart与length必须使用客户端与服务端在initialize请求期间协商一致的编码方式进行编码。该协商机制定义于 3.19 的 initialize.md:

  • 客户端通过general.positionEncodings(类型PositionEncodingKind[])宣告支持的位置编码;为保持向后兼容,UTF-16 是强制编码,若数组中缺失'utf-16',服务端仍可假定客户端支持 UTF-16;省略时默认为['utf-16'](见该文件第 632-653 行)。
  • 服务端通过ServerCapabilities.positionEncoding返回选定的编码;若客户端未提供任何编码,服务端唯一合法的返回值是'utf-16',省略时同样默认为'utf-16'(见第 759-773 行)。

3.5 跨行与重叠约束

  • 一个 token 是否可以跨多行,由客户端能力multilineTokenSupport决定。若不支持跨行,token 长度超过行尾时,应视为 token 在行尾结束,不会折行到下一行。
  • 客户端能力overlappingTokenSupport决定 token 之间是否允许重叠。

四、编码实战:从绝对位置到数字数组

规范用 3 个 token 的完整示例演示编码全过程。假设文件中有 3 个不重叠的单行 token:

{ line: 2, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, { line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, { line: 5, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] }

第一步:设计 Legend

Legend 必须在注册时预先提供,并覆盖所有可能的类型与修饰符。本示例使用:

{ tokenTypes: ['property', 'type', 'class'], tokenModifiers: ['private', 'static'] }

第二步:类型/修饰符转整数

利用 legend 将类型和修饰符编码为整数(类型查索引、修饰符用位标志):

{ line: 2, startChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, { line: 2, startChar: 10, length: 4, tokenType: 1, tokenModifiers: 0 }, { line: 5, startChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 }

这里tokenType: 0对应tokenTypes[0](property),tokenModifiers: 3对应private与static两个修饰符。

第三步:转为相对位置

将每个 token 相对前一个 token 表示。第二个 token 与第一个同行,startChar取差值10 - 5;第三个 token 与第二个不同行,startChar保持不变:

{ deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 }, { deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 1, tokenModifiers: 0 }, { deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 }

第四步:内联为单一数组

将每个 token 的 5 个字段平铺进单个数组,得到内存友好的表示:

// 1st token, 2nd token, 3rd token [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ]

五、Delta 机制:增量更新的数学原理

现在假设用户在文件开头输入了一个新的空行,文件中的 token 变为:

{ line: 3, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] }, { line: 3, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] }, { line: 6, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] }

执行同样的变换后得到:

// 1st token, 2nd token, 3rd token [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0]

注意:只有第一个数字从2变成了3。delta 就定义在这两个数字数组之间,不解释这些数字的任何语义——这与服务端发送给客户端用于修改文件内容的文本编辑(text edits)是同一思想:字符级编辑不假设字符的含义。

于是,[ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ]变换为[ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0]只需一条编辑描述:

{ start: 0, deleteCount: 1, data: [3] }

即:将数组中的第一个数字2替换为3。

5.1 多编辑的应用算法

语义 token 编辑在概念上与文档上的文本编辑行为一致:如果一条编辑描述包含 n 个编辑,所有 n 个编辑都基于数字数组的同一状态 Sm,将它们整体移到状态 Sm+1。客户端在应用编辑时不能假定它们是有序的。一个简单可靠的算法是:先对编辑排序,再从数组的尾部向头部逐个应用,从而避免前面编辑影响后面编辑的偏移量。

六、客户端能力:SemanticTokensClientCapabilities

语义标记请求的客户端能力定义如下(属性名可选:textDocument.semanticTokens):

interface SemanticTokensClientCapabilities { /** * Whether the 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; /** * Which requests the client supports and might send to the server * depending on the server's capability. Please note that clients might not * show semantic tokens or degrade some of the user experience if a range * or full request is advertised by the client but not provided by the * server. If, for example, the client capability `requests.full` and * `request.range` are both set to true but the server only provides a * range provider, the client might not render a minimap correctly or might * even decide to not show any semantic tokens at all. */ requests: ClientSemanticTokensRequestOptions; /** * The token types that the client supports. */ tokenTypes: string[]; /** * The token modifiers that the client supports. */ tokenModifiers: string[]; /** * The formats the client supports. */ formats: TokenFormat[]; /** * Whether the client supports tokens that can overlap each other. */ overlappingTokenSupport?: boolean; /** * Whether the client supports tokens that can span multiple lines. */ multilineTokenSupport?: boolean; /** * Whether the client allows the server to actively cancel a * semantic token request, e.g. supports returning * ErrorCodes.ServerCancelled. If a server does so, the client * needs to retrigger the request. * * @since 3.17.0 */ serverCancelSupport?: boolean; /** * Whether the client uses semantic tokens to augment existing * syntax tokens. If set to `true`, client side created syntax * tokens and semantic tokens are both used for colorization. If * set to `false`, the client only uses the returned semantic tokens * for colorization. * * If the value is `undefined` then the client behavior is not * specified. * * @since 3.17.0 */ augmentsSyntaxTokens?: boolean; }

6.1 请求选项子类型

客户端具体支持哪些请求,通过requests字段细分:

export type ClientSemanticTokensRequestOptions = { /** * The client will send the `textDocument/semanticTokens/range` request if * the server provides a corresponding handler. */ range?: boolean | { }; /** * The client will send the `textDocument/semanticTokens/full` request if * the server provides a corresponding handler. */ full?: boolean | ClientSemanticTokensRequestFullDelta; };

其中full还可以进一步声明是否支持 delta:

export type ClientSemanticTokensRequestFullDelta = { /** * The client will send the `textDocument/semanticTokens/full/delta` request if * the server provides a corresponding handler. */ delta?: boolean; };

6.2 能力协商的坑:请求能力与服务端能力必须匹配

规范在requests字段的注释中明确提醒了一个实战陷阱:客户端声明了requests.full与requests.range为 true,但服务端只提供 range 提供者时,客户端可能无法正确渲染 minimap,甚至决定完全不显示任何语义标记。因此客户端声明什么、服务端就应尽量提供什么,能力协商要保持对称。

七、服务端能力:SemanticTokensOptions 与注册选项

服务端能力属性名(可选):semanticTokensProvider,类型为SemanticTokensOptions | SemanticTokensRegistrationOptions:

export interface SemanticTokensOptions extends WorkDoneProgressOptions { /** * The legend used by the server. */ legend: SemanticTokensLegend; /** * Server supports providing semantic tokens for a specific range * of a document. */ range?: boolean | { }; /** * Server supports providing semantic tokens for a full document. */ full?: boolean | SemanticTokensFullDelta; }

服务端是否支持全文档 delta:

/** * Semantic tokens options to support deltas for full documents */ export type SemanticTokensFullDelta = { /** * The server supports deltas for full documents. */ delta?: boolean; };

注册选项(用于动态注册)继承文本文档注册选项、语义标记选项与静态注册选项:

export interface SemanticTokensRegistrationOptions extends TextDocumentRegistrationOptions, SemanticTokensOptions, StaticRegistrationOptions { }

关键点:由于注册选项统一处理 range、full 与 delta 三类请求,用于注册语义标记请求的方法统一是textDocument/semanticTokens,而不是下面描述的具体方法之一。这一点在 3.19 metaModel.json 中也有印证:textDocument/semanticTokens/full、/full/delta、/range三个请求中,仅full与full/delta声明了SemanticTokensRegistrationOptions作为注册选项。

八、请求一:请求整个文件的语义标记(full)

请求:

  • method:textDocument/semanticTokens/full
  • params:SemanticTokensParams
export interface SemanticTokensParams extends WorkDoneProgressParams, PartialResultParams { /** * The text document. */ textDocument: TextDocumentIdentifier; }

响应:

  • result:SemanticTokens | null
export interface SemanticTokens { /** * An optional result ID. If provided and clients support delta updating, * the client will include the result ID in the next semantic token request. * A server can then, instead of computing all semantic tokens again, simply * send a delta. */ resultId?: string; /** * The actual tokens. */ data: uinteger[]; }
  • 部分结果(partial result):SemanticTokensPartialResult
export interface SemanticTokensPartialResult { data: uinteger[]; }
  • error:请求过程中出现异常时,返回错误 code 与 message。

SemanticTokens.resultId是 delta 机制的关键:一旦服务端提供了 resultId 且客户端支持 delta 更新,客户端会在下一次请求中携带该 ID,服务端即可只发送 delta而非重新计算全部 token。

九、请求二:整个文件的语义标记 Delta(full/delta)

请求:

  • method:textDocument/semanticTokens/full/delta
  • params:SemanticTokensDeltaParams
export interface SemanticTokensDeltaParams extends WorkDoneProgressParams, PartialResultParams { /** * The text document. */ textDocument: TextDocumentIdentifier; /** * The result ID of a previous response. The result ID can either point to * a full response or a delta response, depending on what was received last. */ previousResultId: string; }

响应:

  • result:SemanticTokens | SemanticTokensDelta | null
export interface SemanticTokensDelta { readonly resultId?: string; /** * The semantic token edits to transform a previous result into a new * result. */ edits: SemanticTokensEdit[]; }

其中每个编辑:

export interface SemanticTokensEdit { /** * The start offset of the edit. */ start: uinteger; /** * The count of elements to remove. */ deleteCount: uinteger; /** * The elements to insert. */ data?: uinteger[]; }
  • 部分结果:SemanticTokensPartialResult | SemanticTokensDeltaPartialResult
export interface SemanticTokensDeltaPartialResult { edits: SemanticTokensEdit[]; }
  • error:请求过程中出现异常时,返回错误 code 与 message。

注意previousResultId可以指向上一次收到的任意响应(全量响应或 delta 响应均可),服务端据此计算从该状态到新状态的编辑序列。

十、请求三:请求指定范围的语义标记(range)

在两种场景下,只计算可见范围内的语义标记是有益的:

  1. 加速渲染:用户打开文件时,仅渲染可见区域以加快 UI 响应。此场景下服务端还应同时实现textDocument/semanticTokens/full,以支持无闪烁滚动与 minimap 的语义着色。
  2. 全量计算代价过高:如果为整个文档计算语义标记过于昂贵,服务端可以只提供 range 调用。但此时客户端可能无法正确渲染 minimap,甚至决定完全不显示任何语义标记。

服务端的响应范围允许超出请求范围,但前提是:超出部分的语义标记必须完整且正确。如果位于范围起点或终点的 token 与请求范围只有部分重叠,服务端应在响应中包含这些 token。

请求:

  • method:textDocument/semanticTokens/range
  • params:SemanticTokensRangeParams
export interface SemanticTokensRangeParams extends WorkDoneProgressParams, PartialResultParams { /** * The text document. */ textDocument: TextDocumentIdentifier; /** * The range the semantic tokens are requested for. */ range: Range; }

响应:

  • result:SemanticTokens | null
  • 部分结果:SemanticTokensPartialResult
  • error:请求过程中出现异常时,返回错误 code 与 message。

十一、服务端发起的全量刷新:workspace/semanticTokens/refresh

与前三个由客户端发起的请求不同,workspace/semanticTokens/refresh请求由服务端发送给客户端。服务端可借此要求客户端刷新由该服务端提供语义标记的编辑器;作为结果,客户端应要求服务端重新计算这些编辑器的语义标记。

典型场景:服务端检测到项目级的配置变更,需要重新计算所有语义标记。注意客户端仍保留延迟重算的自由——例如某编辑器当前不可见时,客户端可以推迟重算。

11.1 客户端能力

  • 属性名(可选):workspace.semanticTokens
  • 类型:SemanticTokensWorkspaceClientCapabilities
export interface SemanticTokensWorkspaceClientCapabilities { /** * 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 * semantic tokens 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; }

规范特别强调:该事件是全局的,会强制客户端刷新当前显示的所有语义标记,必须极其谨慎地使用。

11.2 请求定义

  • method:workspace/semanticTokens/refresh
  • params:无
  • result:void
  • error:请求过程中出现异常时,返回错误 code 与 message。

十二、请求方法全景速查

结合 3.19 metaModel.json 中的请求元数据,可将语义标记相关的四条请求及其能力归属整理如下:

请求方法方向客户端能力服务端能力注册选项
textDocument/semanticTokens/full客户端 → 服务端textDocument.semanticTokenssemanticTokensProviderSemanticTokensRegistrationOptions
textDocument/semanticTokens/full/delta客户端 → 服务端textDocument.semanticTokens.requests.full.deltasemanticTokensProvider.full.deltaSemanticTokensRegistrationOptions
textDocument/semanticTokens/range客户端 → 服务端textDocument.semanticTokens.requests.rangesemanticTokensProvider.range无
workspace/semanticTokens/refresh服务端 → 客户端workspace.semanticTokens.refreshSupport无无

十三、实现要点总结

结合规范文档与仓库源码,服务端实现语义标记时的关键决策清单如下:

  1. Legend 先行:在注册/能力声明时提供完整的legend,覆盖所有会出现的类型与修饰符;类型与修饰符尽量取自预定义枚举(SemanticTokenTypes、SemanticTokenModifiers),自定义值需在客户端能力中声明。
  2. 统一采用relative格式:目前协议唯一指定的格式,按"5 整数元组 + 相对位置"编码;tokenType保持小于 65536。
  3. 位置编码协商:deltaStart与length依据initialize阶段协商的positionEncoding(默认为 UTF-16)编码,参考 initialize.md。
  4. 尊重客户端边界能力:multilineTokenSupport为 false 时 token 不得跨行;overlappingTokenSupport为 false 时 token 不得重叠。
  5. 善用 resultId + delta:full 响应附带resultId,客户端下次请求携带previousResultId,服务端返回SemanticTokensEdit[]描述数字数组的变换;多编辑应用采用"排序 + 从后往前应用"算法。
  6. range 与 full 的能力匹配:若只提供 range,需接受 minimap 渲染可能降级或完全不显示的后果;若返回超出请求范围的数据,必须保证其完整正确。
  7. 刷新请求谨慎使用:workspace/semanticTokens/refresh是全局操作,仅在检测到项目级配置变更等场景使用,并依赖客户端refreshSupport能力。

以上全部内容均可在仓库对应规范文档与 3.19 metaModel.json 中交叉验证,读者可继续查阅 3.19 规范总文档 获取语义标记与其他请求能力的完整上下文。

  • 开发工具

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

Defines a common protocol for language servers.

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

相关推荐

上一篇:Joy-Con Toolkit:终极Switch手柄自定义与修复完全指南
下一篇:Joy-Con Toolkit终极指南:让你的Switch手柄重获新生

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

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

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

立即咨询