# 语法高亮

语法高亮决定源代码的颜色和样式,它主要负责关键字(如javascript中的iffor)、字符串、注释、变量名等等语法的着色工作。

语法高亮由两部分工作组成:

  • 根据语法将文本解析成符号和作用域
  • 然后根据这份作用域映射应用对应的颜色和样式

本文档只教你第一部分:根据语法将文本解析成符号和作用域,然后使用现成的颜色和样式。自定义样式的部分请参考色彩主题指南

# TextMate 语法


VS Code使用TextMate 语法将文本分割成一个个符号。TextMate语法是Oniguruma正则表达式的集合,一般是一份plist或者JSON格式的文件。你可以在这里找到更棒的介绍文档,在里面可以找到你感兴趣的TextMate语法。

# 符号和作用域

符号是由一门编程语言中最常见的一到几个字符组成的。符号包括运算符(如:+*),变量名(如:myVar),或者字符串(如:"my string")。

每个符号都有其作用域,作用域描述了这个符号的上下文。一个符号可被由符号序列查找到,比如javascript中的+符号有这样的作用域keyword.operator.arithmetic.js

主题会把颜色和样式映射到作用域上,这样一来就实现了语法高亮。TextMate提供了一些主题中常用的作用域,如果你想要尽可能全面地支持语法,最好从现成的主题中入手,避免重新编写主题。

作用域支持嵌套,每个符号都会关联到它的父作用域上。下面的例子使用了作用域检查器,可以清晰地看到javascript函数中的运算符+和它的作用域层级:

scopes

父作用域的信息也同样是主题中的一部分。当主题指定了作用域,该作用域下的所有符号都会进行对应的着色,除非主题里面对单个作用域有其特殊配置。

# 配置基本语法

VS Code支持JSON格式的TextMate语法。你可以在发布内容配置里面的grammers进行配置。

这个配置点可以配置的内容有:语言的id,顶层语法作用域的名称,语法文件的路径。下面是一个abc语言的语法配置文件:

