vscode语法插件开发——如何用syntaxes去描述一门语言

3 篇文章 1 订阅

背景

最近在研究vscode插件开发。作为恰饭工具里最常用的ide,如果能够做到一些自己想要的功能,那是可以极大的提高生产效(xin)率(qing)的,说不定还可以事(tou)半(tou)功(mo)倍(yu)。

萌生这个想法的初衷是因为工作当中使用了一个叫做ClearSilver的c++直出模板,这些模板代码因为没有解析器,在html文件当中显示就是一坨白色,非常影响开发心情,以及摧残视力。

就像这样:
就像这样
然而vscode的插件市场并没有可以识别它的插件,可能因为太过于小众了。但是出于身为程序猿的自己身心健康,本着长痛不如短痛,决定从头撸一个语法高亮插件去解决这个问题。

最终的效果:
最终效果

国内有不少vscode的插件入门教程,不过基本都没有提过语法解析这块内容,官网写的例子也是非常简单。经过一段比较长时间的摸索,大概可以给出一些结论,这里记录下来,欢迎同行大神指正和交流。

vscode插件开发入门知识储备请戳这里,作者写的很好很全面。

开始

-demo

vscode官方有一个简单的语法高亮栗子,介绍了一个"abc"语言如何去实现高亮操作。

然而个人认为官网上的栗子不够直观,这里小小的改造了一下:

效果预期:

  • abc三个词高亮显示
  • 括号当中的x高亮显示
  • 括号当中的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

就可以看到对应的语法高亮了

前置知识(syntaxes)

-简介

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.<lang_name>或者text.<lang_name>。前者一般用于程序语言,后者一般用于标记性的语言。比如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}
...
  • name[可选]:规则名称,会作为语法堆栈解析(后面会提到)上的名称
  • match:匹配规则
  • captures:正则表达式分组对应的名称,会作为语法堆栈解析上的名称,分组从1开始

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: .
...
  • begin:开始边界
  • beginCaptures:对应begin正则表达式分组对应的名称,同captures的规则
  • end:结束边界
  • endCaptures:对应end正则表达式分组对应的名称,同captures的规则
  • patterns:同开头的patterns
  • include:引入其他scope。当引用repository当中的scope时,需要加上#,顶级scope不需要这个符号。比如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的规则一致。

scope命名

细心的同学可能有些疑问,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的样式信息,故显示出了黄色
Inspect TM Scopes
那么有没有比较完整的推荐命名规则呢?有,点这里慢慢参考。

abc分析

开始分析前面提到的abc语言的例子(终于开始分析了)。

-package

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"  # 语法配置文件
...

-abc.tmGrammar.json

在分析语法配置之前,先回忆一下这个demo预期想要达到的结果

  • abc三个词高亮显示
  • 括号当中的x高亮显示
  • 括号当中的abc高亮显示

按照思路,解析应该分为以下步骤:

  1. 找到代码中的a、b和c
  2. 找到开括号标识,去找x,同时找a、b和c,直到找到了闭括号
  3. 继续回到第1步,直到文件末尾

按照这个思路我们来看语法文件

# [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语言就解析完毕了。
abc
其实这上面的匹配规则有不少bug,比如没有区分括号对,只是简单粗暴的遇到闭括号就结束。不过作为例子,这个bug反而更能进一步说明内部解析的机制。有兴趣的同学可以尝试改造一下。

回到开头

通过以上的知识点,我断断续续的花了比较久的时间跨度才完成开头所说的ClearSilver这个语法插件。事实上代码量不是很多,300多行,不过工作量主要在于这些正则的匹配是否全面、到位,调了非常久的时间。

关于如何组织代码,有兴趣的同学可以去我的github页面上去了解下这个文件,欢迎提出改进的建议~

另外也可以参考vscode内置的syntaxes去学习一些组织方式和技巧,地址戳这里,目录为./extensions/<languageName>/syntaxes/<languageName>.tmLanguage.json

或者可以直接打开已发布的插件,拖进项目目录学习。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的一些类型的说明

  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值