如何开发一款vscode插件

2,432 阅读6分钟

Visual Studio Code

vscode 在近几年颇受开发人员喜爱,这得益于它的轻量性和非常的插件市场。你可以在插件市场找到大量功能强大的插件帮助提高编码效率。但是(总有那么个但是🙄)在实际的开发中,还是会有一些特殊的定制需求找不到特别合适的插件,此时我们就可以撸起袖子,自己造一个轮子。

插件功能

vscode 赋予了插件 非常多的能力,让它们几乎可以在方方面面对 vscode 进行武装升级,插件的主要能力包括:

  • 提供新主题(包括编辑器配色,文件图标等等)
  • 支持新编程语言的语法(提供高亮,代码补全,错误检查等等功能)
  • 提供程序调试功能
  • 自定义命令,快捷键,菜单
  • 自定义工作区 Webview

开发怎样的插件?

在笔者写单元测试时,常常会遇到以下问题:

  1. 需要在源文件和对应的单测文件来回切换,而 vscode 并不具有 webstorm 那样的源文件测试文件快速切换的功能。使得整个切换体验不佳
  2. 为源文件创建单测文件时,如果文原则是将源文件文件夹和测试文件文件夹分开时,往往需要递归创建测试文件。比如src/module/child/a.ts,需要层层创建tests/module/child/a.test.ts文件,这尤为麻烦😫

基于这些痛点,笔者开发一款 vscode 插件,用于在源文件测试文件之间快速切换,以及快速创建单测文件。插件效果:

预览效果

插件功能:

  • 查找源文件对应的测试文件。如果只找到一个可能的文件,则直接切换;如果找到多个,则显示下拉选择框供用户选择
  • 快速创建单测文件。支持两种模式,将单测文件和源文件放在一起,或者将所有单测文件单独放在特定文件夹中,并按源文件目录结构组织
  • 提供丰富的配置能力,用户可自定义单测文件后缀规范

你可以点击 此链接,或者在 vscode 插件市场搜索** Find Test File **体验:

插件

环境准备

首先安装 yeoman 脚手架工具,以及 vscode 官方提供的脚手架工具:

npm install -g yo generator-code

接下来执行以下命令交互式创建插件项目:

yo code

创建项目

项目结构

现在,插件项目已经创建完成,项目目录结构如下:

.
├── node_modules
├── CHANGELOG.md
├── README.md
├── package.json
├── src
│   ├── extension.ts
│   └── test
│       ├── runTest.ts
│       └── suite
│           ├── extension.test.ts
│           └── index.ts
├── tsconfig.json
├── vsc-extension-quickstart.md
├──.vscode
	  ├── extensions.json
		├── launch.json
		├── settings.json
		└── tasks.json
└── yarn.lock

其中:

  • .vscode文件夹存放程序运行和调试相关命令
  • vsc-extension-quickstart.md文档介绍了插件的入门知识和运行方式,是项目创建后需要首先关注的点
  • src文件夹中存放插件源代码,需要重点关注这一块(src/test存放插件 e2e 测试代码,暂时不用关注)
  • package.json除了包含常规 node 项目配置属性外,还包含插件配置属性,也需要重点关注

默认情况下,项目已经配置好运行调试参数,按下F5即可运行插件(其实就是运行.vscode/launch.json中的Run Extension命令):

运行

我们先从package.json入手,插件配置相关代码片段如下:

{
  "name": "test",
	"displayName": "test",
	"engines": {
		"vscode": "^1.57.0"
	},
	"categories": [
		"Other"
	],
	"activationEvents": [
        "onCommand:test.helloWorld"
	],
	"main": "./out/extension.js",
	"contributes": {
		"commands": [
			{
				"command": "test.helloWorld",
				"title": "Hello World"
			}
		]
	}
}
  • namedisplayName不必多提,是插件的名字和显示名字(即在插件市场的名字)
  • enginesvscode版本是指该插件所兼容的 vscode 版本
  • categories表示插件的分类,但是它和keywords有所不同,只能从选择 官方指定的列表 中选择
  • main表示插件程序入口
  • activationEvents表示何时 激活插件
  • contributes表示如何 定制插件功能

