写一个VS Code插件——让Rust项目像NPM一样可以在侧边栏中运行命令行脚本

724 阅读5分钟

在前端项目中,VS Code内置的插件会自动识别package.json里的scripts字段,将对应的命令行脚本显示在侧边栏中,方便我们点击运行。

受此启发,我开发了一款简单的VS Code的插件,以便在Rust项目中实现类似的功能。

项目地址:taiyuuki/vscode-cargo-scripts

插件地址:Cargo Scripts - Visual Studio Marketplace

Cargo Scripts

插件名称就Cargo Scripts,下载插件后,在Cargo.toml中添加[package.metadata.scripts][workspace.metadata.scripts],然后在侧边栏中就会显示一栏CARGO SCRIPTS,每条命令后都有一个运行图标,点击即可运行对应的命令。

cargo_scripts_01.jpg

插件的一些开发细节

对VS Code插件开发感兴趣的可以继续读下去,我以前也写过一篇文章:

我写了个提示颜色代码的VS Code插件——中国传统色 记录一下我自学开发VS Code插件的过程 - 掘金 (juejin.cn)

这是我曾经开发的另一款插件:Chinese Colors,它可以通过代码提示,自动补全预设的颜色代码。

cargo_scripts_02.gif

项目创建

这次我使用pnpm、tsup构建项目,初始的项目结构如下:

vscode-cargo-scripts    项目文件夹
 ├─src                  源码目录	
 │   └─index.ts         入口文件  
 ├─eslint.config.mjs    eslint配置文件
 ├─LICENSE.md           许可文件
 ├─package.json         包管理文件
 ├─README.md            readme
 ├─tsconfig.json     	TS配置文件
 └─tsup.config.ts       tsup配置文件

需要安装这些依赖:

  • @types/node - Node API类型提示
  • @types/vscode - VS Code插件API的类型提示
  • @vscode/test-electron - 插件调试依赖
  • tsup - 打包、构建

这里要注意@types/vscode的版本,必须与在package.json中配置VS Code的版本一致,也就是下面engines -> vscode 这一项,它是插件需要兼容的VS Code版本。

package.json配置

{
    "name": "vscode-cargo-scripts",
    "displayName": "Cargo Scripts",
    "publisher": "taiyuuki",
    "version": "0.3.1",
    "description": "Run scripts from Cargo.toml",
    "main": "./dist/index.js",
    "engines": {
        "vscode": "^1.74.0"
    },
    "activationEvents": [
        "workspaceContains:**/Cargo.toml",
        "onLanguage:rust"
    ],
    "contributes": {},
    "scripts": {
        "lint": "eslint --fix",
        "dev": "tsup --watch",
        "build": "tsup"
    },
    "keywords": [
        "rust",
        "cargo",
        "scripts"
    ],
    "author": "taiyuuki <taiyuuki@qq.com>",
    "license": "MIT",
    "files": [
        "dist"
    ],
    "devDependencies": {
        "@taiyuuki/eslint-config": "^1.4.14",
        "@types/node": "^18.11.18",
        "@types/vscode": "^1.74.0",
        "@vscode/test-electron": "^2.2.2",
        "eslint": "^9.6.0",
        "tsup": "^8.1.0"
    }
}

各个字段的作用:

名称必要类型说明
namestring插件名称,必须为小写且不能有空格。
versionstring插件版本
publisherstring发布者
enginesobject一个至少包含vscode键值对的对象,该键表示的是本插件可兼容的VS Code的版本,其值不能为*。比如 ^0.10.5 表示插件兼容VS Code的最低版本是0.10.5
licensestring授权。如果有授权文档LICENSE.md,可以把license值设为"SEE LICENSE IN LICENSE.md"
displayNamestring插件市场中显示的名字。
descriptionstring描述,说明本插件是什么以及做什么。
categoriesstring[]插件类型:[Languages, Snippets, Linters, Themes, Debuggers, Other]
keywordsarray一组 关键字 或者 标记,方便在插件市场中查找。
galleryBannerobject插件市场中横幅的样式。
previewboolean在市场中把本插件标记为预览版本。
mainstring插件的入口文件。
contributesobject一个描述插件 贡献点 的对象。
activationEventsarray插件的激活事件。
dependenciesobject生产环境Node.js依赖项。
devDependenciesobject开发环境Node.js依赖项。
extensionDependenciesarray一组本插件所需的其他插件的ID值。格式 ${publisher}.${name}。比如:vscode.csharp
scriptsobject和 npm的 scripts一样,但还有一些额外VS Code特定字段。
iconstring一个128x128像素图标的路径。

打包设置

tsup.config.ts

import { defineConfig } from 'tsup'

export default defineConfig({
    entry: ['src/index.ts'],
    target: 'esnext',
    splitting: false,
    sourcemap: true,
    clean: true,
    external: [
        'vscode', // 打包时排除vscode内置库
    ],
    noExternal: [], // 如果第三方库需要打包进插件的,最好配置在这一项
    format: ['cjs'],
})

