VScode插件开发--M2D文档转换插件
VSCode m2d插件开发
VSCode结构
VScode基于Electron构建的,分为三个部分
- Electron: UI
-
- Monaco Editor: 基于网页的编辑器,有符合LSP的插件就可以进行高亮,悬停,格式化等功能
- Extension Host: VScode的主进程和插件进程是分开管理的,
Extension Host
就是管理插件进程,这也是VScode启动快速的原因
- Language Server Protocol && Debug Adapter Protocol
Language Server Protocol :即语言服务器协议,用于编辑器和编辑环境与语言服务器之间的一种协议.就是当符合LSP的代码输入时会发送给语言服务器,语言服务器响应给客户端关于语法检测,自动补全之类的信息.在idea中,不同语言的检索需要不同的插件,这会导致极大的内存损耗,而LSP使得编程语言社区能专注于不断完善一个能提供语法检查、自动补全、跳转位置、引用查找等语言特性检查的高性能“语言服务器”.
Debug Adapter Protocol: 一般而言,不同的编辑器对于不同的语言的调试模块都是不同的,这对于语言供应商和编辑器开发者而言都是一场灾难,因此,DAP便应运而生,此后各编辑器可以通过相同的协议与debugger通信,不同的编辑器通过统一的DAP(Debug Adapter Protocol)协议与各个debugger对应的适配器(Debug Adapter)通信.
Extension Host: VScode的主进程和插件进程是分开管理的,Extension Host
就是管理插件进程,这也是VScode启动快速的原因.它所管理的Extension实现了VScode的大部分功能,比如主题拓展,各种通用功能的拓展等.
生命周期
从生命周期上来看,插件编写有三大个部分:
- Activation Event:设置插件激活的时机。位于 package.json 中。
- Contribution Point:设置在 VSCode 中哪些地方添加新功能,也就是这个插件增强了哪些功能。位于 package.json 中。
- Register:在
extension.ts
中给要写的功能用vscode.commands.register...
给Activation Event
或Contribution Point
中配置的事件绑定方法或者设置监听器。位于入口文件(默认是extension.ts
)的activate()
函数中。
activationEvents
配置项配置插件的激活数组,即在什么情况下插件会被激活,目前支持以下8种配置:
- onLanguage: 在打开对应语言文件时
- onCommand: 在执行对应命令时
- onDebug: 在 debug 会话开始前
- onDebugInitialConfigurations: 在初始化 debug 设置前
- onDebugResolve: 在 debug 设置处理完之前
- workspaceContains: 在打开一个文件夹后,如果文件夹内包含设置的文件名模式时
- onFileSystem: 打开的文件或文件夹,是来自于设置的类型或协议时
- onView: 侧边栏中设置的 id 项目展开时
- onUri: 在基于 vscode 或 vscode-insiders 协议的 url 打开时
- onWebviewPanel: 在打开设置的 webview 时
- *: 在打开 vscode 的时候,如果不是必须一般不建议这么设置
contributes
配置项是整个插件的贡献点,也就是说这个插件有哪些功能。contributes
字段可以设置的key
也基本显示了vscode
插件可以做什么。
- configuration:通过这个配置项我们可以设置一个属性,这个属性可以在
vscode
的settings.json
中设置,然后在插件工程中可以读取用户设置的这个值,进行相应的逻辑。 - commands:命令,通过
cmd
+shift
+p
进行输入来实现的。 - menus:通过这个选项我们可以设置右键的菜单
- keybindings:可以设置快捷键
- languages:设置语言特点,包括语言的后缀等
- grammars:可以在这个配置项里设置描述语言的语法文件的路径,vscode可以根据这个语法文件来自动实现语法高亮功能
- snippets:设置语法片段相关的路径
环境准备
-
yeoman脚手架工具
npm install -g yo
-
generator-code VSCode代码生成器
npm install -g generator -code
创建项目
1.执行代码创建项目
yo code
2.出现选项界面,按需选择
创建完成会自动创建文件夹并帮助初始化完成文件,目录结构如下
|-- src
|-- test //插件单测文件
|-- extension.js //插件入口文件
|-- CHANGELOG.md //修改日志,发布后会展示
|-- package-lock.json
|-- package.json
|-- README.md //插件说明 README,发布后会展示
|-- tsconfig.json
|-- tslint.json
|-- vsc-extension-quickstart.md //插件开发说明
项目内容
需求: 将选定的内容进行文档转换,将markdown文件转换成符合公司语言规范样式的docx文件,方便公司内部使用,本人只负责编写插件,由后台传给jar包实现功能
过程:
1.在extension.ts中编写代码
在插件激活activate中编写注册代码的命令,以及把注册的disposable对象推入subscription中,具体代码如下:
//该命令是针对文件中所有内容进行转换
vscode.commands.registerTextEditorCommand('helloworld.helloWorld', (textEditor,edit) => {
const doc = textEditor.document;
const start = new vscode.Position(0, 0);
const end = new vscode.Position(doc.lineCount - 1, doc.lineAt(doc.lineCount - 1).text.length);
//获取全部文本区域
const selection = new vscode.Range(start, end);
let text = doc.getText(selection);
//替换文件内容/
//调用jar包方法,执行相应转换
textEditor.edit(builder => {
builder.replace(selection, "aaa");
});
vscode.window.showInformationMessage(text);
});
//该注册命令针对选中内容
let disposable = vscode.commands.registerTextEditorCommand('extension.px2rpxInSelection', (textEditor, edit) => {
const doc = textEditor.document;
let selection: vscode.Selection | vscode.Range = textEditor.selection;
//获取选中区域
if (selection.isEmpty) {
const start = new vscode.Position(0, 0);
const end = new vscode.Position(doc.lineCount - 1, doc.lineAt(doc.lineCount - 1).text.length);
selection = new vscode.Range(start, end);
}
let text = doc.getText(selection);
//替换文件内容
textEditor.edit(builder => {
builder.replace(selection, aaa();//执行相应转换文字的方法
});
});
context.subscriptions.push(disposable);
之后需求改成了在资源管理器中选中文件或文件夹进行转换,之前写的压根没用上⊙﹏⊙∥
//使用了fs,和path库
// 注册命令,并获取URL
context.subscriptions.push(vscode.commands.registerCommand('extension.demo.getCurrentFilePath', (uri) => {
//获取文件的逻辑拉出
function fsReadDir(dir: string) {
return new Promise<string[]>((resolve, reject) => {
fs.readdir(dir, (err, files) => {
if (err) reject(err);
resolve(files);
});
});
}
// 获取fs.stats的逻辑拉出
function fsStat(path: string) {
return new Promise<fs.Stats>((resolve, reject) => {
fs.stat(path, (err, stat) => {
if (err) reject(err);
resolve(stat);
});
});
}
// 搜索文件主方法
async function fileSearch(dirPath: string) {
//获取文件路径
const files = await fsReadDir(dirPath);
const promises = files.map(file => {
//调用fsStat函数
return fsStat(path.join(dirPath, file));
});
//获取每一项文件绝对路径
const datas = await Promise.all(promises).then(stats => {
for (let i = 0; i < files.length; i += 1) files[i] = path.join(dirPath, files[i]);
return { stats, files };
});
//遍历每一项的stats,判断是否为文件夹
datas.stats.forEach(stat => {
const isFile = stat.isFile();
const isDir = stat.isDirectory();
//如果是文件夹,递归自身
if (isDir) {
fileSearch(datas.files[datas.stats.indexOf(stat)]);
}
//如果是文件,直接创建终端,执行命令
if (isFile) {
let terminalA = vscode.window.createTerminal({ name: "m2d" });
terminalA.show(true);
//替换文件后缀为docx
let str = datas.files[datas.stats.indexOf(stat)].replace(/\.\w+$/, ".docx")
//终端执行命令
terminalA.sendText("m2d convert " + datas.files[datas.stats.indexOf(stat)] + " " + str); //输入命令
console.log(datas.files[datas.stats.indexOf(stat)]);
}
});
};
//获取执行命令的路径
let str = uri.path.substring(1);
fs.stat(str, (error, stats) => {
if (error) {
console.log('fail')
return
}
const isFil = stats.isFile();
const isDi = stats.isDirectory();
if (isDi) {
// 当前选中的为文件夹
fileSearch(str);
}
if (isFil) {
//当前选中的为文件
let terminalA = vscode.window.createTerminal({ name: "m2d" });
terminalA.show(true);
let path = str.replace(/\.\w+$/, ".docx")
terminalA.sendText("m2d convert " + str + " " + path);
}
})
}));
2.在package.json中配置
"contributes": {
"commands": [
{
"command": "helloworld.helloWorld",
"title": "M2D全部转换"
},
{
"command": "extension.px2rpxInSelection",
"title": "M2D选中转换"
},
{
"command": "extension.demo.getCurrentFilePath",
"title": "M2D选中转换a"
}
],
"keybindings": [
{
"command": "helloworld.helloWorld",
"key": "ctrl+f10",
"mac": "cmd+f10",
"when": "editorTextFocus"
},
{
"command": "extension.px2rpxInSelection",
"key": "Alt+shift+p"
}
],
"menus": {
//在编辑器中显示
"editor/context": [
//获取所有文本内容
{
"when": "editorFocus",
"command": "helloworld.helloWorld",
"group": "navigation"
},
//当内容选中时
{
"when": "editorHasSelection",
"command": "extension.px2rpxInSelection",
"group": "6_px"
}
],
//资源管理器中显示
"explorer/context": [
{
"command": "extension.demo.getCurrentFilePath",
"group": "navigation"
}
]
}
},
难点: 怎样在typescript中调用jar包中的方法
个人认为,由于LSP的问题,在VScode中创建了ts文件,它便不会符合Java的语言服务器协议,无法使用Java的语法规范,完全和Java的规则不相同,便无法直接引用jar包.
所以,重心变成了调用jar包中的方法,我想出的第一个方法是创建一个java文件,在Java文件中调用jar包中的方法,然后再从ts文件中调用Java里的方法,但这个问题在于处理ts和java文件通信的问题,这个问题在Android中很容易解决,通过webview的方法调用就能实现,pc端好像并不能用,所以我又想到了使用Ajax来传递,但也不太理想
调用个鬼,创建bat文件,然后在typescipt中代码直接创建终端,输入命令调用bat文件就完事
插件发布
安装vsce
npm i install vsce -g
执行命令生成vsix文件
vsce package
问题:
1.出现publisher is missing:直接在package.json中创建publisher即可
2.make sure to edit the README.md file before you package or publish you extension:随便修改以下README.md,然后保存即可
仅用来本人学习记录
参考:官方api文档
https://code.visualstudio.com/api/references/vscode-api
张宇:VScode插件入门开发
https://zhuanlan.zhihu.com/p/99198980
雪山飞狐:vscode插件开发教程
https://www.jianshu.com/p/e642856f6044
littleTommyTan:Typescript遍历文件
https://segmentfault.com/a/1190000016841072
小茗同学插件开发全攻略:(详细,强烈推荐)
https://www.cnblogs.com/liuxianan/p/vscode-plugin-overview.html