vscode插件开发教程

1.概览

1.1 vscode插件可以做什么

vscode编辑器是可高度自定义的,我们使用vscode插件几乎可以对vscode编辑器进行所有形式的自定义,只要你想做,基本上没有不能实现的。

vscode插件开发的官方文档为:
https://code.visualstudio.com/api
中文文档:
https://liiked.github.io/VS-Code-Extension-Doc-ZH/#/
vscode插件可以实现

  • 自定义命令
  • 快捷键
  • 自定义菜单项
  • 自定义跳转
  • 自动补全
  • 悬浮提示
  • 新增语言支持
  • 语法检查
  • 语法高亮
  • 代码格式化
    ····

1.2 如何创建插件

可以通过官方脚手架来生成vscode插件模板工程。

首先安装脚手架
npm install -g yo generator-code
然后进入工作目录,使用脚手架
yo code

1.png

通过上图可以看到,生成一个vscode插件工程时可以选择是创建一个已有的语言的插件还是一个全新的语言的插件,并且可以选择插件开发语言。
本文以创建一个新语言的插件为例。
vscode插件开发可以使用TypeScript开发,也可以使用JS,两种方式能实现的功能是一样的。
下面是自动生成的插件工程文件

3.png

其中,最核心的两个文件是package.jsonextension.jspackage.json是整个插件工程的配置文件,extension.js则是工程的入口文件。下面将对这两个文件进行详细的介绍。

1.3 package.json详解

4.png
5.png

:<u>以上配置项在刚创建完的工程文件中不全存在,本文为了更全面介绍配置项,所以后面人为添加了一些配置项。</u>

activationEvents

activationEvents配置项配置插件的激活数组,即在什么情况下插件会被激活,目前支持以下8种配置:

  • onLanguage: 在打开对应语言文件时
  • onCommand: 在执行对应命令时
  • onDebug: 在 debug 会话开始前
  • onDebugInitialConfigurations: 在初始化 debug 设置前
  • onDebugResolve: 在 debug 设置处理完之前
  • workspaceContains: 在打开一个文件夹后,如果文件夹内包含设置的文件名模式时
  • onFileSystem: 打开的文件或文件夹,是来自于设置的类型或协议时
  • onView: 侧边栏中设置的 id 项目展开时
  • onUri: 在基于 vscode 或 vscode-insiders 协议的 url 打开时
  • onWebviewPanel: 在打开设置的 webview 时
  • *: 在打开 vscode 的时候,如果不是必须一般不建议这么设置
contributes

contributes配置项是整个插件的贡献点,也就是说这个插件有哪些功能。contributes字段可以设置的key也基本显示了vscode插件可以做什么。

  • configuration:通过这个配置项我们可以设置一个属性,这个属性可以在vscodesettings.json中设置,然后在插件工程中可以读取用户设置的这个值,进行相应的逻辑。
  • commands:命令,通过cmd+shift+p进行输入来实现的。
  • menus:通过这个选项我们可以设置右键的菜单
  • keybindings:可以设置快捷键
  • languages:设置语言特点,包括语言的后缀等
  • grammars:可以在这个配置项里设置描述语言的语法文件的路径,vscode可以根据这个语法文件来自动实现语法高亮功能
  • snippets:设置语法片段相关的路径
    . . . . .

extension.js

extension.js是插件工程的入口文件,当插件被激活,即触发package.json中的activationEvents配置项时,extension.js文件开始执行。
extension.js中对需要的功能进行注册,主要使用vscode.commands.register...相关的api,来为package.json中的contributes配置项中的事件绑定方法或者监听器。
vscode.commands.register...相关的api主要有:

  • vscode.languages.registerCompletionItemProvider()
  • vscode.commands.registerCommand()
  • vscode.languages.registerCodeActionsProvider()
  • vscode.languages.registerCodeLensProvider()
  • vscode.languages.registerHoverProvider()
    . . . . .
    6.png

1.4 插件生命周期

下面我们运行一下这个插件工程,按F5运行插件,这个时候会自动打开一个新的vscode界面,我们按cmd+shift+p,在命令框输入plugin-demo.helloWorld命令,既可以看到在vscode的界面的右下角弹出一个弹框,弹框显示Hello World from plugin-demo2,这正是我们在extension.js中为plugin-demo.helloWorld中为plugin-demo.helloWorld命令绑定的事件。

7.png

下面我们梳理一下这个弹框出现的整个流程:


