【VSCode拡張機能開発に入門】anyを使うとプレバトの先生が叱ってくれる拡張機能

## はじめに

自分は2021年に新卒でWeb系の開発会社にフロントエンジニアとして入社し2022年で2年目になります。[クソアプリカレンダー](https://qiita.com/advent-calendar/2022/kuso-app)の14日目を担当します。

今回はTypeScriptでもお馴染みの「**any**」を使うとプレバトのあの先生が赤ペンを入れてくれるVSCode拡張機能の「**any-checker**」を開発しました。

## 成果物
下記のように``any``を記述した状態で拡張機能を起動しコードを保存すると、
![スクリーンショット 2022-12-12 22.36.55.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/64af4bbb-464f-da6c-031b-d2fd00ebcf2f.jpeg)

[プレバト](https://www.mbs.jp/p-battle/)でもお馴染みの[あの先生](https://ja.wikipedia.org/wiki/%E5%A4%8F%E4%BA%95%E3%81%84%E3%81%A4%E3%81%8D)が叱って``any``に赤ペンを入れてくれます。
![スクリーンショット 2022-12-12 22.39.28.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/166c4d74-11d9-7d8d-6e54-f72fdc351799.jpeg)

![スクリーンショット 2022-12-12 22.39.57.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/cbaae247-0040-4f3f-cc5c-510c4cb39812.jpeg)

``any``を全て消して保存をすると褒めて来れます。
![スクリーンショット 2022-12-12 22.42.42.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/1db14192-fc8c-8499-6567-e7913afdee49.jpeg)

## この記事で学べること

- VSCode拡張機能の開発方法を基礎から学べる

クソアプリといえど何かしらの学びを共有したいので、開発方法を基礎から解説します。

これからVSCode拡張機能開発に挑戦してみたいしてみたい方はぜひ参考にしていただければと思います。

## おことわり
- あくまで**クソアプリ**なのでアプリの実用性の保証はないです(any依存症を治せるかも)
- 今後追加したい機能については最後の章に追記しています
- 「完成」を第一に考えていたので紹介するコードにはまだリファクタリングに余地があります

なおVSCode拡張機能まとめの記事も書いているので合わせて読んでいただけると嬉しいです。

https://qiita.com/KNR109/items/5f933df1292564e6dc70

##  環境構築と準備

まず初めに公式ドキュメントにも掲載されているVSCodeの拡張機能チュートリアルを解説していきます。

### 環境構築

公式ドキュメントに従って簡易的な拡張機能をまずは開発していきます。

https://code.visualstudio.com/api/get-started/your-first-extension

まず拡張機能開発に必要なツールを``npm``でインストールします。

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

次にVSCodeの拡張機能開発をするための雛形を作成します。該当のディレクトリ上で下記のコマンドを実行します。

```
$ yo code
```

下記の質問が出てくるので回答していきます。

```
What type of extension do you want to create?
--New Extension (TypeScript)を選択

What's the name of your extension?
--拡張機能名を入力 (今回はany-checker)

What's the identifier of your extension?
--何も入れずにエンター

What's the description of your extension?
--何も入れずにエンター

Initialize a git repository?
--yes

Bundle the source code with webpack?
--yes

Which package manager to use?
-- npmを選択 (yarnでも大丈夫です)
```

インストール中に下記の質問がされます。
```
Do you want to open the new folder with Visual Studio Code?
--Open with `code`を選択します
```

下記のディレクトリが作成されていればOKです。

![スクリーンショット 2022-12-13 8.58.09.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/912cb9ca-d029-1721-0184-09b36ae9a831.jpeg)

### Hello Worldを表示する

VSCodeのメニューにHelloを追加し、選択すると「Hello World」という文字が出てくるような拡張機能を最初に開発していきます。

![スクリーンショット 2022-12-13 10.07.46.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/d16340bf-11b1-1409-19bc-788564e0e2e2.jpeg)

![スクリーンショット 2022-12-13 10.09.04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/bfcea29e-88e7-d0f4-3be8-c5d8688b588d.jpeg)

VSCodeの拡張機能開発は``src/extension.ts``と``package.json``の2つのファイルに処理記述をしていきます。

#### package.jsonの記述

``package.json``には処理を呼び出すための操作を記述していきます。

今回はメニューに「Hello」と追加してクリックされた際に``extension.ts``(後で記述)の処理が実行されるような設定を記述します。

【該当箇所のみ抜粋】
```json:package.json
"contributes": {
    "commands": [
      {
        "command": "vscode-context.Hello",
        "title": "Hello"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "when": "editorHasSelection",
          "command": "vscode-context.Hello",
          "group": "myGroup@1"
        }
      ]
    }
  },
```

- ``commands``で``⇧⌘P``(F5)を押した際に呼び出すコマンドを設定します。
- ``menus``でVSCodeのメニューに表示されるコマンドを設定します。

コンパイル(後で解説)すると下記のように確認できます。

【⇧⌘Pを押した時】
![スクリーンショット 2022-12-13 10.18.07.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/db5539d7-c739-d742-41ad-4b227f83f436.jpeg)

【メニュー】
![スクリーンショット 2022-12-13 10.19.09.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/0308b8a7-f979-c6ff-61a4-9232aa6892fd.jpeg)

#### extension.tsの記述

次に先ほどpackage.jsonに登録したHelloコマンドを実行した時に、行われる処理を記述していきます。

```ts:src/extension.ts
import * as vscode from "vscode";
export function activate(context: vscode.ExtensionContext) {
  // package.jsonに追記したコマンドを設定(今回は「vscode-context.Hello」)
  let cmd = vscode.commands.registerCommand("vscode-context.Hello", () => {
    customContext("vscode-context.Hello");
  });
  context.subscriptions.push(cmd);
}

// コマンドの内容はkeyに格納されている
const customContext = (key: string) => {
  // Helloコマンドが実行された場合の処理
  if ((key = "vscode-context.Hello")) {
    vscode.window.showInformationMessage("Hello World");
  }
};

export function deactivate() {}
```

#### コンパイルと動作確認

記述した内容をコンパイルしVSCodeのデバックで動作がうまくいっているかを確認します。

```
$ npm run compile
```

``⇧⌘P``を押して「``Debug: Debug npm Script`` > ``vscode:prepublish``」を選択します。

![スクリーンショット 2022-12-13 10.22.15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/3580c911-d36a-8fb5-49ca-338c50e09158.jpeg)

するとVSCodeの左下に「Run Extension」が表示されるのでクリックします。(別タブでVSCodeが開かれる)

![スクリーンショット 2022-12-13 10.24.48.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/284b6ba9-ef2c-13d2-7137-5374fbfbba8e.jpeg)

ファイルを作成して適当に記述をし、記述した文字を選択してメニューの中から「Hello」を選択します。

![スクリーンショット 2022-12-13 10.25.48.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/d93a1826-9f4b-4fc6-86e7-6e4c3e2d4ef3.jpeg)

Helloをクリックすると右下に「Hello World」という文字が出力されればOKです。

同様に``⇧⌘P``からHelloと入力しても同じ処理が実行されることが確認できます。

![スクリーンショット 2022-12-13 10.27.02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/edc457cd-e08d-30fd-fc0f-ead380585e9b.jpeg)

![スクリーンショット 2022-12-13 10.27.15.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/954e49c2-965d-6261-a1b3-7f2ecab3054b.jpeg)

これでチュートリアルは完了です。次の章から具体的にアプリを開発していきます。

## クソアプリの開発

ここからは今回開発するクソアプリの処理内容を``src/extension.ts``に記述していきます。

下記の手順で下記発を進めていきます

- プレバトの先生の画像を表示する
- anyの個数に応じて警告文を変える
- anyという文字があったら赤線を引く

### package.jsonにコマンドを登録する

今回はメニューに「**any-checker**」を追加し選択されると拡張機能が呼び出されるような挙動にします。

チュートリアルでの説明と同様にpackage.jsonにコマンドを追加します。

```json:package.json
  "activationEvents": [
    "onCommand:vscode-context.any-check"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "vscode-context.any-check",
        "title": "anyチェック"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "when": "editorHasSelection",
          "command": "vscode-context.any-check",
          "group": "myGroup@4"
        }
      ]
    }
  },
```

このように登録することデバックを起動した時に下記のようにメニューに「anyチェック」が追加されています。(後で説明)

![スクリーンショット 2022-12-13 22.43.52.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/c1f82fd4-f0e2-870f-fd9e-06b628995944.jpeg)

コマンドに関する準備は以上です。

### 画像を表示する

VSCodeの拡張機能ががメニューから呼び出された時にプレバトの先生が表示される処理を``src/extension.ts``に記述していきます。

画像のプレビューは公式ドキュメントのWebview APIを参考に実装を進めました。

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

コメントアウトとともに画像をプレビューするための記述は下記の通りです。
```ts:src/extension.ts
export const activate = (context: vscode.ExtensionContext) => {
  // package.jsonに記述したコマンドの登録
  let cmd = vscode.commands.registerCommand("vscode-context.any-check", () => {
    customContext("vscode-context.any-check");
  });

  context.subscriptions.push(cmd);
};

// 画像呼び出し用のHTMLを記述
function getWebviewContent() {
  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="https://www.mbs.jp/mbs-column/p-battle/thumb/20190924175210-dfacb10b6a288de4a8506b3c24a433dd3684b841.jpg" />
   </body>
   </html>`;
}

const customContext = (key: string) => {
  const editor = vscode.window.activeTextEditor;
  // 「anyチェック」が選択された時に処理を開始
  if (editor && key === "vscode-context.any-check") {
    // 別タブでgetWebviewContentで定義したHTMLを呼び出し
    const panel = vscode.window.createWebviewPanel(
      "t",
      `t`,
      vscode.ViewColumn.Beside,
      {}
    );
    panel.webview.html = getWebviewContent();
  }
};
```

画像表示に使った``createWebviewPanel``に関しては公式ドキュメントで詳しく解説されています。

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

### anyがあったら赤線を引く

anyという文字列の上に付与する赤線のスタイルを``createTextEditorDecorationType``を用いて定義します。

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

```ts:src/extension.ts
/* anyの上に付与するデコレーション(赤色打ち消し線) */
const decorationType = vscode.window.createTextEditorDecorationType({
  textDecoration: "line-through;",
  color: "red;",
});
```

次にanyという文字があった場合に上記の赤線を文字の上に追加する処理を記述します。

コードを1行づつ取得してループ処理でanyという文字があるかを確認しています。
```ts:src/extension.ts
/* anyがエディター内にあった場合に赤線を引く */
const decorate = (editor: vscode.TextEditor) => {
  const sourceCode = editor.document.getText();
  /* 適応したい任意の文字を入れる */
  const regex = /(any)/;

  const decorationsArray: vscode.DecorationOptions[] = [];
  /* 1行ごとにソースコードを取得する */
  const sourceCodeArr = sourceCode.split("\n");
  let anyCount = 0;

  for (let line = 0; line < sourceCodeArr.length; line++) {
    const match = sourceCodeArr[line].match(regex);

    if (match !== null && match.index !== undefined) {
      let range = new vscode.Range(
        new vscode.Position(line, match.index),
        new vscode.Position(line, match.index + match[1].length)
      );
      anyCount += 1;
      decorationsArray.push({ range });
    }
  }
};
```

### 警告文を出す

最後に``anyCount``に応じて出力される文字が変わる処理を記述します。

ここでは先程のチュートリアルでも説明した``showInformationMessage``を利用します。

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

```ts:src/extension.ts
/* anyがエディター内にあった場合に赤線を引く */
const decorate = (editor: vscode.TextEditor) => {
   // 省略
  const textLabel = anyCount > 1 ? "いい加減にしなさい" : "あっぱれ";
  vscode.window.showInformationMessage(`${textLabel}`, {
    modal: true,
  });
  editor.setDecorations(decorationType, decorationsArray);
};
```

**【完成系コード】**
```ts:src/extension.ts
import * as vscode from "vscode";

/* anyの上に付与するデコレーション(赤色打ち消し線) */
const decorationType = vscode.window.createTextEditorDecorationType({
  textDecoration: "line-through;",
  color: "red;",
});

/* anyがエディター内にあった場合に赤線を引く */
const decorate = (editor: vscode.TextEditor) => {
  const sourceCode = editor.document.getText();
  /* 適応したい任意の文字を入れる */
  const regex = /(any)/;

  const decorationsArray: vscode.DecorationOptions[] = [];
  /* 1行ごとにソースコードを取得する */
  const sourceCodeArr = sourceCode.split("\n");
  let anyCount = 0;

  for (let line = 0; line < sourceCodeArr.length; line++) {
    const match = sourceCodeArr[line].match(regex);

    if (match !== null && match.index !== undefined) {
      let range = new vscode.Range(
        new vscode.Position(line, match.index),
        new vscode.Position(line, match.index + match[1].length)
      );
      anyCount += 1;
      decorationsArray.push({ range });
    }
  }
  const textLabel = anyCount > 1 ? "いい加減にしなさい" : "あっぱれ";
  vscode.window.showInformationMessage(`${textLabel}`, {
    modal: true,
  });
  editor.setDecorations(decorationType, decorationsArray);
};

export const activate = (context: vscode.ExtensionContext) => {
  let cmd = vscode.commands.registerCommand("vscode-context.any-check", () => {
    customContext("vscode-context.any-check");
  });

  context.subscriptions.push(cmd);
};

// 画像呼び出し用のHTMLを記述
function getWebviewContent() {
  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="https://www.mbs.jp/mbs-column/p-battle/thumb/20190924175210-dfacb10b6a288de4a8506b3c24a433dd3684b841.jpg" />
   </body>
   </html>`;
}

const customContext = (key: string) => {
  const editor = vscode.window.activeTextEditor;
  // 「anyチェック」が選択された時に処理を開始
  if (editor && key === "vscode-context.any-check") {
    const panel = vscode.window.createWebviewPanel(
      "t",
      `t`,
      vscode.ViewColumn.Beside,
      {}
    );
    panel.webview.html = getWebviewContent();

    // 色つける
    vscode.workspace.onWillSaveTextDocument((event) => {
      const openEditor = vscode.window.visibleTextEditors.filter(
        (editor) => editor.document.uri === event.document.uri
      )[0];
      decorate(openEditor);
    });
  }
};

export function deactivate() {}
```

```json:package.json
{
  "name": "vscode-context",
  "displayName": "vscode-context",
  "description": "custom context",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.69.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [
    "onCommand:vscode-context.any-check"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "vscode-context.any-check",
        "title": "anyチェック"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "when": "editorHasSelection",
          "command": "vscode-context.any-check",
          "group": "myGroup@4"
        }
      ]
    }
  },
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "tsc -p ./",
    "watch": "tsc -watch -p ./",
    "pretest": "npm run compile && npm run lint",
    "lint": "eslint src --ext ts",
    "test": "node ./out/test/runTest.js"
  },
  "devDependencies": {
    "@types/glob": "^7.2.0",
    "@types/mocha": "^9.1.1",
    "@types/node": "16.x",
    "@types/vscode": "^1.69.0",
    "@typescript-eslint/eslint-plugin": "^5.30.0",
    "@typescript-eslint/parser": "^5.30.0",
    "@vscode/test-electron": "^2.1.5",
    "eslint": "^8.18.0",
    "glob": "^8.0.3",
    "mocha": "^10.0.0",
    "typescript": "^4.7.4",
    "vsce": "^2.9.2"
  }
}
```

F5(⌘+shift+p)をクリックしタブに「Debug npm Script」を入れてデバック機能を起動させます。
![スクリーンショット 2022-12-13 23.00.57.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/6e076087-0de1-e5ce-e56a-8966716cf50c.jpeg)

左下の``Run Extension``をクリックし開発した拡張機能の動作を確認します。

メニューからanyチェックを選択すると「any」に赤ペンが入っていることが確認できます。

![スクリーンショット 2022-12-13 23.03.02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2695521/96ccf0e7-2857-874e-8318-0985139781a0.jpeg)

## 最後に

いかがだったでしょうか。今回はVSCodeの拡張機能をクソアプリを開発しながら学ぶ記事を書きました。

今回開発した拡張機能はまだ実用的でないので、下記の機能を追加した上でリリースしようと思っています。

- anyではない型候補を表示してくれる
- 型定義のany以外は反応しないようにする(変数名など)
- コードのリファクタリング

普段はフロントエンド及び、サーバーなど幅広く記事を書いているのでぜひ読んでいただけると嬉しいです。

https://qiita.com/KNR109/items/e13a5c5b8b461e846902

https://qiita.com/KNR109/items/d3b6aa8803c62238d990

https://qiita.com/KNR109/items/5d4a1954f3e8fd8eaae7