再来看看src/extension.ts中的内容:

import * as vscode from "vscode";

export function activate(context: vscode.ExtensionContext) {
  console.log('Congratulations, your extension "test" is now active!');
  // 自定义命令
  let disposable = vscode.commands.registerCommand("test.helloWorld", () => {
    // 触发弹出框
    vscode.window.showInformationMessage("Hello World from test!");
  });
	// 将命令注册到执行上下文中
  context.subscriptions.push(disposable);
}

export function deactivate() {}

extension.ts暴露两个方法:

  1. active:在插件激活时运行,通常在其中注册自定义命令
  2. deactive:在插件禁用时运行

这两部分就是开发插件的核心,我们将整个流程拉通一下:

激活插件流程

撸起袖子开始干

插件配置

首先在package.json中声明插件的命令,以及插件激活的条件:

{
 
  "activationEvents": [
    "onCommand:find-test-file.jumpToTest",
    "onCommand:find-test-file.createTestFile"
  ],
  "contributes": {
    "commands": [
      {
        "command": "find-test-file.jumpToTest",
        "title": "Jump To Source/Test File",
        "category": "Find Test File",
        "icon": "$(preferences-open-settings)",
        "enablement": "resourceExtname =~ /.[jt]sx?/"
      },
      {
        "command": "find-test-file.createTestFile",
        "title": "Create Test File For Current",
        "category": "Find Test File",
        "icon": "$(file-add)",
        "enablement": "resourceExtname =~ /.[jt]sx?/"
      }
    ]
  }
}

commands配置项中,添加jumpToTestcreateTestFile两个命令,除了常规commandtitle配置外,还多出了几项配置(更多commands配置请查阅 官方文档):

  • category:用于在命令面板中将命令分组 命令分组
  • icon:命令所对应图标(后续menus配置才会用到),格式为$(name),vscode 自带 一套图标
  • enablement:表明何时在命令面板中显示命令,上述配置表明只有在当前打开的文件是tstsx),jsjsx)时,才能在命令面板中显示

activationEvents保持是在执行对应命令时再激活插件(更多激活条件请查阅 官方文档)。图省事的话,也可以直接设置为*,即在 vscode 启动后就激活,毕竟能用就行,是吧。

又不是不能用

当然,保持在必要时候才激活插件是一个更好的实践。

通过命令面板选择命令执行总是不够方便,快捷键才是王道。可以通过keybindings属性增加快捷键配置(更多keybindings配置请查阅 官方文档):

{
  "contributes": {
    "keybindings": [
      {
        "command": "find-test-file.jumpToTest",
        "key": "ctrl+shift+t",
        "mac": "cmd+shift+t",
        "when": "resourceExtname =~ /.[jt]sx?/"
      },
      {
        "command": "find-test-file.createTestFile",
        "key": "ctrl+alt+t",
        "mac": "cmd+alt+t",
        "when": "resourceExtname =~ /.[jt]sx?/"
      }
    ]
  }
}
  • Mac 和 Window 的快捷键有所不同,Mac 中的cmd键等同于 Window 的ctrl,需要分别配置
  • when表示快捷键什么时候启用,和commandenablement配置类似

既然有了快捷键,能不能有鼠标右键之类的快捷方式呢?能!通过menus属性增加配置(更多menus配置请查阅 官方文档):

{
  "contributes": {
    "menus": {
      "editor/context": [
        {
          "command": "find-test-file.jumpToTest",
          "group": "1_find-test-file",
          "when": "resourceExtname =~ /.[jt]sx?/"
        },
        {
          "command": "find-test-file.createTestFile",
          "group": "1_find-test-file",
          "when": "resourceExtname =~ /.[jt]sx?/"
        }
      ],
      "editor/title": [
        {
          "command": "find-test-file.jumpToTest",
          "group": "navigation",
          "when": "resourceExtname =~ /.[jt]sx?/"
        },
        {
          "command": "find-test-file.createTestFile",
          "group": "navigation",
          "when": "resourceExtname =~ /.[jt]sx?/"
        }
      ]
    }
  }
}

