VSCodeでオレオレExtensionを作ってみました。
参考にした情報や手順をまとめます。

Table of Contents

はじめに

想定する読者

VSCodeでこれからExtensionを作ろうと思っている方 を対象にしています。

また、以下の知識があることを前提にしています。

  • TypeScript
  • npm
  • VSCode
  • Promise
  • async/await

実装する機能

Markdownの見出し(#を使わない方)を自動で挿入する機能を作ります。

全角文字は2つ、半角文字は1つでカウントします。

成果物について

GitHubに公開しています。

Marketplaceには公開していません。内容があまりに特化しすぎているからです。
利用する場合はGitHubのREADMEをご覧下さい。

参考文献

基本的に公式ドキュメントの内容をかみ砕いているだけです。
英語に抵抗無い方は公式ドキュメントを読んだ方がいいと思います。

準備

npmVSCodeが必要です。

Extensionプロジェクトの作成

公式ドキュメントでは以下のページに対応しています。

Yeomanのインストール

Yeomanを使うのでgeneratorと共にインストールします。

$ npm install -g yo generator-code
npm ERR! write after end

npmをupgradeしたらなおりました。

$ npm install -g npm@latest
$ npm --version
6.4.1

雛形の生成

$ yo code

上記コマンドを実行するといくつか質問されます。
今回のプロジェクトは以下の様に回答しています。

? What type of extension do you want to create? New Extension (TypeScript)
? What's the name of your extension? hello
? What's the identifier of your extension? hello
? What's the description of your extension?
? What's your publisher name (more info: https://code.visualstudio.com/docs/tools/vscecli#_publishing-extensions)? hello
? Enable stricter TypeScript checking in 'tsconfig.json'? Yes
? Setup linting using 'tslint'? Yes
? Initialize a git repository? Yes

プロジェクトタイプをTypeScriptにしているのが重要なポイントです。

helloディレクトリが作成されていれば成功です。

VSCodeで動作確認する

VSCodeで先ほど作成したhelloディレクトリを開きましょう。
そのままデバッグ実行すると、新しいVSCodeが別のウィンドウで立ち上がるはずです。

コマンドパレットでHello Worldと検索し、見つかったコマンドを実行しましょう。
VSCode下部にメッセージが表示されれば成功です。

プロジェクト構成

ここからはサンプルをベースに作成したowlcodeのコードを例として紹介していきます。
まずはプロジェクトを構成するファイルの中から重要なものだけ紹介します。

package.json

以下のセクションが大事です。

contributes

コマンドの定義などを記載します。

    "contributes": {
        "commands": [
            {
                "command": "extension.headerLV1",
                "title": "Add header LV1"
            },
            {
                "command": "extension.headerLV2",
                "title": "Add header LV2"
            }
        ],
        "keybindings": [
            {
                "command": "extension.headerLV1",
                "key": "alt+shift+-"
            },
            {
                "command": "extension.headerLV2",
                "key": "alt+-"
            }
        ]
    },

上記commands配下のプロパティの意味です。

  • command
    • Extensionのソースコード内で使用する識別子
  • title
    • VSCode利用時にコマンドパレットで表示されるコマンド名

keybindings配下のプロパティの意味です。

  • command
    • Extensionのソースコード内で使用する識別子
  • key
    • デフォルトのkey binding

GitLensの設定が大変参考になりました。

activationEvents

Extensionが読み込まれるタイミングのイベントを指定できます。

    "activationEvents": [
        "onCommand:extension.headerLV1",
        "onCommand:extension.headerLV2"
    ],

onCommand:は特定のコマンドが実行される直前です。
必ず読み込むために*を指定しても良さそうです。

    "activationEvents": [
        "*"
    ],

src/extension.ts

activateの中にcommandを登録します。

export function activate(context: vscode.ExtensionContext) {
    const register = vscode.commands.registerCommand;

    context.subscriptions.push(
        register('extension.headerLV1', async () => await setHeader("=")),
        register('extension.headerLV2', async () => await setHeader("-")),
    );
}

上の例ではextension.headerLV1extension.headerLV2のコマンドを登録しています。
それぞれsetHeaderが異なる引数で呼び出されます。

実装

エディタ操作のIFをラップする

vscodeが提供するデフォルトのIFでは記述量が増えてしまうため、editor.tsというラッパ層を作りました。

editor.ts
'use strict'
import * as vscode from 'vscode'

const edit = (editor: vscode.TextEditor, editFunc: (editBuilder: vscode.TextEditorEdit) => void): Promise<{}> =>
    new Promise((resolve, reject) => {
        editor.edit(editBuilder => {
            editFunc(editBuilder)
            resolve()
        })
    })

export const getActiveLineText = (editor: vscode.TextEditor): string =>
    editor.document.lineAt(editor.selection.active.line).text

export const getSelectionText = (editor: vscode.TextEditor): string =>
    editor.document.getText(editor.selection)


export const replaceActiveLine = async (editor: vscode.TextEditor, str: string): Promise<{}> =>
    await edit(editor, editBuilder => {
        const activeLine = editor.document.lineAt(editor.selection.active.line).range
        editBuilder.replace(activeLine, str)
    })

export const replaceSelection = async (editor: vscode.TextEditor, str: string): Promise<{}> =>
    await edit(editor, editBuilder => editBuilder.replace(editor.selection, str))


export const insertNextLine = async (editor: vscode.TextEditor, str: string): Promise<{}> =>
    await edit(editor, editBuilder => editBuilder.insert(
        new vscode.Position(editor.selection.active.line, 999), str)
    )

今回は使っていないIFもありますが、利用機会が多そうなものは実装しています。

実現する機能を作る

今回実現する機能をextension.tsに実装します。

extension.ts
'use strict'
import * as vscode from 'vscode'
import { insertNextLine, getActiveLineText, getSelectionText, replaceSelection } from './editor'

// Shift_JIS: 0x0 ~ 0x80, 0xa0 , 0xa1 ~ 0xdf , 0xfd ~ 0xff
// Unicode : 0x0 ~ 0x80, 0xf8f0, 0xff61 ~ 0xff9f, 0xf8f1 ~ 0xf8f3
const getBytes = (c: number | undefined): number =>
    c === undefined ? 0 :
        (c >= 0x0 && c < 0x81) || (c === 0xf8f0) || (c >= 0xff61 && c < 0xffa0) || (c >= 0xf8f1 && c < 0xf8f4) ?
            1 : 2

const countLength = (str: string): number => str.split('')
        .map(x => getBytes(x.codePointAt(0)))
        .reduce((x, y) => x + y)


function setHeader(symbol: string) {
    const editor = vscode.window.activeTextEditor
    if (!editor) {
        return
    }

    const titleLength = countLength(getActiveLineText(editor))
    insertNextLine(editor, `\n${symbol.repeat(titleLength)}`)
}

export function activate(context: vscode.ExtensionContext) {
    const register = vscode.commands.registerCommand

    context.subscriptions.push(
        register('extension.headerLV1', async () => await setHeader("=")),
        register('extension.headerLV2', async () => await setHeader("-")),
    )
}

export function deactivate() {
}

setHeaderで指定された文字の繰り返しを次の行に挿入します。
ただし、現在行の文字カウント(全角は2)と一致するように。

動作確認して上手く動けばOKです。

Extensionのローカルインストール

Marketplaceには公開しないので直接ローカルにインストールします。
Windowsの場合は%USERPROFILE%\.vscode\extensions配下にプロジェクトディレクトリをぶっ込むだけでOKです。

今回はインストール用のbatを作成しました。

git clone https://github.com/tadashi-aikawa/owlcode.git %HOMEPATH%\.vscode\extensions\owlcode ^
  && cd %HOMEPATH%\.vscode\extensions\owlcode ^
  && npm i ^
  && npm run compile

アップデート用は以下です。

cd %HOMEPATH%\.vscode\extensions\owlcode ^
  && git pull ^
  && npm i ^
  && npm run compile
実行に必要な最小限のファイルは...

今回はdependenciesが無いため、以下2ファイルがあれば動くはずです。

  • package.json
  • ./out/extension (package.jsonmainで指定されているファイル)

とはいえ高々数十MBなので、更新時のことも考えてプロジェクトを丸丸取得するようにしています。

総括

VSCodeでExtensionを作成して、Marketplaceに公開せずインストールする方法を紹介しました。

公式ドキュメントには他のサンプルやAPIドキュメントがまだまだ沢山あります。
VSCode利用時に欲しい機能ができたら、都度owlcodeに実装していきたいと思います。