最近在研究vscode插件开发。作为恰饭工具里最常用的ide,如果能够做到一些自己想要的功能,那是可以极大的提高生产效(xin)率(qing)的,说不定还可以事(tou)半(tou)功(mo)倍(yu)。
萌生这个想法的初衷是因为工作当中使用了一个叫做ClearSilver的c++直出模板,这些模板代码因为没有解析器,在html文件当中显示就是一坨白色,非常影响开发心情,以及摧残视力。
就像这样:
然而vscode的插件市场并没有可以识别它的插件,可能因为太过于小众了。但是出于身为程序猿的自己身心健康,本着长痛不如短痛,决定从头撸一个语法高亮插件去解决这个问题。
国内有不少vscode的插件入门教程,不过基本都没有提过语法解析这块内容,官网写的例子也是非常简单。经过一段比较长时间的摸索,大概可以给出一些结论,这里记录下来,欢迎同行大神指正和交流。
vscode插件开发入门知识储备请戳这里,作者写的很好很全面。
vscode官方有一个简单的语法高亮栗子,介绍了一个"abc"语言如何去实现高亮操作。
然而个人认为官网上的栗子不够直观,这里小小的改造了一下:
效果预期:
这里可以看到,a、b和c被蓝色单独突出显示,而x只会在括号中高亮显示。已经初具一定的高亮规模了。
这里有个小细节,最后的x也高亮显示了。至于为什么会这样,稍安勿躁,会在下文中解释。
实现以上效果可以参考vscode的例子进行,也可以直接通过以下的步骤进行。
package.json添加如下代码
{
"contributes": {
"languages": [
{
"id": "abc",
"extensions": [".abc"]
}
],
"grammars": [
{
"language": "abc",
"scopeName": "source.abc",
"path": "./syntaxes/abc.tmGrammar.json"
}
]
}
}
./syntaxes/abc.tmGrammar.json代码如下
{
"scopeName": "source.abc",
"patterns": [
{ "include": "#expression" }
],
"repository": {
"expression": {
"patterns": [
{ "include": "#letter" },
{ "include": "#paren-expression" }
]
},
"letter": {
"match": "a|b|c",
"name": "keyword.letter"
},
"letterX": {
"match": "x",
"name": "keyword.letterX"
},
"paren-expression": {
"begin": "\\(",
"end": "\\)",
"beginCaptures": {
"0": { "name": "punctuation.paren.open" }
},
"endCaptures": {
"0": { "name": "punctuation.paren.close" }
},
"name": "expression.group",
"patterns": [
{ "include": "#letter" },
{ "include": "#letterX" }
]
}
}
}
按下F5(调试),新建一个文件test.abc文件,输入
a
(
b
)
x
(
(
c
xyz
)
)
(
a
a(b)x(((c xyz))) (x
就可以看到对应的语法高亮了
vscode(以及大部分ide)使用syntaxes来描述一门语言。syntaxes一般使用json格式,但是因为json格式比较啰嗦,同时不能加注释,作为配置文件,在代码较多的情况下,会显得非常难以阅读。
这里(包括官方)推荐使用yaml(教程点这里)来作为源代码配置,然后再手动或者通过自动化转为json。
为了保证可读性,如不特殊说明,以下部分均使用yaml代替json
一份syntax文件的模板是这个样子的:
# [PackageDev] target_format: plist, ext: tmLanguage
---
name: Syntax Name
scopeName: source.syntax_name
patterns: []
repository: {}
...
各个属性含义如下:
name[选填]:语法名称。在某些ide里会当做名称放在列表菜单里。
scopeName[必填]:语法的顶级scope名称,可以在syntax文件中随处引用。命名方式有source.
或者text.
。前者一般用于程序语言,后者一般用于标记性的语言。比如javascript的scopeName为source.js
,html的scopeName为text.html.basic
。
patterns[必填]:语法正文。
repository[选填]:用于储存子scope,类似于变量的存在,挂在顶级scope名下。可以用include
引入。
syntax使用正则表达式匹配语法,这里的正则使用的是这个版本。相对于前端同学熟悉的阉割版的js正则,这个版本的正则更完整,有更多的语法(是的,你还得学)。
patterns有两种配置:
完整匹配:
# Fine Tuning Matches
---
patterns:
- name: keyword.other.ssraw
match: \$([A-Za-z][A-Za-z0-9_]+)
captures:
'1': {name: constant.numeric.ssraw}
...
begin-end匹配:
# Begin-End Rules
---
patterns:
- name: string.other.ssraw
begin: (\$)(\{)([0-9]+):
beginCaptures:
'1': {name: keyword.other.ssraw}
'3': {name: constant.numeric.ssraw}
end: \}
patterns:
- include: 'source.js#string'
- include: '#command-set'
- name: support.other.ssraw
match: .
...
source.js#string
,意思是引入source.js
(javascript)下repository中的string
(字符串语法规则)。这里有一点要特别注意的是,begin-end规则匹配顺序是:begin
-> patterns[].include
-> end
。
这就是开头demo为什么最后一个x
还会高亮的原因了。另外,如果patterns里面匹配了end的部分,则这条规则不会在预想的end部分闭合。
常用的解决方式是patterns中的规则要用正则的前瞻(?=)去匹配end的边缘部分。
Repository:
repository与patterns类似,区别在于需要以scope名称(也就是key名)开头。
# Repository Rules
---
repository:
# set
command-set:
name: meta.namespace.set
begin: (set)(:)
beginCaptures:
'1': { name: keyword.commands.set }
'2': { name: punctuation.separator.key-value.csr }
end: (?=\s*\?>)
patterns:
- include: '#expression'
# name
command-name:
name: meta.namespace.name
match: (name)(:)([a-zA-Z0-9._]+)
captures:
'1': { name: storage.type.name }
'2': { name: punctuation.separator.key-value.csr }
'3': { name: variable.other.readwrite }
...
repository各属性下与patterns的规则一致。
细心的同学可能有些疑问,name以及captures当中的这些命名是根据什么来的。
我们回想一下vscode当中,不同语法模块显示的颜色不一样,具有不同的样式。这里的这些命名可以理解为某些特定样式的class下的属性。比如storage.type
有一个预设样式为"foreground": "#569cd6"
,那么storage.type.function.js
就会命中这个样式,显示#569cd6
这个颜色。vscode皮肤插件就是针对这些class设计不同的样式,至于怎么使用这些样式,就是我们的问题了。
同时根据这些命名不难看出,针对大部分语法场景已经有一套成熟的使用场景。
在captures当中,比如单引号用string.quoted.single
,比较运算符用keyword.operator.relational
。
在name当中,一般以meta开头命名代表语法块,比如if语法块叫做meta.tag.if
,方法里面的参数叫做meta.function.parameters
。
想要参考已有的语法设计结构,可以在vscode编辑器区域当中用组合键:cmd(win)+shift+p,选择"Developer: Inspect TM Scopes",会弹出一个语法结构的浮层,显示当前光标悬浮位置的语法信息。
下图标红的位置就是由captures和name组成的语法堆栈解析信息了,而位于栈顶的entity.name.function.js
就是foo这个方法名的语法名称,命中了entity.name.function
的样式信息,故显示出了黄色
那么有没有比较完整的推荐命名规则呢?有,点这里慢慢参考。
开始分析前面提到的abc语言的例子(终于开始分析了)。
abc涉及了三个部分,首先是在package里面增加的那部分代码,作用是注册一门语言。每一句的配置作用如下
# Package.contributes
---
contributes:
languages:
- id: abc # 语言唯一id
extensions: # 所适配的拓展名
- ".abc"
grammars:
- language: abc # 对应languages里的id
scopeName: source.abc # 语法顶级scope名称
path: "./syntaxes/abc.tmGrammar.json" # 语法配置文件
...
在分析语法配置之前,先回忆一下这个demo预期想要达到的结果
按照思路,解析应该分为以下步骤:
按照这个思路我们来看语法文件
# [PackageDev] target_format: plist, ext: tmLanguage
---
# 顶级scope名称
scopeName: source.abc
patterns:
# 引用repository#expression
- include: '#expression'
repository:
# 定义expression规则
expression:
patterns:
# expression使用#letter规则
- include: '#letter'
# expression使用#letter规则
- include: '#paren-expression'
# 定义letter规则
letter:
# 匹配a或者b或者c
match: a|b|c
# 匹配规则的名称
name: keyword.letter
letterX:
match: x
name: keyword.letterX
paren-expression:
# 从'('开始
begin: '\('
# 遇到')'结束
end: '\)'
beginCaptures:
'0':
name: punctuation.paren.open
endCaptures:
'0':
name: punctuation.paren.close
name: expression.group
patterns:
# 在'('后使用#letter的规则
- include: '#letter'
# 在'('后使用#letterX的规则
- include: '#letterX'
...
这样,abc语言就解析完毕了。
其实这上面的匹配规则有不少bug,比如没有区分括号对,只是简单粗暴的遇到闭括号就结束。不过作为例子,这个bug反而更能进一步说明内部解析的机制。有兴趣的同学可以尝试改造一下。
通过以上的知识点,我断断续续的花了比较久的时间跨度才完成开头所说的ClearSilver这个语法插件。事实上代码量不是很多,300多行,不过工作量主要在于这些正则的匹配是否全面、到位,调了非常久的时间。
关于如何组织代码,有兴趣的同学可以去我的github页面上去了解下这个文件,欢迎提出改进的建议~
另外也可以参考vscode内置的syntaxes去学习一些组织方式和技巧,地址戳这里,目录为./extensions/
或者可以直接打开已发布的插件,拖进项目目录学习。mac下的目录为~/.vscode/extensions/
,windows为C:\Users\用户名\.vscode\extensions
。
这个项目参考了vue的语法插件octref.vetur
和javascript的syntaxessource.js
的代码。
如果语法块变量include的不恰当,可能会造成死循环。可以在vscode上通过Help->Toggle Developer Tools查看到具体报错信息。
如果是作为前端的一员,可能会对这么多正则堆叠上来的效率有疑问。然而vscode在匹配效率上做的非常好。在不开启语法堆栈解析的情况下,1000行以内基本可以做到秒出。另外就是vscode的内置javascript的syntaxes上的正则数量和长度都是非常多和长的,所以不用过于担心这点。当然如果能够有时间和精力优化正则效率最好了,不过要看优化的边际成本是否大于边际利益了(XD)。
vscode当中一行解析的字符长度是有限的,系统默认是20000,相关设置的key是editor.maxTokenizationLineLength
,超过这个长度,将只有一坨白色代码躺在那里。所以直接打开压缩的代码在视觉效果上可能会比较难受。
sublime里关于语法的说明以及这里
sublime里关于scope的一些类型的说明