8.png
  • 1.activationEvents:在package.jsonactivationEvents配置项中设置插件激活时机,这里设置的是onCommand:plugin-demo.helloWorld,即输入命令onCommand:plugin-demo.helloWorld时激活。
  • 2.contributespackage.json中的contributes配置项表示这个插件增加了哪些功能,这里设置了commands,增加的命令,在这一项中声明了一个命令plugin-demo.helloWorld
  • 3.Register:在extension.js文件中的activate(context)方法中,使用vscode.commands.registerCommand()这一API为命令plugin-demo.helloWorld绑定事件,绑定的事件为vscode.window.showInformationMessage('Hello World from plugin-demo2!'),即弹出弹框。
  • 4.在命令框中输入plugin-demo.helloWorld,此时插件被激活,进入extension.js中执行activate()方法,由于已经在contributes配置项中声明了命令plugin-demo.helloWorld,所以在activate()方法中为该命令绑定一个事件,由于在命令框中输入了这个命令,所以命令绑定的事件立即被触发执行,所以在vscode的右下角弹出了弹出框。

VSCode的插件都运行在一个独立的进程里, 被称为 Extension Host, 它加载并运行插件, 让插件感觉自己好像在主进程里一样, 同时又严格限制插件的响应时间, 避免插件影响主界面进程。

9.png

2.具体的功能介绍

命令

关于命令我们之前在分析插件的生命周期的时候就已经讲过,首先在package.jsoncontributes配置项中声明命令:

"commands": [
    {
        "command": "plugin-demo.helloWorld",
        "title": "Hello World"
    }
]

然后在extension.jsactivate()中去注册该命令,绑定事件:

let disposable = vscode.commands.registerCommand('plugin-demo.helloWorld', function () {
    
        vscode.window.showInformationMessage('Hello World from plugin-demo2!');
    });

所有注册类的API执行后都要将将结果放到context.subscriptions中去:

context.subscriptions.push(disposable);

这样当插件被激活后,输入命令,命令绑定的事件就会被执行。

菜单

菜单也是通过和命令关联起来来实现其功能的

"menus": {
    "editor/title": [{
    "when": "editorFocus",
    "command": "plugin-demo.helloWorld",
    "alt": "",
    "group": "navigation"
    }]
}

以上是一个菜单项的完整配置.

  • editor/title: 定义这个菜单出现在哪里,这里是定义出现在编辑标题菜单栏。
  • when: 菜单在什么时候出现,这里是有光标的时候出现
  • command: 点击这个菜单要执行的命令
  • alt: 按住alt再选择菜单时应该执行的命令
  • group: 定义菜单分组

菜单项对应的命令为plugin-demo.helloWorld,我们再在contributionscommands中找到这个命令:

"commands": [
    {
        "command": "plugin-demo.helloWorld",
        "title": "菜单栏测试"
    }
        ]

这里命令的title将作为菜单项的名字显示,当然我们也可以设置菜单项的icon
我们之前已经在extension.js中注册过这个命令了,因此不用再注册。

F5运行插件,保证插件被激活后,使光标出现,在编辑器的右上角我们可以看到出现一个新增的菜单:

10.png

当我们点击这个菜单时,其会执行关联的commandextension.js中绑定的事件。

快捷键

快捷键的设置比较简单,其执行功能同样依赖于其关联的命令command

"keybindings": [
    {
        "command": "plugin-demo.helloWorld",
        "key": "ctrl+{",
        "mac": "cmd+{",
        "when": "editorTextFocus"
    }
]
  • command: 快捷键关联的命令
  • key: Windows平台对应的快捷键
  • mac: mac平台对应的快捷键
  • when: 什么时候快捷键有效

当插件被激活后,并且满足快捷键有效的时间,按快捷键就可以找到extension.js中与快捷键关联的command所不绑定的事件并执行。

悬停提示

悬停提示的思路是在extension.js中注册一个悬停事件,然后根据提供的docuemntposition已经文件名,文件路径等信息作出相应的逻辑。

主要API:

function registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Disposable;
这一API返回一个HoverProvider对象,这一对象需要加入到context.subscription中。

provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<Hover>;
这一API返回一个PrioviderResult对象,当我们把光标放在某个位置时显示的内容,就是这个对象封装的。

下面我们写一个简单的demo,我们对package.json文件中的main这个单词进行悬停提示:

function activate(context) {

    const hover = vscode.languages.registerHoverProvider('json', {
        provideHover(document, position, token) {
            const fileName = document.fileName;
            const word = document.getText(document.getWordRangeAtPosition(position));
            if (/\/package\.json$/.test(fileName) && /\bmain\b/.test(word)) {
                return new vscode.Hover("测试悬停提示");
            }
            return undefined;
        }
    });

    context.subscriptions.push(hover);
}

运行插件,保证插件被激活的状态下,将光标放在package.json文件的main单词上:

12.gif

代码片段

