语法高亮
语法高亮决定源代码的颜色和样式,它主要负责关键字(如javascript中的if,for)、字符串、注释、变量名等等语法的着色工作。
语法高亮由两部分工作组成:
在本章开始之前,建议你先玩一下 作用域检查器 工具看看文件中的符号都长什么样子,他们都应用了哪些主题样式。用内置主题(比如 Dark+)查看一份 TypeScript 文件,你就能同时看到语义高亮和语法高亮了。
分词
文本分词是指将文本打碎成一个个片段,并将每个片段根据符号类型(单词、标点等)进行分类。
VS Code 的分词引擎是通过 TextMate 驱动的。TextMate 语法是一套使用 plist(XML) 或 JSON 格式的结构化正则表达式集合。VS Code 则可通过 grammar 配置点进行语法配置。
TextMate 分词引擎和渲染引擎在同一个进程上运行,用户输入时,对应的符号也会实时更新。符号是语法高亮的最小单位,它将代码分为注释、字符串、正则等类型。
从1.43 版本开始,VS Code 也允许插件通过 语义化分词供应器函数 提供分词功能。语义供应器函数通常由语言服务器实现,它必须能够深入理解源代码,并且能够解析上下文的各类符号。比如,一个常量的名称应该在整个项目中都使用常量类型的语法高亮,而不是只在它声明的地方。
基于语义化分词的语法高亮,一般被认为是基于 TextMate 语言高亮的一个补充。语义化高亮是语法高亮的上层建筑。由于语言服务器通常都要花不少时间加载和分析项目,所以语义化高亮展现也可能会有所延迟。
本章侧重于介绍基于 TextMate 的分词和语法高亮,语义化分词高亮请查看语义高亮。
TextMate 语法
VS Code使用TextMate 语法将文本分割成一个个符号。TextMate语法是Oniguruma正则表达式的集合,一般是一份plist或者JSON格式的文件。你可以在这里找到更棒的介绍文档,在里面可以找到你感兴趣的TextMate语法。
TextMate符号和作用域
符号是由一门编程语言中最常见的一到几个字符组成的。符号包括运算符(如:+和*),变量名(如:myVar),或者字符串(如:"my string")。
每个符号都有其作用域,作用域描述了这个符号的上下文。一个符号可被由点符号序列查找到,比如javascript中的+符号有这样的作用域keyword.operator.arithmetic.js。
主题会把颜色和样式映射到作用域上,这样一来就实现了语法高亮。TextMate提供了一些主题中常用的作用域,如果你想要尽可能全面地支持语法,最好从现成的主题中入手,避免重新编写主题。
作用域支持嵌套,每个符号都会关联到它的父作用域上。下面的例子使用了作用域检查器,可以清晰地看到javascript函数中的运算符+和它的作用域层级:

父作用域的信息也同样是主题中的一部分。当主题指定了作用域,该作用域下的所有符号都会进行对应的着色,除非主题里面对单个作用域有其特殊配置。
配置基本语法
VS Code支持JSON格式的TextMate语法。你可以在发布内容配置里面的grammers进行配置。
这个配置点可以配置的内容有:语言的id,顶层语法作用域的名称,语法文件的路径。下面是一个abc语言的语法配置文件:
{"contributes": {"languages": [{"id": "abc","extensions": [".abc"]}],"grammars": [{"language": "abc","scopeName": "source.abc","path": "./syntaxes/abc.tmGrammar.json"}]}}
这个语法文件本身包含了一个顶层规则,里面一般分为两个部分,patterns列出了程序(program)和repository的顶层元素。语法中的其他规则需要从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((cxyz))(a
这个例子中的语法产生了下面的作用域列表(从左到右,从最佳匹配到最不匹配)
a keyword.letter, source.abc( punctuation.paren.open, expression.group, source.abcb expression.group, source.abc) punctuation.paren.close, expression.group, source.abcx source.abc( punctuation.paren.open, expression.group, source.abc( punctuation.paren.open, expression.group, expression.group, source.abcc keyword.letter, expression.group, expression.group, source.abcxyz expression.group, expression.group, source.abc) punctuation.paren.close, expression.group, expression.group, source.abc) punctuation.paren.close, expression.group, source.abc( source.abca keyword.letter, source.abc
注意文本匹配不是单一规则,比如字符串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"}}]}}
现在,如你对应用了meta.embedded.block.javascript的符号进行注释就会有正确的//javascript风格,如果你触发代码片段,也会提示对应的javascript片段。
开发全新的语法插件
使用VS Code的Yeoman模板快速创建一个新的语法插件,运行yo code然后选择New Language:

Yeoman通过问问题的方式最后生成新的插件,对于创建语法插件最重要的几点就是:
Language Id- 这个语言的idLanguage Name- 友好的名称Scope names- TextMate根作用域名称

生成器会假设你要同时对新语言定义好语言id和语法。如果你只是根据已有的语言创建新的语法,那么你只要填好目标语言的信息就好,然后一定要删除生成的package.json中的languages部分。
回答了一大堆问题之后,Yeoman会创建一个新的插件,其结构如下:

!> 注意:如果你只是配置一个VS Code中已有语言的语法,记得删掉生成的package.json中的languages配置。
迁移现成的TextMate语法
yo code也快成帮你把已有的TextMate语法转成一个VS Code插件。使用yo code,选择Language extension,当询问是否从已有TextMate文件插件的时候,填入后缀为.tmLanguage或.json的TextMate语法文件。

用YAML配置语法
随着语言日益复杂,你可能很快就会难以理解和维护你的json文件。如果你发现自己需要写很多正则表达式,或是需要添加大量解释语法层面的注释,你可能需要考虑使用yaml定义语法文件了。
Yaml语法和json有着同样的结构,但是它的语法更加精简,如多行字符串和注释。

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进行高亮。我们在injectTo中用source.js指向目标语言的作用域。
{"contributes": {"grammars": [{"path": "./syntaxes/injection.json","scopeName": "todo-comment.injection","injectTo": ["source.js"]}]}}
除了顶层的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"}}}
注入选择器中的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"}}]}}
符号类型和嵌入语言
对于嵌入语言中的注入语言还会有个副作用,那就是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"}}]}}
主题化
主题化是把颜色和样式应用到符号的过程。色彩主题定义了主题化规则,但用户可以在用户设置中自定义主题化规则。
tokenColors 定义了 TextMate 主题规则,它的语法和常用的 TextMate 主题是完全一样的。每份规则都定义了 TextMate 作用域选择器,并应用对应的颜色和样式。
解析符号的颜色或样式时,当前符号的作用域需要和规则中的选择器相匹配,然后找到最为匹配的样式属性(前景色、加粗、斜体、下划线)。
色彩主题 章节介绍了如何创建新的色彩主题,语义化分词的主题化则在语义高亮中。
作用域检查器
VS Code自带的作用域检查器能帮你调试语法文件。它能显示当前位置符号作用域,以及应用在上面的主题规则和元信息。
在命令面板中输入Developer: Inspect TM Scopes或者使用快捷键启动作用域检查器。
{"key": "cmd+alt+shift+i","command": "editor.action.inspectTMScopes"}

作用域检查器可以显示以下的信息:
- 当前符号
- 关于符号的元信息,这些值都是计算后的值。如果你使用了嵌入语言,那么这里最重要的信息就是
language和token type了 - 符号使用的主题规则。这里只显示当前应用的规则,而不显示被其他样式覆盖的规则。
- 完整的作用域列表,越往上作用域越明确。
