前言
插件系统的设计是很多开源项目实现社区驱动的最为重要的手段,为项目提供了强大的可扩展性。在JavaScript生态圈中,有众多实现了插件机制的工具和库,诸如Babel、Webpack、Vue、Vue Cli,以及今天的主角VS Code。
VS Code插件可以实现哪些功能?
主题 主题包括界面的配色以及文件的图标,比如material theme和material file icon。 自定义UI 扩展vscode的workbench,在侧边栏、底部状态栏添加自定义的按钮&界面。 Webview 在新标签中打开一个webview,最大化自定义界面元素。 New runtime support 对一个新的运行时提供支持。 语言支持 为一门新的语言提供支持(比如我们自定义规则的伪代码)。
今天我们将关注其中最后一个功能,支持一门新的编程语言。也许你会对这个功能感到困惑,为什么VS Code对一门语言的支持,是通过插件的形式?
首先需要明确的是,VS Code本身并没有像大家想象的那样,内置了对各种编程语言的支持,包括JavaScript。VS Code为插件提供了一系列的API,令一个插件可以实现对特定语言的支持。比如HTML,就是通过HTML插件来提供支持的。
再比如console.log
,当你打出console.
的时候,VS Code通知了名为Typescript Language Features的插件,然后插件对这个通知做出响应,让log
出现在了补全提示中。
这也就意味着,我们可以利用这些语言方面的API,来自定义一门自己的编程语言。所以接下来我们来探索一下,怎么实现这个听起来很酷的小目标。
一个小建议:这篇文章的最佳阅读姿势是在电脑上打开,并跟着文章一步步做下去。
效果展示
首先我们写一小段伪代码。这段代码随便写在了一个fakeCode.zz
的文件中,.zz
是VS Code不支持的后缀,所以全部字符都是灰的(zz代表转转公司中的转转二字)。
等文章结束,它们会变成这样:
创建项目
编写插件的第一步,就是创建我们的目录结构。这里我们使用一个叫做yo
的脚手架工具。yo
是一个富有高度扩展性的通用脚手架,可以通过插件来实现不同目录结构和初始选项。VS Code官方提供了名为generator-code
的插件,来进行插件目录的创建。首先我们需要安装yo
以及插件generator-code
。
npm i -g yo generator-code
在安装完成以后,使用下面的命令来创建目录结构。
yo code
在运行yo code
以后,它会问你下面这些问题。建议大家和我的输入保持相同,以免遇到意外。这里我们给伪代码的取名为zhuanzhuan
,并且告诉VS Code,当碰到一个文件的扩展名为.zhuanzhuan
或者.zz
时,就要运行我们这个插件。
输入完所有的选项以后,我们的插件目录就创建完成了。结构是这样子的:
├── CHANGELOG.md
├── README.md
├── language-configuration.json
├── package.json
├── syntaxes
├── # 在tmLanguage.json中自定义语法
│ └── zhuanzhuan.tmLanguage.json
└── vsc-extension-quickstart.md
了解Scope
想要实现语法高亮,就需要将一串代码字符串,拆分成无数的小碎片,然后分别为它们指定color
等样式。这些拆分后的小碎片,被称作token
。这里的token
与jwt
中的token
不同,并没有安全、令牌等方面的意思,而是更偏向"符号"的含义。我们来看一个简单例子🌰来理解一下这段话。
function sum(a: number, b: number): number {
return a + b;
}
在这段TS代码中,我们定义了一个用于求和的函数。这时候我们按下VS Code快捷键,shift+cmd+p
,然后输入inspect editor tokens and scopes
,就可以看到每个token对应的类型。比如sum
这个token的类型就是function
,a
和b
的类型是parameter
。
另外从截图的底部中,我们还可以看到,每个token
还具有一个叫textmate scope
的属性。通俗地说,scope
指这个token
所处的位置。
比如下面的代码片段里,有两个a变量。第一个是一个变量声明,而第二个是函数的参数之一。虽然它们都可以被笼统地称为变量,但是因为所处的scope
不同(也就是处于不同环境),所以在VS Code中会被显示成不同的颜色。(由于微信文章中的代码高亮较弱,看不出区别。)
const a = 1;
function sum(a: number, b: number): number {
return a + b;
}
支持注释
至此前置知识已经介绍完了,现在开始真正修改脚手架创建的代码。我们的第一个目标是,让zhuanzhuan
语言支持注释。
首先打开根目录下的language-configuration.json
文件,找到comments
字段,将lineComment
从默认的//
修改为注释:
。完成以后按下F5启动Debug程序,VS Code会打开一个新的窗口,且我们的插件会在其中生效。在新窗口中,我们随意打开一个空文件夹,然后新建名为fakeCode.zz
,并输入以下内容进行测试。
注释: 当我们用中文伪代码来描述执行过程的时候,
注释: 不管什么内容都被显示成灰色的字符串了。
注释: 我们的目标就是让它们变得五彩斑斓。
注释: 不要试图重构这个方法,不然你会虚度一天的光阴。
如果 ([某个条件]) {
做一些条件成立时的事情
} 否则 {
当条件不成立的时候...
}
遍历 商品
打印 《商品id》
结束
函数 [函数名] {
函数的内容
}
这时,我们按下注释转换的快捷键cmd+/
,就会惊讶地发现,VS Code会为你自动转换注释内容,在这之间转换:注释:具体内容
⇔具体内容
。
这样,我们的Hello World项目就完成了,开始做稍微复杂一些的事情。打开/syntexes/zhuanzhuan.tmLanguage.json
文件,将这个文件的所有内容替换成下面的内容:
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "zhuanzhuan",
"scopeName": "source.zz",
"patterns": [
{
"include": "#comments"
}
],
"repository": {
"comments": {
"name": "punctuation.comment",
"begin": "注释:",
"end": "\\n",
"beginCaptures": {
"0": {
"name": "punctuation.comment.open"
}
},
"endCaptures": {
"0": {
"name": "punctuation.comment.close"
}
}
}
}
}
第一眼看的时候都会懵的,我们慢慢理解一下这到底是什么意思。
patterns & repository
repository是规则的仓库,它规定了该条规则如何识别其适用的对象。而patterns则是规定了规则仓库中,哪些规则是需要生效的。所以如果我们想要加一条新的规则,需要在repository中加规则的内容,并在patterns中将这条规则
include
,不然即使在repository添加了规则也不会生效。comments
这是我们加入的自定义规则,comments是规则的名字。
begin & end
决定这条规则的适用对象。这里我们将
注释:
开头,回车符结尾的这部分字符作为适用对象。beginCapture & endCapture & name
这三个属性,代表着我们赋予适用对象的scope名称。比这样一条注释"注释:不要试图重构这个方法,不然你会虚度一天的光阴"。,对应到我们这条规则,就是
注释:
这部分被赋予了punctuation.comment.open
scope,不要试图重构这个方法,不然你会虚度一天的光阴。scope为punctuation.comment
,最后的回车符scope为punctuation.comment.open
。
然后切换到刚才使用F5
打开的Debug窗口,按下cmd+shift+p
,运行reload window
,让我们的修改生效,就可以看到scope名称的变化:
支持关键字
关键字同样是编辑这个文件/syntexes/zhuanzhuan.tmLanguage.json
,在repository
中新加入keywords
规则(记得在顶部的patterns中include它):
{
"keywords": {
"patterns": [
{
"match": "\\b(如果|遍历|结束|打印|函数)\\b",
"name": "keyword.control.zhuanzhuan",
}
]
}
}
解释一下这里的意思:
match
这是一个正则,如果碰上如果|遍历|结束|打印|函数
其中之一,就将它标记为关键字。name
这些关键字对应的scope是什么。
效果如图:
从图中我们可以看出,正则中匹配的字符(如果、遍历、函数等)已经被一一高亮了。不过你的VS Code中不一定是蓝色,这取决于你当前使用的主题。
支持字符串
接着我们让zhuanzhuan
语言支持字符串功能,同样是修改json文件。
{
"repository": {
"strings": {
"name": "string.quoted.book.zhuanzhuan",
"begin": "《",
"end": "》",
"beginCaptures": {
"0": {
"name": "string.quoted.book.open"
}
},
"endCaptures": {
"0": {
"name": "string.quoted.book.close"
}
}
}
}
}
就像JS中使用单双引号和模板字符串作为字符串的标志,为了体现zhuanzhuan
语言的不同之处,我们使用书名号,而不是单双引号,来标志一个字符串。例如《xxxx》
,它被分成了《
xxxx
》
三个部分,这三个部分有各自的scope
,对应关系如下:
《 string.quoted.book.open
xxxx string.quoted.book.zhuanzhuan
》 string.quoted.book.close
为了看到修改后的效果,需要在调试窗口中,cmd+shift+p
并运行reload window
。重载后的效果是这样的:
这时候第13行发生了变化,从原来的黑色,变成了绿色。
深入理解scope
看到效果以后,再回过头看那份json文件,它到底表达了什么意思?
首先我们规定了顶层的scope
名字叫source.zz
。也就是说,当我们新建了.zz
结尾的文件,开始写代码,这时所有的代码都处在顶层scope
。
patterns
属性规定了在顶层scope
中,有哪些方式可以开辟一个子scope
。patterns
数组inclucde
(即引入)了名为strings
和keywords
的规则,这些规则被放在了repository
(也就是仓库,一个规则的仓库)。
{
"strings": {
"name": "string.quoted.book.zhuanzhuan",
"begin": "《",
"end": "》"
}
}
在repository
中,以strings
规则为例,当VS Code解析引擎遇到以“《”开头,“》”结尾的token
时,中间的内容会被认为是字符串。也就是说,我们让书名号具备了和JS中的单双引号相同的功能。字符串的scope
变成了我们规定的string.quoted.book.zhuanzhuan
。我们可以通过inspect editor tokens and scopes
命令来验证这一点。
图片中,xxxx
所属的scope
有两个,一个是constant.character.escape.zhuanzhuan
,另一个就是根scope
。一个token
往往拥有多个scope
,就像字符串,同时处于根scope
和书名号创建的一个scope
。
在上文的json文件中,还有一个叫keywords
的属性,当有字符串满足match
字段中的正则表达式时,会被认为是一个关键字。
事实上,当我们把上文的json规则进行更多的扩展和嵌套,就会越来越接近现流行的其他语言,存在无数的嵌套。一个token
会属于无数的scope
。
那么问题来了,这些scope
的作用是什么?我们花了很多的力气去定义json格式,来让不同位置的token
拥有不同的scope
。这样我们就拥有了一个类似于CSS选择器的东西,我们可以为不同scope
指定不同的样式,从而让我们自创的语言高亮起来。
使用Scope
接下来我们要使用上文中定义的几个scope
。因为目前为止,我们只是重新定义了zhuanzhuan
语言中一部分情景下的scope
名称,我们可以利用这些自定义的scope
,做出更细致的高亮配置。
使用scope
的方式就是创建一个theme
类型的插件(没错我们要写第二个插件了)。这次我们需要cd到用户文件夹下的.vscode/extensions
,这样我们的主题就可以免安装,可以直接出现在VS Code主题列表中。
使用VS Code打开项目,然后编辑theme/zhuanzhuan-lang-theme-color-theme.json
文件,文件的结构是这样的:
{
"name": "zhuanzhuan-lang-theme",
"type": "light",
"colors": {
"editor.background": "#f5f5f5",
"editor.foreground": "#333333"
},
"tokenColors": [
{
"name": "Comments",
"scope": [
"comment",
"punctuation.definition.comment"
],
"settings": {
"fontStyle": "italic",
"foreground": "#AAAAAA"
}
}
]
}
其中,tokenColors
字段是我们需要关心的地方,它针对了不同的scope
,指定不同的样式。name
是这条规则的名字,可以随意命名,保证唯一性即可。scope
类似于CSS选择器,是规则应用的对象。settings
则是具体的样式。
然后我们在tokenColors
中,加上我们自定义的样式。
{
"tokenColors": [
// 省略其他原有的规则,仅列出新增的规则。
{
"name": "quotedBookOpen",
// 将scope为string.quoted.book.open的token,
// 也就是《,颜色设置成#33ec0e(原谅色)。
"scope": "string.quoted.book.open",
"settings": {
"foreground": "#33ec0e"
}
}
// 省略了大段雷同的配置。
// 只要scope和我们的zhuanzhuan语言定义中的scope相同,
// 就可以高亮对应的token。
]
}
然后在zhuanzhuan-lang
插件的调试窗口,打开主题选择列表,选择zhuanzhuan-lang-theme
主题,就可以看到上面的三条规则对《商品id》
这部分生效了。
文字及其对应的scope和颜色如下:
《
scope: string.quoted.book.open
颜色:#33ec0e 》
scope: string.quoted.book.close
颜色:#33ec0e xxxx
scope: string.quoted.book.open
颜色:#eb8837
成果
经过上面一系列的努力,然后再添加亿点点细节,最终的效果就是下图。
总结
通过阅读文章,我们总共创建了两个VS Code插件。一个是语言支持插件,通过简单的配置,使zhuanzhuan
语言支持了中文关键字、书名号字符串以及中括号表示的变量。第二个是主题插件,为zhuanzhuan
语言中自定义的scope
提供了高亮规则。scope
名称,是连接两个插件的枢纽。
不过zhuanzhuan
语言离一门完善的语言还需要海量的工作,我们需要定义更多的scope规则,规则之间往往还存在复杂的嵌套关系。这篇文章只是讲了冰山露出海面的那一角。如果想深入学习这方面的知识,仍需参考VS Code官方的文档,以及学习编译原理相关知识。
另外附上文章中两个插件最终的代码:
https://github.com/inkyMountain/zhuanzhuan-lang
https://github.com/inkyMountain/zhuanzhuan-lang-theme
参考文档:https://code.visualstudio.com/api#vscode
水平有限,若有错漏,敬请指正。