ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SemanticTokens 完整指南:LSP 语义标记协议解析

SemanticTokens 完整指南:LSP 语义标记协议解析 开发工具【免费下载链接】language-server-protocolDefines a common protocol for language servers.项目地址https://gitcode.com/gh_mirrors/la/language-server-protocol点击查看免费下载本文基于开源仓库 language-server-protocolLSP 规范仓库中的 semanticTokens.md3.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或functiontoken 修饰符类似于static或async。协议预定义了一套 token 类型和修饰符但允许客户端扩展并在对应的客户端能力client capability中宣告其支持的值。2.1 预定义 Token 类型SemanticTokenTypesexport enum SemanticTokenTypes { namespace namespace, /** * Represents a generic type. Acts as a fallback for types which * cant 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 修饰符SemanticTokenModifiersexport 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*i1deltaStarttoken 起始字符相对前一个 token 的起始字符同行时为相对差值否则相对 05*i2lengthtoken 的长度5*i3tokenType在SemanticTokensLegend.tokenTypes中查表规范要求tokenType 655365*i4tokenModifiers每个置位 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: [] }第一步设计 LegendLegend 必须在注册时预先提供并覆盖所有可能的类型与修饰符。本示例使用{ 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]propertytokenModifiers: 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将它们整体移到状态 Sm1。客户端在应用编辑时不能假定它们是有序的。一个简单可靠的算法是先对编辑排序再从数组的尾部向头部逐个应用从而避免前面编辑影响后面编辑的偏移量。六、客户端能力SemanticTokensClientCapabilities语义标记请求的客户端能力定义如下属性名可选textDocument.semanticTokensinterface 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 servers 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还可以进一步声明是否支持 deltaexport 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 | SemanticTokensRegistrationOptionsexport 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请求methodtextDocument/semanticTokens/fullparamsSemanticTokensParamsexport interface SemanticTokensParams extends WorkDoneProgressParams, PartialResultParams { /** * The text document. */ textDocument: TextDocumentIdentifier; }响应resultSemanticTokens | nullexport 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 resultSemanticTokensPartialResultexport interface SemanticTokensPartialResult { data: uinteger[]; }error请求过程中出现异常时返回错误 code 与 message。SemanticTokens.resultId是 delta 机制的关键一旦服务端提供了 resultId 且客户端支持 delta 更新客户端会在下一次请求中携带该 ID服务端即可只发送 delta而非重新计算全部 token。九、请求二整个文件的语义标记 Deltafull/delta请求methodtextDocument/semanticTokens/full/deltaparamsSemanticTokensDeltaParamsexport 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; }响应resultSemanticTokens | SemanticTokensDelta | nullexport 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 | SemanticTokensDeltaPartialResultexport interface SemanticTokensDeltaPartialResult { edits: SemanticTokensEdit[]; }error请求过程中出现异常时返回错误 code 与 message。注意previousResultId可以指向上一次收到的任意响应全量响应或 delta 响应均可服务端据此计算从该状态到新状态的编辑序列。十、请求三请求指定范围的语义标记range在两种场景下只计算可见范围内的语义标记是有益的加速渲染用户打开文件时仅渲染可见区域以加快 UI 响应。此场景下服务端还应同时实现textDocument/semanticTokens/full以支持无闪烁滚动与 minimap 的语义着色。全量计算代价过高如果为整个文档计算语义标记过于昂贵服务端可以只提供 range 调用。但此时客户端可能无法正确渲染 minimap甚至决定完全不显示任何语义标记。服务端的响应范围允许超出请求范围但前提是超出部分的语义标记必须完整且正确。如果位于范围起点或终点的 token 与请求范围只有部分重叠服务端应在响应中包含这些 token。请求methodtextDocument/semanticTokens/rangeparamsSemanticTokensRangeParamsexport interface SemanticTokensRangeParams extends WorkDoneProgressParams, PartialResultParams { /** * The text document. */ textDocument: TextDocumentIdentifier; /** * The range the semantic tokens are requested for. */ range: Range; }响应resultSemanticTokens | null部分结果SemanticTokensPartialResulterror请求过程中出现异常时返回错误 code 与 message。十一、服务端发起的全量刷新workspace/semanticTokens/refresh与前三个由客户端发起的请求不同workspace/semanticTokens/refresh请求由服务端发送给客户端。服务端可借此要求客户端刷新由该服务端提供语义标记的编辑器作为结果客户端应要求服务端重新计算这些编辑器的语义标记。典型场景服务端检测到项目级的配置变更需要重新计算所有语义标记。注意客户端仍保留延迟重算的自由——例如某编辑器当前不可见时客户端可以推迟重算。11.1 客户端能力属性名可选workspace.semanticTokens类型SemanticTokensWorkspaceClientCapabilitiesexport 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 请求定义methodworkspace/semanticTokens/refreshparams无resultvoiderror请求过程中出现异常时返回错误 code 与 message。十二、请求方法全景速查结合 3.19 metaModel.json 中的请求元数据可将语义标记相关的四条请求及其能力归属整理如下请求方法方向客户端能力服务端能力注册选项textDocument/semanticTokens/full客户端 → 服务端textDocument.semanticTokenssemanticTokensProviderSemanticTokensRegistrationOptionstextDocument/semanticTokens/full/delta客户端 → 服务端textDocument.semanticTokens.requests.full.deltasemanticTokensProvider.full.deltaSemanticTokensRegistrationOptionstextDocument/semanticTokens/range客户端 → 服务端textDocument.semanticTokens.requests.rangesemanticTokensProvider.range无workspace/semanticTokens/refresh服务端 → 客户端workspace.semanticTokens.refreshSupport无无十三、实现要点总结结合规范文档与仓库源码服务端实现语义标记时的关键决策清单如下Legend 先行在注册/能力声明时提供完整的legend覆盖所有会出现的类型与修饰符类型与修饰符尽量取自预定义枚举SemanticTokenTypes、SemanticTokenModifiers自定义值需在客户端能力中声明。统一采用relative格式目前协议唯一指定的格式按5 整数元组 相对位置编码tokenType保持小于 65536。位置编码协商deltaStart与length依据initialize阶段协商的positionEncoding默认为 UTF-16编码参考 initialize.md。尊重客户端边界能力multilineTokenSupport为 false 时 token 不得跨行overlappingTokenSupport为 false 时 token 不得重叠。善用 resultId deltafull 响应附带resultId客户端下次请求携带previousResultId服务端返回SemanticTokensEdit[]描述数字数组的变换多编辑应用采用排序 从后往前应用算法。range 与 full 的能力匹配若只提供 range需接受 minimap 渲染可能降级或完全不显示的后果若返回超出请求范围的数据必须保证其完整正确。刷新请求谨慎使用workspace/semanticTokens/refresh是全局操作仅在检测到项目级配置变更等场景使用并依赖客户端refreshSupport能力。以上全部内容均可在仓库对应规范文档与 3.19 metaModel.json 中交叉验证读者可继续查阅 3.19 规范总文档 获取语义标记与其他请求能力的完整上下文。赞分享开发工具【免费下载链接】language-server-protocolDefines a common protocol for language servers.项目地址https://gitcode.com/gh_mirrors/la/language-server-protocol点击查看免费下载相关推荐Dart SDK analysis_server 语义高亮实现机制从 HighlightRegion 到 LSP SemanticTokens 的完整链路Dart SDK analysis_server 语义高亮实现机制从 HighlightRegion 到 LSP SemanticTokens 的完整链路 本编程语言编译器语言运行时标准库开发工具Dart 语义高亮Semantic Highlighting设计解析从 language fidelity 原则到 LSP semanticTokens 实现Dart 语义高亮Semantic Highlighting设计解析从 language fidelity 原则到 LSP semanticTokens编程语言编译器语言运行时标准库开发工具如何在Helix编辑器中配置LSP客户端提升代码编辑效率的完整指南如何在Helix编辑器中配置LSP客户端提升代码编辑效率的完整指南 Helix是一款后现代模态文本编辑器以其高效的编辑体验和强大的功能而受到开发者喜爱。其中代码编辑器开发工具CLI上一篇Joy-Con Toolkit终极Switch手柄自定义与修复完全指南下一篇Joy-Con Toolkit终极指南让你的Switch手柄重获新生创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表