VSCode 插件开发入门到实践

VSCode 有非常强大的功能,原因在于它的很多功能都是基于插件来实现的,可以使用插件来丰富和扩展它的功能。
但是我们的需求总是复杂多变的,总有一些场景是现有的插件无法满足,这时候就需要我们借助 VSCode 的开放接口,手动实现我们需要的功能。

这篇文章主要带大家从一个简单插件开发入手来介绍插件开发的一些具体事项,最后根据实际需要实现一个插件。

VSCode 插件能做什么

  • 自定义命令、快捷键
  • 自定义上下文菜单操作,如:平时我们右键的菜单栏
  • 自定义跳转、自动补全、悬浮提示
  • 自定义主题
  • 自定义代码片段
  • 新增语言支持
  • 语法检查
  • 语法高亮
  • 代码格式化
  • ……

新手教程——如何编写一个插进

VSCode 官方提供了脚手架 yomen,用它来生成项目:

  npm install -g yo generator-code

初始化一个插件项目,这个脚手架会生成一个可以立马开发的项目:

  yo code

选择生成插件(JS 或 TS),初始化过程中需要我们做一些偏好设置,按照需求选择:

1280X1280.PNG

打开生成的项目,其中最重要的两个文件是插件的入口 extension.js 和配置文件 package.json

image.png

注册命令

extension.js 文件主要会导出两个方法:activatedeactivate

  • activate,插件被激活时执行的方法;
  • deactivate,插件被销毁时调用的方法;
    在插件被激活时(activate方法内),使用 vscode.commands.registerCommand来注册命令,第一个参数是命令ID,第二个参数是执行命令后的回调函数。
    例如,下面这段代码:
// 入口文件 extension.js
const vscode = require('vscode');

function activate(context) {
  let disposable = vscode.commands.registerCommand('helloWorld', function () {
    vscode.window.showInformationMessage('Hello World from !');
  });

  context.subscriptions.push(disposable);
}

function deactivate() { }

module.exports = {
  activate,
  deactivate
}

注册了一个命令 helloWorld。当用户执行 helloWorld 命令,就会调用对应的处理函数。你也可以通过 vscode.commands.executeCommandAPI 去调用它。

创建面向用户的命令

vscode.commands.registerCommand 仅仅是将命令id绑定到了处理函数上,如果想让用户从命令面板中搜索到这个命令,还需要在 package.json 中配置对应的命令配置项:

{
    "contributes": {
        "commands": [
            {
                "command": "helloWorld",
                "title": "Hello World"
            }
        ]
    }
}

上面的配置,给命令 helloWorld 设置了一个 title Hello World,通过设置title控制命令在UI中的显示。在搜索命令时,显示的是标题 Hello World

调试

在开发插件的时候,按下F5就会弹出一个新的 VSCode 窗口。
新窗口标题会注明扩展开发宿主,表示是插件调试,新窗口已经成功加载了我们的插件:


image.png

按下Ctrl+Shift+P搜索命令,输入刚刚注册的命令Hello World,选择执行命令,就会触发命令的回调函数(右下角弹出Hello World from !提示)

image.png

最简单的插件就实现了!


接下来,了解一下插件的一些配置字段。

插件激活

插件并不是安装启用之后就会激活。像这样,这个插件就处于安装启用但是尚未激活状态:

image.png

插件具体什么时候激活是在 package.json 中的 activationEvents 声明。所有插件都是按需加载的,每个插件都应该声明具体的加载时机。
以下是几种常用的声明方式:

  • * ,VSCode 启动时,插件开始触发:
{
    "activationEvents": ['*']
}
image.png
  • onStartupFinished,VSCode 启动一段时间后才会激活插件。类似于 * 类激活事件,但它不会减慢 VSCode 启动,该事件在所有 * 类插件激活完成后触发:
{
    "activationEvents": [
        "onStartupFinished"
    ]
}
image.png
  • onCommand:Hello World , 当执行命令 Hello World 时激活插件:
