教你用VS Code插件实现五彩斑斓的伪代码

原创 陈亦涛 大转转FE

#VS Code 1 #高亮 1#伪代码 1

前言

插件系统的设计是很多开源项目实现社区驱动的最为重要的手段,为项目提供了强大的可扩展性。在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时,就要运行我们这个插件。

img

输入完所有的选项以后,我们的插件目录就创建完成了。结构是这样子的:

├── CHANGELOG.md
├── README.md
├── language-configuration.json
├── package.json
├── syntaxes
├──     # 在tmLanguage.json中自定义语法
│   └── zhuanzhuan.tmLanguage.json
└── vsc-extension-quickstart.md

了解Scope

想要实现语法高亮,就需要将一串代码字符串,拆分成无数的小碎片,然后分别为它们指定color等样式。这些拆分后的小碎片,被称作token。这里的tokenjwt中的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的类型就是functionab的类型是parameter

img

另外从截图的底部中,我们还可以看到,每个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 & repositoryrepository是规则的仓库,它规定了该条规则如何识别其适用的对象。而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是什么。

效果如图:

img

从图中我们可以看出,正则中匹配的字符(如果、遍历、函数等)已经被一一高亮了。不过你的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。重载后的效果是这样的:

img

这时候第13行发生了变化,从原来的黑色,变成了绿色。

深入理解scope

看到效果以后,再回过头看那份json文件,它到底表达了什么意思?

首先我们规定了顶层的scope名字叫source.zz。也就是说,当我们新建了.zz结尾的文件,开始写代码,这时所有的代码都处在顶层scope

patterns属性规定了在顶层scope中,有哪些方式可以开辟一个子scopepatterns数组inclucde(即引入)了名为stringskeywords的规则,这些规则被放在了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命令来验证这一点。

img

图片中,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主题列表中。

img

使用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》这部分生效了。

img

文字及其对应的scope和颜色如下:

    • scope: string.quoted.book.open
    • 颜色:#33ec0e
    • scope: string.quoted.book.close
    • 颜色:#33ec0e
  • xxxx
    • scope: string.quoted.book.open
    • 颜色:#eb8837

其他的scope可以自行挑选喜欢的色值一一定义,这里就不再重复罗列。

成果

经过上面一系列的努力,然后再添加亿点点细节,最终的效果就是下图。

img

总结

通过阅读文章,我们总共创建了两个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

水平有限,若有错漏,敬请指正。

发表评论

邮箱地址不会被公开。 必填项已用*标注

相关