文件头部 右键菜单

前面command中定义的icon属性,就是在editor/title中使用。

插件还需要提供用户配置的能力。比如:不同项目对测试文件的后缀命名是不同的,有些是a.test.ts,有些是a.spect.ts。这部分能力由configuration配置提供:

{
  "contributes": {
    "configuration": {
      "title": "Find Test File",
      "properties": {
        "findTestFile.basic.testSuffix": {
          "type": "string",
          "default": "\\.(spec|test)",
          "markdownDescription": "xxxxxx"
        },
        "findTestFile.basic.excludeFolder": {
          "type": "array",
          "default": [
            "node_modules"
          ],
          "items": {
            "type": "string"
          },
          "uniqueItems": true,
          "markdownDescription": "xxxxxx"
        },
        "findTestFile.createIfNotFind.enable": {
          "type": "boolean",
          "default": false,
          "markdownDescription": "xxxxxx"
        },
        "findTestFile.createIfNotFind.preferStructureMode": {
          "type": "string",
          "default": "separate",
          "enum": [
            "separate",
            "unite"
          ],
          "markdownEnumDescriptions": [
            "xxxxxx",
            "xxxxxx"
          ],
          "markdownDescription": "xxxxxx"
        },
        "findTestFile.createIfNotFind.preferTestDirectory": {
          "type": "object",
          "default": {
            "separate": "__tests__",
            "unite": "__tests__"
          },
          "properties": {
            "separate": {
              "type": "string"
            },
            "unite": {
              "type": "string"
            }
          },
          "required": [
            "separate",
            "unite"
          ],
          "additionalProperties": false,
          "markdownDescription": "xxxxxx"
        }
      }
    }
  }
}

在设置页面的显示结果如下:

配置

配置项可以是数组,字符串,布尔值,枚举,对象,具体配置参见 官方文档

插件代码

后续代码会省略涉及到本插件的业务逻辑,只关注其中对 vscode 插件的配置和 api 的使用,毕竟业务代码只是针对特定场景😁。如果你希望了解完整代码,请查阅 代码仓库

src/extension.ts主要功能:

  • 注册插件两个命令
  • 判断 vscode 当前打开的文件以及当前工作目录,只有在打开了特定的文件时,才需要执行业务逻辑
import vscode from "vscode";

function doPrepare() {
  // 获取当前编辑的文件对象,如果没有打开任何文件,则为空  
  const activeEditor = vscode.window.activeTextEditor;
  if (!activeEditor) {
    return;
  }
	// 获取编辑文件的文件名
  const activeFilePath = activeEditor.document.fileName;
	// 业务代码,不用关心
  const result = createValidFileReg().exec(getBasename(activeFilePath));

  if (!result) {
    // 弹出警告信息
    vscode.window.showWarningMessage(INVALID_FILE_WARNING_MESSAGE);
    return;
  }
	// 获取当前 vscode 所打开的工作区目录地址
  const workspaceFilePath = vscode.workspace.getWorkspaceFolder(
    activeEditor.document.uri
  )!.uri.fsPath;

  // 省略业务代码
}

export function activate(context: vscode.ExtensionContext) {
  // 注册 jumpToTest 命令
  const jumpToTestCommand = vscode.commands.registerCommand(
    "find-test-file.jumpToTest",
    () => {
     // 省略业务代码
    }
  );
  // 注册 createTestFile 命令
  const createTestFileCommand = vscode.commands.registerCommand(
    "find-test-file.createTestFile",
    () => {
      // 省略业务代码
    }
  );
  // 将命令注册到执行上下文中
  context.subscriptions.push(jumpToTestCommand, createTestFileCommand);
}

export function deactivate() {}

src/config.ts主要功能:

  • 获取插件配置信息
// src/config.js
import vscode from "vscode";

