
原文地址: https:// macromates.com/manual/e n/language_grammars
为了了解 VSCode 高亮赶时间粗制滥翻的。
语言语法声明用于为文档元素的命名,如关键字、注释、字符串等等。它的目的主要用于语法高亮,使文本编辑器对于光标所在位置的上下文更“智能”。例如你可能想在键入的时候根据不同上下文出现不同的行为,或者说想要当你在某个位置键入的时候禁止拼写检查(如 HTML 标签)。
语言语法声明仅用于解析文档并将命名后的内容设为该文档的子集。这个时候作用域选择器就可以将其用于样式、首选项以及决定键触和触发器的行为。
有关此概念的更全面介绍,请参阅作用域介绍的博客文章。
语法示例
你可以自行创建一个新的语言语法,只需打开 Bundle 编辑器(Window → Show Bundle Editor)然后选择左下角 add 按钮的“New Language”。
这里给出一个初始声明,我将在后续解释。
01 { scopeName = 'source.untitled';
02 fileTypes = ( );
03 foldingStartMarker = '{s*$';
04 foldingStopMarker = '^s*}';
05 patterns = (
06 { name = 'keyword.control.untitled';
07 match = 'b(if|while|for|return)b';
08 },
09 { name = 'string.quoted.double.untitled';
10 begin = '"';
11 end = '"';
12 patterns = (
13 { name = 'constant.character.escape.untitled';
14 match = '.';
15 }
16 );
17 },
18 );
19 }
格式就是一个属性列表,根级元素是 5 个键值对:
scopeName
(第 1 行):这是声明的唯一标识,以点号(.
)进行分隔。通常情况下,该名字应该有两部分,第一部分为text
或者source
,而第二部分为该语言的名字或者文档类型。如果你定义一个已经存在的类型,你可能会想要派生出一个名字来。如 Markdown 就可以是text.html.markdown
,Ruby on Rails(rhtml
文件)就可以是text.html.rails
。从text.html
派生的好处就是所有在text.html
作用域下生效的特性一样会在text.html.«something»
生效(但是优先级会低于当前派生出来的作用域)。fileTypes
(第 2 行):这是一个文件类型后缀的数组。当 TextMate 打开一个文件的时候,不知道是什么类型的文件时会参考的数组。但是如果用户从状态栏的语言弹出窗中选择相应的语言,TextMate 也会记住该选择。foldingStartMarker
/foldingStopMarker
(第 3-4 行):这里面是正则表达式,将与文件内容进行匹配。如果文件中的某一行匹配上了(不是两者都匹配上),那么这行就会被标上折叠标记(详见“折叠”章节)。patterns
(第 5-18 行):这是用于解析文档的真实规则数组。在上面的样例中有两个规则(第 6-8 行和第 9-17 行)。规则将在下个章节中展开解释。
实际上在根级元素还有两种键,只不过在上述样例中没有给出展示:
firstLineMatch
:一个用于匹配文档第一行的正则表达式(首次载入时进行匹配)。如果匹配到了,那么该声明语法就会被用于该文档(除非用户自己覆盖了规则)。例如:^#!/.*brubyb
。repository
:一个可以包含从其它地方声明出来的语法的键值对字典。键名就是规则的名字,值就是真实规则。之后会在介绍include
规则的时候展开介绍。
语言规则
语言规则负责匹配文档内容。通常情况下,规则先会指定一个名字,该名字将会给到匹配到该规则的文档内容。
规则可以通过两种形式进行匹配。它可以是一个正则表达式,也可以是两个。就像上面的那个样例中第一个规则(第 6-8 行),只要匹配到这个正则表达式的内容就可以获得它对应的名字。例如第一个规则对应的名字是 keyword.control.untitled
,对应的匹配项为 if
、while
、for
和 return
这几个关键字。我们接下去就可以使用 keyword.control
这个作用域选择器用相应的主题来高亮这些关键字。
另一种匹配类型就是上面样例的第二个规则了(第 9-17 行)。这里有两个正则表达式,分别在 begin
和 end
里。该规则的名字将被赋予那些匹配到 begin
与 end
之间的内容。如果只匹配到了 begin
,那么 end
会被定为文档结尾。
在第二种匹配类型中,在 begin
与 end
之间的内容还可以继续匹配子规则。在我们的样例中,我们将匹配开始 "
与匹配结束的 "
给括成字符串,并在字符串中将转义符标记为 constant.character.escape.untitled
(第 13-15 行)。
需要注意的是,正则表达式一次只匹配文档的一行。也就是说它无法匹配多行内容。这是由技术原因导致的:解析器可以在任意行重新启动,并且只需要重新解析受编辑影响的最小行数。在大所述情况下,我们都可以使用 begin
/ end
模式来克服此限制。
规则字段
下面的这一系列键名都可以用在规则中。
name
:规则名。它将用于样式、作用域级别的设置和行为,这意味着它通常应该从一个标准的名称中派生出来(详见命名约定)。match
:一个用于匹配的正则表达式,例如'b(true|false)b
。begin
/end
:这两个字段允许匹配多行,并与match
字段互斥。begin
用于匹配开始,end
用于匹配结束。在begin
中捕获的内容可以在end
中被引用。这常用语 here-docs,例如:
{ name = 'string.unquoted.here-doc';
begin = '<<(w+)'; // match here-doc token
end = '^1$'; // match end of here-doc
}
一个 begin
/ end
规则可以用 pattern
嵌套。例如:
{ begin = '<%'; end = '%>'; patterns = (
{ match = 'b(def|end)b'; … },
…
);
};
上面的规则将会在 <% … %>
代码块之间匹配 def
和 end
关键字(实际上嵌入式的语法还可以使用后续讲到的 include
字段)。
contentName
:该字段与name
类似,但是只用于将名字赋给begin
/end
之间的内容。例如如果我们要将#if 0
和#endif
之间的内容标记为注释,就需要:
{ begin = '#if 0(s.*)?$'; end = '#endif';
contentName = 'comment.block.preprocessor';
};
captures
/beginCaptures
/endCaptures
:这些字段允许你为match
、begin
或者end
正则表达式捕获到的内容附上属性。在begin
/end
模式下使用captures
相当于是beginCaptures
与endCaptures
同值的一种缩写。
这些字段的值是一个键值对字典,其中键名为正则中捕获到的序号,值为需要附上的属性。目前只有name
属性可用。例如:
{ match = '(@selector()(.*?)())';
captures = {
1 = { name = 'storage.type.objc'; };
3 = { name = 'storage.type.objc'; };
};
};
这个例子中,我们将会匹配形如 @selector(windowWillClose:)
字样,storage.type.objc
只会赋给 @selector(
与 )
。
include
:该字段允许你从别的语言引用规则,可以递归引用。- 从别的语言引用,使用该语言的作用域名:
{ begin = '<?(php|=)?'; end = '?>'; patterns = (
{ include = "source.php"; }
);
}
-
- 引用自身,使用
$self
:
- 引用自身,使用
{ begin = '('; end = ')'; patterns = (
{ include = "$self"; }
);
}
-
- 引用当前语法库,在名称前加上井号(
#
):
- 引用当前语法库,在名称前加上井号(
patterns = (
{ begin = '"'; end = '"'; patterns = (
{ include = "#escaped-char"; },
{ include = "#variable"; }
);
},
…
); // end of patterns
repository = {
escaped-char = { match = '.'; };
variable = { match = '$[a-zA-Z0-9_]+'; };
};
这该可以用于匹配递归结构,如嵌套字符集:
patterns = (
{ name = 'string.unquoted.qq.perl';
begin = 'qq('; end = ')'; patterns = (
{ include = '#qq_string_content'; },
);
},
…
); // end of patterns
repository = {
qq_string_content = {
begin = '('; end = ')'; patterns = (
{ include = '#qq_string_content'; },
);
};
};
它可以用于匹配这样的内容:qq( this (is (the) entire) string).
。
命名约定
在 TextMate 中你用语法声明规则可以将任何的名字赋给文档的任意部分,然后在作用域选择器中使用这些名字。
然而,有些惯例可以使得一个主题可以适配更多的语言,而不需要针对每种语言都出不同的规则。例如你可以不希望在插入字符串和注释的时候自动匹配 ’
,这与语言无关,而在任何场景都是比较有必要的。
在介绍命名约定之前,我需要说一下几个要点:
- 一个最小化的主题只会定义 11 个根组中的 10 个样式(其中
meta
是不会有视觉样式的),所以你需要“展开”你的命名,而不是将所有内容都放到keyword
下面。你需要问自己“我需要将这两种元素的样式做到不一样吗?”如果是的,它们就应该被放到不同的根组中。 - 即使你需要“展开”你的命名,当你找到相应组(例如
storage
)的时候,你也应该先复用该组下已经存在的名字(例如storage
下的modifier
或者type
),而不是马上写一个新的名字。你还该为你所选的子类型添加尽可能多的信息。例如,如果你在 storage modifier 匹配到了static
,那么应该将其命名为storage.modifier.static.«language»
,而不是仅仅叫它storage.modifier
。为其加上更多的附加信息就可以将其余其它的storage.modifier
区别开来。 - 在名字的最后加上语言名。这看起来是多余的,因为你通常可以使用一个作用域选择器:
source.«language» storage.modifier
,但是对于嵌套声明语言来说这就不是一直会生效的了。
下面是 11 个根组,并附加上一些解释。这里的排版为分层列表,但是就是作用域名是通过每个级别的名称用点号(.
)拼接起来的。例如 double-slash
全称就是 comment.line.double-slash
。
comment
:注释。line
:行注释,我们将注释分化,以便可以从范围中提取注释开始字符的类型。double-slash
:// comment
double-dash
:-- comment
number-sign
:# comment
percentage
:% comment
- 字符:其它行类型
block
:多行注释,如/* … */
和<!-- … -->
。documentation
:嵌入式文档
constant
:常量。numeric
:匹配数字,如42
、1.3f
、0x4AB1U
。character
:匹配字符,如<
、e
、031
。escape
:转义序列,如e
应该为转义序列。
language
:由语言层面定义的特殊常量,如true
、false
、nil
、YES
、NO
等。other
:其它常量,如 CSS 中的颜色。
entity
:实体,指文档中比较大坨的部分,如章节、类、函数或者标记。我们并不讲所有的实体都归于entity.*
,有些会归于meta.*
。但我们确实会使用entity.*
作为一些大坨实体的占位符,例如如果一个实体是章节,那么我们会用entity.name.section
作为章节标题。name
:用于命名一个大实体。function
:函数实体。type
:类型定义或者类。tag
:标记。section
:章节。
other
:其它实体。inherited-class
:超级、基类。attribute-name
:属性(通常在 tag 中出现)。
invalid
:非法内容。illegal
:非法,如 HTML 中的&
等(非实体 / tag 中的部分)。deprecated
:过时内容,如使用了不推荐使用的 API 函数。
keyword
:关键字(当它无法被归并到别的组时使用)。control
:通常是流程控制关键字,如continue
、while
、return
等。operator
:文本(如or
)或者字符类型的操作符。other
:其它关键字。
markup
:适用于标记语言,通常适用于较大的文本子集。underline
:下划线文本。link
:用于超链接,为了方便起见,它是从markup.underline
派生的,这的话即使没有专门针对markup.underline.link
的主题规则,也能继承下划线样式。
bold
:加粗文本。heading
:章节头。(可选)为接下去的元素提供标题级别,如 HTML 中的markup.heading.2.html
为<h2>…</h2>
。italic
:斜体文本。list
:列表项。numbered
:有序列表项。unnumbered
:无序列表项。
quote
:引用文本。raw
:逐字文本,如代码项。通常对于markup.raw
是禁用拼写检查的。other
:其它标记结构。
meta
:meta 作用域通常用于标记文档的较大部分。如声明函数的整行将是meta.function
,子集将是storage.type
、entity.name.function
、variable.parameter
等,并且只有后者才会被样式化。storage
:与存储相关的内容。type
:类型,如class
、function
、int
、var
等。modifier
:存储相关的修饰符,如static
、final
、abstract
等。
string
:字符串。quoted
:引号字符串。single
:单引号字符串,如'foo'
。double
:双引号字符串,如"foo"
。triple
:三引号字符串,如"""Python"""
。other
:其它字符串,如$'shell'
、%s{...}
。
unquoted
:如 here-docs 和 here-strings。interpolated
:需要被计算的字符串,如反引号date反引号
、$(pwd)
。regexp
:正则表达式。other
:其它字符串类型。
support
:框架或者库提供的内容。function
:框架或者库提供的函数,如 Objective-C 中的NSLog
就应该是support.function
。class
:框架或者库提供的类。type
:框架或者库提供的类型,有可能只在 C 系语言中使用,如typedef
等。大多数其它语言都会被认为是类。constant
:框架或者库提供的常量。variable
:框架或者库提供的变量,如 AppKit 中的NSApp
。other
:其它内容。
variable
:变量。parameter
:参数。language
:语言保留变量,如this
、super
、self
等。other
:其它变量,如$some_variables
。