2015-05-10 更新:Sublime Text 3 build 3084 引入了一种全新的sublime-syntax 格式来编写语法定义。它比 Sublime 从 TextMate 继承的旧系统要好得多。新系统应该很快就会登陆公共 ST3 beta 版本。由于 ST3 是 recommended version of Sublime,我建议使用 new system 而不是下面描述的系统编写任何新的荧光笔。
这是 Sublime Text 语法高亮的速成课程。
设置
首先,正如@lthreed 所指出的,您可以使用PackageResourceViewer 查看Sublime Text 附带的默认包。那些 .tmLanguage 文件都是 plist 格式,阅读和理解起来非常困难。 PackageDev 可以将 plist 文件转换为可读性更高的 JSON 或 YAML 格式。当您通过查看默认包进行学习时,请务必先将其转换为 YAML。请注意,PackageDev 可能无法完美转换它。没关系。您只是将代码用作参考。
plist 是 Sublime 可以理解的原生格式,但这并不意味着你应该这样写。我强烈建议使用 YAML 编写荧光笔并将其转换为带有 PackageDev 的 plist。不要用 JSON 写它。 JSON 不支持原始字符串。所有的正则表达式都必须双重转义。这绝对是一场噩梦。只需使用 YAML。
您可以通过打开命令面板(Mac 上为cmd+shift+p)并选择PackageDev: New YAML Syntax Definition 来开始新的语法定义。当您准备好对其进行测试时,打开命令面板并选择PackageDev: Convert (YAML, JSON, PList) to...,PackageDev 会发现您有一个 YAML 文件并想要转换为 plist。转换会获取你的 .YAML-tmLanguage 文件并输出一个 Sublime 可以理解的 .tmLanguage 文件。将该文件放在 /Packages/User 目录中,Sublime 将加载并应用它(您可能需要重新启动)。
Sublime 语法高亮的工作原理
您正在编写的语法定义不会直接为文本着色。它将范围名称应用于文本。然后编写 Monokai 和 Solarized 等主题的人会出现并制作将范围名称与颜色相关联的文件。您可以编写自己的范围名称,但您应该坚持使用the official TextMate scope names。这些范围名称对于您匹配的代码可能根本没有任何意义。没关系。只要尽力而为。如果您必须组成范围名称,请使用 TextMate 范围名称作为起点。例如,您可以组成一个名为 string.quoted.triple.xxx 的范围名称,而不是 string.quoted.double.xxx(其中 xxx 是您匹配的语言的文件扩展名)。
代码示例
以下是文件扩展名为 .matt 的合成 matt 语言的语法定义。它只有两条规则:一条用于匹配管道分隔的字符串,另一条用于匹配具有更复杂分隔符的字符串。
# [PackageDev] target_format: plist, ext: tmLanguage
---
name: Mattlang
scopeName: source.matt
fileTypes: [matt]
patterns:
- include: '#pipe-string'
- include: '#complex-string'
# Rules defined in the repository can reference each other. You can include
# one rule inside another.
repository:
# This is a rule of the begin-end form. The rule matches a string bounded by
# pipes, such as |hello there|
pipe-string:
# The optional 'name' field lets you apply a single scope to everything,
# including the begin-end pipes. All the scope names must end with .matt
name: everything.matt
# We have to escape the pipe character, because it's a special character in
# the Oniguruma regex syntax (and most other regex engines).
begin: \|
# 'beginCaptures' is required if you want the pipes to be colored differently
beginCaptures:
# In regex jargon, the begin pipe is 'captured'. Capture group 0 means the
# entire match, which in this case is just the pipe.
'0': {name: entire.begin.match.matt}
# The optional 'contentName' field lets you apply a scope to all the text
# between (but not including) the begin-end pipes.
contentName: stuff.between.the.pipes.matt
patterns:
# These rules will only be applied to the text *BETWEEN* the pipes. Sublime
# will go through the rules from top to bottom and try to match the text, so
# higher rules have a higher "precedence" and will get matched first.
# Given the text |hello there|, Sublime will see an 'h' character and move
# through the rules from top to bottom trying to find a rule that starts
# with 'h'. The #hell rule will match the 'h' and the rest of the
# characters. The #hell scope name will be applied to the 'hell' text and
# Sublime will resume trying to find the next match at the 'o' character.
# The 'o' character WILL NOT match #hello. You can think of the matched text
# as being removed from the stream entirely. The point is: order matters.
- include: '#hell'
- include: '#hello'
- end: \|
endCaptures:
'0': {name: entire.end.match.matt}
# This is the other form of rule you can define. It's extremely simple --
# just a scope name and a regex pattern to match. Note that these rules will
# only match text on the same line, unlike begin-end rules, which can cover
# multiple lines.
hell:
name: some.other.scope.matt
match: hell
hello:
name: some.scope.matt
match: hello
# This rule matches a string that starts with $!! and ends with !!$,
# e.g. !!$hello there!!$
complex-string:
# I've labeled the capture groups.
# |---0---|
# |--1-||3|
begin: (!(!))($)
# |2|
beginCaptures:
'0': {name: full.match.matt}
'1': {name: both.exclamation.marks.matt}
'2': {name: second.exclamation.mark.matt}
'3': {name: dollar.sign.matt}
# It's ok to leave out the 'patterns' field. Technically, all you really
# need is a 'begin' field and an 'end' field.
end: ((!)!)($)
endCaptures:
'0': {name: everything.matt}
'1': {name: both.exclamation.marks.matt}
'2': {name: first.exclamation.mark.matt}
'3': {name: dollar.sign.matt}