VSCode Extensions(拡張機能) 自作入門 〜VSCodeにおみくじ機能を追加する〜

皆さん、初めましての方は初めまして。
アルバイトとしてN予備校の教材制作補助に携わっている者です(2021年12月現在)。
昨年は[自作の Web アプリ](https://note.com/kaisugi/n/na68af9dd700d)に関する記事を投稿しました。

今年は **VSCode Extensions(拡張機能)** に関する記事を書きたいと思います。
VSCode はそれ単体で使っても十分便利なエディタですが、拡張機能を追加することでさらにパワーアップします。例えば

- ファイルの種類をもっと分かりやすいアイコンで表示する [vscode-icons](https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons)
- ファイルを開いているときに Git の変更履歴も表示する [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)

などが私のお気に入りです。

こうした拡張機能は、既に完成されたものをインストールするだけでなく自分でオリジナルのものを作ることもできます（実はN予備校の教材制作の裏でも独自の拡張機能が暗躍（？）しています）。しかしながら、いざ開発に取り組もうとすると、日本語ドキュメントの少なさや VSCode の公式ドキュメントの分かりづらさに苦労させられることでしょう。

今回は、簡単なサンプルとして **「おみくじ」の拡張機能** を作成する過程を示すことで、VSCode 拡張の自作がより身近なものになればと思っています。

## 今回最終的にできあがるもの

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/852bc244-7ece-44f9-73ab-f87cf72f87fe.gif)

中身としては、右下の「おみくじを引く」ボタンをクリックし、名前を入力すると、自分の運勢を表示してくれるというシンプルなものになります。VSCode 拡張を作るときに使いたくなりそうな機能を詰め込んでみました。


## 拡張機能のひな形を準備する