"activationEvents": [
  "onCommand:Hello World"
]
  • onLanguage:languageId, 编辑区包含特定语言文件时激活插件。
image.png

只要编辑区(上图红色区域)打开 json、markdown 或者 ts文件时,激活该插件:

"activationEvents": [
  "onLanguage:json",
  "onLanguage:markdown",
  "onLanguage:typescript"
]

这几个激活事件可以覆盖大部分情况,还有其他一些激活事件,没有一一列举,感兴趣的可以阅读一下官方文档

内置命令

在开发插件的时候,经常需要通过 VSCode 提供的 API 来进行一些UI操作,例如打开文档、修改并保存文档内容、关闭文档、刷新文件夹等。
使用 vscode.commands.executeCommandAPI 可以调用一个命令,你可以通过它将 VSCode 的内置函数构建在你的插件中。
例如,下面的代码用来关闭 VSCode 当前打开的文档:

  vscode.commands.executeCommand('workbench.action.closeActiveEditor');

关闭所有打开的文档:

  vscode.commands.executeCommand('workbench.action.closeAllEditors');

效果就和这个一样


image.png

刷新当前工作区文件夹:

vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer');

更多命令详见:

有些功能可以通过查看 VSCode 的官方文档找到对应的 API 接口和相应的对象,但很多功能是没有写到文档上的。

最简单的查找命令的方式,直接看 VSCode 的键盘快捷方式


image.png

image.png

contributes

contributes 是 VSCode 插件开发中的一个重要配置属性,用来定义插件的命令、快捷键、语言支持、主题、配置等。

下面列举一些常用的属性及其作用:

命令配置

contributes.commands,定义插件提供的命令。
包括命令的标识符(command)、命令的标题(title)。
一个命令注册完后,需要在 commands中配置,用户才能搜索到该命令。

{
    "contributes": {
        "commands": [
            {
                "command": "helloWorld",
                "title": "Hello World"
            }
        ]
    }
}

配置右键菜单

右键菜单有很多配置,这里只列举比较常见的。

编辑器面板菜单

editor/context 配置命令什么时候出现在编辑文件右键菜单中:

{
    "contributes": {
        "menus": {
             "editor/context": [
                {
                  "command": "helloWorld", 
                  "group": "navigation",
                  "when": "true"
                }
             ]
        }
    }
}
  • command 定义菜单被点击后要执行什么操作;
  • group 定义菜单分组;
  • when 控制菜单合适出现;

when语句语法有很多,这里列举几个常用的:

  • resourceLangId == javascript:当编辑的文件是js文件时;
  • resourceFilename == test.js:当当前打开文件名是test.js时;
  • isLinux、isMac、isWindows:判断当前操作系统;
  • editorFocus:编辑器具有焦点时;
  • editorHasSelection:编辑器中有文本被选中时;
  • view == someViewId:当当前视图ID等于someViewId时;
  • ……
    多个条件可以通过与或非进行组合,例如:editorFocus && isWindows && resourceLangId == javascript。

资源管理器面板菜单

explorer/context 配置命令什么时候出现在资源管理区右键菜单中:

{
    "contributes": {
        "menus": {
             "explorer/context": [
                {
                  "command": "helloWorld",
                  "group": "navigation",
                  "when": "true"
                }
             ]
        }
    }
}

右键打开资源管理区菜单,多了一个 Hello World命令,如下:

image.png

控制命令何时可见

有些命令是场景相关的,比如在特定的语言的编辑器中,或者只有用户设置了某些选项时才展示。
menus.commandPalette 限制命令出现在命令面板的时机,你需要配置命令ID和一条when语句:

{
    "contributes": {
        "menus": {
            "commandPalette": [
                {
                    "command": "helloWorld",
                    "when": "editorLangId == markdown"
                }
            ]
        }
    }
}

现在只有在编辑器打开 Markdown 文件,才能搜索到命令 helloWorld

快捷键

快捷键设置的写法比较简单,contributes.keybindings :