代码片段也叫snippets,就是输入一个前缀,会得到一个或多个提示,然后回车带出很多代码。

想要在vscode插件中实现snippets的功能,首先要在package.jsoncontributes配置项中配置代码提示文件的文件路径:

"snippets": [
    {
        "language": "lizard",
        "path": "./snippets.json"
        }
    ]

这里language设置了snippets作用于何种语言,path设置了服务于snippets的文件的路径。
再看一下在snippets.json文件中:

{
    "View组件": {
        "prefix": "View",
        "body": [
            "<View>",
            "${1}",
            "</VIew>"
        ],
        "description": "View组件"
    }
}
  • "View组件": snippet的名称
  • "prefix": 前缀,即输入什么可以出现snippets的提示
  • "body": 按回车后出现的一大段代码,是一个数组,数组里面是字符串,每个字符串代表一行代码,${1}表示第一个光标的位置,同样,${2}表示第二个光标的位置
  • "description": 对于这个snippet的描述,当我们选中这个snipets提示时,描述会出现在后面。
    现在,我们运行插件,并保证插件被激活,在规定的语言下,输入View:
    11.gif

3.详细讲解的插件功能

代码高亮

当我们为一个已有的语言创建插件时,package.json中默认不会有代码高亮相关的配置,当我们为一个新语言开发插件时,插件工程的package.json文件中默认有语法高亮相关的配资。
这一配置仍然在contributes中:

20.png

上图中grammarspath项设置了描述新语言的语法的文件路径。
然后我们看到这个语法文件lizard.tmLanguage.json,vscode会根据这个语法文件自动实现语法高亮的功能。我们找到该文件中的一段:

21.png

先不管这里每一项表示什么含义,首先运行代码,输入forreturnifwhile等关键字中的其中一个,会发现关键字出现了高亮,这便实现了简单的高亮功能。
上面的的代码中。
lizard.tmLanguage.json中的语法是TextMate语法,关于TextMate的介绍:
https://macromates.com/manual/en/language_grammars
https://www.apeth.com/nonblog/stories/textmatebundle.html

22.png

上面的代码中:
match是一个正则表达式,但是使用的是ruby regular expression,进行匹配,name是被匹配的表达式的scope selector,关于scope selector的介绍见链接:
https://macromates.com/manual/en/scope_selectors
vscode根据这个scope selector进行上色。
下面介绍一下本文为新语言写语法文件的案例:
为属性结构写语法,属性结构模板为:style = {width:8, height:9}

23.png

代码提示

代码提示是我们使用vscode开发的时候不可获取的一个功能,即当我们输入代码的一部分的时候,这时候vscode显示一个提示列表,我们可以选择一个提示项,然后回车,这样代码的剩余部分就自动补全了。

代码提示相关的主要的API是:
registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable;

  • 第一个参数是实现代码提示的文件的类型。
  • 第二个参数是一个CompletionItemProvider类型的对象,在创建这个对象内部,我们需要根据documentposition等信息进行逻辑处理,返回一个CompletionItem的数组,每一个CompletionItem就代表一个提示项。
  • 第三个参数是可选的触发提示的字符列表。

下面列出一些与代码提示相关的其他的一些API,这些API大多与文本、单词的处理相关,因为我们进行代码提示时需要知道当前光标所在单词的上下文,这样才能很好的给出智能提示,而要得到当前光标的上下文,就需要对光标附近乃至整个文件进行文本分析。

  • 与TextDocument相关
    TextDocument的对象实际是当前文件对象,所以我们可以根据该对象得到当前文件与文本相关的所有信息。
  • lineAt(line: number): TextLine; 根据行数返回一个行的对象
  • lineAt(position: Position): TextLine; 根据一个位置返回这一行的行对象
  • getText(range?: Range): string; 根据范围,返回这个范围的文本
  • getWordRangeAtPosition(position: Position, regex?: RegExp): Range | undefined; 根据position返回这个位置所在的单词。
  • text.charAt() 返回字符串在某个位置的字符

下面写一个代码提示的简单的demo:

function activate(context) {

    const provider = vscode.languages.registerCompletionItemProvider('plaintext', {
        provideCompletionItems(document, position) {
            const completionItem1 = new vscode.CompletionItem('Hello World!');
            const completionItem2 = new vscode.CompletionItem('World Peace!');
            return [completionItem1, completionItem2];
        }
    });
        
    context.subscriptions.push(provider);
}

我们在这里创建了两个CompletionItem对象,这样,当我们输入Hello World!World Peace的一部分时,插件会自动显示提示项,回车即可进行补全。

13.gif

自定义语言实现代码提示

