跳至内容

语法定义

语法定义使 Sublime Text 能够识别编程和标记语言。最明显的是,它们与颜色一起工作以提供语法高亮。语法定义定义了 *作用域*,将缓冲区中的文本划分为命名区域。Sublime Text 中的几个编辑功能广泛利用了这种细粒度的上下文信息。

本质上,语法定义由用于查找文本的正则表达式以及或多或少任意的、用点分隔的字符串(称为 *作用域* 或 *作用域名称*)组成。对于给定正则表达式的每次出现,Sublime Text 会为匹配的文本赋予其对应的 *作用域名称*。

弃用通知

对于 Sublime Text 3(版本 3084),已添加了一种新的语法定义格式,扩展名为 .sublime-syntax

强烈建议使用它来代替本文档中描述的传统 TextMate 格式,除非需要与旧版本或其他编辑器兼容。

文档可在 官方文档 中找到。

先决条件

为了遵循本教程,您需要安装 PackageDev,这是一个旨在简化 Sublime Text 新语法定义创建的包。请按照自述文件“入门”部分中的安装说明进行操作。

文件格式

Sublime Text 使用 属性列表 (Plist) 文件来存储语法定义。但是,由于编辑 XML 文件是一项繁琐的任务,我们将使用 YAML,然后将其转换为 Plist 格式。这就是 :PackageDev: 包(如上所述)发挥作用的地方。

注意

如果您在本教程中遇到意外错误,则可能是 :PackageDev: 或 YAML 导致的。不要立即认为您的问题是 Sublime Text 中的错误。

如果您更喜欢在 XML 中工作,请随时手动编辑 Plist 文件,但请始终牢记它们在转义序列、许多 XML 标记等方面的不同需求。

作用域

作用域是 Sublime Text 中的一个关键概念。本质上,它们是缓冲区中的命名文本区域。它们本身不会执行任何操作,但 Sublime Text 在需要上下文信息时会查看它们。

例如,当您触发代码片段时,Sublime Text 会检查与代码片段绑定的作用域,并查看文件中的光标位置。如果光标的当前位置与代码片段的作用域选择器匹配,Sublime Text 会将其触发。否则,什么也不会发生。

信息

*作用域* 和 *作用域选择器* 之间存在细微差别:作用域是在语法定义中定义的名称,而作用域选择器用于代码片段和键绑定等项目来定位作用域。在创建新的语法定义时,您关心的是作用域;当您想将代码片段限制在特定作用域时,您使用作用域选择器。

作用域可以嵌套,以实现高度的粒度。您可以像使用 CSS 选择器一样深入层次结构。例如,借助作用域选择器,您可以将键绑定仅在 Python 源代码中的单引号字符串内激活,而不在任何其他语言的单引号字符串内激活。

Sublime Text 从 Textmate(一款适用于 Mac 的文本编辑器)继承了作用域的概念。 Textmate 的在线手册 包含有关作用域选择器的更多信息,对 Sublime Text 用户也很有用。特别是,颜色方案广泛使用作用域来以所需颜色样式化语言的各个方面。

语法定义的工作原理

从本质上讲,语法定义是正则表达式数组与作用域名称的配对。Sublime Text 将尝试将这些模式与缓冲区的文本进行匹配,并将相应的作用域名称附加到所有匹配项。这些正则表达式和作用域名称对被称为规则

规则按顺序应用,一次一行。规则按以下顺序应用

  1. 与一行中第一个位置匹配的规则
  2. 数组中排在最前面的规则

每个规则都会消耗匹配的文本区域,因此该区域将从下一个规则的匹配尝试中排除(除了一些例外)。实际上,这意味着在创建新的语法定义时,您应该注意从更具体的规则到更通用的规则。否则,贪婪的正则表达式可能会吞噬您希望以不同方式设置样式的部分。

来自不同文件的语法定义可以组合在一起,并且也可以递归应用。

您的第一个语法定义

例如,让我们为 Sublime Text 代码段创建语法定义。我们将对实际的代码段内容进行样式设置,而不是对整个 .sublime-snippet 文件进行样式设置。

注意

由于语法定义主要用于启用语法高亮显示,因此我们将使用短语设置样式来表示将源代码文件分解为作用域。但是请记住,颜色与语法定义不同,作用域除了语法高亮显示之外还有更多用途。

