语法高亮

语法高亮决定源代码的颜色和样式,它主要负责关键字(如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语言的语法配置文件:

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

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

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

  1. {
  2. "scopeName": "source.abc",
  3. "patterns": [{ "include": "#expression" }],
  4. "repository": {
  5. "expression": {
  6. "patterns": [{ "include": "#letter" }, { "include": "#paren-expression" }]
  7. },
  8. "letter": {
  9. "match": "a|b|c",
  10. "name": "keyword.letter"
  11. },
  12. "paren-expression": {
  13. "begin": "\\(",
  14. "end": "\\)",
  15. "beginCaptures": {
  16. "0": { "name": "punctuation.paren.open" }
  17. },
  18. "endCaptures": {
  19. "0": { "name": "punctuation.paren.close" }
  20. },
  21. "name": "expression.group",
  22. "patterns": [{ "include": "#expression" }]
  23. }
  24. }
  25. }

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

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

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

  1. a keyword.letter, source.abc
  2. ( punctuation.paren.open, expression.group, source.abc
  3. b expression.group, source.abc
  4. ) punctuation.paren.close, expression.group, source.abc
  5. x source.abc
  6. ( punctuation.paren.open, expression.group, source.abc
  7. ( punctuation.paren.open, expression.group, expression.group, source.abc
  8. c keyword.letter, expression.group, expression.group, source.abc
  9. xyz expression.group, expression.group, source.abc
  10. ) punctuation.paren.close, expression.group, expression.group, source.abc
  11. ) punctuation.paren.close, expression.group, source.abc
  12. ( source.abc
  13. a keyword.letter, source.abc

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

嵌入式语言

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

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

  1. {
  2. "contributes": {
  3. "grammars": [
  4. {
  5. "path": "./syntaxes/abc.tmLanguage.json",
  6. "scopeName": "source.abc",
  7. "embeddedLanguages": {
  8. "meta.embedded.block.javascript": "source.js"
  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可以帮你完成这个任务:

  1. # Install js-yaml as a development only dependency in your extension
  2. $ npm install js-yaml --save-dev
  3. # Use the command line tool to convert the yaml grammar to json
  4. $ npx js-yaml syntaxes/abc.tmLanguage.yaml > syntaxes/abc.tmLanguage.json

作用域检查器

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

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

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

scope-inspector

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

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

语法注入


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

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

创建一个基础语法注入

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

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

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

除了顶层的injectionSelector,语法本身就应该是标准的TextMate语法。injectionSelector是一个作用域选择器,它指明了语法注入生效的作用域。在我们的例子里,我们想要在所有//注释中的TODO高亮。使用作用域检查器,我们会发现JavaScript的双斜杠存在作用域comment.line.double-slash,所以我们的注入选择器是L:comment.line.double-slash

  1. {
  2. "scopeName": "todo-comment.injection",
  3. "injectionSelector": "L:comment.line.double-slash",
  4. "patterns": [
  5. {
  6. "include": "#todo-keyword"
  7. }
  8. ],
  9. "repository": {
  10. "todo-keyword": {
  11. "match": "TODO",
  12. "name": "keyword.todo"
  13. }
  14. }
  15. }

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

嵌入语法

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

比如高亮JS字符串中的sql查询的插件,可以使用embeddedLanguages为字符串中所有匹配meta.embedded.inline.sql的符号应用sql语言的基本功能,比如括号匹配和片段选择。

  1. {
  2. "contributes": {
  3. "grammars": [
  4. {
  5. "path": "./syntaxes/injection.json",
  6. "scopeName": "sql-string.injection",
  7. "injectTo": ["source.js"],
  8. "embeddedLanguages": {
  9. "meta.embedded.inline.sql": "source.sql"
  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作用域中的任何内容都应视为代码:

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