{
	"contributes": {
		"languages": [
			{
				"id": "abc",
				"extensions": [".abc"]
			}
		],
		"grammars": [
			{
				"language": "abc",
				"scopeName": "source.abc",
				"path": "./syntaxes/abc.tmGrammar.json"
			}
		]
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这个语法文件本身包含了一个顶层规则,里面一般分为两个部分,patterns列出了程序(program)和repository的顶层元素。语法中的其他规则需要从repository中使用{ "include": "#id" }引入。

abc语法标记了字母abc作为关键字,可以被括号包起来成为一个表达式。

{
	"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" }]
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

语法引擎会试着逐步将expression中的规则应用到文本中。比如下面这个简单的程序:

a
(
    b
)
x
(
    (
        c
        xyz
    )
)
(
a
1
2
3
4
5
6
7
8
9
10
11
12
13

这个例子中的语法产生了下面的作用域列表(从左到右,从最佳匹配到最不匹配)

a               keyword.letter, source.abc
(               punctuation.paren.open, expression.group, source.abc
    b           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
(               source.abc
a               keyword.letter, source.abc
1
2
3
4
5
6
7
8
9
10
11
12
13

注意文本匹配不是单一规则,比如字符串xyz,是包含在当前作用域中的。文件的最后一个括号在expression.group里面,因为不会匹配end规则。

# 嵌入式语言

如果你的语法中需要在父语言中嵌入其他语言,比如HTML中的CSS,那么你可以使用embeddedLanguages配置,告诉VSCode怎么处理嵌入的语言。然后嵌入语言的括号匹配,注释,和其他基础语言功能都会正常运作。

embeddedLanguages配置将嵌入语言的作用域映射到顶层语言的作用域上。下面里的例子里,meta.embedded.block.javascript作用域中的任何符号都会以javscript处理:

{
	"contributes": {
		"grammars": [
			{
				"path": "./syntaxes/abc.tmLanguage.json",
				"scopeName": "source.abc",
				"embeddedLanguages": {
					"meta.embedded.block.javascript": "source.js"
				}
			}
		]
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

现在,如你对应用了meta.embedded.block.javascript的符号进行注释就会有正确的//javascript风格,如果你触发代码片段,也会提示对应的javascript片段。

# 开发全新的语法插件


使用VS Code的Yeoman模板快速创建一个新的语法插件,运行yo code然后选择New Language

yo-new-language

Yeoman通过问问题的方式最后生成新的插件,对于创建语法插件最重要的几点就是:

  • Language Id - 这个语言的id
  • Language Name - 友好的名称
  • Scope names - TextMate根作用域名称

yo-new-language-questions

生成器会假设你要同时对新语言定义好语言id和语法。如果你只是根据已有的语言创建新的语法,那么你只要填好目标语言的信息就好,然后一定要删除生成的package.json中的languages部分。

回答了一大堆问题之后,Yeoman会创建一个新的插件,其结构如下:

generated-new-language-extension

注意

如果你只是配置一个VS Code中已有语言的语法,记得删掉生成的package.json中的languages配置。

# 迁移现成的TextMate语法

yo code也快成帮你把已有的TextMate语法转成一个VS Code插件。使用yo code,选择Language extension,当询问是否从已有TextMate文件插件的时候,填入后缀为.tmLanguage.json的TextMate语法文件。

yo-convert

# 用YAML配置语法

随着语言日益复杂,你可能很快就会难以理解和维护你的json文件。如果你发现自己需要写很多正则表达式,或是需要添加大量解释语法层面的注释,你可能需要考虑使用yaml定义语法文件了。

Yaml语法和json有着同样的结构,但是它的语法更加精简,如多行字符串和注释。

yaml-grammar

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
1
2
3
4
5

# 作用域检查器

VS Code自带的作用域检查器能帮你调试语法文件。它能显示当前位置符号作用域,以及应用在上面的主题规则和元信息。

在命令面板中输入Developer: Inspect TM Scopes或者使用快捷键启动作用域检查器

{
	"key": "cmd+alt+shift+i",
	"command": "editor.action.inspectTMScopes"
}
1
2
3
4

scope-inspector

作用域检查器可以显示以下的信息:

  1. 当前符号
  2. 关于符号的元信息,这些值都是计算后的值。如果你使用了嵌入语言,那么这里最重要的信息就是languagetoken type
  3. 符号使用的主题规则。这里只显示当前应用的规则,而不显示被其他样式覆盖的规则。
  4. 完整的作用域列表,越往上作用域越明确。

# 语法注入


你可以通过语法注入扩展一个现成的语法文件。语法注入就是常规的TextMate语法,语法注入的应用有:

  • 高亮注释中的关键字,如TODO
  • 对现有语法添加更明确的作用域信息
  • 向Markdown中的代码区块添加语法高亮

# 创建一个基础语法注入

语法注入也是在package.json中配置的,不过这次不需要配置language,而是配置injectTo指明目需要注入的语言作用域列表。

在这个例子里,我们会新建一个非常简单的注入语法,对javascript注释中的TODO进行高亮。我们在injectTo中用source.js指向目标语言的作用域。

{
	"contributes": {
		"grammars": [
			{
				"path": "./syntaxes/injection.json",
				"scopeName": "todo-comment.injection",
				"injectTo": ["source.js"]
			}
		]
	}
}
1
2
3
4
5
6
7
8
9
10
11

除了顶层的injectionSelector,语法本身就应该是标准的TextMate语法。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"
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

注入选择器中的L:代表注入的语法添加在现有语法规则的左边。也就是说我们注入的语法规则会在任何现有语法规则之前生效。

# 嵌入语法

语法注入也可以用在嵌入语言中,在他们的父级语法中进行配置。就和普通的语法意义,语法注入也可以使用embeddedLanguages将嵌入语言的作用域映射到顶层的语言作用域上。

比如高亮JS字符串中的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": "source.sql"
				}
			}
		]
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 符号类型和嵌入语言

对于嵌入语言中的注入语言还会有个副作用,那就是VS Code把所有字符串(string)中的符号视为字符文本,而且把注释中的所有符号视为符号内容(token content)。 因此诸如括号匹配和自动补全在字符串和注释中是无法使用的,如果嵌入语言刚好出现在字符串或注释中,那么这些功能就无法在嵌入语言中使用。

想要重载这个行为,你需要使用meta.embedded.*作用域重置VS Code标记字符串和注释行为。最佳实践就是始终将嵌入语言放在meta.embedded.*作用域中,确保VS Code能够正确处理嵌入语言。

如果你无法为你的语法添加meta.embedded.*作用域,你可以在语法配置中用tokenTypes,指定作用域到内容模式(content mode)上。 下面的tokenTypes确保my.sql.template.string作用域中的任何内容都应视为代码:

{
	"contributes": {
		"grammars": [
			{
				"path": "./syntaxes/injection.json",
				"scopeName": "sql-string.injection",
				"injectTo": ["source.js"],
				"embeddedLanguages": {
					"my.sql.template.string": "source.sql"
				},
				"tokenTypes": {
					"my.sql.template.string": "other"
				}
			}
		]
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17