打包命令:pnpm buildnpx tsup

Hello World

一个简单的插件示例

src/index.ts

import * as vscode from 'vscode'

// activate方法会在插件被激活时调用
export function activate(context: vscode.ExtensionContext) {

    // 注册命令,第一个参数是命令名称,第二参数是命令的回调函数
    const disposable = vscode.commands.registerCommand('vscode-demo.helloWorld', () => {

        // 弹出消息提示
        vscode.window.showInformationMessage('Hello World from vscode-demo!')
    })

    // 添加到插件上下文
    context.subscriptions.push(disposable)
}

// deactivate方法会在插件失活时调用
export function deactivate(context: vscode.ExtensionContext) {
    context.subscriptions.forEach(d => d.dispose())
}

侧边栏的树状菜单

要在侧边栏创建一个树状菜单,首先要在package.json中配置:

"activationEvents": [
    "workspaceContains:**/Cargo.toml",
    "onLanguage:rust"
],
"contributes": {
    "commands": [
        {
            "command": "cargoScripts.run",
            "title": "Run",
            "icon": "$(debug-start)"
        },
        {
            "command": "cargoScripts.refresh",
            "title": "Refresh",
            "icon": "$(search-refresh)"
        }
    ],
    "views": {
        "explorer": [
            {
                "id": "cargoScripts",
                "name": "Cargo Scripts",
                "when": "showCargoScript"
            }
        ]
    },
    "menus": {
        "view/title": [
            {
                "command": "cargoScripts.refresh",
                "group": "navigation"
            }
        ],
        "view/item/context": [
            {
                "command": "cargoScripts.run",
                "group": "inline",
                "when": "viewItem == script_item",
                "icon": "$(run)"
            }
        ]
    },
    "icons": {
        "custom-icon": {
            "description": "Custom Icon",
            "default": {
                "fontPath": "res/iconfont.woff2",
                "fontCharacter": "\\E694"
            }
        }
    }
}

activationEvents是插件的激活条件,当项目中有Cargo.toml这个文件或者处于Rust语言中激活本插件。

然后是contributes——

  1. commands用于注册命令以及对应的图标
"commands": [
    {
        "command": "cargoScripts.run",
        "title": "Run",
        "icon": "$(debug-start)"
    },
    {
        "command": "cargoScripts.refresh",
        "title": "Refresh",
        "icon": "$(search-refresh)"
    }
]
  • command 是命令的id,命令对应的操作需要在代码中进行注册。 在上面hello world的例子中,vscode.commands.registerCommand注册了一个id为vscode-demo.helloWorld的命令。
  • title 是命令的名称,当鼠标悬停时会显示该名称。
  • icon 是该命令对应的图标,格式为$(name),name就是图标名。

图标可以是VS Code内置的图标,我们可以在官方文档中查询内置图标对应的名称:Product Icon Reference | Visual Studio Code Extension API

除此之外还可以使用自定义的字体图标集。

  1. icons

要想使用自定义的图标,需要在icons中注册图标字体。

"icons": {
    "custom-icon": { // 图标名
        "description": "Custom Icon", // 描述
        "default": {
            "fontPath": "fonts/iconfont.woff2", // 字体文件目录
            "fontCharacter": "\\E694" // 图标在字体中对应的字符编码
        }
    }
}

这样我们就可以使用$(custom-icon)将命令设置为该自定义的图标了。

  1. viewsmenu

viewsmenu设置树状菜单栏,这个地方很难用文字说明,详情如下图所示:

cargo_scripts_02.jpg

至于菜单的内容如何设置,因为过程比较复杂,这里我推荐参考官方提供的示例:vscode-extension-samples/tree-view-sample,以及文档:Tree View API | Visual Studio Code Extension API

这里我只讲解一下when这个字段,它可以根据条件控制菜单栏/图标/按钮的显示或隐藏。

第一种情况是,我们可以给上下文注册一个布尔类型的变量,通过变量的值控制显示状态。

export function activate(context: vscode.ExtensionContext) {
    vscode.commands.executeCommand('setContext', 'showCargoScript', true) // 显示
}

export function deactivate(context: vscode.ExtensionContext) {
    vscode.commands.executeCommand('setContext', 'showCargoScript', false) // 隐藏
}

"when": "showCargoScript"即可控制该菜单的显示状态。

第二种情况是,在树节点构造器中给它一个contextValue属性,例如我给其赋值为script_item,然后在when中使用viewItem == script_item来判断是否显示该图标。

export class ScriptTreeItem extends vscode.TreeItem {

    constructor(label: string, cmd: string, cwd: string) {
        super(label, vscode.TreeItemCollapsibleState.None)
        // 设置命令对应的id和参数
        this.command = {
            title: 'Open',
            command: 'cargoScripts.open',
            arguments: [label, cmd, cwd],
        }
        // 控制显示状态
        this.contextValue = 'script_item'
    }

    iconPath = new vscode.ThemeIcon('wrench')
}