开发个VSCode插件
VSCode是相当流行的代码编辑器,作为开发人员应该不会陌生。除了自身提供的各种便捷功能外,vscode还有着强大的插件机制,用于扩展其功能。相信大家或多或少都安装过插件,今天我们来研究下如何开发一个vscode插件。
插件能做什么?
VSCode的插件可以实现很多方面的功能,比较主要的有如下
-
注册命令、配置、快捷键以及右键菜单项等
-
显示通知信息
-
打开文件选择对话框让用户选择文件
-
修改样式主题
-
支持语言特性,即扩展某编程语言或支持一个全新的编程语言
-
实现Debug功能
从效果上来讲,可以简单的分为对代码内容的修改(如ESLint插件可以修正你代码的问题),以及界面元素的修改。
(图片来自官网)
我们今天尝试着做一个简单的小插件,可以让html或jsx标签由一行改成多行。比如将
<Hello sayHi="helloworld" user="小小前猿" />
改成
<Hello
sayHi="helloworld"
user="小小前猿"
/>
工程搭建
首先,我们安装用于创建vscode插件工程的脚手架
npm install -g yo generator-code
# 或者
yarn global add yo generator-code
脚手架安装成功后我们来创建插件工程,在终端执行如下命令
yo code
然后它会问一些关于工程的问题,按情况做选择或输入即可,如下图
然后用vscode打开创建好的工程,按F5运行工程,这时会自动打开个新的vscode窗口,该窗口用来调试插件。
关于插件配置
项目的package.json
文件包含插件相关配置内容,如刚创建好的工程,插件相关的主要配置内容如下
{
"activationEvents": [
"onCommand:wrap-lines.helloWorld"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "wrap-lines.helloWorld",
"title": "Hello World"
}
]
},
}
其中
-
activationEvents
用来配置插件被激活的事件,这里配置的是在即将执行命令wrap-lines.helloWorld
时。我们可以简单的把激活理解为初始化插件。 -
main
是插件的入口文件。这里需要配置编译后的js文件。 -
contributes
是插件扩展的功能点,这里配置的是在vscode的命令面板中添加一个命令。
入口文件extension.ts
入口文件的源文件是/src/extension.ts
,我们看下它都做了什么。
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "wrap-lines" is now active!');
// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
let disposable = vscode.commands.registerCommand('wrap-lines.helloWorld', () => {
// The code you place here will be executed every time your command is executed
// Display a message box to the user
vscode.window.showInformationMessage('Hello World from wrap-lines!');
});
context.subscriptions.push(disposable);
}
// this method is called when your extension is deactivated
export function deactivate() {}
该文件主要导出了两个方法activate
与deactivate
,分别会在激活(启用)与停用时被vscode调用。文件中的注释已经把各部分解释的比较清楚了,我们目前需要关注的是vscode.commands.registerCommand
方法,在配置文件中我们定义了一个命令,这里就是实现该命令的执行内容。可以看到目前只是显示了一个信息。
我们在调试窗口(即前面说的按F5运行后自动弹出的窗口)按command + shift + p
打开命令面板,输入hello
能看到自动匹配出我们配置的命令Hello World
执行该命令(选中后回车或直接用鼠标点击),能够看到窗口右下角弹出了一个消息对话框
获取文本
要实现我们的目标,我的自然反应是获取到当前编辑器中用户选择的内容或光标所在行的文本内容,然后通过分析按具体情况做相应的替换(由于目前对vscode插件机制不是太熟,所以某些地方先猜测着来)。
通过查看示例代码,发现可以通过vscode.window.activeTextEditor
来拿到当前活动的编辑器。拿到编辑器后再通过其selection
及document
获取文本内容。
我们添加一个新命令来实现我们的目标功能。首先在package.json
中配置命令内容,在contributes->commands
中添加如下内容
{
"command": "wrap-lines.wrapline",
"title": "Wrap Line"
}
然后在extension.ts
中的activate
方法里添加该命令的实现,代码如下
// 添加折行命令
context.subscriptions.push(vscode.commands.registerCommand('wrap-lines.wrapline', () => {
// 拿到当前活动的编辑器
const editor = vscode.window.activeTextEditor;
// 当前可能没有活动的编辑器,所以要判断
if (editor) {
// 我们要修改的目标内容
let text = '';
// 如果当前没有选择文本内容则在当前行查找,否则使用当前选择的内容
if (editor.selection.isEmpty) {
// 光标在编辑器中的位置
const position = editor.selection.active;
// 获取当前行的文本内容
text = editor.document.lineAt(position.line).text;
} else {
// 获取选中的内容
text = editor.document.getText(editor.selection);
}
// 显示内容,以便调度
vscode.window.showInformationMessage(`将要被修改的内容:${text}`);
} else {
// 没有编辑器时显示的提示信息
vscode.window.showInformationMessage('你应该在编辑器中执行该命令');
}
}));
重新加载调试窗口(在调度窗口里打开命令面板并输入Reload Window
可以看到重新加载窗口的命令),然后打开个文件,在命令面板输入我们新添加的命令Wrap Line
,执行该命令
但这时并没有按照我们预想的弹出当前行的内容,而是报错了
原来这是因为重新加载窗口后,我们的插件还没有激活。之所以没有激活,是因为我们配置激活插件的方法是在执行命令wrap-lines.helloWorld
时,而重新加载窗口后我们没有执行过该命令。
我们把新添加的命令也配置到激活插件的条件里,在activationEvents
里添加如下内容
"onCommand:wrap-lines.wrapline"
然后再重新加载调试窗口,执行我们的Wrap Line
命令,可以看到弹出了如下信息
关闭所有文件后再次执行会弹出如下提示
不过这应该是个错误信息,我们把这个提示改成如下
// 没有编辑器时显示的提示信息
vscode.window.showErrorMessage('你应该在编辑器中执行该命令');
效果如下
功能实现
现在已经可以拿到文本内容,可以对其做相应的修改了。当然,由于今天主要是为了研究开发插件,所以对功能的严谨性不怎么要求。
我们简单的用正则表达式解析出标签内容,并把标签属性换行,代码如下
// 解析出标签内容
const content = text.match(/<\w+(\s+\w+(="[^"]*")*)*\s*\/?>/);
if (content) {
const markText = content[0];
// 将所有属性折行
let result = markText.replace(/\s+\w+(="[^"]*")*/g, (m) => {
// 打印出被换行的属性以便调试
console.log('replace: ', m);
// 替换成换行的属性
return `\n${m.trim()}`;
});
// 换行关闭标签
result = result.replace(/\s*\/?>$/, (m) => `\n${m.trim()}`);
// 显示结果
console.log(`被修改后:\n${result}`);
} else {
console.log('未找到标签内容');
}
重新加载调试窗口后再次执行我们的换行命令,然后在开发窗口的调试控制台能够看到如下信息
可以看到标签的属性都被换行了,下面就该把结果写到编辑器中了。
要修改编辑器中的内容,需要使用TextEditor.edit
方法,前面我们拿到的当前编辑activeTextEditor
就是TextEditor
实例。我们的目标是替换当前内容,所以还需要拿到被替换内容在编辑器中的位置与范围。位置是用行数和该行中的字符位置索引来表示的;而范围就是一个起始位置和一个结束位置之间的内容。
如果是选中了内容再执行的命令,则范围直接可以利用editor.selection
,否则需要自己找到相应位置。具体代码如下
// 最终被替换的结果
result = text.replace(markText, result);
// 最终编辑器中会被替换的范围
let range: vscode.Range;
if (editor.selection.isEmpty) {
// 行数
const line = editor.selection.active.line;
range = new vscode.Range(line, 0, line, text.length);
} else {
range = editor.selection;
}
// 写入编辑器
editor.edit(editBuilder => editBuilder.replace(range, result));
重新加载调试窗口,执行换行命令,换行成功,如下
但很明显,现在没有自动缩进。为了填补上缩进,我们需要拿到当前编辑器关于缩进的设置。经过一番查找,发现可以在editor.options
中拿到最终值。计算缩进的代码如下
// 文本在该行的起始位置
let startCharPos = content.index || 0;
// 如果是选择的内容,则在选中的范围内找到标签所在行的起始位置
if (!editor.selection.isEmpty) {
// 拆分成行
const lines = text.split('\n');
// 标签所在行之前的行的内容总长度
let beforeStart = 0;
for (const l of lines) {
// 加上该行的长度,由于换行符也会占一个位置所以需要加上1
beforeStart += l.length + 1;
// 如果标签在当前行
if (beforeStart > startCharPos) {
// 减掉当前行的长度
beforeStart = beforeStart - l.length - 1;
break;
}
}
// 如果标签在第一行,则标签在当前行的位置需要加上选择范围的起始位置
if (beforeStart === 0) {
startCharPos += editor.selection.start.character;
} else { // 否则,直接减去前面所有行的总长
startCharPos -= beforeStart;
}
}
// 缩进字符
const indentChar = editor.options.insertSpaces
// 使用空格做缩进,
? Array(editor.options.tabSize).fill(' ').join('')
// 使用tab缩进
: '\t';
// 当前行的缩进
const prevIndent = Array(startCharPos).fill(indentChar[0]).join('');
然后将缩进添加到标签的属性前
`\n${prevIndent}${indentChar}${m.trim()}`
以及关闭标签前面的缩进
`\n${prevIndent}${m.trim()}`
快捷键
现在已经实现基本功能,但每次都要调出命令面板再执行命令,总是有些麻烦。所以方便起见,我们来给换行命令绑定个快捷键。
为命令绑定快捷键非常简单,只需在配置文件package.josn
中的contributes
下添加keybindings
内容即可
{
"keybindings": [
{
"command": "wrap-lines.wrapline",
"key": "ctrl+alt+l",
"mac": "cmd+alt+l",
"when": "editorTextFocus"
}
]
}
打包
插件开发完后需要打包才能发布到vscode插件市场或分享给其他开发者。打包插件需要安装专门的工具vsce
,即vs code extensions
,或者更全称一点Visual Studio Code Extensions
。安装如下
npm install -g vsce
安装后在终端进入插件工程目录,然后执行
vsce package
但执行后直接报错
搜索了一下,应该是需要在package.json
中添加publisher
字段,添加如下
{
"publisher": "guofei",
}
再次执行,又报了另一个错误
工程创建好后的确还没有修改README.md
文件,清除默认内容,随便写点东西,再次执行打包,成功。生成了一个扩展名为.vsix
的文件,vscode可以直接通过该文件来安装插件
如果要发布到插件市场,则继续执行如下命令
vsce publish
当然,在执行发布之前还需要其他的必要操作,比如创建发布者账号,登录等,这里就不尝试了。
总结
vscode的插件机制十分强大,插件几乎可以扩展或修改vscode自身所有方面,利用Webview
更是能做出强大复杂的东西。我们今天只做了个最基础的小插件以探索vscode的插件开发,总结如下
-
关于插件的能力我们只列举了一小部分,详细的内容见这里https://code.visualstudio.com/api/extension-capabilities/overview
-
工程搭建需要使用vscode提供的脚手架
-
插件需要配置激活事件,除了我们使用的
onCommand
外还有许多,详细见这里https://code.visualstudio.com/api/references/activation-events -
关于插件的
contributes
,我们用到了commands
与keybindings
,即命令面板与快捷键,更多详细内容可看这里https://code.visualstudio.com/api/references/contribution-points -
插件还有很多其他的配置内容,详细见这里https://code.visualstudio.com/api/references/extension-manifest
-
打包发布需要使用
vsce
,详细见这里https://code.visualstudio.com/api/working-with-extensions/publishing-extension