const getCfgByKey = <K1 extends keyof Config, K2 extends keyof Config[K1]>(
  primary: K1,
  key: K2
) => {
  // 结合配置,获取用户自定义的配置,获取所有以 findTestFile 开头的配置
  // 前面 configuration 配置中的 properties 各个属性名就是配置的路径, 如 findTestFile.basic.testSuffix
  const config = vscode.workspace.getConfiguration("findTestFile");

  // 获取具体某一项配置信息 如 basic.testSuffix
  const cfg = config.get<Config[K1][K2]>(`${primary}.${key}`)!;
  // 通过 inspect 方法获取默认配置,用于垫底
  const defaultCfg = config.inspect<Config[K1][K2]>(`${primary}.${key}`)!
    .defaultValue!;
  return [cfg, defaultCfg];
};

// 省略业务代码

src/jumpToFile.ts主要功能:

  • 如果找到一个可能的目标文件,直接切换到目标文件
  • 如果找到多个可能的目标文件,需要弹出选择框供用户选择
import vscode, { QuickPickItem } from "vscode";

export const openFile = async (filePath: string) => {
  // 打开对应地址的文件,注意这个操作是异步的
  const document = await vscode.workspace.openTextDocument(filePath);
  // 将当前窗口切换到该文件,注意这个操作是也异步的
  await vscode.window.showTextDocument(document);
};

export const jumpToPossibleFiles = async (
  current: string,
  relativeFiles: string[],
  isJumpToTestFile: boolean,
  createTestFileOption: CreateTestFileOption
) => {
  
  // 显示选择框,获取用户选择结果,注意这是异步操作
  const select = await vscode.window.showQuickPick(pickItems);
	// 取消选择时,返回空
  if (!select) {
    return;
  }
  // 省略业务代码
};

下拉选择

src/createTestFile.ts主要功能:

  • 如果用户对搜索结果不满意或者未找到测试文件,需要弹出输入框,帮助用户快速新建的测试文件
import vscode, { QuickPickItem } from "vscode";

export const createTestFile = async (
  { basename, ext, parent, root }: CreateTestFileOption,
  manualCreate: boolean = true
) => {
  // 显示输入框
  const userInputPath = await vscode.window.showInputBox({
    prompt: NEW_TEST_FILE_PROMPT,
    value: filePath,
    valueSelection: [filePath.length, filePath.length],
    // 验证输入内容是否合法,返回 null 表示验证成功
    validateInput(value) {
      return isValidFile(basename, ext, value, true)
        ? null
        : INVALID_TEST_FILE_WARNING_MESSAGE;
    },
  });
	// 取消输入,返回结果为空
  if (!userInputPath) {
    return;
  }
};

创建单测文件

更多 vscode api 使用参见 官方文档

发布插件

啪的一下就写完了代码,很快啊。接下来需要将插件打包发布。

打包插件需要安装vsce库:

npm i -g vsce

然后执行打包命令:

vsce package

打包完成后,会生成find-test-file-x.x.x.vsix文件。此时可以通过插件市场直接安装插件,验证效果:

安装插件

验证完功能的正确性后,就可以真正发布到插件市场:

  1. vscode 的插件市场基于微软的 Azure DevOps,插件的身份验证、托管和管理都是在这里。所以发布前,首先得注册 Azure Devops 账号,就像 npm 一样:

注册账号

  1. 注册完成后,创建 Personal Access Token:

创建 Token

创建 Token

复制这个创建好的 token,后续要用。

  1. 接下来还需要在插件市场中 新建一个 publisher 用来发布插件:

创建 Publisher

  1. 登录 vsce 账户信息,使用之前注册好的 publisher 以及 token:
vsce login publisher-name

别忘了在package.json中添加 publisher,icon,categories 等相关信息:

{
  "publisher": "xxxx",
  "icon": "xxxx/icon.png",
  "categories": [
    "Other"
  ],
}
  1. 登录完成后,就可以愉快的发布啦:
vsce publish

发布成功后,就可以在插件市场搜索到插件了(通常需要等几分钟),也可以在 网页管理端 查看插件的使用情况。

插件详情

总结

至此就是笔者在开发插件中所运用到的 vscode 相关知识,希望本文能对你有所帮助,也欢迎大家使用笔者开发的插件和提 issue