以下是我们想要在代码段中设置样式的元素

  • 变量($PARAM1$USER_NAME\ ...)
  • 简单字段($0$1\ ...)
  • 带有占位符的复杂字段(${1:Hello}
  • 嵌套字段(${1:Hello ${2:World}!}
  • 转义序列(\$\<、…)
  • 非法序列($<\、…)

以下是我们不想设置样式的元素,因为它们对于此示例来说过于复杂

  • 变量替换(${1/Hello/Hi/g}

注意

在继续之前,请确保您已安装 :PackageDev: 包,如上所述。

创建新的语法定义

要创建新的语法定义,请执行以下步骤

  1. 转到工具 | 包 | 包开发 | 新的语法定义
  2. 将新文件保存在您的 Packages/User 文件夹中,作为 .YAML-tmLanguage 文件。

现在您应该看到一个这样的文件

yaml
# [PackageDev] target_format: plist, ext: tmLanguage
---
name: Syntax Name
scopeName: source.syntax_name
fileTypes: []
uuid: 0da65be4-5aac-4b6f-8071-1aadb970b8d9

patterns:
-
...

让我们检查一下关键元素。

  • name
    Sublime Text 将在语法定义下拉列表中显示的名称。使用简短、描述性的名称。通常,您将使用您正在为其创建语法定义的编程语言的名称。

  • scopeName
    此语法定义的最顶层作用域。它采用 source.<lang_name>text.<lang_name> 的形式。对于编程语言,请使用 source。对于标记语言和其他所有内容,请使用 text

  • fileTypes
    这是一个文件扩展名列表(不带前导点)。当打开这些类型的文件时,Sublime Text 将自动为它们激活此语法定义。

  • uuid
    这是一个语法定义的唯一标识符。每个新的语法定义都有自己的 uuid。即使 Sublime Text 本身会忽略它,也不要修改它。

  • patterns
    用于存放你的模式。

对于我们的示例,请使用以下信息填充模板

yaml
# [PackageDev] target_format: plist, ext: tmLanguage
---
name: Sublime Snippet (Raw)
scopeName: source.ssraw
fileTypes: [ssraw]
uuid: 0da65be4-5aac-4b6f-8071-1aadb970b8d9

patterns:
-
...

注意

YAML 不是一种非常严格的格式,但如果你不了解它的约定,可能会导致头痛。它支持单引号和双引号,但只要内容不会创建另一个 YAML 字面量,你也可以省略它们。如果转换为 Plist 失败,请查看输出面板以获取有关错误的更多信息。我们将在后面解释如何将 YAML 中的语法定义转换为 Plist。这也会涵盖模板中的第一行注释。

---... 是可选的。

分析模式

patterns 数组可以包含几种类型的元素。我们将在接下来的部分中介绍其中的一些。如果你想了解更多关于模式的信息,请参考 Textmate 的在线手册。

匹配

匹配采用以下形式

yaml
match: (?i:m)y \s+[Rr]egex
name: string.format
comment: This comment is optional.

Sublime Text 在语法定义中使用 Oniguruma 的正则表达式语法。一些现有的语法定义使用了该正则表达式引擎支持但不是 perl 风格正则表达式的一部分的功能,因此需要 Oniguruma。

match
: Sublime Text 将用来查找匹配项的正则表达式。

name
: 应该应用于 match 任何出现的范围的名称。

comment
: 关于此模式的可选注释。

让我们回到我们的示例。它看起来像这样

yaml
# [PackageDev] target_format: plist, ext: tmLanguage
---
name: Sublime Snippet (Raw)
scopeName: source.ssraw
fileTypes: [ssraw]
uuid: 0da65be4-5aac-4b6f-8071-1aadb970b8d9

patterns:
-
...

也就是说,确保 patterns 数组为空。

现在我们可以开始为 Sublime 代码段添加规则。让我们从简单的字段开始。这些可以用这样的正则表达式匹配

perl
\$[0-9]+
# or...
\$\d+

然后我们可以这样构建我们的模式

yaml
name: keyword.other.ssraw
match: \$\d+
comment: Tab stops like $1, $2...

选择正确的范围名称

有时命名范围并不明显。查看 Textmate 命名约定 以获取有关范围名称的指南。:PackageDev: 会根据这些约定自动提供范围名称的补全。如果你想实现与现有颜色的最高兼容性,重新使用那里概述的基本类别非常重要。

配色方案中硬编码了范围名称。它们不可能包含你能想到的每个范围名称,因此它们偶尔会针对标准范围名称以及一些更罕见的范围名称(例如 CSS 或 Markdown)。这意味着两个使用相同语法定义的配色方案可能会以不同的方式呈现文本!

还要记住,你应该使用最适合你的需求或偏好的范围名称。如果你有充分的理由,将 constant.numeric 之类的范围分配给除数字以外的任何东西都是完全可以的。

我们也可以将其添加到我们的语法定义中

yaml
# [PackageDev] target_format: plist, ext: tmLanguage
---
name: Sublime Snippet (Raw)
scopeName: source.ssraw
fileTypes: [ssraw]
uuid: 0da65be4-5aac-4b6f-8071-1aadb970b8d9

patterns:
- comment: Tab stops like $1, $2...
  name: keyword.other.ssraw
  match: \$\d+
...

注意

你应该使用两个空格进行缩进。这是 YAML 的推荐缩进,并且与上面显示的列表对齐。

现在我们已经准备好将我们的文件转换为 .tmLanguage。语法定义使用 Textmate 的 .tmLanguage 扩展名以确保兼容性。如上所述,它们只是 Plist XML 文件。

按照以下步骤执行转换

  • 确保在 **工具 | 构建系统** 中选择了 **自动**,或选择 **转换为 ...**。
  • Ctrl B。一个 .tmLanguage 文件将为你生成,与你的 .YAML-tmLanguage 文件位于同一个文件夹中。
  • Sublime Text 将重新加载对语法定义的更改。

如果你想知道为什么 :PackageDev: 知道你想将你的文件转换为什么:它是在第一行注释中指定的。

你现在已经创建了你的第一个语法定义。接下来,打开一个新文件并使用 .ssraw 扩展名保存它。缓冲区的语法名称应该自动切换到“Sublime 代码段(原始)”,如果你键入 $1 或任何其他简单的代码段字段,你应该会得到语法高亮。

让我们继续为环境变量创建另一个规则。

yaml
comment: Variables like $PARAM1, $TM_SELECTION...
name: keyword.other.ssraw
match: \$[A-Za-z][A-Za-z0-9_]+

重复上述步骤以更新.tmLanguage文件。

微调匹配

您可能已经注意到,例如,$PARAM1 中的整个文本都以相同的方式进行样式设置。根据您的需求或个人喜好,您可能希望$脱颖而出。这就是captures发挥作用的地方。使用捕获,您可以将模式分解为组件以单独定位它们。

让我们重写我们之前的一个模式以使用captures

yaml
comment: Variables like $PARAM1, $TM_SELECTION...
name: keyword.other.ssraw
match: \$([A-Za-z][A-Za-z0-9_]+)
captures:
  '1': {name: constant.numeric.ssraw}

捕获会给您的规则带来复杂性,但它们非常简单。请注意,数字指的是从左到右的括号组。当然,您可以拥有任意数量的捕获组。

注意

在新建行上写1并按Tab键,将自动完成为'1': {name: },这要归功于:PackageDev:。

可以说,您希望另一个范围在视觉上与这个范围保持一致。继续更改它。

注意

与通常的正则表达式和替换一样,捕获组'0'适用于整个匹配项。

开始-结束规则

到目前为止,我们一直在使用一个简单的规则。虽然我们已经了解了如何将模式分解成更小的组件,但有时您可能希望定位源代码中更大的一部分,这些部分由开始和结束标记明确分隔。

用引号或其他分隔符构造括起来的文字字符串最好通过开始-结束规则来处理。这是其中一个规则的骨架

yaml
name:
begin:
end:

好吧,至少在它们最简单的版本中。让我们看看一个包含所有可用选项的规则

yaml
name:
contentName:
begin:
beginCaptures:
  '0': {name: }
  # ...
end:
endCaptures:
  '0': {name: }
  # ...
patterns:
- name:
  match:
# ...

有些元素可能看起来很熟悉,但它们的组合可能令人生畏。让我们逐个检查它们。

name : 与简单的捕获一样,这将以下范围名称设置为整个匹配项,包括beginend标记。实际上,这将为在此规则中定义的beginCapturesendCapturespatterns创建嵌套范围。可选。

contentName : 与name不同,它只将范围名称应用于封闭的文本。可选。

begin : 此范围的开头标记的正则表达式。

end : 此范围的结束标记的正则表达式。

beginCaptures : begin标记的捕获。它们的工作原理与简单匹配的捕获相同。可选。

endCaptures : 与beginCaptures相同,但适用于end标记。可选。

patterns : 一个模式数组,用于匹配开始-结束的内容;它们不会与beginend本身消耗的文本匹配。可选。

我们将使用此规则来设置片段中嵌套的复杂字段的样式

yaml
name: variable.complex.ssraw
contentName: string.other.ssraw
begin: '(\$)(\{)([0-9]+):'
beginCaptures:
  '1': {name: keyword.other.ssraw}
  '3': {name: constant.numeric.ssraw}
end: \}
patterns:
- include: $self
- name: support.other.ssraw
  match: .

这是我们在本教程中将看到的最复杂的模式。beginend键不言自明:它们定义了${<NUMBER>:}之间封闭的区域。我们需要将开始模式包装在引号中,因为否则尾随的:将告诉解析器期望另一个字典键。beginCaptures进一步将开始标记划分为更小的范围。

然而,最有趣的部分是patterns。递归和排序的重要性终于在这里出现了。

我们在上面看到过,字段可以嵌套。为了解决这个问题,我们需要递归地设置嵌套字段的样式。这就是当我们向它提供$self值时include规则的作用:它将我们的整个语法定义递归地应用于我们的开始-结束规则捕获的文本。这部分排除了beginend的正则表达式分别消耗的文本。

请记住,匹配的文本会被消耗;因此,它将从下一次匹配尝试中排除,并且无法再次匹配。

为了完成复杂的字段,我们将占位符设置为字符串。由于我们已经匹配了复杂字段中所有可能的标记,因此我们可以安全地告诉Sublime Text将任何剩余的文本(.)设置为文字字符串范围。请注意,如果我们使模式贪婪(.+),则此方法不起作用,因为这包括可能的嵌套引用。

注意

我们可以使用contentName: string.other.ssraw代替最后一个模式,但这样我们引入了排序的重要性以及匹配是如何被消耗的。

收尾工作

最后,让我们来设置转义序列和非法序列的样式,然后就可以收尾了。

yaml
- comment: Sequences like \$, \> and \<
  name: constant.character.escape.ssraw
  match: \\[$<>]

- comment: Unescaped and unmatched magic characters
  name: invalid.illegal.ssraw
  match: '[$<>]'

这里唯一难的地方是不要忘记在 YAML 中 [] 用于括起数组,因此必须用引号括起来。除此之外,如果您熟悉正则表达式,规则就非常简单明了。

但是,您必须注意将第二个规则放在任何匹配 $ 字符的其他规则之后,否则它将被消耗掉,导致所有后续表达式不匹配。

此外,即使添加了这两个额外的规则,请注意我们上面提到的递归开始-结束规则仍然按预期工作。

最后,这是最终的语法定义

yaml
# [PackageDev] target_format: plist, ext: tmLanguage
---
name: Sublime Snippet (Raw)
scopeName: source.ssraw
fileTypes: [ssraw]
uuid: 0da65be4-5aac-4b6f-8071-1aadb970b8d9

patterns:
- comment: Tab stops like $1, $2...
  name: keyword.other.ssraw
  match: \$(\d+)
  captures:
    '1': {name: constant.numeric.ssraw}

- comment: Variables like $PARAM1, $TM_SELECTION...
  name: keyword.other.ssraw
  match: \$([A-Za-z][A-Za-z0-9_]+)
  captures:
    '1': {name: constant.numeric.ssraw}

- name: variable.complex.ssraw
  begin: '(\$)(\{)([0-9]+):'
  beginCaptures:
    '1': {name: keyword.other.ssraw}
    '3': {name: constant.numeric.ssraw}
  end: \}
  patterns:
  - include: $self
  - name: support.other.ssraw
    match: .

- comment: Sequences like \$, \> and \<
  name: constant.character.escape.ssraw
  match: \\[$<>]

- comment: Unescaped and unmatched magic characters
  name: invalid.illegal.ssraw
  match: '[$<>]'
...

使用“存储库”还有更多可用的结构和代码重用技术,但上面的解释应该可以帮助您开始创建语法定义。

注意

如果您之前使用 JSON 来定义语法,您仍然可以这样做,因为 :PackageDev: 向后兼容。

如果您想考虑切换到 YAML(无论是从 JSON 还是直接从 Plist),它提供了一个名为 PackageDev: Convert to YAML and Rearrange Syntax Definition 的命令,它会自动以一种愉悦的方式格式化生成的 YAML。