VSCode 插件编写
约 439 个字 217 行代码 预计阅读时间 4 分钟
Reference: 编写一个 Vscode 插件 | 从创建到发布 | 一键删除函数 (较完善的 Repo)
从零编写一个 VSCode 插件并发布至插件市场,支持以下功能:
- 一键删除 选中函数 (函数声明 / 函数表达式 / 箭头函数)
- 支持 命令 / 快捷键 调用
1 初始化项目(CLI)
-
安装脚手架:
generattor-code提供了一系列项目模版 -
初始化项目:选择 TS、不使用 webpack
-
初始化后的项目结构如下:
-
运行、调试
# 1 运行: 使用 package.json 中提供的对应命令即可 npm run compile # => 该命令会生成 /out 文件夹 npm run watch # => 如果需要应用热更新 # 2 调试: 使用 F5 即可在新窗口中进行调试由于模版在
src/extension.ts中定义了命令:const disposable = vscode.commands.registerCommand('[plugin].helloWorld', () => { vscode.window.showInformationMessage('Hello World from [plugin]!'); });因此,在命令面板中选择
Hello World后即会在右下角出现对应内容的弹窗 -
命令注册
-
快捷键注册
2 功能实现
该插件涉及的关键步骤如下:
- 如何在 VSCode 中删除指定字符
- 如何获取 光标所在的 Function 范围
2.1 在 VSCode 中删除字符
row从 0 开始,col从 1 开始
删除指定范围内的所有字符
const editor = vscode.window.activeTextEditor;
if (!editor) return;
editor.edit((editBuilder) => {
// 删除两个 Range(row, col) 之间的所有字符
editBuilder.delete(
new vscode.Range(
new vscode.Position(), new vscode.Position()
))
})
2.2 基于 AST 获取函数范围
-
基于 Babel 解析 AST
单独拎一个文件出来,方便做单元测试
src/main.tsimport {parse} from "@babel/parser"; import traverse from "@babel/traverse"; // 提供类型提示 interface FunctionNode { name: string, start: { line: number, column: number, index: number }, end: { line: number, column: number, index: number } } export function getFunctionNode( code: string, // 待解析代码字符串 index: number // 光标位置 ): FunctionNode | undefined { // 解析 const ast = parse(code); console.log(ast); // 遍历 AST let functionNode; traverse(ast, { // 遇到 FunctionDeclaration 结点时,触发回调 FunctionDeclaration(path) { console.log(path.node); // 判断光标位置:start & end 相当于将代码连成一行时的位偏 let node = path.node; if (index >= node?.start! && index <= node?.end!) { // 提取部分需要的信息 functionNode = { name: node?.id?.name, start: node?.loc?.start, end: node?.loc?.end } } }, // 支持箭头函数: 连着变量一起删掉 ArrowFunctionExperssion(path) { function getName() { // 变量名 return Object.keys(path.parentPath.getBindingIndentifiers())[0]; } const varDeclarePath = path.parentPath.parentPath; // 指向变量 const xxx = () => {} if (varDeclarePath?.isVariableDeclaration()) { let node = varDeclarePath.node; if (index >= node?.start! && index <= node?.end!) { functionNode = { name: getName(), start: node?.loc?.start, end: node?.loc?.end } } } }, // 支持函数表达式 ... }) return functionNode; // 绑定了 babel 结构 }
2.3 自动化单元测试
每次热更新都需要重新 F5 还是太痛苦面具了
-
安装测试框架
vitest -
修改
package.json中的测试命令 -
同时,修改
tsconfig.json将其编译范围限制在/src路径下 -
在
src/test下新建用于检查 AST 的单元测试只涉及业务逻辑,不涉及 UI 逻辑
main.spec.tsimport {test, expect} from "vitest"; import {getFunctionNode} from "@/src/main"; // 待测函数 test("init". () => { // expect(true).toBe(true); // 通过断言测试 vitest 是否正确引入 // 待解析文本 code = ` function getName() { return 'name'; }; // 箭头函数 const getNameA = () => { return 'name_a'; }; // 函数表达式 const getNameB = function () { return 'name_b'; }; `; // 实际测试函数 const node = getFunctionNode(code); // 检查 code 返回是否符合 babel 格式 expect(node).toEqual({ name: "getName", start: { line: 2, column: 6, index: 7, }, end: { line: 4, column: 7, index: 51, }, }) })
2.4 集成到 UI 逻辑
这边涉及到 UI 逻辑,必须用 VSCode 本体跑了
src/extension.ts
import * as vscode from "vscode";
import {getFunctionNode} from "./main";
export function activate(context: vscode.ExtensionContext) {
vscode.command.registerCommand(
"[plugin].helloWorld", () => {
// 如果没有打开的窗口,就可以直接润了
const editor = vscode.window.activeTextEditor;
if (!editor) { return; }
// 1 获取删除范围
const code = editor.document.getText();
const index = editor.document.offsetAt(
editor.selection.active // 这边返回的是 (row, col),需要转化成整体位偏
);
const functionNode = getFunctionNode(code, index);
// 光标不在任何函数内,也可以跑路了
if (!functionNode) { return; }
// 2 删除(指定位置处的)函数
editor.edit((editBuilder) => {
editBuilder.delete(new vscode.Range(
new vscode.Position(functionNode?.start.line, functionNode?.start.column),
new vscode.Position(functionNode?.end.line, functionNode?.end.column)
))
})
}
)
}
3 插件发布
# 1 安装依赖
npm install -g vsce
# 2 登录
vsce login [projectName] # => 需要在 Azure DevOps 中注册账号
Do you want to overwrite its PAT? [y/N] y # 重写
Personal Access Token for publisher 'USER': # 输入在 Azure DevOps 中生成的 Token
# 至少需要 Marketplace - Manage 权限
# 3 打包(Optional):生成 .vsix 文件、可以手动发布在 VSCode MarketPlace
vsce package # 用 npm 管理可能会报错
## 可以通过 --yarn 指定其他包管理器,但需要生成 .lock
rm -rf node_modules/
yarn # 生成 lock 文件
# 4 直接发布
vsce publish # 自动打包完 + 上传