VSCode语法高亮插件
前言
大家好!最近我在做自己的毕业设计,简单来说需要自制一个编程语言,当然具体情况还要更复杂。现在语言本身已经有一定完成度了,但是在我测试的时候,看白底黑字的Notepad总是感觉差那么点意思。VSCode那多彩的高亮,自动补全括号真是用了就忘不掉,那有没有办法让我们自己的语言也能被VSCode高亮呢?
当然有!本篇博客我就来教大家如何使用Yeoman generator
来制作VSCode代码高亮插件。
一、安装Yeoman generator
Yeoman是一个通用型脚手架工具,简单来说就是帮你搭建一个项目的基本结构。在Yeoman所能完成的各种项目中,就包括VSCode插件,这也是我们所需要的:
npm install -g yo generator-code
这里我们使用nodejs安装Yeoman以及对应的Generator,下面开始搭建项目。
二、插件项目搭建
在全局安装好Yeoman之后,我们打开一个项目文件夹,然后执行:
yo code
这时你会看到如下界面,我们选择New Language Support
:
回车后会开始让我们回答一系列问题,分别为:
-
引入文件/URL,或者留空以搭建新项目。我们直接回车留空
-
该扩展的名字是什么。给我们自己的扩展起一个名字,比如: “MyLang Syntax Highlighting”
-
该扩展的标识符是什么。这个是当你安装插件后,在VSCode的扩展一栏中的显示名。一般会根据你上面的起名给一个默认的名字,我这里就是用默认名了
-
请描述你的扩展。写点东西让别人直到是干什么的啦
-
请输入你的语言的id。注意,这里的id唯一标记了你的语言,格式要求为单词(无空格)、小写。比如:“mylang”
-
请输入你的语言的名字。当你在VSCode中新建了一个文件,它会提示“选择语言以开始”,这里的起名就是显示在这里的。没有格式要求,随意起啦
-
请输入启用扩展的文件后缀。顾名思义,
C++
扩展为.cpp
启用,Python
扩展为.py
启用。这里你需要指定哪些文件后缀启用我们自定义的扩展,比如:".ml" -
请输入语法根作用域名。这个一般就是
source.{7中定义的后缀}
,比如:“source.ml” -
是否需要初始化一个git仓库。如果只是为了自己使用,选no就好了
现在,你会看到自动生成了一个以3
中自定义标识符为名的文件夹,我们所需要的项目文件就全在里面了!
三、项目结构
大体看一下文件目录:
D:.
│ .vscodeignore
│ CHANGELOG.md
│ language-configuration.json
│ package.json
│ README.md
│ vsc-extension-quickstart.md
│
├─.vscode
│ launch.json
│
└─syntaxes
mylang.tmLanguage.json
-
package.json,保存了我们在第二步中给出的答案,你也可以在此处进行修改。
-
几个Markdown文件,为你提供了标准的通知模板,如果你有留意过VSCode下载插件界面的内容,会发现与该模板大差不差。
-
language-configuration.json,该文件中的内容,相当于全局定义,它包括以下几部分:注释方式、允许的括号、自动补全的括号、选中文字后输入会自动括起来的符号。
-
/syntexes/mylang.tmLanguage.json,该文件是本项目的重点,我们需要在此处定义编程语言的文法,下面我们将展开讲述这一部分。
四、Syntaxes
TextMate官方文档指路
tm是TextMate的缩写,原本是一个Mac系统上的一个编辑器,支持一系列用户自定义功能。微软为VSCode添加了一个vscode-textmate的解释器,使得VSCode也可以使用TextMate文法定义的扩展组件。
我们打开mylang.tmLanguage.json
,看看里面的内容:
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "MyLang",
"patterns": [
{
"include": "#keywords"
}
],
"repository": {
"keywords": {
"patterns": [
{
"name": "keyword.control.mylang",
"match": "\\b(if|while|for|return)\\b"
}
]
}
},
"scopeName": "source.ml"
}
4.1 $schema
模式。这个模式所指向的文件,将定义接下来tmLanguage文件中键值对的意义,接受的值类型,以及预定义的高亮主题。
注意该文件最后的枚举值,我们自定义的匹配规则应当总是继承自这些已有的枚举值:
// 限于篇幅,只列出了部分枚举值
{
"type": "string",
"enum": [
// 注释
"comment",
"comment.block",
"comment.line",
"comment.line.double-dash",
"comment.line.double-slash",
// 常量
"constant",
"constant.character",
"constant.character.escape",
"constant.language",
"constant.numeric",
"constant.other",
"constant.regexp",
"constant.rgb-value",
"constant.sha.git-rebase",
// 实例
"entity",
"entity.name",
"entity.name.class",
"entity.name.function",
"entity.name.method",
"entity.name.type",
// 无效
"invalid",
"invalid.deprecated",
"invalid.illegal",
// 关键字
"keyword",
"keyword.control",
"keyword.control.less",
"keyword.operator",
"keyword.operator.new",
// 标记(加粗、斜体等)
"markup",
"markup.bold",
"markup.italic",
"markup.list",
"markup.quote",
"markup.underline",
// 存储(外部文件相关,如import)
"storage",
"storage.modifier",
"storage.modifier.import.java",
"storage.modifier.package.java",
"storage.type",
// 字符串
"string",
"string.quoted",
"string.quoted.single",
"string.quoted.double",
"string.quoted.triple",
"string.regexp",
"string.xml",
// 内置支持(用于内部函数、内部类)
"support",
"support.class",
"support.constant",
"support.function",
"support.other",
"support.property-value",
"support.type",
"support.variable",
// 变量
"variable",
"variable.language",
"variable.name",
"variable.parameter"
]
}
具体的使用示例我将在之后演示。
4.2 patterns
规则。规则是一个列表
,每个列表项是一个键值对
(字典),这些规则将用来解析我们的代码。一般来讲,我们只需要在patterns中定义,解析该语言需要哪些规则,而规则的具体内容,将在之后的repository中完成。例如:
"patterns": [
{
"include": "#comments"
},
{
"include": "#keywords"
},
{
"include": "#statements"
}
{
"include": "#strings"
}
]
这里我们定义了几个规则,分别是:注释规则、关键字规则、语句规则、字符串规则。接下来我们需要实现这些规则的定义
4.3 repository
仓库中需要我们来实现patterns里面定义的规则,仓库本身是一个字典
,它的键值对为:规则:[规则内容]
,规则内容包含很多可选项,我们举个例子说明:
"keywords": {
"patterns": [
{
"name": "keyword.control.mylang",
"match": "\\b(if|else|while|for|return|and|or|break|continue)\\b"
}
{
"name": "constant.language.boolean.mylang",
"match": "\\b(true|false)\\b"
}
]
},
"strings": {
"name": "string.quoted.double.mylang",
"begin": "\"",
"end": "\""
},
在上面这两个键值中,我们实现了规则中定义的“关键字”和“字符串”,让我们来看看都使用了什么规则内容:
-
patterns
。如果一个规则中包含着很多个子规则,你可以用一个列表
对它们进行定义。或者,你也可以使用类似4.2中patterns的语法,include
一个子规则,而后再后面继续定义。如果你的语言语法比较复杂,那么建议使用后一种方法,对语法进行细分,更详细的划分方式可以参考VSCode如何为Python撰写的tmLanguage文件 -
name
,你需要为每一个规则提供一个名字。注意,这里的名字需要继承
自schema中提供的枚举值。就像上例中的strings规则,它的名字是string.quoted.double.mylang
,就是继承自string.quoted.double
,双引号括起的字符串。这个名字将决定最后使用主题
中定义的哪个颜色为其上色,关于主题,请看VSCode的官方文档。 -
match
,匹配规则。TextMate中的所有匹配规则都以正则表达式
的形式定义,又因为我们是在json中以字符串形式传递这些正则表达式,所以不可避免地要进行转义字符。match语句用于定义一条完整的正则表达式,例如上面的:"\\b(true|false)\\b"
,用于捕获单词true/false,然后设定为boolean高亮。 -
begin & end
。除了使用match定义一条正则语句外,你还可以定义起始匹配标志与终止匹配标志,来实现区间捕获。可以看到上例中对于字符串的处理,就是以双引号起始,双引号终结的一段。 -
captures
。上面的例子很简单,我们一条正则表达式就对应一个语法,但有时候语法可以拆分成很多部分,而且有些部分是可选的,那么就可以使用captures来精确细分:{ "match": "\\b(func)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([A-Za-z0-9,\\s]*)\\)", "captures": { "1": { "name": "storage.type.function.cploxplox" }, "2": { "name": "entity.name.function.cploxplox" }, "3": { "patterns": [ { "match": "\\b[A-Za-z_][A-Za-z0-9_]*\\b", "name": "variable.parameter.cploxplox" } ] } } },
上面给的这个例子,是我自己的语言cploxplox中对于函数的定义语法,bnf形式表述为:
func (identifier)? "(" (identifier (, identifier)*)? ")" block
这里面可以分出几部分:
-
func,一个标志着接下来开始定义函数的字符
-
identifier,如果有就是一个有名函数,否则为匿名函数
-
参数列表,可以为空
-
函数体
前三部分,分别对应正则表达式中三个
()
分割开的捕获分组
,它们三个不是用同样的颜色高亮,因此我们使用captures,分别对1、2、3分组做了更进一步的高亮定义。 -
-
beginCaptures & endCaptures
。captures是针对match的,那么同理针对begin和end也有相应的captures,使用的方法和上面captures的例子类似,不再赘述。
以上便是repository中比较常用的几个规则内容了,当然还有更多,比如为了方便别人理解,你可以添加comment字段;想要为begin和end之间的部分添加高亮,可以使用contentName来完成;引用其他语言的高亮规则,可以使用include一个文件/URL的方式实现。这些就需要你详细的阅读官方文档了。
下面我放出本次教程中我书写的完整tmLanguage文件内容,供大家参考:
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "MyLang",
"patterns": [
{
"include": "#comment"
},
{
"include": "#statements"
},
{
"include": "#keywords"
},
{
"include": "#strings"
}
],
"repository": {
"comment": {
"begin": "//",
"end": "\\n",
"name": "comment.line.double-slash"
},
"keywords": {
"patterns": [
{
"name": "keyword.control.mylanguage",
"match": "\\b(if|else|while|for|return|and|or|break|continue)\\b"
},
{
"name": "constant.language.boolean.mylanguage",
"match": "\\b(true|false)\\b"
}
]
},
"statements": {
"match": "\\b(func)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(([A-Za-z0-9,\\s]*)\\)",
"captures": {
"1": {
"name": "storage.type.function.mylang"
},
"2": {
"name": "entity.name.function.mylang"
},
"3": {
"patterns": [
{
"match": "\\b[A-Za-z_][A-Za-z0-9_]*\\b",
"name": "variable.parameter.mylang"
}
]
}
}
},
"strings": {
"name": "string.quoted.double.mylang",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.mylang",
"match": "\\\\."
}
]
}
},
"scopeName": "source.ml"
}
五、安装插件
好,现在我们已经完成了插件的实现部分,要怎么才能让VSCode识别到呢?非常简单,把整个插件文件夹,拷贝到%USERPROFILE%/.vscode/extensions
(Linux是~/.vscode/extensions
),重启VSCode就可以啦!
结语
不知道大家有没有自制编程语言的兴趣呢?如果有的话,可以来我的仓库中,帮我一同开发cploxplox哦!在仓库的scripts中我提供了扩展cploxplox的指南,有兴趣的小伙伴可以在该项目中出一份力!