likes
comments
collection
share

VSCode 语法插件开发指南

作者站长头像
站长
· 阅读数 71

简介

谁是最受欢迎的编辑器?

在 stack overflow 进行的 2021 开发者调查报告中 ,可以看到 VS Code 以一骑绝尘的姿态甩开了所有的其他编辑器,以 71 % 的使用率可以说,几乎是现在最流行最受欢迎的编辑器(希望此刻没有IDE大战,每款编辑器都有自己特色)。

VSCode 语法插件开发指南

为什么是VS Code

为什么是vscode 拥有最高的使用率?VS Code 以轻量、插件化、免费等特性在开源的基础吸引了大量的开发者来完善编辑器体验,甚至拥有了很多 IDE 并不拥有的功能,比如个人最喜欢的 vscode ssh remote development,可以将代码运行、语法分析等编辑器工作都放在devbox 上面。

为什么是学习 vscode 插件开发? VS Code 本身是采用了 Electron 来实现的,Electron 是由 Github 开发,用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库。 Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用。可以说VS Code 天生就对前端或者任何熟悉 JS 的开发者来说都特别友好。

VSCode 语法插件开发指南

因此对于在拥有最多人气的编辑器来说,对于前端开发者来说,若要提高编程的效率,或者对于创造了一门新语言或是新框架的开发者来说,开发一款编辑器插件来说,VS Code 插件是最适合不过了。

VSCode 能做什么

我们这里将 VS 能力分成如下几类:

通用能力

  • 注册命令、配置项、键盘快捷键绑定和菜单栏

  • 显示通知消息

主题

  • 改变编辑器中代码显示颜色

  • 改变整体编辑器颜色

  • 自定义文件图标

声明语言特性

  • 打包通用的代码片段

  • 告诉 VS Code 认识新语言

  • 基于已存在的语法继承现有的一门新语言语法

编程语言特性

  • 悬浮出现代码提示

  • 识别代码编写或者lint 错误

  • 智能识别

Debugging

  • chrome debugging

甚至还有各种奇奇怪怪的插件:

比如能在上班时候看到今天亏了多少钱的股票插件:marketplace.visualstudio.com/items?itemN…

VSCode 语法插件开发指南

比如上班累了听听知乎刚编的故事:marketplace.visualstudio.com/items?itemN…

还有各种正经和不正经的插件,目前来看 vscode 称得上是最大的插件库市场。

快速开发

在开始进入语法插件开发之前,我们先来展示一盘开胃菜,来做一个最简单 Hello World 的 demo 来演示最基础的 vscode 插件开发。

# 1. 我们首先需要下载一个 vscode 编辑器
# 2. 安装 yo 脚手架快速生成项目

npm install -g yo generator-code

yo code

# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press <Enter> to choose default for all options below ###

# ? What's the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Initialize a git repository? Yes
# ? Bundle the source code with webpack? No
# ? Which package manager to use? npm

# ? Do you want to open the new folder with Visual Studio Code? Open with `code`

这样之后我们就生成了一个简单的 vscode 项目基本框架,可以查看下目录结构如下:

.
├── .vscode
│   ├── launch.json     // 运行和调试项目配置
│   └── tasks.json      // 编译 typescript 的的配置
├── src
│   └── extension.ts    // 插件源代码
├── package.json        // 插件配置元信息
├── tsconfig.json       // typescript 配置

package.json

{
        "name": "helloworld", // 插件名
        "displayName": "HelloWorld",  // 显示在应用市场的名字
        "description": "",  // 具体描述
        "version": "0.0.1", // 插件的版本号
        "engines": {
                "vscode": "^1.65.0" // 最低支持的vscode版本
        },
        "categories": [
                "Other" // 扩展类别
        ],
        "activationEvents": [
                "onCommand:helloworld.helloWorld" 
        ],
        "main": "./out/extension.js",  // 主入口路径
        // 通过扩展注册contributes用来扩展VSCode中的各项技能
        "contributes": {
                "commands": [
                        {
                                "command": "helloworld.helloWorld",
                                "title": "Hello World"
                        }
                ]
        },
        "scripts": {
            // ...
        },
        "devDependencies": {
            // ...
        }
}

