1.3 语法
Sublime Text 可以使用.sublime-syntax
和.tmLanguage
文件进行语法高亮,本文介绍.sublime-syntax
文件。
概述
Sublime 语法文件是 YAML,此文件有一个小的头部,紧跟一个上下文列表。每个上下文都有一个匹配模式的列表,这个列表描述了在这个上下文中如何高亮文本、如何改变当前文本。
这里有一个高亮 C 语言语法的示例:
%YAML 1.2
---
name: C
file_extensions: [c, h]
scope: source.c
contexts:
main:
- match: \b(if|else|for|while)\b
scope: keyword.control.c
核心是一个语法定义,把作用域安排给文本区域,这个作用域由配色方案使用来对文本进行高亮。
这个语法文件包含一个 context,main,匹配到[if, else, for, while]等单词时,就会把它们放进keyword.control.c
的作用域。每个语法都必须定义一个 main 上下文,这会最先被使用。
是正则表达式,使用了Ruby 语法。上面的例子中,\b
用来确保单词的边界被匹配上,其他位置的这些词不会被作为关键词。
注意:由于 YAML 语法,.sublime-syntax
文件中不允许有 tab 字符。
Header
header 区域允许的 key 有:
- name. 菜单栏中语法的名称。可选,如果没有时将会从文件名中提取。
- file_extensions.一个字符串列表,定义使用这种语法高亮的文件的扩展名。
- first_line_match. 当打开一个未识别的文件时,文件的第一行内容将会通过这个正则表达式去识别,从而判断是否应该应用这个语法高亮。
- scope. 文件中全部文本的默认作用域。
- hidden. 菜单栏中不会显示隐藏的语法定义,但仍然可以被插件使用,或是包含在其它语法定义中。
Contexts
对于大多数语言来说,你可能需要不止一种 context。如,C 语言中,我们不需要一个字符串中间被匹配到的单词有关键词的语法高亮。这里有个例子:
%YAML 1.2
---
name: C
file_extensions: [c, h]
scope: source.c
contexts:
main:
- match: \b(if|else|for|while)\b
scope: keyword.control.c
- match: '"'
push: string
string:
- meta_scope: string.quoted.double.c
- match: \\.
scope: constant.character.escape.c
- match: '"'
pop: true
main
上下文中第二个匹配项是一个双引号(注意"必须是'"'这样的,单独一个双引号会报 YAL 语法错误),添加一个字符串上下文到 context 的堆中。这就意味着这个文件的余下部分将使用contexts
中的string
来处理, 而不是main
,直到string
从堆中被移除。
string
上下文介绍了一个新的模式:meta_scope
,档string
上下文在堆中时,这会把string.quoted.double.c
作用域分配给文本。
在 Sublime Text 中编辑时,你可以通过按下ctrl+shift+p
(OS X)或ctrl+alt+shift+p
(Windows 和 Linux)来查看光标选中的文本应用的作用域。
string
有两个匹配项:第一个是反斜杠字符跟一个任意字符,第二个是一个双引号字符。注意最后一个匹配项指定了一个行为:当遇到转义引号时,string
将被从 context 堆栈中移除,然后重新去使用main
中的作用域。
当一个 context 有多个匹配项,最左边的一个将被发现。当同一位置匹配到多个模式时,将会应用第一个定义的模式。
Meta 模式
- meta_scope. 这把给定的作用域分配给这个上下文中的所有文本,包括把 context 添加到堆栈或从中移除的 patterns。
- meta_content_scope. 和上面一样,但是不应用于触发此 context 的文本(如,上面的
string
例子中,当前作用域不会应用于引号字符)。 - meta_include_prototype. 用来阻止当前 context 自动包含原型 context。
Meta 模式必须放在 contexts 中的第一位。
Match 模式
match 模式允许有如下 key:
- match. 正则表达式。YAML 允许很多字符串不带双引号,这可以使正则表达式更清晰,但你仍然需要知道何时把正则表达式用引号包起来。如果你的正则表达式包含了
#
、:
、-
、{
、[
或>
等字符,这时你就需要 用到引号了。正则表达式同一时间只能对单行文本生效。 - scope. 安排给匹配到的文本的作用域。
- captures. 一个数字到作用域的映射,给正则表达式匹配到的部分分配作用域。
- push. 追加到 contexts 堆栈中的 context,这可以是一个 context 名称、context 名称列表或是一个行内、匿名的 context。
- pop. 把当前上下文从 contexts 堆栈中移除,这个 key 唯一可被接收的值是
true
。 - set. 和
push
接收同样的参数,但是首先会把这个 context 移除,然后才把给定的 context 添加到堆栈中。 - syntax. 看下面的包含其它文件。
注意:push、pop、set 和 syntax 都是独立的,一个单独的匹配模式中只能使用其中的一个。
这个例子中,正则表达式包含了两个捕获,捕获的 key 用来给每一个分配不同的作用域。
- match: "^\\s*(#)\\s*\\b(include)\\b"
captures:
1: meta.preprocessor.c++
2: keyword.control.include.c++
Include 模式
便于在一个 context 中包含另外一个。例如,你可以定义多种不同的 context 来解析 C 语言,它们几乎都可以包含comments
。你可以包含他们,而不是在每个 context 中复制一份:
expr:
- include: comments
- match: \b[0-9]+\b
scope: constant.numeric.c
...
这里,所有的 match 匹配项和 include 模式都将被拉取,它们将在 include 模式的位置被插入,因此你仍然可以控制模式的顺序。所有定义在 comments 上下文中的 meta 模式都将被忽视。
当有一个元素,如comments
时,这个被包含简直太常见了,对每一个 context 自动包含这个是很简单的,只需要列出异常。你可以创建一个 context 明明的原型,这将会自动被包含到每一个其它 context 的顶部,除非 context 使用了meta_include_prototype
meta 模式。例如:
prototype:
- include: comments
string:
- meta_include_prototype: false
...
C 语言中,字符串中的/*
不会作为注释的开始,因此string
context 表明原型不应该被包含。
包含其它文件
Sublime 语法文件支持在一个语法定义中嵌入另一个,如,HTML 可以嵌入 JavaScript。这里有一个 HTML 的基本语法定义的例子:
scope: text.html
contexts:
main:
- match: <script>
push: Packages/JavaScript/JavaScript.sublime-syntax
with_prototype:
- match: (?=</script>)
pop: true
- match: "<"
scope: punctuation.definition.tag.begin
- match: ">"
scope: punctuation.definition.tag.end
注意上面的第一条规则,它表明了当遇到<script>
标签时,JavaScript.sublime-syntax
将被 push 到 context 的堆栈中。它还定义了另外一个 keywith_prototype
。包含了一个将被插入到每一 个JavaScript.sublime-syntax
中定义的 context 中的匹配项列表。注意with_prototype
在概念上和prototype
上下文类似,然而它将始终被插入到每一个被引用的上下文,无需考虑meta_include_prototype
设置。
这个例子中,当下一个标签是</script>
时,插入的 patter 会被从当前 context 中移除。注意这并不是真正匹配</script>
标签,只是使用了一个在这里扮演了两个角色的预判:既允许 HTML 规则匹配结束标签,按照正常 方式高亮,保证 JavaScript 上下文可以被移出。上下文堆栈也许在 JavaScript 字符串中间,例如,一旦遇到</script>
标签,JavaScript 字符和main
context 都将被移出。
注意:不能把.tmLanguage
文件包含到.sublime-syntax
文件中。
一种常见的情景是包含 HTML 的模板语言,这里有一个例子:
scope: text.jinja
contexts:
main:
- match: ""
push: "Packages/HTML/HTML.sublime-syntax"
with_prototype:
- match: "
push: expr
expr:
- match: "
pop: true
- match: \b(if|else)\b
scope: keyword.control
这个和 HTML 嵌套 JavaScript 的例子大不一样,因为模板语言往往是从内到外进行操作:默认情况,它需要像 HTML 一样,只有转码成特定表达式的底层模板语言。
上面的例子,我们可以看到它默认以 HTML 模式运行:main
上下文包含一个单独的永远被匹配到的模式,只包含了 HTML 语法。
变量
几个正则表达式有共同部分并不罕见,你可以使用变量来避免重复输入:
variables:
ident: '[A-Za-z_][A-Za-z_0-9]*'
contexts:
main:
- match: '\b\b'
scope: keyword.control
变量必须定义在 .sublime-syntax
文件的顶部,在正则表达式中通过 \{\{varname\}\}
的形式来引用,变量本身可以引用其它变量。注意任何不匹配 \{\{[A-Za-z0-9_]+\}\}
的文本将不会被认为是一个变量,因此正则 表达式仍然可以包含常量 \{\{
字符。
示例
括号平衡
这个例子高亮没有对应的开括号的闭合括号:
name: C
scope: source.c
contexts:
main:
- match: \(
push: brackets
- match: \)
scope: invalid.illegal.stray-bracket-end
brackets:
- match: \)
pop: true
- include: main
连续的 context
这个例子将高亮包含很多分号的 C 语言风格的for
语句:
for_stmt:
- match: \(
set: for_stmt_expr1
for_stmt_expr1:
- match: ";"
set: for_stmt_expr2
- match: \)
pop: true
- include: expr
for_stmt_expr2:
- match: ";"
set: for_stmt_expr3
- match: \)
pop: true
- include: expr
for_stmt_expr3:
- match: \)
pop: true
- match: ";"
scope: invalid.illegal.stray-semi-colon
- include: expr
高级堆栈的使用
C 语言中,符号通常是和一个typedef
关键词一起定义的。符号有一个附加到它的entity.name.typescope
。
这样做事带有一点取巧,因为虽然类型定义很简单,但你也可以把它玩的很复杂。
typedef int coordinate_t;
typedef struct
{
int x;
int y;
} point_t;
认识到这些,当匹配到typoedef
关键词后,两个 context 会被添加到堆栈中:第一个将识别typename
,然后移出;第二个将识别这个类型介绍的名称:
main:
- match: \btypedef\b
scope: keyword.control.c
set: [typedef_after_typename, typename]
typename:
- match: \bstruct\b
set:
- match: "{"
set:
- match: "}"
pop: true
- match: \b[A-Za-z_][A-Za-z_0-9]*\b
pop: true
typedef_after_typename:
- match: \b[A-Za-z_][A-Za-z_0-9]*\b
scope: entity.name.type
pop: true
上面的例子中,typename
是一个可复用的 context,它将在typename
中读取然后在完成时从堆栈中移出。它可以在任意上下文中使用,如:typedef 中,或作为一个函数的参数。
PHP Heredocs
这个例子展示了如何匹配 PHP 中的Heredocs。main
context 中的match
捕获 heredoc 的识别符,heredoc
context 中相应的pop
指的 是捕获到的文本中有\1
的。
name: PHP
scope: source.php
contexts:
main:
- match: <<<([A-Za-z][A-Za-z0-9_]*)
push: heredoc
heredoc:
- meta_scope: string.unquoted.heredoc
- match: ^\1;
pop: true
Testing
当构建了一个语法定义,你可以定义一个语法测试文件来帮你检测作用域,而不是手动通过show_scope_name
指令进行检测。
// SYNTAX TEST "Packages/C/C.sublime-syntax"
#pragma once
// <- source.c meta.preprocessor.c++
// <- keyword.control.import
// foo
// ^ source.c comment.line
// <- punctuation.definition.comment
/* foo */
// ^ source.c comment.block
// <- punctuation.definition.comment.begin
// ^ punctuation.definition.comment.end
#include "stdio.h"
// <- meta.preprocessor.include.c++
// ^ meta string punctuation.definition.string.begin
// ^ meta string punctuation.definition.string.end
int square(int x)
// <- storage.type
// ^ meta.function entity.name.function
// ^ storage.type
{
return x * x;
// ^^^^^^ keyword.control
}
"Hello, World! // not a comment";
// ^ string.quoted.double
// ^ string.quoted.double - comment
遵循以下规则:
- 确保文件名以"syntax_test"开头。
- 确保文件存储在
Packages
目录下的某个地方:推荐和相应的.sublime-syntax
文件放在一起。 - 确保文件第一行以
<comment_token> SYNTAX TEST "<syntax_file>"
开头。注意语法文件可以是.sublime-syntax
或.tmLanguage
文件。
一但上述要求都满足了,运行 build 指令就可以在输出面板看到结果。下一个结果(F4)可以用来导航到第一个失败的测试。
语法测试文件中的每一个测试必须首先以注释标记开头,然后才是^
或<-
标记。
这两种类型的测试分别是:
- 尖号:
^
将测试针对最近一次的非测试行的作用域选择器,它将测试和^
所在列。连续的^
将测试选择器对应的每一列。 - 箭头:
<-
将测试针对最近一次的非测试行的作用域选择器,它将测试注释字符所在的列。