Semantic Tokens

从版本 3.16.0 开始

请求从客户端发送到服务器,以解析给定文件的语义令牌。语义标记用于向依赖于语言特定符号信息的文件添加其他颜色信息。语义令牌请求通常会产生较大的结果。因此,该协议支持用数字对令牌进行编码。此外,还提供对增量的可选支持。

没有语义突出显示:

no-semantic-highlighting

使用语义突出显示:

with-semantic-highlighting

一般概念

令牌使用一种令牌类型和多个个令牌修饰符表示。令牌类型类似于 classfunction,令牌修饰符类似于 staticasync。该协议定义了一组令牌类型和修饰符,但允许客户端扩展这些标记类型和修饰符,并在相应的客户端功能中宣布它们支持的值。预定义的值为:

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'
}
export enum SemanticTokenModifiers {
	declaration = 'declaration',
	definition = 'definition',
	readonly = 'readonly',
	static = 'static',
	deprecated = 'deprecated',
	abstract = 'abstract',
	async = 'async',
	modification = 'modification',
	documentation = 'documentation',
	defaultLibrary = 'defaultLibrary'
}

该协议定义了一个额外的令牌格式功能,以允许将来扩展该格式。当前指定的唯一格式是 relative,表示使用相对位置描述标记(请参阅下面的标记的整数编码)。

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

export type TokenFormat = 'relative';

令牌的整数编码

在功能级别上,类型和修饰符是使用字符串定义的。但是,真正的编码是使用数字进行的。因此,服务器需要让客户端知道它对哪些类型和修饰符使用了哪些数字。它们使用图例执行此操作,图例定义如下:

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

	/**
	 * The token modifiers a server uses.
	 */
	tokenModifiers: string[];
}

令牌类型按索引查找,因此 tokenType 值为 1 表示 tokenTypes[1]。由于令牌类型可以有 n 个修饰符,因此可以使用位标志设置多个令牌修饰符,因此 tokenModifier 值 3 首先被视为二进制 0b00000011,这意味着 [tokenModifiers[0], tokenModifiers[1]],因为设置了bit位 0 和 1。

在文件中表示令牌的位置有多种方式。绝对位置或相对位置。标记格式相对的协议使用相对位置,因为在文件中进行编辑时,大多数标记彼此保持稳定。如果服务器支持增量,则可以简化增量的计算。因此,每个标记都使用 5 个整数表示。文件中的特定令牌 i 由以下数组索引组成:

  • 在索引 5*ideltaLine: 标记行号,相对于前一个标记
  • 在索引 5*i+1deltaStart: 标记开始字符,相对于前一个标记(相对于 0 或前一个标记的开始,如果它们在同一行上)
  • 在索引 5*i+2length: 令牌的长度。
  • 在索引 5*i+3tokenType:: 将在 SemanticTokensLegend.tokenTypes 中查找。我们目前要求 tokenType < 65536。
  • 在索引 5*i+4 - tokenModifiers:每个设置的位将在 SemanticTokensLegend.tokenModifiers 中查找

deltaStartlength 值必须使用客户端和服务器在初始化请求期间商定的编码进行编码(另请参阅 TextDocuments)。令牌是否可以跨多行由客户端功能 multilineTokenSupport 定义。如果不支持多行标记,并且标记长度使其超过行尾,则应将其视为令牌在行尾结束,并且不会换行到下一行。

客户端功能 overlappingTokenSupport 定义令牌是否可以相互重叠。

示例

让我们看一个具体的例子,它使用没有重叠的单行标记来编码数字数组中具有 3 个标记的文件。我们从绝对位置开始,演示如何轻松地将它们转换为相对位置:

{ 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: [] }

首先,必须设计一个图例。此图例必须在注册时预先提供,并捕获所有可能的令牌类型和修饰符。对于示例,我们使用以下图例:

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

第一个转换步骤是使用图例将 tokenTypetokenModifiers 编码为整数。如前所述,token 类型是按索引查找的,因此 tokenType 值为 1 表示 tokenTypes[1]。可以使用位标志设置多个令牌修饰符,因此 tokenModifier3 首先被视为二进制 0b00000011,这意味着 [tokenModifiers[0]、tokenModifiers[1]],因为设置了bit位 0 和 1。使用此图例,令牌现在为:

{ 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 }

下一步是表示相对于文件中前一个标记的每个标记。在本例中,第二个标记与第一个标记在同一行上,因此第二个标记的 startChar 是相对于第一个标记的 startChar 创建的,因此它将是 10 - 5。第三个令牌与第二个令牌位于不同的行上,因此不会更改第三个令牌的 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 }