contributes: 这边官方翻译成功能贡献,类似插件具体的功能点,几乎每个插件都会拥有对应的 contributes,比如下面是 jsx snippets 的 contributes,即贡献了代码片段补全。

VSCode 语法插件开发指南

activationEvents:指明该插件在何种情况下才会被激活,因为只有激活后插件才能被正常使用,官网已经指明了激活的时机,这样我们就可以按需设置对应时机。(具体每个时机用的时候详细查看即可),onCommand 代表在调用命令时被激活。

extension.ts

// 导入 vscode
import * as vscode from 'vscode';

// 该方法将会在插件在激活的时候被调用
export function activate(context: vscode.ExtensionContext) {
         // 此处实现注册了在package.json 中声明的插件命令
        let disposable = vscode.commands.registerCommand('helloworld.helloWorld', () => {
                // 每次运行命令的时候都将会执行
                // 右下角弹窗显示信息
                vscode.window.showInformationMessage('Hello World from HelloWorld!');
        });

        context.subscriptions.push(disposable);
}

下面演示一下如何在 vscode 中进行调试工作:

语法高亮(Syntax Highlight)

对于一款编辑器来说,语法高亮莫过于是最普遍和通用的一个功能,VSCode 自带了对于一些语言的语法高亮功能,但是对于更多的新语言来说,更多需要做的是可能需要安装插件来支持,对于语言开发者来说,给用户提供一个语法高亮的插件也是无可厚非的。

代码高亮功能由 「语言扩展」 类插件实现,根据实现方式又可以细分为:

  • 「声明式」 :以特定 JSON 结构声明一堆匹配词法的正则,无需编写逻辑代码即可添加如块级匹配、自动缩进、语法高亮等语言特性,vscode 内置的 extension/css、extension/html 等插件都是基于声明式接口实现的

  • 「编程式」 :vscode 运行过程中会监听用户行为,在特定行为发生后触发事件回调,编程式语言扩展需要监听这些事件,动态分析文本内容并按特定格式返回代码信息

实现目标

下面将以例子来具体说明,演示下如何书写具体的语法规则。假如我们有一种叫做 xss 的新语言。我们目标将实现对于 * 包围的文字,譬如 *should bold * 在编辑器上显示加粗处理的效果。对于 _ 包围的文字,譬如 _ should italic _ 在编辑上显示斜体的效果。熟悉 markdown 语法的同学,应该想到了 markdown 中也有类似的语法规则。

VSCode 语法插件开发指南

分词(Tokenization)

为了实现语法高亮功能,我们首先需要的是将一系列的文本进行分词处理,分词就是将其字符流分割成一个个的词( token )。所谓 token ,就是源文件中不可再进一步分割的一串字符,类似于英语中单词,或汉语中的词,了解编译原理前端部分的应该熟悉这部分工作。(编译原理涉及到的知识点太多,所以不深入展开了)

VSCode 语法插件开发指南

主题色

分词之后的每个词语都将会拥有一个自己的上下文,我们称之为scope,scope 用来描述当前的环境上下文,我们可以使用 vscode 提供的 scope inspector 功能去方便快捷的查看当前的 scope 是什么。

VSCode 语法插件开发指南

scope 是一种 . 分割的层级结构,例如 keyword 与 keyword.control 形成父子层级,这种层级结构在样式处理逻辑中能实现一种类似 css 选择器的匹配,后面会讲到细节。

举例来说,对于在 JavaScript 中的 + 操作来说,他的 scope 是keyword.operator.arithmetic.js 。主题将会映射 scope 到对应的颜色和样式中提供代码高亮功能(参考下方主题色引用)。textmate grammar 已经定义了一系列默认的 scope,比如 comment.line、constant.numeric、constant.character ... ,完整可以参考该文档 。