上面的demo中我们实现了一个简单的代码提示的demo,但是这种异常简单的代码提示机会是没有任何价值的,为一个语言实现代码提示必须要结合当前光标位置的上下文来实现,根据上下文来分析当前光标所在单词属于类名、变量名、函数名等等,再提供相对应的提示。

为一个语言实现代码提示的主要方式有两种,第一种是使用抽象语法树,分析语法节点,分析当前位置属于哪一节点,第二种方式是直接使用正则匹配等方式来粗略判断当前位置的上下文,目前成熟的开发语言的代码提示均使用第一种方式,但是第一种方法同时也要处理语法错误时的分析,因此对个人而言难度相对比较大,本文采用第二种方式对新语言提供代码提示。

下面是新语言的一个模板:


14.png

其中主要包括两种结构:组件全局变量,这两种结构的形式都是非常固定的,组件 的一般结构如下:

<ComponentName propertyName = {key: value, key: value} propertyName2 = {}···/>

或者

<ComponentName propertyName = {key: value, key: value} propertyName2 = {}···></ComponentName>

全局变量的结构如果我们把globalVar =这部分结构忽略,只看{}里面的内容,很容易发现其结构与json无异,这提醒我全局变量的结构可以把它当成一个json对象进行解析,当然在此之前还需要做许多额外工作,保证解析的正确进行。

简而言之,对这个新语言的代码提示主要集中在5个部分:组件名称、组件的属性名称、组件的属性名称里的key、组件的属性名称里的value(有一些value是枚举值,因此需要进行提示)、全局变量的key。

我们前面一直提到实现代码提示要结合当前光标的上下文进行分析,其实质就是根据光标位置的上下文分析当前光标的位置属于哪一类,是属于组件名还是属性名等等。

因此问题就转化为如何根据当前光标的上下文得到光标处属于哪一类。本文处理次问题的逻辑如下:

  • 1.首先从当前位置开始往前寻找,找到象征一个组件开始的<,在此过程中如果遇见一个组件结尾标志的/>或者</NAME>结构,则停止寻找,说明当前光标不在组件里,可以判断当前光标是在全局变量处。
  • 2.在1中对光标在组件内还是组件外进行了区分。如果光标在组件内,则需要判断属于组件内的组件名、属性名、属性key、属性value的哪一种。
  • 3.使用api找到当前光标所在单词的起始位置,然后判断该其起始位置与<位置之间是否有除空格外的其他字符,若没有,则当前位置是组件名,若有,则当前位置不是组件名,需要继续区分,通过是否在括号内判断当前位置是不是属性名。
  • 4.{}内是keyvalue,使用:来进行区分。

这样就对五种情况进行了区分,然后可以针对每种情况给出有正对性的提示。
下面是最后的实现效果:

代码自动补全

html中当我们输入<label 再输入一个>时,这时候vscode会自动帮我们添加上</label>,不需要我们敲回车就能完成,这种自动补全的方式能提高开发效率,下面就谈一下其实现。

下面就以<label></label>的实现为例:
当敲入>时,首先要计算得到组件名componnetName,然后:

16.png

实现效果:

17.gif

加载本地的文件

有时候插件可能想要读取用户自己自定义的文件,来实现某个功能,这个时候就需要把用户的文件路径传递给插件。

解决这个问题的办法可以是给vscodesettings增加有一个设置项,用户填写对应的值,vscode插件就可以读取这个值,进而读取相关的文件。
settings增加设置项可以通过package.json文件的contributes进行配置:

18.png

然后我们就可以在settings中设置customPath这个设置项的值:

19.png

最后在插件工程中读取customPath的值:
const path = vscode.workspace.getConfiguration('lizard').get('customPath');

4.打包、发布、升级

发布插件到插件市场

  • 1.安装vsce(Visual Studio Code Extension)
    npm install -g vsce
  • 2.在网站https://dev.azure.com/vscode获取一个access token,这个token用来创建一个publisher
  • 3.创建publisher
    vscr create-publisher (publisher name)
  • 4.登入一个publisher
    vscde login (publisher name)
  • 5.打包
    vsce package
  • 6.发布
    vsce publish

升级

  • 1.首先在package.json文件中修改插件的版本号。
  • 2.使用命令vsce publish升级

参考

vscode插件开发官方文档

vscode插件开发中文文档

TextMate官方介绍
对理解TextMate极有帮助的文档
https://www.apeth.com/nonblog/stories/textmatebundle.html

知乎讲vscode原理的
https://zhuanlan.zhihu.com/p/99198980
vscode入门的博文教程
https://www.cnblogs.com/liuxianan/p/vscode-plugin-overview.html


原文地址https://www.jianshu.com/p/e642856f6044