最后,最后一步是将令牌的 5 个字段中的每一个都内联到单个数组中,这是一种内存友好的表示形式:

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

现在假设用户在文件开头键入一个新的空行,这会导致文件中出现以下标记:

{ 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]

现在,增量(delta) 体现在这些数字数组上,没有任何形式的表单去解释这些数字的含义。这类似于从服务器发送到客户端以修改文件内容的文本文档编辑。这些都是基于字符的,不会对字符的含义做出任何假设。因此,[ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] 可以使用编辑描述 { start: 0, deleteCount: 1, data: [3] } 转换为 [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0] ,它告诉客户端简单地将数组中的第一个数字(例如 2)替换为 3

语义标记编辑在概念上的行为类似于文档上的 TextEdit:如果编辑描述包含 n 个编辑,则所有 n 个编辑都基于数字数组的相同状态 Sm。他们会将数字数组从状态 Sm 移动到 Sm+1。应用编辑的客户端不得假定它们已排序。将它们应用于数字数组的一种简单算法是对编辑内容进行排序,然后从数字数组的后部到前部应用它们。

消息文档

客户端能力(Client capability):

  • 属性路径: textDocument.semanticTokens
  • 属性类型: SemanticTokensClientCapabilities, 定义如下:
interface SemanticTokensClientCapabilities {
	/**
	 * 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;

	/**
	 * 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: {
		/**
		 * 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 | {
			/**
			 * The client will send the `textDocument/semanticTokens/full/delta`
			 * request if the server provides a corresponding handler.
			 */
			delta?: boolean;
		};
	};

	/**
	 * The token types that the client supports.
	 */
	tokenTypes: string[];

	/**
	 * The token modifiers that the client supports.
	 */
	tokenModifiers: string[];

	/**
	 * The formats the clients 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 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;
}

服务端能力(Server capability):

  • 属性路径: 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 | {
		/**
		 * The server supports deltas for full documents.
		 */
		delta?: boolean;
	};
}

注册选项(Registration Options): SemanticTokensRegistrationOptions, 定义如下:

export interface SemanticTokensRegistrationOptions extends
	TextDocumentRegistrationOptions, SemanticTokensOptions,
	StaticRegistrationOptions {
}

由于注册选项处理范围、完整和增量请求,因此用于注册语义令牌请求的方法是 textDocument/semanticTokens,而不是下面描述的特定方法之一。

请求整个文件的语义令牌

请求(Request):

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

响应(Response):

  • 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: codemessage,以防在请求期间发生异常。

请求整个文件的语义令牌增量

请求(Request):

  • 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;
}

响应(Response):

  • 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[];
}
  • partial result: SemanticTokensDeltaPartialResult, 定义如下:
export interface SemanticTokensDeltaPartialResult {
	edits: SemanticTokensEdit[];
}
  • error: codemessage,以防在请求期间发生异常。

请求范围的语义令牌

在两种用例中,仅计算可见范围的语义标记可能会有所帮助:

  • 以便在用户打开文件时在用户界面中更快地呈现令牌。在此用例中,服务器还应该实现 textDocument/semanticTokens/full 请求,以允许小地图的闪烁自由滚动和语义着色。

  • 如果为完整文档计算语义标记的成本太高,服务器只能提供范围调用。在这种情况下,客户端可能无法正确呈现小地图,甚至可能决定根本不显示任何语义标记。

允许服务器计算比客户端请求的范围更广的语义令牌。但是,如果服务器这样做,则更广泛范围的语义标记必须完整且正确。

请求(Request):

  • 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;
}

响应(Response):

  • result: SemanticTokens | null
  • partial result: SemanticTokensPartialResult
  • error: codemessage,以防在请求期间发生异常。

请求刷新所有语义标记

workspace/semanticTokens/refresh 请求从服务器发送到客户端。服务器可以使用它来要求客户端刷新此服务器为其提供语义标记的编辑器。因此,客户端应要求服务器重新计算这些编辑器的语义标记。如果服务器检测到需要重新计算所有语义标记的项目范围的配置更改,这将非常有用。请注意,例如,如果编辑器当前不可见,客户端仍然可以自由延迟语义标记的重新计算。

客户端能力(Client capability):

  • 属性路径: 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 detect a project
	 * wide change that requires such a calculation.
	 */
	refreshSupport?: boolean;
}

请求(Request):

  • method: "workspace/semanticTokens/refresh"
  • params: none

响应(Response):

  • result: none
  • error: codemessage,以防在请求期间发生异常。