实际上当你开发一个主题色插件的时候,就是在给不同的 scope 的 token 赋予不同的颜色和样式。

vscode 默认主题色。

VSCode 语法插件开发指南

        "contributes": {
                "themes": [
                        {
                                "label": "Sample Light",
                                "uiTheme": "vs",
                                "path": "./Sample_Light.tmTheme"
                        },
                ]
        },

总结来说,所谓的主题色就是对于分词之后的 token 根据所在的 scope 上色。因此当我们确定一门新语法的时候,我们要怎么把不同 token 确定不同的 scope 呢。

TextMate grammars

VS Code 采用 TextMeta grammars 作为词法分析的引擎,你可以采用编写 plist(XML) 或者 JSON 文件的形式去实现分词,因此你不需要从 0 到 1 写一个分词器。TextMeta grammars 由 TextMate 编辑器所创造,因为大量的编辑器和 IDE 都采用了该方案,因此开源社区内已经存在大量的语言包,可以直接使用。

关于 textmate:最近,一个私营的小公司叫做Macromates,该公司开发了一款很成功的文本编辑器产品叫做TextMate,他们决定重写已有的应用并升级到TextMate 2。这项决定出乎意料的花了整个开发团队六年的时间才发布了第一个beta版,在这六年内,TextMate损失掉了大本分的市场。当他们发布beta版后,他们意识到产品发布得太迟了已经不能扭转乾坤了,大势已去,六个月后,TextMate 2出现在 Github上,走上了开源之路。 链接:www.zhihu.com/question/38…

让我们通过 yo code 生成一份默认的 language extension 项目,我们将新的语言取名为xss。观察如下 package.json 。在 package.json 文件中声明插件的 contributes 属性,可以理解为插件的入口:

{
    "name": "xss-lang",
    "displayName": "xss Lang",
    "description": "",
    "version": "0.0.1",
    "engines": {
        "vscode": "^1.64.0"
    },
    "categories": [
        "Programming Languages"
    ],
    "contributes": {
        "languages": [{
            "id": "xss", // 定义新语言的唯一名称
            "aliases": ["xss"], // 别名
            "extensions": ["xss"], // 新语言的后缀名
            "configuration": "./language-configuration.json" // 配置文件
        }],
        "grammars": [{ // 定义新语言的语法规则
            "language": "xss",
            "scopeName": "source.xss", // 下文解释
            "path": "./syntaxes/xss.tmLanguage.json" // 配置文件
        }]
    }
}
  • scopeName: 这应该是一个语法唯一的名称,我们约定俗成的以 . 作为一个分割符分割各个名称。通常这会由两部分组成,第一部分是 text 或者 source ,第二部分是新语言的名称。举例来说对于标记性语言会把 text 作为第一部分,比如对于 markdown 来说 text.html.markdown 。而对于编程语言来说,会以 source 开头,比如 JavaScript 为 source.js 。

规则描写

xss.tmLanguage.json:

{
        "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
        "name": "xss",
        "patterns": [{ "include": "#bold" }, { "include": "#italic" }],
        "repository": {
          "bold": {
                "name": "markup.bold.xss",
                "begin": "\\*",
                "patterns": [{ "include": "#italic" }],
                "end": "\\*"
          },
          "italic": {
                "name": "markup.italic.xss",
                "begin": "_",
                "end": "_",
                "patterns": [{ "include": "#bold" }]
          }
        },
        "scopeName": "source.xss"
  }
  
  • patterns:由实际的解析文档规则组成的数组。

    • begin| end: 实际解析文档的规则,由正则组成。begin 代表语法匹配的开始, end 则表示结束。

    • name:即主题色章节中提到的 scope 名称。这边我们通过查看 markdown 中加粗的 scope 为 markup.bold,所以我们定义我们的加粗 scope 为 markup.bold.xss, 复用 markdown 样式中自带的加粗效果。斜体同理。

    VSCode 语法插件开发指南

    • include: 即可以实现进一步的嵌套效果,粗中有斜,斜中有粗,层层嵌套。

