開發(fā)個VSCode插件
VSCode是相當流行的代碼編輯器,作為開發(fā)人員應該不會陌生。除了自身提供的各種便捷功能外,vscode還有著強大的插件機制,用于擴展其功能。相信大家或多或少都安裝過插件,今天我們來研究下如何開發(fā)一個vscode插件。
插件能做什么?
VSCode的插件可以實現(xiàn)很多方面的功能,比較主要的有如下
-
注冊命令、配置、快捷鍵以及右鍵菜單項等
-
顯示通知信息
-
打開文件選擇對話框讓用戶選擇文件
-
修改樣式主題
-
支持語言特性,即擴展某編程語言或支持一個全新的編程語言
-
實現(xiàn)Debug功能
從效果上來講,可以簡單的分為對代碼內容的修改(如ESLint插件可以修正你代碼的問題),以及界面元素的修改。

(圖片來自官網)
我們今天嘗試著做一個簡單的小插件,可以讓html或jsx標簽由一行改成多行。比如將
<Hello sayHi="helloworld" user="小小前猿" />
改成
<Hello
sayHi="helloworld"
user="小小前猿"
/>
工程搭建
首先,我們安裝用于創(chuàng)建vscode插件工程的腳手架
npm install -g yo generator-code
# 或者
yarn global add yo generator-code
腳手架安裝成功后我們來創(chuàng)建插件工程,在終端執(zhí)行如下命令
yo code
然后它會問一些關于工程的問題,按情況做選擇或輸入即可,如下圖

然后用vscode打開創(chuàng)建好的工程,按F5運行工程,這時會自動打開個新的vscode窗口,該窗口用來調試插件。
關于插件配置
項目的package.json文件包含插件相關配置內容,如剛創(chuàng)建好的工程,插件相關的主要配置內容如下
{
"activationEvents": [
"onCommand:wrap-lines.helloWorld"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "wrap-lines.helloWorld",
"title": "Hello World"
}
]
},
}
其中
-
activationEvents用來配置插件被激活的事件,這里配置的是在即將執(zhí)行命令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方法,在配置文件中我們定義了一個命令,這里就是實現(xiàn)該命令的執(zhí)行內容??梢钥吹侥壳爸皇秋@示了一個信息。
我們在調試窗口(即前面說的按F5運行后自動彈出的窗口)按command + shift + p打開命令面板,輸入hello能看到自動匹配出我們配置的命令Hello World

執(zhí)行該命令(選中后回車或直接用鼠標點擊),能夠看到窗口右下角彈出了一個消息對話框

獲取文本
要實現(xiàn)我們的目標,我的自然反應是獲取到當前編輯器中用戶選擇的內容或光標所在行的文本內容,然后通過分析按具體情況做相應的替換(由于目前對vscode插件機制不是太熟,所以某些地方先猜測著來)。
通過查看示例代碼,發(fā)現(xiàn)可以通過vscode.window.activeTextEditor來拿到當前活動的編輯器。拿到編輯器后再通過其selection及document獲取文本內容。
我們添加一個新命令來實現(xiàn)我們的目標功能。首先在package.json中配置命令內容,在contributes->commands中添加如下內容
{
"command": "wrap-lines.wrapline",
"title": "Wrap Line"
}
然后在extension.ts中的activate方法里添加該命令的實現(xiàn),代碼如下
// 添加折行命令
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('你應該在編輯器中執(zhí)行該命令');
}
}));
重新加載調試窗口(在調度窗口里打開命令面板并輸入Reload Window可以看到重新加載窗口的命令),然后打開個文件,在命令面板輸入我們新添加的命令Wrap Line,執(zhí)行該命令

但這時并沒有按照我們預想的彈出當前行的內容,而是報錯了

原來這是因為重新加載窗口后,我們的插件還沒有激活。之所以沒有激活,是因為我們配置激活插件的方法是在執(zhí)行命令wrap-lines.helloWorld時,而重新加載窗口后我們沒有執(zhí)行過該命令。
我們把新添加的命令也配置到激活插件的條件里,在activationEvents里添加如下內容
"onCommand:wrap-lines.wrapline"
然后再重新加載調試窗口,執(zhí)行我們的Wrap Line命令,可以看到彈出了如下信息

關閉所有文件后再次執(zhí)行會彈出如下提示

不過這應該是個錯誤信息,我們把這個提示改成如下
// 沒有編輯器時顯示的提示信息
vscode.window.showErrorMessage('你應該在編輯器中執(zhí)行該命令');
效果如下

功能實現(xiàn)
現(xiàn)在已經可以拿到文本內容,可以對其做相應的修改了。當然,由于今天主要是為了研究開發(fā)插件,所以對功能的嚴謹性不怎么要求。
我們簡單的用正則表達式解析出標簽內容,并把標簽屬性換行,代碼如下
// 解析出標簽內容
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('未找到標簽內容');
}
重新加載調試窗口后再次執(zhí)行我們的換行命令,然后在開發(fā)窗口的調試控制臺能夠看到如下信息

可以看到標簽的屬性都被換行了,下面就該把結果寫到編輯器中了。
要修改編輯器中的內容,需要使用TextEditor.edit方法,前面我們拿到的當前編輯activeTextEditor就是TextEditor實例。我們的目標是替換當前內容,所以還需要拿到被替換內容在編輯器中的位置與范圍。位置是用行數(shù)和該行中的字符位置索引來表示的;而范圍就是一個起始位置和一個結束位置之間的內容。
如果是選中了內容再執(zhí)行的命令,則范圍直接可以利用editor.selection,否則需要自己找到相應位置。具體代碼如下
// 最終被替換的結果
result = text.replace(markText, result);
// 最終編輯器中會被替換的范圍
let range: vscode.Range;
if (editor.selection.isEmpty) {
// 行數(shù)
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));
重新加載調試窗口,執(zhí)行換行命令,換行成功,如下

但很明顯,現(xiàn)在沒有自動縮進。為了填補上縮進,我們需要拿到當前編輯器關于縮進的設置。經過一番查找,發(fā)現(xiàn)可以在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()}`
快捷鍵
現(xiàn)在已經實現(xiàn)基本功能,但每次都要調出命令面板再執(zhí)行命令,總是有些麻煩。所以方便起見,我們來給換行命令綁定個快捷鍵。
為命令綁定快捷鍵非常簡單,只需在配置文件package.josn中的contributes下添加keybindings內容即可
{
"keybindings": [
{
"command": "wrap-lines.wrapline",
"key": "ctrl+alt+l",
"mac": "cmd+alt+l",
"when": "editorTextFocus"
}
]
}
打包
插件開發(fā)完后需要打包才能發(fā)布到vscode插件市場或分享給其他開發(fā)者。打包插件需要安裝專門的工具vsce,即vs code extensions,或者更全稱一點Visual Studio Code Extensions。安裝如下
npm install -g vsce
安裝后在終端進入插件工程目錄,然后執(zhí)行
vsce package
但執(zhí)行后直接報錯

搜索了一下,應該是需要在package.json中添加publisher字段,添加如下
{
"publisher": "guofei",
}
再次執(zhí)行,又報了另一個錯誤

工程創(chuàng)建好后的確還沒有修改README.md文件,清除默認內容,隨便寫點東西,再次執(zhí)行打包,成功。生成了一個擴展名為.vsix的文件,vscode可以直接通過該文件來安裝插件

如果要發(fā)布到插件市場,則繼續(xù)執(zhí)行如下命令
vsce publish
當然,在執(zhí)行發(fā)布之前還需要其他的必要操作,比如創(chuàng)建發(fā)布者賬號,登錄等,這里就不嘗試了。
總結
vscode的插件機制十分強大,插件幾乎可以擴展或修改vscode自身所有方面,利用Webview更是能做出強大復雜的東西。我們今天只做了個最基礎的小插件以探索vscode的插件開發(fā),總結如下
-
關于插件的能力我們只列舉了一小部分,詳細的內容見這里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
-
打包發(fā)布需要使用
vsce,詳細見這里https://code.visualstudio.com/api/working-with-extensions/publishing-extension