"contributes": {
   "keybindings": [
       {
            // 指定快捷键执行的操作
            "command": "helloWorld",
            // windows下快捷键
            "key": "ctrl+shift+l",
            // mac下快捷键
            "mac": "cmd+shift+l",
        }
   ]
 }

这个快捷键最终会在命令后面提示快捷键


image.png

代码片段

snippets,输入一个前缀,有许多提示,选择回车后生成代码。
提供了一些快捷键来帮助你更快速地使用代码片段。

 // 配置
 "contributes": {
     "snippets": [
          {
            "language": "typescript",
            "path": "./snippets/index.json"
          }
       ]
}

这里 language 设置了 snippets 作用于哪种语言(打开文件时判断),path 设置了代码片段地址:

// snippets/index.json:
{
  "Snippets测试": {
    "prefix": "hello",
    "body": [
      "console.log($1)"
    ],
    "description": "描述"
  }
}

"Snippets测试" ,自定义snippet的名称;
"prefix", 输入什么可以出现snippets的提示;
"body", 按回车后出现的一大段代码,是一个数组,数组里面是字符串,每个字符串代表一行代码,{1}表示第一个光标的位置,同样,{2}表示第二个光标的位置;
"description", 对于这个snippet的描述,当我们选中这个snipets提示时,描述会出现在后面。

配置

configuration,通过这个配置项我们可以设置属性,这个属性可以在 vscode 的 settings.json 中设置,然后在插件工程中可以读取用户设置的这个值,进行相应的逻辑。

 "contributes": {
     "configuration": {
         // 显示在配置页左侧
          "title": "测试configuration",
          "properties": {
          // 全局唯一的配置ID
            "test_boolean": {
              "type": "boolean",
              "default": false,
              "description": "测试布尔"
            },
            // 全局唯一的配置ID
            "test_number": {
              "type": "number",
              "default": 14,
              "description": "测试number"
            },
            // 全局唯一的配置ID
            "test_string": {
              "type": "string",
              "default": "1",
              "description": "测试字符串"
            }
          }
    }
 }

打开设置页,搜索配置如下:


image.png

读取用户配置的值:

function activate(context) {
   let disposable = vscode.commands.registerCommand('helloWorld', function () {
   // 获取用户设置
    const configurate = vscode.workspace.getConfiguration().get('test_string')
    vscode.window.showInformationMessage(configurate);
  });
 }

执行命令 helloWorld:

image.png

其他

  • languages:定义插件对特定语言的支持,可以为特定的文件类型提供语法高亮、代码片段、自动完成等功能。
  • grammars: 语法
  • debuggers:调试
  • breakpoints:断点
  • themes:主题
  • jsonValidation:自定义JSON校验
  • views:左侧侧边栏视图
  • viewsContainers:自定义activitybar
  • ……

实践——自定义插件

针对业务,可以开发这样一个插件:

  • 右键点击文件夹,在菜单中增加两个选项:
    1. "生成ReactLynx页面"
    2. "生成ReactLynx组件"
      具体功能:
  • 根据"文件夹名称",写入预先定义好的代码,类名首字母大写
  • 把代码写入 "文件夹/index.tsx" 文件中
  • 刷新文件目录,打开文件

新建项目

image.png

右键生成组件/页面

注册命令
在插件里面注册两个命令 reactLynxPage 和 reactLynxComponent, 根据命令执行 generateCode 函数:

export function activate(context: vscode.ExtensionContext) {
  let pageDisposable = vscode.commands.registerCommand(pageCommand, (url: vscode.Uri) => generateCode(url, pageCommand));

  let componentDisposable = vscode.commands.registerCommand(componentCommand, (url: vscode.Uri) => generateCode(url, componentCommand));
  
  context.subscriptions.push(pageDisposable);
  context.subscriptions.push(componentDisposable);
}

在 generateCode 函数中,包含两个参数 url 和 commandId:

  • 如果是通过右键点击文件执行命令,参数 url 才有文件夹地址,如果不存在 url?.fsPath 则为不规范的命令执行;
  • 通过 commondId 判断是生成组件还是生成页面;
