自从使用过 VSCode 后就再也离不开 VSCode,其轻量的代码编辑器与诸多插件让多数开发者爱不释手。同样我也不例外,一年前的我甚至还特意买本《Visual Studio Code 权威指南》的书籍,来更进一步了解与使用。
在购买这本书时就想写一个 vscode 插件(扩展),奈何当时事务繁忙加之不知做何功能,就迟迟未能动手。如今有时间了,就顺带体验下 vscode 扩展开发,并记录整个开发过程。
扩展地址:VSCode-extension
开源地址:kuizuo/vscode-extension (github.com)
Vscode 相关
关于 Vscode 及其插件就不过多介绍,相信这篇文章 VSCode 插件开发全攻略(一)概览 - 我是小茗同学 - 博客园能告诉你 Vscode 插件的作用。
工具准备
在开发前,建议关闭所有功能性扩展,以防止部分日志输出与调试效率。
vscode 插件脚手架
vscode 提供插件开发的脚手架 vscode-generator-code 来生成项目结构,选择要生成的类型
? ==========================================================================
We're constantly looking for ways to make yo better!
May we anonymously report usage statistics to improve the tool over time?
More info: https://github.com/yeoman/insight & http://yeoman.io
========================================================================== Yes
_-----_ ╭──────────────────────────╮
| | │ Welcome to the Visual │
|--(o)--| │ Studio Code Extension │
`---------´ │ generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What type of extension do you want to create? (Use arrow keys)
> New Extension (TypeScript)
New Extension (JavaScript)
New Color Theme
New Language Support
New Code Snippets
New Keymap
New Extension Pack
New Language Pack (Localization)
New Web Extension (TypeScript)
New Notebook Renderer (TypeScript)
根据指示一步步选择,这里省略勾选过程,最终生成的项目结果如下
运行 vscode 插件
既然创建好了工程,那必然是要运行的。由于我这里选择的 ts + webpack 进行开发(视情况勾选 webpack),所以是需要打包,同时脚手架已经生成好了对应.vscode 的设置。只需要按下 F5 即可开始调试,这时会打开一个新的 vscode 窗口,Ctrl+Shift+P
打开命令行,输入Hello World
,右下角弹出提示框Hello World from kuizuo-plugin!
注意: 由于是 webpack 开发,在调用堆栈中可以看到有两个进程,一个是 webpack,另一个是新开的插件窗口的,同时在该调试窗口也能查看调试输出信息。
切记一定要等到第二个调试进程加载完毕(时间根据电脑性能而定),再打开命令行输入 Hello World 才会有命令,否则会提示 没有匹配命令。
至此,一个 vscode 的开发环境就已经搭建完毕,接下来就是了解项目结构,以及 vscode 插件的 api 了。
代码解读
import * as vscode from 'vscode'
export function activate(context: vscode.ExtensionContext) {
let disposable = vscode.commands.registerCommand('kuizuo-plugin.helloWorld', () => {
vscode.window.showInformationMessage('Hello World from kuizuo-plugin!')
})
context.subscriptions.push(disposable)
}
export function deactivate() {}
vscode.commands.registerCommand
用于注册命令,kuizuo-plugin.helloWorld
为命令 ID,在后续package.json
中要与之匹配。第二个参数为一个回调函数,当触发该命令时,弹出提示框。
在 package.json 中关注 activationEvents 与 contributes
{
"activationEvents": ["onCommand:kuizuo-plugin.helloWorld"],
"contributes": {
"commands": [
{
"command": "kuizuo-plugin.helloWorld",
"title": "Hello World"
}
]
}
}
activationEvents 激活事件,onCommand:kuizuo-plugin.helloWorld
中kuizuo-plugin
是插件 ID 要与 extension.ts 中的注册命令匹配,helloWorld
则是命令标识,而 onCommand 则是监听的类型,此外还有onView
、onUri
、onLanguage
等等。
contributes 则是配置那些地方来显示命令,像官方的例子中,就是在 Ctrl + Shift + P 命令行中输入 Hello World 来调用kuizuo-plugin.helloWorld
命令。此外还可以设置按键与菜单
"keybindings": [
{
"command": "kuizuo-plugin.helloWorld",
"key": "ctrl+f10",
"mac": "cmd+f10",
"when": "editorTextFocus"
}
],
"menus": {
"editor/context": [
{
"when": "editorFocus",
"command": "kuizuo-plugin.helloWorld",
"group": "navigation"
}
]
}
设置完毕后,可以按 Ctrl + Alt + O 或者命令行中键入 reload 来重启 vscode
这里也要注意,如果重启后并无生效,请查看 package.json 是否配置正确(多一个逗号都不行),或者尝试重新调试。如果还不行,那么很有可能就是代码报错,但日志输出并没有,那么在弹出的新窗口中打开开发人员工具(Ctrl+Alt+I 或帮助 → 切换开发人员工具),这里有报错相关的提示信息。
功能
首次启动弹窗与配置项
先说首次启动弹窗的实现,要实现该功能,肯定要保证插件在 VSCode 一打开就运行,而这取决于 vscode 触发插件的时机,也就是 activationEvents,所以activationEvents
需要设置成onStartupFinished
。想要更高的优先级,可以选择 *
(但官方不建议,除非其他事件无法实现的前提下),这里为了演示就使用*
。
其实现代码主要调用 vscode.window.showInformationMessage
函数如下
import * as vscode from 'vscode'
import { exec } from 'child_process'
export function activate(context: vscode.ExtensionContext) {
vscode.window
.showInformationMessage('是否要打开愧怍的小站?', '是', '否', '不再提示')
.then(result => {
if (result === '是') {
exec(`start 'https://kuizuo.cn'`)
} else if (result === '不再提示') {
// 其他操作 后文会说
}
})
}
此时重启窗口,就会有如下弹窗显示
但如果你是 mac 用户的话,你会发现无法打开,其原因是 window 下打开链接的指令是 start,而 mac 则是 open,所以需要区分不同的系统。要区分系统就可以使用 node 中的 os 模块的 platform 方法获取系统,如下(省略部分代码)
import * as os from 'os'
const commandLine = os.platform() === 'win32' ? `start https://kuizuo.cn` : `open https://kuizuo.cn`
exec(commandLine)
当然了,当用户选择不再提示的时候,下次再打开 vscode 就别提示了,不然大概率就是卸载插件了。这里就需要设置全局参数了,在 package.json 中 contributes 设置 configuration,具体如下,注意kuizuoPlugin.showTip
为全局参数之一
"contributes": {
"configuration": {
"title": "kuizuo-plugin",
"properties": {
"kuizuoPlugin.showTip": {
"type": "boolean",
"default": true,
"description": "是否在每次启动时显示欢迎提示!"
}
}
}
}
该参数可以在设置 → 扩展中找到kuizuo-plugin
插件来手动选择,也可以是通过 api 来修改
然后读取vscode.workspace.getConfiguration().get(key)
和设置该参数vscode.workspace.getConfiguration().update(key, value)
export async function activate(context: vscode.ExtensionContext) {
const key = 'kuizuoPlugin.showTip'
const showTip = vscode.workspace.getConfiguration().get(key)
if (showTip) {
const result = await vscode.window.showInformationMessage(
'是否要打开愧怍的小站?',
'是',
'否',
'不再提示',
)
if (result === '是') {
const commandLine =
os.platform() === 'win32' ? `start https://kuizuo.cn` : `open https://kuizuo.cn`
exec(commandLine)
} else if (result === '不再提示') {
//最后一个参数,为true时表示写入全局配置,为false或不传时则只写入工作区配置
await vscode.workspace.getConfiguration().update(key, false, true)
}
}
}
即便是调试状态下,重启也不会影响全局参数。最终封装完整代码查看源码,这里不再做展示了。
右键资源管理器(快捷键)新建测试文件
我日常开发中写的最多的文件就是 js/ts 了,有时候就会在目录下创建 demo.js 来简单测试编写 js 代码,那么我就要点击资源管理器,然后右键新建文件,输入 demo.js。于是我想的是将该功能封装成快捷键的方式,当然右键也有新建测试文件这一选项。
功能其实挺鸡肋的,也挺高不了多少效率,这里可以说为了演示和测试这个功能而实现。
总之前面这么多废话相当于铺垫了,具体还是看功能实现吧。
首先就是注册命令,具体就不解读代码了,其逻辑就是获取调用vscode.window.showQuickPick
弹出选择框选择 js 还是 ts 文件(自定义),接着获取到其目录,判断文件是否存在,创建文件等操作。
import * as vscode from 'vscode'
import * as fs from 'fs'
export async function activate(context: vscode.ExtensionContext) {
let disposable = vscode.commands.registerCommand('kuizuo-plugin.newFile', (uri: vscode.Uri) => {
vscode.window.showQuickPick(['js', 'ts'], {}).then(async item => {
if (!uri?.fsPath) {
return
}
const filename = `${uri.fsPath}/demo.${item}`
if (fs.existsSync(filename)) {
vscode.window.showErrorMessage(`文件${filename}已存在`)
return
}
fs.writeFile(filename, '', () => {
vscode.window.showInformationMessage(`demo.${item}已创建`)
vscode.window.showTextDocument(vscode.Uri.file(filename), {
viewColumn: vscode.ViewColumn.Two, // 显示在第二个编辑器窗口
})
})
})
})
context.subscriptions.push(disposable)
}
export function deactivate() {}
然后再 keybindins 中添加一条
"keybindings": [
{
"command": "kuizuo-plugin.newFile",
"key": "shift+alt+n",
}
],
然后就当我实现完功能的时候,我在想自带的新建文件是不是就是个 command?只是没有绑定快捷键? 于是我到键盘快捷方式中找到答案
图中的explorer.newFile
就是资源管理器右键新建文件的命令,只是没有键绑定。所以我只需要简单的加上shift + alt + n
即可实现我一开始想要的快捷键功能,此时再次右键资源管理器新建文件右侧就有对应的快捷键。
此时的我不知该哭该笑,折腾半天的功能其实只是设置个快捷键的事情。
这些命令在 vscode 中作为内置命令Built-in Commands。要查看 vscode 所有命令的话,也可以通过vscode.commands.getCommands
来获取所有命令 ID,要在插件中执行也只需要调用vscode.commands.executeCommand(id)
键盘快捷键(光标移动)
接着我就在想,既然很多 vscode 功能都是命令的形式,那是不是在插件级别就能做键盘映射,而不用让用户在 vscode 设置,很显然是可以的。只需要在 package.json 中 contributes 的 keybindings 中设置,就可以实现组合键来进行光标的移动。下面是我给出的答案
"keybindings": [
{
"command": "cursorUp",
"key": "shift+alt+i",
"when": "textInputFocus"
},
{
"command": "cursorDown",
"key": "shift+alt+k",
"when": "textInputFocus"
},
{
"command": "cursorLeft",
"key": "shift+alt+j",
"when": "textInputFocus"
},
{
"command": "cursorRight",
"key": "shift+alt+l",
"when": "textInputFocus"
},
{
"command": "cursorHome",
"key": "shift+alt+h",
"when": "textInputFocus"
},
{
"command": "cursorEnd",
"key": "shift+alt+;",
"when": "textInputFocus"
}
]
仔细看右侧来源就可以知道是没问题的,第一个为我之前设置的,而扩展则是通过上面的方法。
自定义扩展工作台
在 vscode 中有几个地方可以用于扩展,具体可看Extending Workbench | Visual Studio Code Extension API
-
左侧图标(活动栏):主要有资源管理器、搜索、调试、源代码管理、插件
-
编辑器右上角:代码分栏、code runner 的运行图标
-
底部(状态栏):git、消息、编码等等
在 contributes 添加 viewsContainers 与 views,注意,views 的属性要与 viewsContainers 的 id 对应。
"viewsContainers": {
"activitybar": [
{
"id": "demo",
"title": "愧怍",
"icon": "public/lollipop.svg"
}
]
},
"views": {
"demo": [
{
"id": "view1",
"name": "视图1"
},
{
"id": "view2",
"name": "视图2"
}
]
}
编辑器右上角是在 menus 中设置 editor/title,图标则是对应命令下设置,不然就是显示文字
"commands": [
{
"command": "kuizuo-plugin.helloWorld",
"title": "Hello World",
"icon": {
"light": "public/lollipop.svg",
"dark": "public/lollipop.svg"
}
}
],
"menus": {
"editor/title": [
{
"when": "resourceLangId == javascript",
"command": "kuizuo-plugin.helloWorld",
"group": "navigation"
}
],
}
至于底部状态栏,这里借用官方例子vscode-extension-samples/statusbar-sample at main · microsoft/vscode-extension-samples (github.com),最终效果如下
那个 🍭 就是所添加的图标,不过并不实际功能,这里只是作为展示。
自定义颜色、图标主题
在 vscode 中分别有三部分的主题可以设置
主题 | 范围 | 推荐 |
---|---|---|
文件图标主题 | 资源管理器内的文件前的图标 | Material Icon Theme |
颜色主题 | 代码编辑器以及整体颜色主题 | One Dark Pro |
产品图标主题 | 左侧的图标 | Carbon Product Icons |
不过关于主题美化就不做深入研究,上面所推荐的就已经足够好看,个人目前也在使用。
代码片段
代码片段,也叫snippets
,相信大家都不陌生,就是输入一个很简单的单词然后一回车带出来很多代码。平时大家也可以直接在 vscode 中创建属于自己的snippets
代码片段相对比较简单,这里就简单跳过了
xxx.log → console.log(xxx)包装
功能描述:在一个变量后使用.log,即可转化为 console.log(变量)的形式就像 xxx.log => console.log('xxx', xxx)
有点像 idea 中的.sout
这里我把 jaluik/dot-log 这个插件的实现逻辑给简化了,这里先给出基本雏形
import * as vscode from 'vscode'
class MyCompletionItemProvider implements vscode.CompletionItemProvider {
constructor() {}
// 提供代码提示的候选项
public provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
const snippetCompletion = new vscode.CompletionItem('log', vscode.CompletionItemKind.Operator)
snippetCompletion.documentation = new vscode.MarkdownString('quick console.log result')
return [snippetCompletion]
}
// 光标选中当前自动补全item时触发动作
public resolveCompletionItem(item: vscode.CompletionItem) {
return null
}
}
export function activate(context: vscode.ExtensionContext) {
const disposable = vscode.languages.registerCompletionItemProvider(
['html', 'javascript', 'javascriptreact', 'typescript', 'typescriptreact', 'vue'],
new MyCompletionItemProvider(),
'.', // 注册代码建议提示,只有当按下“.”时才触发
)
context.subscriptions.push(disposable)
}
在 vscode 插件中通过vscode.languages.registerCompletionItemProvider
提供像补全,代码提示等功能,第一个参数为所支持的语言,第二个参数为提供的服务vscode.CompletionItemProvider
这里只是封装成类的形式,目的是为了保存一些属性,例如光标位置 position,也可以传递对象形式 { provideCompletionItems, resolveCompletionItem }
,第三个参数则是触发的时机。
provideCompletionItems
需返回一个数组,成员类型为vscode.CompletionItem
,可通过new vscode.CompletionItem()
来创建。
当你尝试运行上述代码时,会发现在任何值后面输入.
都会有log
提示。
但是点击后只是满足了代码补全的功能,而选择 log 选项后所要执行的操作则是在 resolveCompletionItem
中实现,这里仅仅只是返回一个 null,即只有简单的补全功能,这里对整个过程进行描述(可以自行下个断点调试查看):。
-
当输入
.
时,程序进入到provideCompletionItems
函数内,这里可以获取到当前正在编辑的代码文档(文件名,代码内容)对应第一个参数,以及光标所在位置也就是第二个参数。还有其他参数,但这里用不到。具体可看CompletionItemProvider -
选择完毕后,便会进入到 resolveCompletionItem 里面,这里可以获取到用户所选的选项内容,然后执行一系列的操作。
要做代码替换的话就需要注册文本编辑命令vscode.commands.registerTextEditorCommand
,内容如下
const commandId = 'kuizuo-plugin.log'
const commandHandler = (
editor: vscode.TextEditor,
edit: vscode.TextEditorEdit,
position: vscode.Position,
) => {
const lineText = editor.document.lineAt(position.line).text
// match case name.log etc.
const matchVarReg = new RegExp(`\(\[^\\s\]*\[^\'\"\`\]\).${'log'}$`)
// match case 'name'.log etc. /(['"`])([^'"])\1.log/
const matchStrReg = new RegExp(`\(\[\'\"\`\]\)\(\[^\'\"\`\]*\)\\1\.${'log'}$`)
let matchFlag: 'var' | 'str' = 'var'
let text,
key,
quote = "'",
insertVal = ''
;[text, key] = lineText.match(matchVarReg) || []
if (!key) {
;[text, quote, key] = lineText.match(matchStrReg) || []
matchFlag = 'str'
}
// if matched
if (key) {
const index = lineText.indexOf(text)
edit.delete(
new vscode.Range(
position.with(undefined, index),
position.with(undefined, index + text.length),
),
)
if (matchFlag === 'var' && key.includes("'")) {
quote = '"'
}
// format like console.log("xxx", xxx)
if (matchFlag === 'var') {
// only console.log(xxx)
insertVal = `${'console.log'}(${key})`
}
// if key is string format like console.log("xxx")
if (matchFlag === 'str') {
insertVal = `${'console.log'}(${quote}${key}${quote})`
}
edit.insert(position.with(undefined, index), insertVal)
}
return Promise.resolve([])
}
context.subscriptions.push(vscode.commands.registerTextEditorCommand(commandId, commandHandler))
registerTextEditorCommand
不同于registerCommand
,它只针对编辑器的命令,例如可以删除代码中的某个片段,增加代码等等。上面的代码就是为了找到.log 前(包括.log)匹配的代码,进行正则提取,然后调用 edit.delete 删除指定范围,再调用 edit.insert 来插入要替换的代码,以此达到替换的效果。
命令注册完毕了就需要调用了,也就到了 resolveCompletionItem 的时机
public resolveCompletionItem(item: vscode.CompletionItem) {
const label = item.label
if (this.position && typeof label === 'string') {
item.command = {
command: 'kuizuo-plugin.log',
title: 'refactor',
arguments: [this.position.translate(0, label.length + 1)], // 这里可以传递参数给该命令
}
}
return item
}
将命令赋值给 item.command,会自动调用其 command 命令,同时把 arguments 参数传入给 command。最终达到替换的效果。
Position
这里要说下 vscode 编辑器中的 Position,了解这个对代码替换、代码定位、代码高亮有很大帮助。
position 有两个属性line
和character
,对应的也就是行号和列号(后文以line
和character
为称),和 都是从 0 开始算起,而在 vscode 自带的状态栏提示中则是从 1 开始算起,这两者可别混淆了。
其中 position 有如下几个方法
position.translate
根据当前坐标计算,例如当前 position 的 line 0,character1。position.translate(1, 1)
得到 line 1,character 2,这不会改变远 position,这很好理解。但如果计算后得到的 line 与 character 有一个为负数则直接报错。
position.with
从自身创建一个新的 postion 对象
Range
知道了坐标信息,那么就可以获取范围了。可以通过 new vscode.Range() 来截取两个 position 之间的内容,得到的是一个 对象,有 start 与 end 属性,分别是传入的两个 position。
同样的 Range 和 Postion 方法都一致,这里就不多叙述了,可查看其声明文件。知道范围就可以通过 editor 来获取范围内的代码或是 edit 来删除代码等操作。
知道了这些内容,再看上面的代码也不难理解了。