从 TextMate Grammar 到 Semantic Token:VSCode 语法高亮到底分几层
很多人第一次做 VSCode 语言扩展时,会把“高亮”当成一个功能来理解。结果一查文档,发现同时有 TextMate grammar、Semantic Token、主题、Language Server,看起来都像在给代码上色。
其实 VSCode 的高亮由几层机制叠在一起。它们解决的问题不同,出现的时机也不同。如果不先把这一点想清楚,扩展很容易做到一半就陷入混乱:该写 grammar 的地方上了 LSP,该交给语义层的事情又硬塞进正则。
第一层:TextMate Grammar
最先起作用的是 TextMate grammar。编辑器一打开文件,就会根据 grammar 里的规则把文本切成一串 token,并给这些 token 赋上 scope,比如:
keyword.control
string.quoted.double
comment.line
entity.name.function这一层的特点很明确:
- 它是基于文本模式工作的,不理解类型系统、作用域和符号表
- 它响应很快,因为只依赖当前文件内容
- 它负责的是“基础语法着色”,并不承担完整语义理解
所以,像关键字、字符串、注释、数字字面量、简单的函数定义头,这些都适合交给 TextMate grammar。
第二层:主题和 Scope 映射
TextMate grammar 产出的 scope,本身还不是颜色。真正决定“这个 token 显示成什么样”的,是当前主题如何映射这些 scope。
例如同样是:
keyword.control在不同主题下,可能会显示成不同颜色、不同字重,甚至不同字形风格。
这也是为什么 grammar 设计时应该尽量复用已有 scope 名称。你发明一个没人认识的新 scope,主题往往就不会给它任何特殊样式,最终看起来等于没高亮。
第三层:Semantic Token
Semantic Token 是更晚到达的一层。它通常由 Language Server 或扩展里的语义分析器提供,不再只看文本模式,而是基于语法树、符号表、类型信息甚至整个项目上下文来给 token 分类。
它解决的是 TextMate grammar 做不到的问题,比如:
- 同样一个标识符,到底是变量、参数、字段还是类型名
- 某个名字在这里是函数调用,还是类型构造
- 一个变量是不是
readonly、defaultLibrary、deprecated
举个最常见的例子:
const Point = 1;
function f(Point: number) {
return Point;
}对 TextMate grammar 来说,这几个 Point 很可能只是“看起来像标识符”。但对 Semantic Token 来说,它们处在不同位置,语义角色也不同。
为什么两层都要有
因为它们覆盖的是两个不同阶段。
TextMate grammar 的优势是快。文件一打开,基础高亮立刻就能出现。Semantic Token 的优势是准。等语言服务完成分析之后,它再把更细的语义信息叠上去。
实际效果通常是这样的:
- 编辑器先用 TextMate grammar 给出基础配色
- 语言服务启动并分析当前文件
- Semantic Token 返回后,覆盖或补充更细的着色结果
因此在 VSCode 里,TextMate grammar 和 Semantic Token 更接近前后叠加关系,不是简单替代关系。
哪些问题该交给哪一层
一个简单的判断标准是:只靠当前文本表面形式能不能稳定判断。
如果能,优先放在 grammar:
- 关键字
- 注释
- 字符串
- 数字字面量
- 明确边界的内嵌片段
如果不能,就该放在 semantic token:
- 变量、参数、字段、类型的区分
- 同名符号在不同作用域里的角色变化
- 废弃符号、内建符号、只读符号等修饰信息
一个常见误区是试图用 TextMate grammar 区分“函数调用”和“普通变量引用”。只要语言稍微复杂一点,这件事就会立刻失控,因为它需要的已经超出正则范围,进入语义分析了。
覆盖关系是怎么工作的
VSCode 的最终着色结果,通常是 grammar token 和 semantic token 共同作用的结果。粗略理解可以是:
- TextMate grammar 先提供基础 token 和 scope
- 主题根据 scope 给出默认样式
- Semantic Token 再根据语义类型提供更精细的分类
- 主题如果配置了 semantic token 规则,就会用它们进一步覆盖显示效果
所以你看到的“最后颜色”,未必只来自 grammar,也未必只来自 semantic token,更多是两套系统和主题共同叠加出来的结果。
对扩展作者意味着什么
如果你在做一门语言的 VSCode 支持,这种分层关系意味着:TextMate grammar 适合先承担基础高亮,语言服务再补上更细的语义能力。
等 grammar 稳定以后,再逐步补上 Semantic Token,就能把 grammar 做不到、但用户肉眼很在意的区分补齐。
反过来说,如果把所有着色问题都交给 Semantic Token,那么语言服务尚未返回结果时,编辑器在基础可读性上就会留下明显空档。
小结
VSCode 的语法高亮至少可以拆成两层:TextMate grammar 负责快速的文本级 tokenization,Semantic Token 负责更晚到达的语义级分类。两者之间还夹着主题对 scope 和 token type 的映射规则。
把这几层拆开之后,很多扩展开发里的问题就会清楚很多:什么应该写进 grammar,什么必须交给语言服务,什么只是主题表现,本身并不属于解析能力。
参考资料:Syntax Highlight Guide | VSCode API / Semantic Highlight Guide | VSCode API