Vscodeの拡張機能を開発していつでも飯テロ画像を見れるようにしたい

## 初めに

VSCodeの拡張機能をTypescriptで作ってみました。
実行するとランダムな飯テロ画像を表示してくれる画期的なアプリです。

![スクリーンショット 2023-02-12 222707.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527164/51fe07e3-618e-304b-c8b1-5da0d9b37c0a.png)

公開できるほど整理されていないので、身内のみで楽しむ予定です。

## 開発環境
node：16.17.1

## 環境構築＆開発

### 環境構築

1. まず開発に必要なパッケージをインストールします。
    ```cmd
    npm install -g yo generator-code
    ```

1. インストールが終わったら、以下のコマンドでプロジェクトを生成しましょう。
    ```cmd
    yo code
    ```

    実行すると、以下のことが聞かれます。```->```は私の回答です。
    ```
    $ yo code

         _-----_     ╭──────────────────────────╮
        |       |    │   Welcome to the Visual  │
        |--(o)--|    │   Studio Code Extension  │
       `---------´   │        generator!        │
        ( _´U`_ )    ╰──────────────────────────╯
        /___A___\   /
         |  ~  |
       __'.___.'__
     ´   `  |° ´ Y `
    
    ? What type of extension do you want to create? -> New Extension (TypeScript)
    ? What's the name of your extension? -> test-extension
    ? What's the identifier of your extension? -> test-extension
    ? What's the description of your extension? -> 
    ? Initialize a git repository? -> No
    ? Bundle the source code with webpack? -> Yes
    ? Which package manager to use? -> npm
    ```

    プロジェクトが作成されると、VSCodeで開くか聞いてきます。
    このようなフォルダ構成が生成されています。環境構築については以上で完了となります。
    ![スクリーンショット 2023-02-12 223901.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527164/ade5e17a-bded-6ee8-7eff-8091682cb0e9.png)

    
### 環境構築後の動作確認

1. 動作確認をしてみましょう。実際に処理を書くファイルは```src\extension.ts```です。
    日本語に翻訳すると、以下のようなことが書かれています。

    ```typescript
    import * as vscode from "vscode";
    
    // この方法は、拡張機能がアクティブ化されたときに呼び出されます
    // 拡張機能がアクティブ化されますコマンドが最初に実行されたとき
    export function activate(context: vscode.ExtensionContext) {
      // コンソールを使用して診断情報（Console.log）とエラー（Console.Error）を出力します
      // このコード行は、拡張機能がアクティブ化されたときにのみ実行されます
      console.log('Congratulations, your extension "test" is now active!');
    
      // コマンドはpackage.jsonファイルで定義されています
      // 次に、登録コマンドでコマンドの実装を提供します
      // commandIdパラメーターは、package.jsonのコマンドフィールドに一致する必要があります
      let disposable = vscode.commands.registerCommand("test.helloWorld", () => {
        // ここに配置するコードは、コマンドが実行されるたびに実行されます
        // ユーザーにメッセージボックスを表示します
        vscode.window.showInformationMessage("Hello World from test!");
      });
    
      context.subscriptions.push(disposable);
    }
    
    //この方法は、拡張機能が無効になっているときに呼び出されます
    export function deactivate() {}
    ```

1. 試しに```F5```を押下すると、拡張機能を試すことができます。
    新しくVSCodeが立ち上がるので、```F1```でコマンドパレットを起動し、```Hello World```と入力してみましょう
    ※エラーが出てくると思いますが、無視します。
    ![スクリーンショット 2023-02-12 225450.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527164/95bdf607-f4e2-426a-f729-e7985326eafe.png)
    すると、右下にメッセージが出力されます。
    ![スクリーンショット 2023-02-12 225627.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527164/1edcf92b-7355-68f3-2b30-7a3fd62c7f1c.png)

    ```vscode.window.showInformationMessage("Hello World from test!");```の部分が実行されていますね。
    これにて環境構築後の動作確認は終了です。

### 実際に開発をしていく

今回作成するアプリは以下の手順で開発を進めます。
    1. ```showInformationMessage```にてメッセージを表示する
    2. Localに飯テロ画像を用意し、出力される対象を選択する
    3. 現在開いている別タブに```createWebviewPanel```にて選択された飯テロを表示する

1. まず```src\extension.ts```の```activate```メソッドを整理し、
    コマンドパレットで```meshi-tero```が呼び出された際に起動する関数を用意します。
    とりあえず```showInformationMessage```に出力するテキストを入力します。

    ```typescript
    export function activate(context: vscode.ExtensionContext) {
      let cmd = vscode.commands.registerCommand("meshi-tero", () => {
        customContext("meshi-tero", context);
      });
      context.subscriptions.push(cmd);
    }

    const customContext = (_key: string, context: vscode.ExtensionContext) => {
      vscode.window.showInformationMessage("飯の時間だ！！！");
    };
    ```

1. ```vscode.commands.registerCommand```の内容を変えたので```package.json```の内容も変えましょう。
    ここで呼び出されるコマンドが定義されています。

    ```js
    "activationEvents": [
        "onCommand:meshi-tero"
    ],
    "main": "./dist/extension.js",
    "contributes": {
        "commands": [
          {
            "command": "meshi-tero",
            "title": "meshi-tero"
          }
        ]
    },
    ```

1. 飯テロ画像を用意します。
1. では次に表示する画像をランダムに選択し、表示するファイルのURIを生成します。
    今回はとりあえず1~27個の画像を用意し、ファイルを番号順に命名したので```getImage()```の内容でランダムな画像を選択します。フォルダ内の一覧を持ってくるなどしたかったのですが、読み込めませんでした…
    方法はあると思いますが今回は諦めて、```vscode.Uri.joinPath```を使用しローカルファイルへのURIを生成します。
    引数に「拡張機能を含むディレクトリのURI＋フォルダパス＋フォルダ名」を置き、結合しています。

    また、```imageFilePath```で指定している```"images"```はプロジェクト直下に置いています。
    今回作ってわかりましたが、拡張機能の開発はローカルファイルへの参照に癖があります…

    ```typescript
    const getImage = () => {
      const min = 1;
      const max = 27;
      const index = Math.floor(Math.random() * (max + 1 - min)) + min;
    
      return `${index}.jpg`;
    };

    const customContext = (context: vscode.ExtensionContext) => {
      /** 画像ファイル置き場 */
      const imageFilePath = "images";
    
      /** メッセージ出力 */
      vscode.window.showInformationMessage("飯の時間だ！！！");
    
      /** ファイルパスを生成 */
      const onDiskPath = vscode.Uri.joinPath(
        context.extensionUri,
        imageFilePath,
        getImage()
      );
      /** ファイルパスのURIを生成 */
      const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
    };
    ```

1. ```createWebviewPanel```でWebViewのパネルを生成し、表示します。
    今回で追加した処理は```viewMeshiTeroImage()```と```vscode.window.createWebviewPanel```です。
    ```viewMeshiTeroImage()```では表示する内容をHTML形式で記載しています。

    ```vscode.window.createWebviewPanel```に関しては公式の解説を見るのが一番かと思います。
    [extension-guides webview](https://code.visualstudio.com/api/extension-guides/webview)
    
     最後に、```panel.webview.html```に読み込めば完成です！

    ```typescript
    /** 画像呼び出し用のHTML */
    function viewMeshiTeroImage(path: Uri) {
      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=${path} />
        </body>
      </html>`;
    }

    const customContext = (context: vscode.ExtensionContext) => {
      const imageFilePath = "images";
    
      /** メッセージ出力 */
      vscode.window.showInformationMessage("飯の時間だ！！！");
    
      /** WebViewのパネルを作成 */
      const panel = vscode.window.createWebviewPanel(
        "meshi",
        "meshi",
        vscode.ViewColumn.Beside,  // 新しいタブに生成
        {
          enableScripts: true,
          enableFindWidget: true,
          localResourceRoots: [
            vscode.Uri.joinPath(context.extensionUri, imageFilePath),
          ],
        }
      );
    
      /** ファイルパスを生成 */
      const onDiskPath = vscode.Uri.joinPath(
        context.extensionUri,
        imageFilePath,
        getImage()
      );
      /** ファイルパスのURIを生成 */
      const catGifSrc = panel.webview.asWebviewUri(onDiskPath);
    
      /** 別タブに画像を表示 */
      panel.webview.html = viewMeshiTeroImage(catGifSrc);
    };
    ```

### 最終的にできたコード

```typescript
import * as vscode from "vscode";
import { Uri } from "vscode";

export function activate(context: vscode.ExtensionContext) {
  let cmd = vscode.commands.registerCommand("meshi-tero", () => {
    customContext(context);
  });

  context.subscriptions.push(cmd);
}

/** 画像呼び出し用のHTML */
function viewMeshiTeroImage(path: Uri) {
  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=${path} />
    </body>
  </html>`;
}

/** 表示する画像をランダムに選定 */
const getImage = () => {
  const min = 1;
  const max = 27;
  const index = Math.floor(Math.random() * (max + 1 - min)) + min;

  return `${index}.jpg`;
};

/** meshi-tero実行 */
const customContext = (context: vscode.ExtensionContext) => {
  const imageFilePath = "images";

  /** メッセージ出力 */
  vscode.window.showInformationMessage("飯の時間だ！！！");

  /** WebViewのパネルを作成 */
  const panel = vscode.window.createWebviewPanel(
    "meshi",
    "meshi",
    vscode.ViewColumn.Beside,
    {
      enableScripts: true,
      enableFindWidget: true,
      localResourceRoots: [
        vscode.Uri.joinPath(context.extensionUri, imageFilePath),
      ],
    }
  );

  /** ファイルパスを生成 */
  const onDiskPath = vscode.Uri.joinPath(
    context.extensionUri,
    imageFilePath,
    getImage()
  );
  /** ファイルパスのURIを生成 */
  const catGifSrc = panel.webview.asWebviewUri(onDiskPath);

  /** 別タブに画像を表示 */
  panel.webview.html = viewMeshiTeroImage(catGifSrc);
};

// 拡張機能無効時発火
export function deactivate() {}
```

## パッケージ作成＆導入方法

### パッケージ作成

1. ローカルにてインストールできる形式に出力するために以下コマンドを起動します。
    ```npx vsce package``` 

    以下のことが聞かれると思います。
    - ReadMeが更新されていないよ！
        - 適当に更新してあげてください。
    - LICENSEに関する文章がないよ！
        - 無視して大丈夫です。(yを入力)

1. プロジェクト直下に```.vsix```ファイルが生成されます。
    この生成されたファイルをインストールすることで拡張機能が使えるようになります。

### 導入＆使い方

1. vs-code の拡張機能にて、「VSIX からのインストール...」を選択し、インストールします
    ![スクリーンショット 2023-02-14 224040.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527164/f4eca847-3b38-80d2-f844-7c9c916c3680.png)

1. F1 キーにてコマンドパレットを起動し、「meshi-tero」と入力する
   ![スクリーンショット 2023-02-14 224144.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527164/25058205-22ca-3eaa-cf19-ada3db0339ee.png)

1. (∩´∀｀)∩ﾜｰｲ
    ![スクリーンショット 2023-02-14 224504.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527164/6ad43b0c-4090-96ae-d29d-1c6e570a8fa9.png)



## 終わりに

ネタアプリでしたが、思ったより簡単にVSCodeの拡張機能を作成することができました。
ローカルファイルへの参照に癖があり少し工夫が必要ですね。
タイマー起動やファイルSave時の起動にも興味があります、いつかやってみたいです。

また、ReadMe.mdにローカルの画像を使用したかったのですが現在はまだ対応していないようです…

**記事の内容に間違いがありましたらご連絡ください！**

## 参考サイト・書籍
- [Extension API get-started](https://code.visualstudio.com/api/get-started/your-first-extension)
- [Loading local content](https://code.visualstudio.com/api/extension-guides/webview#loading-local-content)