まず、**ローカル環境で** Node.js が動く環境を準備してください。直接[公式サイト](https://nodejs.org/ja/)からインストーラ経由で入れてもいいですし、[nvm](https://github.com/nvm-sh/nvm) のようなバージョン管理ツールを使うのでもいいでしょう。

Node.js が正しくインストールされると、Node.js のパッケージマネージャーである npm も同時にインストールされます。そうしたら

```bash
npm install -g yo generator-code
```

でひな形作成に必要なツールを入れます。
インストールが終わったら、適当な場所で

```bash
yo code
```

と入力し、拡張機能のひな形の作成を開始します。

**What type of extension do you want to create?** では **New Extension (TypeScript)** を選択して Enter を押します。
**What's the name of your extension?** では、**vscode-omikuji** と入力しておきます。
**What's the identifier of your extension?** ではそのまま Enter を押します。
**What's the description of your extension?** では、適当に **Omikuji in VSCode.** としておきます。
**Initialize a git repository?** はどちらでも構いませんが、ここでは **Y**(Yes) にしておきます。
**Bundle the source code with webpack?** は **Y**(Yes) にしておきます。
**Which package manager to use?** はどちらでも構いませんが、ここでは **npm** にしておきます。

これで、`vscode-omikuji` というフォルダ名でひな形作成が始まります。少し待つと **Do you want to open the new folder with Visual Studio Code?** が表示されるので **Open with `code`** を選択しておきます。
すると、VSCode が自動的に `vscode-omikuji` フォルダを開きます。

## ひな形のファイルを確認する

では、`src` というディレクトリの下にある `extensions.ts` を開いてみましょう。（元のひな形作成ツールのバージョンにより多少異なっているかもしれませんが）大体以下のようなファイル内容になっているかと思います。

```src/extensions.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 "vscode-omikuji" 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('vscode-omikuji.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 vscode-omikuji!');
	});

	context.subscriptions.push(disposable);
}

// this method is called when your extension is deactivated
export function deactivate() {}
```

なにやら英語のコメントがたくさんありますね。英語圏のユーザーにはとても親切な内容なのかもしれませんが、日本人にとっては逆に怖いですね...。ですが、実装されている内容は非常に単純です。

まず、

```ts
export function activate(context: vscode.ExtensionContext) {
  ...
}
```

という関数は、拡張機能が **有効になった時に** に呼び出される関数です。なので、処理はこの関数の中身に書けばよいです。
中身の処理は、コメント部を除くと

```ts
  console.log('Congratulations, your extension "vscode-omikuji" is now active!');
  let disposable = vscode.commands.registerCommand('vscode-omikuji.helloWorld', () => {
    vscode.window.showInformationMessage('Hello World from vscode-omikuji!');
  });
  context.subscriptions.push(disposable);
```

となっています。`console.log()` はおなじみの標準出力です。その下にいくつかポイントがあります。

`vscode.commands.registerCommand()` は拡張機能のコマンドを定義する関数です。第一引数にコマンドの名前（ここでは `vscode-omikuji.helloWorld`）、第二引数にそのコマンドの処理内容を書きます。返り値が `disposable` という変数に入っていますが、これを `context.subscriptions.push()` に追加することで、定義されたコマンドが実際にその拡張機能で使えるようになります。
もちろん、わざわざ `disposable` という変数を置かずに

```ts
  context.subscriptions.push(
    vscode.commands.registerCommand('vscode-omikuji.helloWorld', () => {
      vscode.window.showInformationMessage('Hello World from vscode-omikuji!');
    })
  );
```

と書いても同じことです。

`vscode.window.showInformationMessage()` は、VSCode の画面で、その引数内容のメッセージボックスを表示する関数です。


次に、`package.json` を開いて見ましょう。**VSCode 拡張機能開発においては、 `package.json` は単なる依存モジュール一覧ではなく、コマンドに関する重要な情報も持ちます。**

```json
  "activationEvents": [
    "onCommand:vscode-omikuji.helloWorld"
  ],
```

ここでは、`vscode-omikuji.helloWorld` というコマンドが呼び出された際に拡張機能が有効になることを示します。

```json
  "contributes": {
    "commands": [
      {
        "command": "vscode-omikuji.helloWorld",
        "title": "Hello World"
      }
    ]
  },
```

ここでは、`vscode-omikuji.helloWorld` というコマンドには `Hello World` という名前が与えられていることを示します。


## 拡張機能をデバッグする

それでは、拡張機能がきちんと動くかどうかを確かめるために **デバッグ** してみましょう。デバッグは **F5キー**、あるいは VSCode の **三角形のボタンのパネルから緑色の三角形をクリック** することで行えます。ここで警告等が出ても一旦無視して「続ける」をクリックしてください。

すると、**[拡張機能開発ホスト]** という文字がタイトルに入った VSCode のウィンドウが新しく開くはずです。これが、**今あなたが作ろうとしている拡張機能をその場で試せる** ウィンドウになります。

それではコマンドパレットからコマンドを呼び出して見ましょう。**Ctrl(Command) + Shift + P** を入力してコマンドパレットを開き、`Hello World` と入力してください。うまくいけば、以下のようにコマンドがサジェストされます。

![スクリーンショット 2021-11-03 22.30.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/a3f7764f-f950-5e25-36d6-83838c9b2716.png)

Enter を押すと、右下にメッセージが表示されます。これが先ほど紹介した `vscode.window.showInformationMessage()` の機能になります。

![スクリーンショット 2021-11-03 22.31.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/328960f0-67d4-cd14-0b49-20e6a85a02c3.png)

また、元々あった方の VSCode のウィンドウ（オレンジ色に光っているはずです）について、デバッグコンソールを確認すると、`Congratulations, your extension "vscode-omikuji" is now active!` という文字列があるのが分かります。これは先ほどのコードの `console.log()` の中身なので、`console.log()` によるデバッグもうまく行えていることが分かりました。

![スクリーンショット 2021-11-03 22.33.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/f26c0d62-1853-7987-a510-a826f0373949.png)

デバッグを終了したいときは、新しく開いた方のウィンドウを閉じれば自動的に終了します。

## おみくじのメッセージを表示させる

ひな形についての理解が深まったところで、本題のおみくじを実装していきましょう。まず、`src/extensions.ts` を以下のように書き換えます。

```src/extensions.ts
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  
  context.subscriptions.push(
    vscode.commands.registerCommand('vscode-omikuji.omikuji', () => {

      const omikujiCandidates = ['大吉', '吉', '中吉', '小吉', '末吉', '凶', '大凶'];
      const omikujiResult = omikujiCandidates[Math.floor(Math.random() * omikujiCandidates.length)];

      vscode.window.showInformationMessage(`あなたの運勢は${omikujiResult}です！`);
    })
  );
}
```

`vscode-omikuji.omikuji` という名前のコマンドを実装してみました。運勢を乱数で決め、表示させています。

`package.json`の方も合わせて変更しておきましょう。

まず、これまでの実装では、拡張機能が有効になるのは `vscode-omikuji.helloWorld` というコマンドが呼び出された際でしたが、これを **VSCode が立ち上がったらいつでも有効になる** ように変えます。このためには、ワイルドカードである `*` を用いれば良いです。

```diff:package.json
   "activationEvents": [
-    "onCommand:vscode-omikuji.omikuji"
+    "*"
   ],
``` 

なお、この他の拡張有効化タイミング設定のオプションは以下をご覧ください。

https://code.visualstudio.com/api/references/activation-events

また、コマンドの方も新しい内容に更新しておきます。

```diff:package.json
   "contributes": {
     "commands": [
       {
-        "command": "vscode-omikuji.helloWorld",
-        "title": "Hello World",
+        "command": "vscode-omikuji.omikuji",
+        "title": "おみくじを引く"
       }
     ]
   },
```

実装できたら、デバッグして確認してみましょう。うまくいけば以下のように表示されるはずです。

![スクリーンショット 2021-11-04 9.16.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/8d54d7f8-930e-7645-94d8-7452af1a3c17.png)

![スクリーンショット 2021-11-04 9.16.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/dd260aa8-c909-1948-3d53-26a362c3b0f0.png)

## vscode.window.createStatusBarItem() を使って、ステータスバーにボタンを表示させる

毎回おみくじを引くときに Ctrl(Command) + Shift + P でコマンドパレットを開くのは面倒なので、ボタンを押すだけで実行できるようにしましょう。
`src/extensions.ts` に以下の内容を追記します。

```diff:src/extensions.ts
       vscode.window.showInformationMessage(`あなたの運勢は${omikujiResult}です！`);
     })
   );
+  const button = vscode.window.createStatusBarItem(
+    vscode.StatusBarAlignment.Right, 
+    0
+  );
+  button.command = 'vscode-omikuji.omikuji';
+  button.text = 'おみくじを引く';
+  context.subscriptions.push(button);
+  button.show();
 }
```

`vscode.window.createStatusBarItem()` でステータスバーのメニューアイテムを追加することができます。第一引数にボタンの位置、第二引数にボタンの優先度（値が大きいほど左側に表示される）を受け取ります。詳しくは以下の API ドキュメントもご参照ください。

https://code.visualstudio.com/api/references/vscode-api#window

デバッグして確認してみましょう。右下に「おみくじを引く」というボタンが表示されていますね。これをクリックすれば、すぐにおみくじの結果が確認できるようになりました。

![スクリーンショット 2021-11-04 10.09.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/8bd7c24a-526b-163e-ba84-bf7809d03d28.png)


## vscode.window.showInputBox() を使って、入力を受け取る

ここまでで既におみくじらしきものは出来上がっていますが、メッセージの「あなたの運勢は...」という部分が何となく味気ないですね。なので、今度は「あなた」の部分に入力した名前が入るようにしてみましょう。

```diff:src/extensions.ts
-    vscode.commands.registerCommand('vscode-omikuji.omikuji', () => {
+    vscode.commands.registerCommand('vscode-omikuji.omikuji', async () => {

       const omikujiCandidates = ['大吉', '吉', '中吉', '小吉', '末吉', '凶', '大凶'];
       const omikujiResult = omikujiCandidates[Math.floor(Math.random() * omikujiCandidates.length)];
+      const name = await vscode.window.showInputBox({
+        title: 'あなたの名前は？'
+      });
-      vscode.window.showInformationMessage(`あなたの運勢は${omikujiResult}です！`);
+      if (name !== undefined) {
+        vscode.window.showInformationMessage(`${name}さんの運勢は${omikujiResult}です！`);
+      }
```

`vscode.window.showInputBox()` は、ユーザーからの入力を待って、入力された値を返す関数です。非同期処理になるので、ここでは `await` で待っておきます。
注意点としては、この操作の途中に Esc キーを押したりして操作が中断された場合は `undefined` が返ってくることです。VSCode で TypeScript を使ってコードを書いていれば、変数にカーソルを当てるだけでその型もわかるので、こういったトラップにも未然に気づくことができ便利ですね。

![スクリーンショット 2021-11-06 11.29.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/f9a4fbb5-bc8a-86a2-81ba-c42196ccb3a2.png)

では、デバッグして確認してみましょう。「おみくじを引く」というボタンを押すと、以下のような入力ボックスがウィンドウ上部に表示されます。

![スクリーンショット 2021-11-06 11.32.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/3300d926-4a89-fab1-ab1d-4b7e5c032ecd.png)

ここで入力した名前に応じて、おみくじの結果表示の名前の部分も変わります。

![スクリーンショット 2021-11-06 11.32.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/f456dd58-dc48-8676-4583-6e500f35945a.png)

なお、今回は「あなたの名前は？」というタイトルを表示させるのを `{title: 'あなたの名前は？'}` というオブジェクトを入れることにより実現しましたが、他にも、あらかじめ決まった値を入力しておくことなどもできます。詳しくは、以下の `InputBoxOptions` のドキュメントをご覧ください。

https://code.visualstudio.com/api/references/vscode-api#InputBoxOptions

## vscode.window.createWebviewPanel() を使って、HTML のプレビューを表示させる

おみくじをさらに視覚的に楽しむために、[Webview API](https://code.visualstudio.com/api/extension-guides/webview) を使って画面を表示させてみましょう。

```diff:src/extension.ts
 export function activate(context: vscode.ExtensionContext) {
+
+  const omikujiFigures = {
+    '大吉': 'https://2.bp.blogspot.com/-q62M2UFmkwQ/UZNyKgFFdNI/AAAAAAAASiI/_cZv1Si4jWE/s400/syougatsu2_omikuji.png',
+    '吉': 'https://2.bp.blogspot.com/-27IG0CNV-ZE/VKYfn_1-ycI/AAAAAAAAqXw/fr6Y72lOP9s/s400/omikuji_kichi.png',
+    '中吉': 'https://3.bp.blogspot.com/-_z-n-7gO3KA/T3K7MU3MdGI/AAAAAAAAE-k/8qs-jxqS4LE/s400/omikuji_chuukichi.png',
+    '小吉': 'https://3.bp.blogspot.com/-nZt5pjGWT9E/T3K7TJ4wEZI/AAAAAAAAE_E/c1X2-N54EYo/s400/omikuji_syoukichi.png',
+    '末吉': 'https://3.bp.blogspot.com/-JLNa8mwZRnU/T3K7StR-bEI/AAAAAAAAE-8/rQrDomz5MSw/s400/omikuji_suekichi.png',
+    '凶': 'https://4.bp.blogspot.com/-qCfF4H7YOvE/T3K7R5ZjQVI/AAAAAAAAE-4/Hd1u2tzMG3Q/s400/omikuji_kyou.png',
+    '大凶': 'https://2.bp.blogspot.com/-h61ngruj0tE/T3K7RDUWmPI/AAAAAAAAE-0/KXtPY8fDwco/s400/omikuji_daikyou.png'
+  };
+
+  function getWebviewContent(omikujiResult: keyof typeof omikujiFigures) {
+    return `<!DOCTYPE html>
+  <html lang="ja">
+  <head>
+      <meta charset="UTF-8">
+      <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  </head>
+  <body>
+      <img src="${omikujiFigures[omikujiResult]}" />
+  </body>
+  </html>`;
+  }

   context.subscriptions.push(
     vscode.commands.registerCommand('vscode-omikuji.omikuji', async () => {
-      const omikujiCandidates = ['大吉', '吉', '中吉', '小吉', '末吉', '凶', '大凶'];
+      const omikujiCandidates = ['大吉', '吉', '中吉', '小吉', '末吉', '凶', '大凶'] as const;
       const omikujiResult = omikujiCandidates[Math.floor(Math.random() * omikujiCandidates.length)];

       const name = await vscode.window.showInputBox({
         title: 'あなたの名前は？'
       });

       if (name !== undefined) {
-        vscode.window.showInformationMessage(`${name}さんの運勢は${omikujiResult}です！`);
+        const panel = vscode.window.createWebviewPanel(
+          'omikuji',
+          `${name}さんの運勢は${omikujiResult}です！`,
+          vscode.ViewColumn.One,
+          {}
+        );
+
+        panel.webview.html = getWebviewContent(omikujiResult);
       }
```

実装内容を解説します。

`omikujiFigures` は、おみくじの結果の内容から、その内容のいらすとや画像 URL を返すためのオブジェクトです。
`getWebviewContent` は Webview で表示させる HTML の中身になります。
`vscode.window.createWebviewPanel()` は Webview パネルを作成する関数になります。この返り値の `panel` に対し、 `panel.webview.html = (表示させたいHTML)` と代入することで、実際にその HTML が表示されるようになります。
なお、`vscode.window.createWebviewPanel()` の第三引数で `vscode.ViewColumn.One` の代わりに `vscode.ViewColumn.Two` を指定すると、画面が縦に二分割され、その右側にパネルが表示されるようになります（ソースコードを見ながら何らかの画面を確認したい場合に便利です）。

デバッグすると、冒頭のデモ通りに画面が動くはずです。

Webview API に関しては非常に多彩な機能が用意されていますが、以下のドキュメントはその使い方についてよくまとまっているので、Webview をやってみたい方は一読することをオススメします。特に、セキュリティの観点上、 PC 内のローカルファイルを読み込む際には一工夫必要（`vscode-resource:` という文字列をパスの前につける必要がある）なので注意してください。

https://code.visualstudio.com/api/extension-guides/webview

## 自作した拡張機能を .vsix ファイルにして配布する

最後に、作った拡張機能を他の人が使えるようにしてみましょう。

まず、ディレクトリ内にある `README.md` を編集します。これをテンプレートのまま放置していると後で怒られてしまいます。

```markdown:README.md
# vscode-omikuji

VSCode でおみくじ！
```

適当に書いてみました。
そうしたら、プロジェクトのディレクトリ（今回は `vscode-omikuji` の下）において、以下のコマンドを入力します。

```
npx vsce package
```

途中で何か質問が来ても y (Yes) で答えておきましょう。
すると、うまくいけば以下のような表示になるはずです。

```
 DONE  Packaged: /***/vscode-omikuji/vscode-omikuji-0.0.1.vsix (6 files, 3.34KB)
```

これで、拡張子 `.vsix` のファイルが完成しました。
作成した拡張機能をインストールしたい場合は、VSCode の「拡張機能」タブから右上のアイコンをクリックし、「VSIXからのインストール...」をクリックして、`.vsix` のファイルを選択します。

![スクリーンショット 2021-11-06 21.23.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233069/ef4dd0e2-fa1b-36d2-052c-10afbf582f33.png)

これで、みんなが VSCode でおみくじを引けるようになりましたね。

## 自作した拡張機能を VSCode Extension MarketPlace で公開する

世界中の人に自分が作った拡張機能を使ってもらいたい場合は、vsix ファイルを配るのではなく [MarketPlace](https://marketplace.visualstudio.com/vscode) で公開するのがよいでしょう。

この記事では詳細は省きますが、以下のサイトなどに手順が詳述されています。

https://code.visualstudio.com/api/working-with-extensions/publishing-extension

https://ysss.hateblo.jp/entry/20200710/1594307803

ちなみに私も [Markdown Table Maker](https://marketplace.visualstudio.com/items?itemName=hellorusk.markdown-table-maker) という拡張機能を昔自作したことがあり、今現在 1,000 回以上インストールされています。インストール回数が見れるのは励みになりますね。


## おわりに

今回は VSCode 拡張機能の作成手順について概観していきました。普段の何気ない面倒な作業を拡張機能で効率的に処理できるようになるといいですね。