语法突出显示指南
语法突出显示决定了 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 标记和范围
令牌是属于同一程序元素的一个或多个字符。示例标记包括运算符(例如+
and )*
、变量名称(例如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
定义每个元素的部分。语法中的其他规则可以引用 using 中的repository
元素{ "include": "#id" }
。
示例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" }]
}
}
}
语法引擎将尝试连续将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
请注意,与规则之一不匹配的文本(例如 string xyz
)包含在当前范围内。expression.group
即使end
规则不匹配,文件末尾的最后一个括号也是 的一部分,就像在规则end-of-document
之前找到的那样end
。
嵌入式语言
如果您的语法包含父语言中的嵌入语言(例如 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 范围名称。
生成器假设您想要定义一种新语言以及该语言的新语法。如果您正在为现有语言创建语法,只需用目标语言的信息填写这些内容,并确保删除生成languages
的package.json
.
回答完所有问题后,Yeoman 将创建一个具有以下结构的新扩展:
请记住,如果您正在为 VS Code 已经了解的语言贡献语法,请务必删除生成languages
的package.json
.
Converting an existing TextMate grammar
yo code
还可以帮助将现有的 TextMate 语法转换为 VS Code 扩展。再次,首先运行yo code
并选择Language extension
。.tmLanguage
当要求提供现有语法文件时,请为其提供 a或TextMate 语法文件的完整路径.json
:
Using YAML to write a grammar
随着语法变得越来越复杂,理解和维护 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 围栏代码块添加新语言的突出显示。
Creating a basic injection grammar
注入语法的贡献与package.json
常规语法一样。language
但是,注入语法不是指定,injectTo
而是用于指定要将语法注入到的目标语言范围列表。
对于此示例,我们将创建一个简单的注入语法,TODO
在 JavaScript 注释中突出显示为关键字。为了在 JavaScript 文件中应用我们的注入语法,我们使用source.js
目标语言范围injectTo
:
{
"contributes": {
"grammars": [
{
"path": "./syntaxes/injection.json",
"scopeName": "todo-comment.injection",
"injectTo": ["source.js"]
}
]
}
}
除顶级injectionSelector
条目外,语法本身是标准 TextMate 语法。这是一个范围选择器,指定注入语法应应用的范围。对于我们的示例,我们希望突出显示所有注释中的injectionSelector
单词。使用范围检查器,我们发现JavaScript的双斜杠注释具有范围,因此我们的注入选择器是:TODO
//
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:
表示将注入添加到现有语法规则的左侧。这基本上意味着我们注入的语法规则将在任何现有语法规则之前应用。
Embedded languages
注入语法还可以为其父语法贡献嵌入语言。就像普通语法一样,注入语法可用于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"
}
}
]
}
}
Token types and embedded languages
注入语言嵌入式语言还有一个额外的复杂性:默认情况下,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 主题规则在常规 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 令牌的范围列表,最具体的范围位于顶部。它还显示了与范围匹配的最具体的主题规则。这仅显示负责令牌当前样式的主题规则,不显示覆盖的规则。如果存在语义标记,则仅当主题规则与匹配语义标记的规则不同时才显示主题规则。