编写vscode插件

在编写vscode插件之前先得说说vscode的设计原则以及他所用到的模式

1. 插件原则

  • 单独的插件进程

vscode为了核心编辑器不受影响,为插件单独设置一个进程,这样插件的任何动作都不会影响到核心编辑器的响应用户的操作.

  • 不允许直接操作DOM

vscode只允许间接操作DOM,因为它认为完全暴露DOM可能使得vscode整体结构发生改变,影响用户体验

  • 通过Activation Event触发插件加载

vscode希望扩展尽可能慢地启动,所以希望通过Activation Event触发插件加载,Activation Event有以下事件

  1. onLanguage:${language}
  2. onCommand:${command}
  3. onDebug:${type}
  4. workspaceContains:${toplevelfilename}
  5. *
  • 可使用node中模块开发
  • 通过协议互联插件与编辑器

一些插件例如language servers和debug adapters通过协议来与编辑器交换信息

  • 通过插件清单获取插件所有信息

每个插件都有一个package.json文件,里面有关于插件的启动文件,还有插件的启动触发事件,配置参数,插件命令等,还可以通过npm安装依赖

2. 插件API中的模式

  • Promise

vscode通过Promise来实现异步操作,并返回一个Thenable类型的对象

  • Cancellation Tokens

在操作还没完成就因为结束的时候十分有用的一个对象

  • Disposables

管理对象的,使用它的dispose方法,可以销毁对象

  • Events
var listener = function(event) {
   console.log("It happened", event);
};

// start listening
var subscription = fsWatcher.onDidDelete(listener);

// do more stuff

subscriptions.dispose(); // stop listening 

3. RestWhile插件

这是我的第一个vscode插件,主要是提醒程序员不要工作过度,不要久坐

  • 开始

安装vscode插件的项目骨架

npm install -g yo generator-code 

开始生成项目

yo code 

选择New Extension(Javascript)(我不会写TypeScript)

  • 书写主要逻辑

采用ES6编写

// extension.js

let { window, workspace, Disposable, StatusBarAlignment, commands } = require('vscode');

const EXTENSIONNAME = "rest";

class RestController {

   constructor() {
       let subscriptions = [];
       this.initConfig();
       this._startTime = this._getCurrentTime(); // 编辑器开始启动的时间
       this._lastTime = this._startTime; // 当前时间
       this._nowTime = this._lastTime; // 上一次触发的时间
       this._continueTime = 0;
       this._nextTime = this._codingTime; // 预计下次提醒时间

       this._statusItem = window.createStatusBarItem(StatusBarAlignment.Left, 3);

       window.onDidChangeTextEditorSelection(this.onPopTips, this, subscriptions);
       window.onDidChangeActiveTextEditor(this.onPopTips, this, subscriptions);
       this._disposable = Disposable.from(...subscriptions);
       
   }

   changeStatusBarItem(minutes) {
       if(!this._statusItem) {
           this._statusItem = window.createStatusBarItem(StatusBarAlignment.Left, 3);
       }
       this._statusItem.text = `编程时长 ${minutes} 分钟`;
       this._statusItem.show();
   }

   popTips(minutes) {
       let hours = Math.floor(minutes/60);
       minutes = minutes % 60;
       let tmp = hours === 0 ? "" : (hours+"小时");
       window.showWarningMessage(`你已经编程超过${tmp}${minutes}分钟! 休息一会吧!`);
   }

   onPopTips() {

       this._lastTime = this._nowTime;
       this._nowTime = this._getCurrentTime();
       if(this._nowTime - this._lastTime >= this._deadTime ) {
           this._startTime = this._nowTime;
           this._nextTime = this._codingTime;
           this._continueTime = 0;
           return;
       }
       this._continueTime = this._nowTime - this._startTime;
       let minutes = this._reverseTime(this._continueTime, false);
       this.changeStatusBarItem(minutes);
       if(this._continueTime >= this._nextTime) {
           this._nextTime += this._codingTime;
           this.popTips(minutes);
           
       }
   }
   getContinueTime() {
       this._lastTime = this._nowTime;
       this._nowTime = this._getCurrentTime();
       this._continueTime = this._nowTime - this._startTime;
       return this._continueTime;
   }
   _getConfigTime(value) {
       return this._reverseTime(this._config.get(value));
   }
   // 时间转换,true是分钟转毫秒,false是毫秒转分
   _reverseTime(time, to) {
       if(to || to === undefined) {
           return time*60000;
       } else {
           return Math.floor(time/60000);
       }
   }

   initConfig() {
       this._config = workspace.getConfiguration(EXTENSIONNAME);
       if(!this._config) return;
       this._codingTime = this._getConfigTime("codingTime");
       if(!this._codingTime) return;
       this._deadTime = this._getConfigTime("deadTime");
       if(!this._deadTime) return;
   }

   _getCurrentTime() {
       return new Date().getTime();
   }