function generateCode(url: vscode.Uri, commandId: string) {
  // 获取右键点击的文件夹路径, 绝对路径
  const folderPath = url?.fsPath;

  if (!folderPath) {
    return;
  }
  
 // …… 省略一些判断逻辑

 // 获取文件夹名称, 首字母大写, 改为驼峰式命名
 
  // 生成代码片段内容
  const codeSnippet = commandId === pageCommand ? generatePageSnippet(name) : generateComponentSnippet(name);
  
  // ......
 }

folderPath 是文件夹的绝对路径,那么该文件夹下组件或页面的地址就是 ${folderPath}/index.tsx,样式文件地址 ${folderPath}/index.less

通过 fs.existsSync 判断文件是否存在,文件已存在则不生成(防止误操作),如果不存在则生成文件写入代码。

    // 待生成文件地址
  const tsxFileName = `${folderPath}/index.tsx`;
  const styleFileName = `${folderPath}/index.less`;
  
   // 将代码片段写入文件
  if (fs.existsSync(tsxFileName)) {
      vscode.window.showInformationMessage('该目录下已经存在index.tsx');
  }
  else {
      fs.writeFileSync(tsxFileName, codeSnippet);
   }

  if (fs.existsSync(styleFileName)) {
    vscode.window.showInformationMessage('该目录下已经存在index.less');
   }
  else {fs.writeFileSync(styleFileName, `.container { 
  }`);}
  
  }

通过 vscode 提供的内置命令刷新文件目录:

vscode.commands.executeCommand('workbench.files.action.refreshFilesExplorer');

最后,在编辑区自动打开生成的 index.tsx :

  vscode.workspace.openTextDocument(tsxFileName).then(document => {
    vscode.window.showTextDocument(document, {
      selection: new vscode.Range(0, 0, 0, 0)
    });
  }, () => {
     vscode.window.showErrorMessage(`打开${tsxFileName}失败!`);
  });

配置命令
注册完命令之后,在 package.json 配置 contributes.commands,这样就能使用 reactLynxPage 和 reactLynxComponent 这两个命令了。

"main": "./out/extension.js",
"contributes": {
    "commands": [
      {
        "command": "reactLynxPage",
        "title": "生成ReactLynx页面"
      },
      {
        "command": "reactLynxComponent",
        "title": "生成ReactLynx组件"
      }
    ]
}

继续配置 contributes.menus 。当右键点击文件夹("when"等于"explorerResourceIsFolder"),在弹出的菜单中增加 "生成ReactLynx页面" 和 "生成ReactLynx组件" 两个选项。

 "contributes": {
    // .......
    "menus": {
      "explorer/context": [
        {
          "command": "reactLynxPage",
          "group": "1_modification",
          "when": "explorerResourceIsFolder"
        },
        {
          "command": "reactLynxComponent",
          "group": "1_modification",
          "when": "explorerResourceIsFolder"
        }
      ]
    }
  },

group是分组,下图是 vscode 官方菜单分组:

image.png

配置 commandPalette 隐藏命令,无法在命令面板中搜索到 ,但是右键菜单的命令能照常执行。

 "contributes": {
    // .......
    "menus": {
      "explorer/context": [
          // .......
      ],
       "commandPalette": [
            {
              "command": "reactLynxPage",
              "when": "false"
            },
            {
              "command": "reactLynxComponent",
              "when": "false"
            }
      ],
    }
  },

打包

安装对应的模块 vsce

  npm i vsce -g

利用 vsce 打包,生成对应的 vsix 插件文件

  vsce package
image.png

如果不想发到插件市场,直接右键安装到插:


image.png

发布

注册开发者账号,发布到官网应用市场……

总结

以上就是一个简单的入门级实战教程,带大家了解开发一个 VSCode 插件的基本思路。插件开发,说简单也简单,说复杂也不简单,后续大家如果遇到更复杂更定制化的需求,基本上需要查阅 官方文档 深入学习。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容