VSCode 语法插件开发指南

我们看如下的一种情况,如果我们没有正确的闭合加粗语法,可以看到加粗语法依然会生效,即使后续并没有以 * 结束。

VSCode 语法插件开发指南

这边需要注意的是,正则匹配从 begin 开始,如果begin 有匹配到的文本,便会将后续的文本视作当前声明的 scope,也将会立即开始寻找匹配 end 规则的文本,将会一直寻找到匹配 end 的文本或直至文档结束。即使这边没有匹配到对应的 end 语句,该规则也并不会做任何的回溯,依旧会视为匹配成功。

这边 textmate 为了使解析的效率更高,因此采用了类似正则中不回溯的做法。熟悉正则引擎的同学可能会了解,正则引擎一般分为 NFA和DFA 两种,前者将不会做匹配失败的回溯,但是大部分后者的正则引擎会做回溯匹配。

DFA(确定型有穷自动机),从匹配文本入手,从左到右,每个字符不会匹配两次,它的时间复杂度是多项式的,所以通常情况下,它的速度更快,但支持的特性很少,不支持捕获组、各种引用等等;而NFA(非确定型有穷自动机)则是从正则表达式入手,不断读入字符,尝试是否匹配当前正则,不匹配则吐出字符重新尝试,通常它的速度比较慢,最优时间复杂度为多项式的,最差情况为指数级的。

我们也可以复用已存在的语法,比如对于 markdown 文件中编写代码块的情况,我们不需要重新编写已存在的语法文件,直接使用 "include": "source.js" 来声明 JavaScript 的语法。

以上就是实现语法中简单的一个解释,实际上如果要写一个真实可用的 grammar ,需要做的远远比上面复杂的多,可以在这里 github.com/microsoft/v… 查看vscode 官方自带的 css 语法解析器(整体来说,css 语法规则较少,而且对于前端是最为熟悉,因此可以从 css 语法规则入手)的实现过程。

需要注意的是,这边采用的正则表达式引擎跟平时 JavaScript 使用的并不一样,规则更加多样和复杂,比如会经常看到 \G 的规则,在 JavaScript 的正则引擎中是不存在的。具体的正则规则可以参考这边的 oniguruma regular 官方文档。

语法解析的规则到这边为止,其实配置中还有很多选项可以去使用,比如 begin/while 配合,对于更多的解析规则和示例,可以查看 vscode 仓库中各个官方实现的语法规则,github.com/microsoft/v… ,这边由于篇幅关系便不继续展开了。

注入性语法

在前端很多场景下,我们可能会想在原有的语言基础上添加一些高亮提示或者扩展原有的高亮显示方式,在该情况下我们会使用注入的方式来实现该功能。

举例来说,考虑一个简单的场景,我们希望在 JavaScript 中每个注释后面的 TODO 作为一个关键字高亮显示,很多开发者也可能装有类似插件,为了实现该语法,我们将我们新的语法配置注入到 source.js 的 scope 内,这边我们使用 InjectTo 来实现,修改我们的 package.json

{
  "contributes": {
    "grammars": [
      {
        "path": "./syntaxes/injection.json",
        "scopeName": "todo-comment.injection",
        "injectTo": ["source.js"]
      }
    ]
  }
}

下面来实现具体的语法规则,使用 injectionSelector 查看 // 注释内的内容所在的 scope 是 comment.line.double-slash 。因此我们编写如下规则 sytanx 文件:

VSCode 语法插件开发指南

{
  "scopeName": "todo-comment.injection",
  "injectionSelector": "L:comment.line.double-slash",
  "patterns": [
    {
      "include": "#todo-keyword"
    }
  ],
  "repository": {
    "todo-keyword": {
      "match": "TODO",
      "name": "keyword.todo"
    }
  }
}