   dispose() {
       this._disposable.dispose();
   }

}


function activate(context) {
   let controller = new RestController();
   // 用户配置更改时,初始化控制器的配置
   workspace.onDidChangeConfiguration(()=>
       controller.initConfig());
   context.subscriptions.push(controller, 
   commands.registerCommand('rest.showActiveTime', function(){
       let m = controller._reverseTime(controller.getContinueTime(), false);
       window.showInformationMessage(`当前编程时长为: ${m}分钟`);
   }));
}
exports.activate = activate;

function deactivate() {

}
exports.deactivate = deactivate; 

从上面这个例子,我们可以看到这个模块主要到处两个函数,activtedeactivate,很显然,activate是插件加载时调用的函数,deactivate则是当插件用到系统资源时,在插件关闭时用于清空资源的函数

上面例子,主要用到了两种资源,一种是事件订阅,一种是命令注册

  • 事件订阅
window.onDidChangeTextEditorSelection(this.onPopTips, this, subscriptions);
window.onDidChangeActiveTextEditor(this.onPopTips, this, subscriptions); 

分别订阅了编辑器中的内容改变事件以及当前编辑的文件改变事件

workspace.onDidChangeConfiguration(()=>
       controller.initConfig()); 

则是订阅了插件配置参数改变事件

  • 命令注册
commands.registerCommand('rest.showActiveTime', function(){
   let m = controller._reverseTime(controller.getContinueTime(), false);
   window.showInformationMessage(`当前编程时长为: ${m}分钟`);
}) 

注册了rest.showActiveTime命令

另外需要注意将订阅的事件,注册命令返回对象都纳入到context.subscriptions

这样插件主体逻辑完成了

  • 配置package.json
{
   "name": "rest-for-a-while",
   "displayName": "RestWhile",
   "description": "Rest for a while",
   "version": "0.0.2",
   "publisher": "jeffwang",
   "engines": {
       "vscode": "^1.10.0"
   },
   "homepage": "https://github.com/jeffwcx/rest-while/blob/master/README.md",
   "icon": "icon.png",
   "repository": {
       "type": "git",
       "url": "https://github.com/jeffwcx/rest-while.git"
   },
   "categories": [
       "Other"
   ],
   "activationEvents": [
       "*"
   ],
   "main": "./extension",
   "contributes": {
       "configuration": {
           "title": "Rest for a while Configuration",
           "properties": {
               "rest.codingTime": {
                   "type": "number",
                   "default": 60,
                   "description": "the time you have programed"
               },
               "rest.deadTime": {
                   "type": "number",
                   "default": 15,
                   "description": "the time you have stopped"
               }
           }   
       },
       "commands":[
           {
               "command": "rest.showActiveTime",
               "title": "Rest: ShowActiveTime"
           }
       ]
   },
   "scripts": {
       "postinstall": "node ./node_modules/vscode/bin/install",
       "test": "node ./node_modules/vscode/bin/test"
   },
   "devDependencies": {
       "typescript": "^2.0.3",
       "vscode": "^1.0.0",
       "mocha": "^2.3.3",
       "eslint": "^3.6.0",
       "@types/node": "^6.0.40",
       "@types/mocha": "^2.2.32"
   }
} 

主要配置contributes参数

首先是暴露给用户的插件参数

"configuration": {
   "title": "Rest for a while Configuration",
   "properties": {
       "rest.codingTime": {
           "type": "number",
           "default": 60,
           "description": "the time you have programed"
       },
       "rest.deadTime": {
           "type": "number",
           "default": 15,
           "description": "the time you have stopped"
       }
   }   
} 

我们之前通过

workspace.getConfiguration(EXTENSIONNAME) 

在插件内部获取到了这些参数

而这些参数其实会显示在用户设置中,用户可以通过覆盖参数来配置插件

再然后是commands属性

"commands":[
   {
       "command": "rest.showActiveTime",
       "title": "Rest: ShowActiveTime"
   }
] 

命令和我们刚在内部注册的命令一致,而且用户可以通过在命令面板中搜索Rest: ShowActiveTime来执行这个命令

还有一个很重要的参数activationEvents

"activationEvents": [
   "*"
] 

定义了插件启动的触发事件,上面的事件代表vscode启动时就开始加载插件,要慎用

另外还可以设置插件图标

"icon": "icon.png" 

还有发布人

"publisher": "jeffwang" 
  • 发布插件

发布插件需要用到vsce

npm install -g vsce 

然后还要注册微软teamservice帐号,获得Personal Access Token,具体方法参考vscode文档

有了Personal Access Token之后创建一个发布人

vsce create-publisher jeffwang // 注意和package.json中的publisher一致 

开始发布

vsce publish // 可能要输入AccessToken 

在商店中搜索就能找到这个插件了

  • 本地安装插件

先将插件打包成.vsix文件

vsce package 

然后直接安装

code --install-extension rest.vsix 

参考文献


write by jeffwang 2017/04/05