小記VSCode插件amVim的改進以及插件開發

原文首發於個人博客,歡迎關注~git

前一段時間在Mac上用VSCode的時候,發現VSCodeVim這個插件嚴重拖慢了個人開發效率。原本用Vim模式難道不該該是提升效率麼?問題是在Normal模式下,光標的移動會有肉眼可見的長延時。好比我按着j,等我鬆開j後,光標還在移動,並且還移動了一下子。預期的效果應該是按下移動,鬆開中止。爲此我查了一下相關issue,發現跟我同樣的狀況的人還很多。(不過也有很多人沒有這個問題,貌似跟顯卡有關係?個人mac是集顯的)。github

卸載了VSCodeVim以後,光標移動的速度又恢復了正常,不過沒有Vim模式的話很是彆扭。因此我就開始看看VSCode還有沒有其餘Vim模式的插件。因而我又試了另外兩個插件:vimStyleamVim。最終我選擇了後者。不只是支持的Vim命令更多,還有就是開發者的維護一直在繼續。並且很關鍵的一點,amVim的光標移動體驗就是 如絲般順滑typescript

不過它有個讓我很不習慣的地方:不支持:號調起VSCode的Command Line窗口,實現諸如:w保存,:wq退出等常見功能。這些功能在VSCodeVim裏是支持的。因而我就在想有沒有辦法「移植」一下VSCodeVim的功能到amVim來,既能保持光標移動體驗順滑,又能用上Command Line的一些經常使用命令。因此開啓了魔改模式,並在跟開發者的一系列交流後最終我提交的PR被merge了。 npm

本文記錄一下我第一次對VSCode插件(修改)開發的過程。

修改插件

開發前的準備

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-VSCodeasync

運行插件

用VSCode打開這個項目,點擊左側的debug能夠看到一個launch extension的配置:編輯器

運行它,你會獲得另一個窗口,這個就是能夠調試插件功能的窗口了:

改進插件

個人改進源碼在這裏:https://github.com/Molunerfinn/amVim-for-VSCode 做者合併以後作了一些修改,本文是以個人版本爲主。

爲了實現VSCodeVim經過:調起VSCode的inputBox效果,我須要翻閱一下VSCodeVim的源代碼。

大體效果以下:

在查看了amVimVSCodeVim在實現命令上的部分源碼後,發現兩者的實現上差距仍是不小的。不過相比VSCodeVim代碼的龐大(甚至還有neoVim的支持),amVim在實現上就比較精巧了。

在個人PR未被merge以前,amVim插件提供了一個功能,按:打開一個GoToLineinputBox

不過只能用於輸入數字並跳轉到相應行數。好在查看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');
    }

}
複製代碼

因此是經過vscodecommands來打開的gotoLineinputBox窗口。

再來看看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裏能夠大概分四個部分:

  1. src/Modes/Normal.ts做爲入口文件,當用戶輸入:鍵時觸發後續功能。【已有】
  2. src/Actions/CommandLine/CommandLine.ts做爲打開inputBox的入口函數,打開inputBox,而後負責把用戶輸入的內容傳給下一級的parser,用於解析並執行相應命令。
  3. src/Actions/CommandLine/Parser.ts,負責接收上一級傳進來的命令,而後找到命令對應的函數,並執行該函數。若是找不到相應則返回。
  4. src/Actions/CommandLine/Commands/*,存放各個命令的實現函數。

其中src/Actions/CommandLine/CommandLine.ts的邏輯跟VSCodeVimsrc/cmd_line/commandLine.ts很是相似。

具體實現

  1. src/Actions/CommandLine/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]
      };
  }
}
複製代碼
  1. src/Actions/CommandLine/Parser.ts
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;
    }
}
複製代碼
  1. 命令的實現

因爲命令不少,我就舉三個例子。一個是w,一個是q,和一個wq。VSCode本身的一些功能好比關閉當前文件、保存文件等都是有本身的command的。在實現Vim模式的時候,實際上最後也是去調用VSCode自帶的功能而已。

Write
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();
複製代碼
Quit
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();
複製代碼
WriteQuit
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();
複製代碼

這一步就頗有意思了,由於咱們以前實現了WriteQuit的功能,因此能夠在這裏調用它們。看到這裏你可能會有問題,雖然我知道VSCode有這些功能,可是你是怎麼知道這些功能是怎麼寫的呢?

若是隻是我這篇文章的話,我在實現Vim模式的這些命令的時候,大部分是參考了VSCodeVim的一些寫法。它主要的命令實如今src/cmd_line/commands/*裏。可是隻這樣顯然仍是不夠的。所以我給出幾個比較有用的地方供你們開發插件的時候參考:

  1. VSCode官方文檔裏的Extending Visual Studio Code,介紹擴展VSCode的原理和給出了一些例子。
  2. VSCode官方文檔裏的Extensibility Reference,介紹VSCode擴展的api文檔。
  3. VSCode官方文檔裏的Key Bindings for Visual Studio Code,介紹VSCode的快捷鍵和相應的命令id
  4. VSCode自己的快捷鍵編輯面板:

說實話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插件併發布到官方插件商店。也但願本文能給你帶來幫助。

相關文章
相關標籤/搜索