L: 在 injectSelector 中代表这些规则将会注入到现有规则的左侧,意味着我们注入的语法规则将会比现存的语法规则优先生效。

VSCode 语法插件开发指南

语义化高亮

语义化高亮一般而言,需要用户对于具体的语义上下文进行分析,因此可能需要解析整个文档来实现该功能,比如将文档解析到 ast ,然后再告诉 vscode 中各个 token 的具体语义规则,因此需要配合 Language Server 通过编写代码的方式来实现上述功能。

具体可以参考:code.visualstudio.com/api/languag…

Language Server

上面主要介绍了对于语言插件而言,如何去编写他的语法规则而对代码片段进行对应的着色处理。但是对于真正一款成熟的编程语言插件而言远远不够,比如经常会使用到的代码补全、纠错提示、悬浮提示、格式化、查找代码引用等等功能。

更多个性化的特性我们将通过 language sever 的形式来提供,language Server 以 language Server protocol(简称 LSP ) 作为一个传输协议, LSP 协议由微软提出,为了解决插件在不同编辑器之间复用的问题, language Server 讲不再直接与编辑器通信而是 LSP,在各个编辑器和language server 中做了标准化的处理。

VSCode 语法插件开发指南

因此这边分为 client 端和 Server 端两个部分,client 端即为编辑器侧,对用户在编辑器上的一系列行为动作(悬浮、代码更改)之后通过 LSP 通知 language server。如图:

VSCode 语法插件开发指南

  • 当用户通过编辑器打开文档,发送给 language server 通知文档打开事件('textDocument/didOpen')。之后文档内容被载入到内存之中。之后文本的内容将会在 tool 和 language server 中同步。

  • 编辑文档:通知language server 关于文档更改('textDocument/didChange'),之后,language server 分析文档内容并且通知检测到的错误和警告('textDocument/publishDiagnostics')。

有兴趣的可以看下 vscode 官方给的 lsp 插件实例:github.com/microsoft/v…

几乎更多的高级能力都需要language server 来做具体的实现,因此对于一个良好的语言插件来说,language Server 也是非常重要的一部分。对于编写 language server 来说,更多的是于vscode 相关的 api 打交道了,依赖本身对语言的解析和分析。

// client.ts
        client = new LanguageClient(
                'languageServerExample',
                'Language Server Example',
                serverOptions,
                clientOptions
        );
// server.ts
    connection.onCompletionResolve((item: CompletionItem): CompletionItem)
    documents.onDidChangeContent(change => {
        validateTextDocument(change.document);
    });

发布

目前为止,我们已经开发了一套简单的语法插件,那么如何发布我们的应用到应用市场上面让他人使用呢?这边主要有两个方式,一是将打包之后的插件文件(通过编译之后生成的 .visx 文件)直接发送给你的朋友拖到vscode 中即可使用,另一种是比较推荐的,也就是将插件发布到应用市场中公开,让所有人都可以通过市场的方式使用。

官方提供了 vsce 来给我们的插件做一个打包工作

# 全局安装vsce
$ npm install -g vsce 
$ cd myExtension
$ vsce package
# 生成 myExtension.vsix 
$ vsce publish
# 发布插件到应用市场,前提是已经登录了当前的 azure 账号,可以查看(https://code.visualstudio.com/api/working-with-extensions/publishing-extension)

这边我上传了插件的 demo 到应用市场了,可以搜索 xss-lang 来体验这个简单的插件。

VSCode 语法插件开发指南

总结

至此,我们已经简单的学会了对于新语法或者新语言的处理大致流程,下面这边搜集了一下vscode 插件的简单例子,有进一步兴趣的可以参考:

Hello World Demo: github.com/microsoft/v…

主题色demo: github.com/microsoft/v…

Language Server Demo:github.com/microsoft/v…

语法高亮 demo:github.com/AshoneA/xss…

各种 demo 集合:github.com/microsoft/v…

引用: