原文首發於個人博客,歡迎關注~git
前一段時間在Mac上用VSCode的時候,發現VSCodeVim
這個插件嚴重拖慢了個人開發效率。原本用Vim
模式難道不該該是提升效率麼?問題是在Normal
模式下,光標的移動會有肉眼可見的長延時。好比我按着j
,等我鬆開j
後,光標還在移動,並且還移動了一下子。預期的效果應該是按下移動,鬆開中止。爲此我查了一下相關issue,發現跟我同樣的狀況的人還很多。(不過也有很多人沒有這個問題,貌似跟顯卡有關係?個人mac是集顯的)。github
卸載了VSCodeVim
以後,光標移動的速度又恢復了正常,不過沒有Vim
模式的話很是彆扭。因此我就開始看看VSCode還有沒有其餘Vim
模式的插件。因而我又試了另外兩個插件:vimStyle和amVim。最終我選擇了後者。不只是支持的Vim命令更多,還有就是開發者的維護一直在繼續。並且很關鍵的一點,amVim
的光標移動體驗就是 如絲般順滑 !typescript
不過它有個讓我很不習慣的地方:不支持:
號調起VSCode的Command Line
窗口,實現諸如:w
保存,:wq
退出等常見功能。這些功能在VSCodeVim
裏是支持的。因而我就在想有沒有辦法「移植」一下VSCodeVim
的功能到amVim
來,既能保持光標移動體驗順滑,又能用上Command Line
的一些經常使用命令。因此開啓了魔改模式,並在跟開發者的一系列交流後最終我提交的PR被merge了。 npm
VSCode的插件一般是用TypeScript
來寫的。若是你須要開發或者修改它,先要擁有TypeScript
的開發環境。vim
npm install -g typescript
# or
yarn global add typescript
複製代碼
一般TypeScript
的項目都會用上tslint
。因此你也最好全局安裝它:api
npm install -g tslint
# or
yarn global add tslint
複製代碼
而後打開VSCode,安裝一下tslint
這個插件,它將經過咱們上面安裝在系統裏的tslint
給咱們的項目提供代碼檢查。bash
修改別人的插件,能夠先fork
一份別人的代碼。也爲了以後方便提PR作準備。併發
而後就能夠把插件clone
到本地了。好比本文的amVim-for-VSCode。async
用VSCode打開這個項目,點擊左側的debug
能夠看到一個launch extension
的配置:編輯器
運行它,你會獲得另一個窗口,這個就是能夠調試插件功能的窗口了:
個人改進源碼在這裏:https://github.com/Molunerfinn/amVim-for-VSCode 做者合併以後作了一些修改,本文是以個人版本爲主。
爲了實現VSCodeVim
經過:
調起VSCode的inputBox
效果,我須要翻閱一下VSCodeVim
的源代碼。
大體效果以下:
在查看了amVim
和VSCodeVim
在實現命令上的部分源碼後,發現兩者的實現上差距仍是不小的。不過相比VSCodeVim
代碼的龐大(甚至還有neoVim的支持),amVim
在實現上就比較精巧了。
在個人PR未被merge以前,amVim
插件提供了一個功能,按:
打開一個GoToLine
的inputBox
:
不過只能用於輸入數字並跳轉到相應行數。好在查看release更新日誌,追溯這個commit,咱們能夠很容易找到它是如何實現的。
代碼很少,就幾行:
// src/Modes/Normal.ts
{ keys: ':', actions: [ActionCommand.goToLine] }, // 增長`:`打開GoToLine的inputBox的快捷鍵
複製代碼
具體實現代碼以下:
// src/Actions/Command.ts
import {commands} from 'vscode';
export class ActionCommand {
static goToLine(): Thenable<boolean | undefined> {
return commands.executeCommand('workbench.action.gotoLine');
}
}
複製代碼
因此是經過vscode
的commands
來打開的gotoLine
的inputBox
窗口。
再來看看VSCodeVim
是如何打開inputBox
的:
// src/cmd_line/commandLine.ts
export class CommandLine {
// ...
public static async PromptAndRun(initialText: string, vimState: VimState): Promise<void> {
if (!vscode.window.activeTextEditor) {
Logger.debug('CommandLine: No active document');
return;
}
let cmd = await vscode.window.showInputBox(this.getInputBoxOptions(initialText)); // 經過showInputBox打開
if (cmd && cmd[0] === ':' && configuration.cmdLineInitialColon) {
cmd = cmd.slice(1);
}
this._history.add(cmd);
this._history.save();
await CommandLine.Run(cmd!, vimState);
}
// ...
private static getInputBoxOptions(text: string): vscode.InputBoxOptions { // inputBox的Options
return {
prompt: 'Vim command line',
value: configuration.cmdLineInitialColon ? ':' + text : text,
ignoreFocusOut: false,
valueSelection: [
configuration.cmdLineInitialColon ? text.length + 1 : text.length,
configuration.cmdLineInitialColon ? text.length + 1 : text.length,
],
};
}
}
複製代碼
能夠看到關鍵的部分是經過vscode.window.showInputBox
打開的inputBox
。因此我也根據這個關鍵的入口來一步步實現我想要的功能。
參考VSCodeVim
的實現,在amVim
裏能夠大概分四個部分:
src/Modes/Normal.ts
做爲入口文件,當用戶輸入:
鍵時觸發後續功能。【已有】src/Actions/CommandLine/CommandLine.ts
做爲打開inputBox
的入口函數,打開inputBox
,而後負責把用戶輸入的內容傳給下一級的parser
,用於解析並執行相應命令。src/Actions/CommandLine/Parser.ts
,負責接收上一級傳進來的命令,而後找到命令對應的函數,並執行該函數。若是找不到相應則返回。src/Actions/CommandLine/Commands/*
,存放各個命令的實現函數。其中src/Actions/CommandLine/CommandLine.ts
的邏輯跟VSCodeVim
的src/cmd_line/commandLine.ts
很是相似。
import * as vscode from 'vscode';
import { parser } from './Parser';
export class CommandLine {
public static async Run(command: string | undefined): Promise<void> {
if (!command || command.length === 0) { // 若是命令爲空則直接返回
return;
}
try {
const cmd = parser(command); // 將命令傳給parser並返回一個可執行的函數
if (cmd) {
await cmd.execute(command); // 調用該函數的execute方法
}
} catch (e) {
console.error(e);
}
}
public static async PromptAndRun(): Promise<void> {
if (!vscode.window.activeTextEditor) { // 若是當前沒有打開的激活的文本,則命令不執行,返回空。
return;
}
try {
let cmd = await vscode.window.showInputBox(CommandLine.getInputBoxOptions()); // 打開inputBox
if (cmd && cmd[0] === ':') {
cmd = cmd.slice(1); // 若是命令帶有:則將它去掉並傳給parser
}
return await CommandLine.Run(cmd);
} catch (e) {
console.error(e);
}
}
private static getInputBoxOptions(): vscode.InputBoxOptions { // 打開的inputBox框裏的文本和一些其餘配置
return {
prompt: 'Vim command line',
value: ':',
ignoreFocusOut: false,
valueSelection: [1, 1]
};
}
}
複製代碼
import { CommandBase } from './Commands/Base';
import WriteCommand from './Commands/Write';
import WallCommand from './Commands/WriteAll';
import QuitCommand from './Commands/Quit';
import QuitAllCommand from './Commands/QuitAll';
import WriteQuitCommand from './Commands/WriteQuit';
import WriteQuitAllCommand from './Commands/WriteQuitAll';
import VisualSplitCommand from './Commands/VisualSplit';
import NewFileCommand from './Commands/NewFile';
import VerticalNewFileCommand from './Commands/VerticalNewFile';
import GoToLineCommand from './Commands/GoToLine';
const commandParsers = { // 對於命令的解析,用哈希表作映射
w: WriteCommand,
write: WriteCommand,
wa: WallCommand,
wall: WallCommand,
q: QuitCommand,
quit: QuitCommand,
qa: QuitAllCommand,
qall: QuitAllCommand,
wq: WriteQuitCommand,
x: WriteQuitCommand,
wqa: WriteQuitAllCommand,
wqall: WriteQuitAllCommand,
xa: WriteQuitAllCommand,
xall: WriteQuitAllCommand,
vs: VisualSplitCommand,
vsp: VisualSplitCommand,
new: NewFileCommand,
vne: VerticalNewFileCommand,
vnew: VerticalNewFileCommand
};
export function parser(input: string): CommandBase | undefined {
if (commandParsers[input]) {
return commandParsers[input]; // 接收inputBox裏傳來的命令
} else if (Number.isInteger(Number(input))) {
return GoToLineCommand;
} else {
return undefined;
}
}
複製代碼
因爲命令不少,我就舉三個例子。一個是w
,一個是q
,和一個wq
。VSCode本身的一些功能好比關閉當前文件、保存文件等都是有本身的command的。在實現Vim模式的時候,實際上最後也是去調用VSCode自帶的功能而已。
import * as vscode from 'vscode';
import { CommandBase } from './Base';
class WriteCommand extends CommandBase {
constructor() {
super();
}
async execute(): Promise<void> { // 暴露execute方法用於調用
await vscode.commands.executeCommand('workbench.action.files.save'); // 調用vscode的命令保存文件
}
}
export default new WriteCommand();
複製代碼
import * as vscode from 'vscode';
import { CommandBase } from './Base';
class QuitCommand extends CommandBase {
constructor() {
super();
}
async execute(): Promise<void> {
await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); // 調用vscode的命令關閉當前的文件
}
}
export default new QuitCommand();
複製代碼
import { CommandBase } from './Base';
import WriteCommand from './Write';
import QuitCommand from './Quit';
class WriteQuitCommand extends CommandBase {
constructor() {
super();
}
async execute(): Promise<void> {
await WriteCommand.execute();
await QuitCommand.execute();
}
}
export default new WriteQuitCommand();
複製代碼
這一步就頗有意思了,由於咱們以前實現了Write
和Quit
的功能,因此能夠在這裏調用它們。看到這裏你可能會有問題,雖然我知道VSCode有這些功能,可是你是怎麼知道這些功能是怎麼寫的呢?
若是隻是我這篇文章的話,我在實現Vim模式的這些命令的時候,大部分是參考了VSCodeVim
的一些寫法。它主要的命令實如今src/cmd_line/commands/*
裏。可是隻這樣顯然仍是不夠的。所以我給出幾個比較有用的地方供你們開發插件的時候參考:
說實話VSCode的文檔寫得不是特別好。我要實現一個功能,查找文檔查了半天。其實其中很大一部分操做,你能夠在上面的第3點、第4點裏經過快捷鍵的提供的Command id
去實現:
好比你要實現一個剪切的功能,有了Command id
,你就能夠經過vscode.commands.executeCommand('editor.action.clipboardCutAction')
來實現。所以我推薦,若是你要實現的功能有些能夠用已有快捷鍵實現的,那麼就能在這個列表裏找到對應的Command id
來手動實現了。
至於其餘的一些非快捷鍵提供的功能,就還須要閱讀第2點的api文檔作出更深層次的修改了。
在改進完這個插件以後,我向做者提交了PR。在和做者交流後作出了一些修改,並最終被做者接受併合並。爲開源項目貢獻代碼的感受是真的很不錯。而且這個貢獻不只方便了本身,也方便了其餘使用這個插件的人,就感到更開心了。藉此機會學習了VSCode的插件開發,也不失是一件好事。在此以後我也本身寫了一個VSCode的插件——VSCode-RevealFileInFolder,用於方便地在編輯器裏經過右鍵打開文件在系統中的位置。能夠經過VSCode的官方插件商店下載使用:https://marketplace.visualstudio.com/items?itemName=Molunerfinn.revealfileinfolder
以後我會寫一篇文章來說述如何從頭開始寫一個本身的VSCode插件併發布到官方插件商店。也但願本文能給你帶來幫助。