如何做一个简单得VSCODE插件

TD;DR

你可以直接看 vscode extension api,这里写的很清楚,是写插件我使用得唯一的参考文档。然后可以提前看一下坑,你可能会遇到。

我做的是个什么插件

首先这个插件真的很简单,就是一个Chibicms创建文章得插件。如果你不知道Chibicms是什么你可以看这个repo。不过我现在都用Limfx不用chibicms了,但是我还是把我写的很多文章保持这对chibicms得兼容。这个插件估计除了我没人用,这个插件每写一篇文章可以帮我节约30秒时间。

就是Chibicms得文章有个特定的套路,就是一个文件夹是一个文章,里面内容是一个叫做 content.md 文件,还有一个meta.json得文件(这个设计纯属是我当年懒,limfx没这些方便多了)。我写的这个插件就是在一个文件夹上右键,点“new chibi content”,输入一个文章标题,他会创建一个文件夹,并包含上面的文件,文件里面对应的字段会倍填充上你的标题。

非常的简单,我从完全不会做插件,到做好用了2.5小时,实际上可以更快,但是有几个坑,耗费了我至少2小时。

创建一个最基本的插件

假设你已经装了vscode和node.js,你需要装一下 Yeoman and VS Code Extension Generator 。

npm install -g yo generator-code

然后你需要建立一个你的项目文件夹,在这个文件夹中创建一个vscode extension得项目。

yo code

然后你输入一些项目的基本信息,注意我们一般选typescript得项目,就可以了,你就有了一个基本的插件。

然后我们打开package.json这个文件,这个不仅仅是npm得package配置,还有你插件的基本配置,很重要我简单说一下:

"activationEvents": [
		"onCommand:chibicms.new"
	],
	"main": "./out/extension.js",
	"contributes": {
		"commands": [
			{
				"command": "chibicms.new",
				"title": "New Chibi Content"
			}
		],
		"menus": {
			"explorer/context": [
				{
					"command": "chibicms.new",
					"group": "modification",
					"title": "New Chibi Content",
					"when": "explorerResourceIsFolder"
				}
			]
		}
	},

大部分都比较self explanatory,上面这个我简单解释一下,

  1. activationEvents就是你的插件什么时候被激活,被激活以后就会运行你插件里面的代码。里面的key value pair onCommand就是发出某个命令,后面的值就是插件被激活时候得命令ID。

  2. contributes就是说你的插件放在那里,在哪里用户可以找到你的插件,里面有个commands就是说在command pallet里面输入可以找到,里面的对象"command": "chibicms.new",就是这个命令得ID,后面的title是他显示名。下面还有个menu就是把这个命令加到context menu里面就是右键菜单里面,menu里面的我后面再解释。

ok上面的就足够了,然后再来到extension.ts这个文件,这是你插件的代码:

let disposable = vscode.commands.registerCommand(
    "chibicms.new",
    (folderUri) => {
            //do your work here
            vscode.window.showInformationMessage("hello world");
            return;
        });
    }
  );

上面就是你插件的全代码了,其他的你可以先忽略。

vscode.commands.registerCommand(
    "chibicms.new",
    function(){});

这个就是注册一个命令道插件中,当前面对应的激活事件发生的时候,就会执行这个里面注册的函数。这个registerCommand函数第一个参数是命令的ID和你前面package.json里面定义的对应就可以了。后面一个函数是你这个插件这个命令对应的要做的事情,你自己定义。

好了你现在可以说已经会写插件了

添加一个右键菜单

应为我这个插件时在用户选择一个文件夹的时候踩在右键菜单里显示,所以需要加一个右键菜单,你看看:

"contributes": {
		"commands": [
			{
				"command": "chibicms.new",
				"title": "New Chibi Content"
			}
		],
		"menus": {
			"explorer/context": [
				{
					"command": "chibicms.new",
					"group": "modification",
					"title": "New Chibi Content",
					"when": "explorerResourceIsFolder"
				}
			]
		}
	},

这个里面得"menus"就是菜单,"explorer/context"就是我这个菜单想加在文件浏览器得右键菜单,"command": "chibicms.new",就是这菜单对应的命令ID,"group": "modification",就是这个菜单项放在哪个组里面,如下面这个图:

然后title就是他的标题不用说了,"when": "explorerResourceIsFolder"是控制什么时候这个菜单项宣誓出来,我只希望他在文件夹上方显示,所以是"explorerResourceIsFolder"

关于菜单可以参考这里:contributions

处理用户输入和基本的逻辑

首先,上面讲过的插件的逻辑都是些在:

let disposable = vscode.commands.registerCommand(
    "chibicms.new",
    (folderUri) => {
            //do your work here
            vscode.window.showInformationMessage("hello world");
            return;
        });
    }
  );

那个//do your work所在的那个函数里面的,这里我没几行代码就用lambda表达式直接写在里面了。

这个插件需要允许用户输入标题,那就用个input box把:

(folderUri) => {
      vscode.window
        .showInputBox({
          placeHolder: "enter title",
        })
        .then((title) => {
          //replace title
          var real_meta: string = meta;
          real_meta = real_meta.replace("&&title&&", title as string);
          var title_slug: string = slug(title as string, { tone: false });
          //create files
          var dir = folderUri.fsPath + "\\" + title_slug;
          if (fs.existsSync(dir)) {
            vscode.window.showInformationMessage("content already exist");
            return;
          }
          fs.mkdirSync(dir);
          var contentFile = dir + "\\content.md";
          var metaFile = dir + "\\meta.json";
          fs.writeFileSync(contentFile, "# " + (title as string) + prepub);
          fs.writeFileSync(metaFile, real_meta);
        });
    }

说先说一下那个folderUri,这个context menu传进来的,你再那个文件夹上点,他就传进来那个文件的uri。

上面那个代码很简单吧,打开一个input box,然后再用用户输入的title替换带哦meta里面的一个place holder "&&title&&",那个meta就是我存的一个字符串常量,直接替换写到文件中,简单粗暴。

后面是用这个tile产生一个url合法的slug作为文件名,用的是limax库,用NPM可以直接装。在后面是新建一个文件夹,再创建两个文件,写入对应的meta,content.md里面写入一个标题。完事了。

对了其中为了避免覆盖已经存在的文章,如果那个文件夹存在,则不要继续了,弹出一个消息告诉用户,

if (fs.existsSync(dir)) {
            vscode.window.showInformationMessage("content already exists");
            return;
          }

调试

只要按下F5,他就会弹出一个新的vscode窗口,里面有你的插件,你就可以调试了,再extension.ts文件中可以添加断点,都会命中的,vscode自带了node得debugger调试非常方便。

发布

要发布插件很简单,只要装一个VSCE,他是个打包工具

npm install -g vsce

然后打包就可以了,注意我这里没邮件发到market上去,应为这个估计除了我没人用。打包:

vsce package

就会生成一个.vsix得文件,这就是打包好的插件了。你可以安装,也可以打给别人安装:

code --install-extension .\chibicms-0.0.1.vsix

无法调试

这是我遇到的最大的坑,不知道为啥,症状如下:

这个我研究了好久,好像没人遇到我这个问题,后来发现还是我对vscode调试不熟悉,那个需要建一个launch.json文件,然后还要手动往里面添加一个启动extension得configuration,不是到为啥我这个默认没有,网上也找不到。操作如下图:

再launch.json添加一个extensionHost的configuration:

        {
            "type": "extensionHost",
            "request": "launch",
            "name": "Launch Extension",
            "runtimeExecutable": "${execPath}",
            "args": [
                "--extensionDevelopmentPath=${workspaceFolder}"
            ],
            "outFiles": [
                "${workspaceFolder}/out/**/*.js"
            ],
            "preLaunchTask": "npm"
        },

结果还是报错,我有研究了好久,然后自己摸索出来了怎么搞,对这两个完全没有说明。。。如下图,要添加一个task,让launch的之前先调用,我们选npm compile那个task就行,他就是会再启动之前先编译一线,然后上面那个launch.json也要改"preLaunchTask": "npm"这个值要改成和nitask里面的lable一样,也就是下面这个里面的"label": "npm: compile - test",好了你可以正常的调试了。

{
	"version": "2.0.0",
	"tasks": [
		{
			"type": "npm",
			"script": "compile",
			"path": "test/",
			"group": "build",
			"problemMatcher": [],
			"label": "npm: compile - test",
			"detail": "tsc -p ./"
		}
	]
}

转换slug不灵

我用了一个叫做limax的npm包来转slug(非常好用),由于他是js的不是ts(应该也不止这个问题,我不清楚),你按照官方用法:

import slug from 'limax';,

会报错,不知道为啥,研究了一会也没搞定,只能非常不优雅的改成es5的语法,然后莫名其妙就可以了:

import slug = require("limax");

本文章使用limfx的vsocde插件快速发布