语法高亮指南
语法高亮决定了源代码在 Visual Studio Code 编辑器中的颜色和样式。它负责将 JavaScript 中像 if
或 for
这样的关键字与字符串、注释和变量名以不同的颜色显示。
语法高亮有两个组成部分
在深入了解细节之前,一个好的开始是使用作用域检查器工具,探索源文件中存在哪些标记以及它们与哪些主题规则匹配。要查看语义标记和语法标记,请在 TypeScript 文件上使用内置主题(例如,Dark+)。
标记化
文本的标记化是将文本分解为片段,并用标记类型对每个片段进行分类。
VS Code 的标记化引擎由TextMate 语法提供支持。TextMate 语法是结构化的正则表达式集合,以 plist (XML) 或 JSON 文件形式编写。VS Code 扩展可以通过 grammars
贡献点来贡献语法。
TextMate 标记化引擎与渲染器在同一进程中运行,标记会随着用户输入而更新。标记用于语法高亮,但也用于将源代码分类到注释、字符串、正则表达式等区域。
从 1.43 版本开始,VS Code 也允许扩展通过语义标记提供者来提供标记化。语义提供者通常由对源文件有更深层理解并能在项目上下文中解析符号的语言服务器实现。例如,常量变量名可以在整个项目中用常量高亮渲染,而不仅仅是在其声明位置。
基于语义标记的高亮被视为对基于 TextMate 的语法高亮的补充。语义高亮位于语法高亮之上。由于语言服务器加载和分析项目可能需要一些时间,语义标记高亮可能会延迟片刻后出现。
本文重点介绍基于 TextMate 的标记化。语义标记化和主题化在语义高亮指南中进行了解释。
TextMate 语法
VS Code 使用TextMate 语法作为语法标记化引擎。它们是为 TextMate 编辑器发明的,由于开源社区创建和维护了大量的语言包,它们已被许多其他编辑器和 IDE 采用。
TextMate 语法依赖于Oniguruma 正则表达式,通常以 plist 或 JSON 形式编写。你可以在这里找到 TextMate 语法的一个很好的介绍,你也可以查看现有的 TextMate 语法来了解它们的工作原理。
TextMate 标记和作用域
标记是一个或多个字符,它们是同一程序元素的一部分。例如,标记包括运算符,如 +
和 *
;变量名,如 myVar
;或字符串,如 "my string"
。
每个标记都关联着一个定义其上下文的作用域。作用域是一个由点分隔的标识符列表,用于指定当前标记的上下文。例如,JavaScript 中的 +
操作具有作用域 keyword.operator.arithmetic.js
。
主题将作用域映射到颜色和样式以提供语法高亮。TextMate 提供了许多主题所针对的常见作用域列表。为了让你的语法得到尽可能广泛的支持,请尝试在现有作用域的基础上构建,而不是定义新的作用域。
作用域是嵌套的,因此每个标记也关联着父作用域列表。下面的示例使用作用域检查器来显示简单 JavaScript 函数中 +
运算符的作用域层次结构。最具体的作用域列在顶部,更通用的父作用域列在下方
父作用域信息也用于主题化。当一个主题针对某个作用域时,所有具有该父作用域的标记都将被着色,除非该主题也为其个别作用域提供了更具体的着色规则。
贡献基本语法
VS Code 支持 JSON TextMate 语法。这些语法通过 grammars
贡献点来贡献。
每个语法贡献指定:语法所应用的语言标识符、语法标记的顶层作用域名称以及语法文件的相对路径。下面的示例显示了虚构的 abc
语言的语法贡献
{
"contributes": {
"languages": [
{
"id": "abc",
"extensions": [".abc"]
}
],
"grammars": [
{
"language": "abc",
"scopeName": "source.abc",
"path": "./syntaxes/abc.tmGrammar.json"
}
]
}
}
语法文件本身包含一个顶层规则。这通常分为一个列出程序顶层元素的 patterns
部分和一个定义每个元素的 repository
部分。语法中的其他规则可以使用 { "include": "#id" }
来引用 repository
中的元素。
示例 abc
语法将字母 a
、b
和 c
标记为关键字,并将嵌套的圆括号标记为表达式。
{
"scopeName": "source.abc",
"patterns": [{ "include": "#expression" }],
"repository": {
"expression": {
"patterns": [{ "include": "#letter" }, { "include": "#paren-expression" }]
},
"letter": {
"match": "a|b|c",
"name": "keyword.letter"
},
"paren-expression": {
"begin": "\\(",
"end": "\\)",
"beginCaptures": {
"0": { "name": "punctuation.paren.open" }
},
"endCaptures": {
"0": { "name": "punctuation.paren.close" }
},
"name": "expression.group",
"patterns": [{ "include": "#expression" }]
}
}
}
对于一个简单的程序,例如
a
(
b
)
x
(
(
c
xyz
)
)
(
a
示例语法生成以下作用域(从左到右,从最具体到最不具体的作用域)
a keyword.letter, source.abc
( punctuation.paren.open, expression.group, source.abc
b keyword.letter, expression.group, source.abc
) punctuation.paren.close, expression.group, source.abc
x source.abc
( punctuation.paren.open, expression.group, source.abc
( punctuation.paren.open, expression.group, expression.group, source.abc
c keyword.letter, expression.group, expression.group, source.abc
xyz expression.group, expression.group, source.abc
) punctuation.paren.close, expression.group, expression.group, source.abc
) punctuation.paren.close, expression.group, source.abc
( punctuation.paren.open, expression.group, source.abc
a keyword.letter, expression.group, source.abc
请注意,未被规则匹配的文本,例如字符串 xyz
,将包含在当前作用域中。即使 end
规则未匹配,文件末尾的最后一个圆括号仍是 expression.group
的一部分,因为在找到 end
规则之前找到了 end-of-document
。
嵌入式语言
如果你的语法包含父语言中的嵌入式语言,例如 HTML 中的 CSS 样式块,你可以使用 embeddedLanguages
贡献点告诉 VS Code 将嵌入式语言视为与父语言不同的语言。这确保了括号匹配、注释和其他基本语言功能在嵌入式语言中按预期工作。
embeddedLanguages
贡献点将嵌入式语言中的作用域映射到顶层语言作用域。在下面的示例中,meta.embedded.block.javascript
作用域中的任何标记都将被视为 JavaScript 内容
{
"contributes": {
"grammars": [
{
"path": "./syntaxes/abc.tmLanguage.json",
"scopeName": "source.abc",
"embeddedLanguages": {
"meta.embedded.block.javascript": "javascript"
}
}
]
}
}
现在,如果你尝试在标记为 meta.embedded.block.javascript
的一组标记内注释代码或触发代码片段,它们将获得正确的 //
JavaScript 风格注释和正确的 JavaScript 代码片段。
开发新的语法扩展
要快速创建新的语法扩展,请使用VS Code 的 Yeoman 模板运行 yo code
并选择 New Language
选项
Yeoman 将引导你回答一些基本问题来搭建新扩展的框架。创建新语法的重要问题包括
Language id
- 你的语言的唯一标识符。Language name
- 你的语言的人类可读名称。Scope names
- 你的语法的根 TextMate 作用域名称。
生成器假设你想要定义一种新语言并为其定义一个新语法。如果你正在为现有语言创建语法,只需填写你的目标语言信息,并确保删除生成的 package.json
中的 languages
贡献点。
回答所有问题后,Yeoman 将创建一个具有以下结构的新扩展
请记住,如果你正在为 VS Code 已知的语言贡献语法,请务必删除生成的 package.json
中的 languages
贡献点。
转换现有 TextMate 语法
yo code
还可以帮助将现有的 TextMate 语法转换为 VS Code 扩展。同样,首先运行 yo code
并选择 Language extension
。当询问现有语法文件时,请提供 .tmLanguage
或 .json
TextMate 语法文件的完整路径
使用 YAML 编写语法
随着语法的复杂度增加,使用 JSON 可能变得难以理解和维护。如果你发现自己正在编写复杂的正则表达式或需要添加注释来解释语法的某些方面,考虑改用 YAML 来定义你的语法。
YAML 语法与基于 JSON 的语法结构完全相同,但允许你使用 YAML 更简洁的语法,以及多行字符串和注释等特性。
VS Code 只能加载 JSON 语法,因此基于 YAML 的语法必须转换为 JSON。js-yaml
包和命令行工具可以轻松实现这一点。
# Install js-yaml as a development only dependency in your extension
$ npm install js-yaml --save-dev
# Use the command-line tool to convert the yaml grammar to json
$ npx js-yaml syntaxes/abc.tmLanguage.yaml > syntaxes/abc.tmLanguage.json
注入语法
注入语法允许你扩展现有语法。注入语法是一种常规的 TextMate 语法,它被注入到现有语法中的特定作用域内。注入语法的应用示例包括
- 高亮注释中的
TODO
等关键字。 - 向现有语法添加更具体的作用域信息。
- 为 Markdown 围栏代码块添加新语言的高亮。
创建基本注入语法
注入语法像常规语法一样通过 package.json
贡献。然而,注入语法不是指定 language
,而是使用 injectTo
来指定要将语法注入的目标语言作用域列表。
在此示例中,我们将创建一个简单的注入语法,用于将 JavaScript 注释中的 TODO
高亮为关键字。要在 JavaScript 文件中应用我们的注入语法,我们在 injectTo
中使用 source.js
目标语言作用域
{
"contributes": {
"grammars": [
{
"path": "./syntaxes/injection.json",
"scopeName": "todo-comment.injection",
"injectTo": ["source.js"]
}
]
}
}
语法本身是一个标准的 TextMate 语法,除了顶层的 injectionSelector
条目。injectionSelector
是一个作用域选择器,它指定了注入语法应该应用于哪些作用域。对于我们的示例,我们希望在高亮所有 //
注释中的 TODO
单词。使用作用域检查器,我们发现 JavaScript 的双斜杠注释具有作用域 comment.line.double-slash
,因此我们的注入选择器是 L:comment.line.double-slash
{
"scopeName": "todo-comment.injection",
"injectionSelector": "L:comment.line.double-slash",
"patterns": [
{
"include": "#todo-keyword"
}
],
"repository": {
"todo-keyword": {
"match": "TODO",
"name": "keyword.todo"
}
}
}
注入选择器中的 L:
表示注入被添加到现有语法规则的左侧。这基本意味着我们的注入语法的规则将在任何现有语法规则之前应用。
嵌入式语言
注入语法也可以向其父语法贡献嵌入式语言。就像普通语法一样,注入语法可以使用 embeddedLanguages
将嵌入式语言的作用域映射到顶层语言作用域。
例如,一个高亮 JavaScript 字符串中 SQL 查询的扩展可以使用 embeddedLanguages
来确保标记为 meta.embedded.inline.sql
的字符串内的所有标记都被视为 SQL,从而支持基本的语言功能,例如括号匹配和代码片段选择。
{
"contributes": {
"grammars": [
{
"path": "./syntaxes/injection.json",
"scopeName": "sql-string.injection",
"injectTo": ["source.js"],
"embeddedLanguages": {
"meta.embedded.inline.sql": "sql"
}
}
]
}
}
标记类型和嵌入式语言
对于注入式语言的嵌入式语言,还有一个额外的复杂性:默认情况下,VS Code 将字符串内的所有标记视为字符串内容,将注释中的所有标记视为注释内容。由于括号匹配和自动关闭对等功能在字符串和注释内被禁用,如果嵌入式语言出现在字符串或注释内,这些功能在嵌入式语言中也将被禁用。
要覆盖此行为,你可以使用 meta.embedded.*
作用域来重置 VS Code 对标记为字符串或注释内容的标记。最好始终将嵌入式语言包装在 meta.embedded.*
作用域中,以确保 VS Code 正确处理嵌入式语言。
如果无法在语法中添加 meta.embedded.*
作用域,可以改用语法贡献点中的 tokenTypes
将特定作用域映射到内容模式。下面的 tokenTypes
部分确保 my.sql.template.string
作用域中的任何内容都被视为源代码
{
"contributes": {
"grammars": [
{
"path": "./syntaxes/injection.json",
"scopeName": "sql-string.injection",
"injectTo": ["source.js"],
"embeddedLanguages": {
"my.sql.template.string": "sql"
},
"tokenTypes": {
"my.sql.template.string": "other"
}
}
]
}
}
主题
主题化是关于为标记分配颜色和样式。主题规则在颜色主题中指定,但用户可以在用户设置中自定义主题规则。
TextMate 主题规则在 tokenColors
中定义,并与常规 TextMate 主题具有相同的语法。每条规则定义一个 TextMate 作用域选择器以及生成的颜色和样式。
在评估标记的颜色和样式时,当前标记的作用域会与规则的选择器进行匹配,以找到每种样式属性(前景色、粗体、斜体、下划线)的最具体规则
颜色主题指南介绍了如何创建颜色主题。语义标记的主题化在语义高亮指南中进行了解释。
作用域检查器
VS Code 内置的作用域检查器工具有助于调试语法和语义标记。它显示文件中当前位置的标记和语义标记的作用域,以及有关哪些主题规则应用于该标记的元数据。
从命令面板使用 Developer: Inspect Editor Tokens and Scopes
命令或为其创建快捷键来触发作用域检查器
{
"key": "cmd+alt+shift+i",
"command": "editor.action.inspectTMScopes"
}
作用域检查器显示以下信息
- 当前标记。
- 关于标记的元数据及其计算外观的信息。如果你正在使用嵌入式语言,此处重要的条目是
language
和token type
。 - 当当前语言有可用的语义标记提供者且当前主题支持语义高亮时,会显示语义标记部分。它显示当前的语义标记类型和修饰符以及与该语义标记类型和修饰符匹配的主题规则。
- TextMate 部分显示当前 TextMate 标记的作用域列表,最具体的作用域位于顶部。它还显示与这些作用域匹配的最具体主题规则。这仅显示负责标记当前样式的规则,不显示被覆盖的规则。如果存在语义标记,则仅当主题规则与匹配语义标记的规则不同时才显示。