Gemini APIを使用して愛犬がコードレビューしてくれるVSCode拡張機能を自作する

## はじめに

賢すぎる愛犬にいっそコードレビューを頼みたいと思ったことはありませんか？私はあります。
この記事では、Gemini APIを使用して愛犬がコードレビューしてくれるVSCode拡張機能を作成します。

## 動作イメージ

1. VSCodeのサイドバーに愛犬の画像とテキストエリアを表示
2. コードを選択してコマンドを実行すると、犬っぽい口調でレビューを返してくれる
3. レビュー中やレビュー完了時は専用の画像に切り替わる演出付き

## プロジェクトの作成

[公式ドキュメント](https://code.visualstudio.com/api/get-started/your-first-extension)を参考に、Yeomanでプロジェクトを作成します。

```
npm install --global yo generator-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? dog-reviewer
? What's the identifier of your extension? dog-reviewer
? What's the description of your extension?
? Initialize a git repository? No
? Which bundler to use? webpack
? Which package manager to use? npm
```

これで`my-pets-reviewer`フォルダが作成され、拡張機能のプロジェクトが出来上がりました。

今回はGoogle AI Gemini APIを使用するため、[APIキー](https://aistudio.google.com/app/apikey)とライブラリも用意します。

```
npm install @google/generative-ai
```

## WebViewとコマンドの登録
拡張機能の起動時には、`src/extension.ts`内の`activate`関数が呼び出されます。
`activate`関数に以下のコードを記述します。
```src/extensions.ts
export function activate(context: vscode.ExtensionContext): void {
  const provider = new WebViewProvider(context);

  // サイドバーのWebView登録
  context.subscriptions.push(
    vscode.window.registerWebviewViewProvider("dogReviewerSidebar", provider)
  );

  // コードレビューコマンドの登録
  const codeReviewCommand = vscode.commands.registerCommand(
    "extension.codeReview",
    async () => {
      await handleCodeReview(provider);
    }
  );

  context.subscriptions.push(codeReviewCommand);
}
```
VSCodeのサイドバーにWebView（HTML）を表示するため、`WebViewProvider`インスタンスを作成し、`vscode.window.registerWebviewViewProvider`関数を使ってサイドバー（dogReviewerSidebarというIDのビュー）へ登録しています。

`vscode.commands.registerCommand`関数では、コマンド`extension.codeReview`を登録しています。これにより、エディタ上で選択したテキストに対して、ショートカットメニューから「コードレビュー」コマンドを実行できるようになります。

続いて、拡張機能の設定を`package.json`に追記します。

```package.json
  "contributes": {
    "views": {
      "explorer": [
        {
          "type": "webview",
          "id": "dogReviewerSidebar",
          "name": "DogReviewer"
        }
      ]
    },
    "commands": [
      {
        "command": "extension.codeReview",
        "title": "コードレビュー"
      }
    ],
    "menus": {
      "editor/context": [
        {
          "command": "extension.codeReview",
          "when": "editorHasSelection",
          "group": "navigation"
        }
      ]
    },
    "configuration": {
      "type": "object",
      "title": "Dog Reviewer",
      "properties": {
        "dogReviewer.apiKey": {
          "type": "string",
          "default": "",
          "description": "Gemini API Key"
        }
      }
    }
  },
```

`contributes`では、WebViewやコマンドの識別子を定義します。`views`でサイドバー(エクスプローラー)にWebViewを追加し、`commands`で「コードレビュー」コマンドを登録、`menus`でメニューにコマンドを表示する設定をしています。
また、ユーザー設定からGemini APIキーを取得できるように、`configuration`で設定項目を定義しています。

## コマンド実行時の処理

`extension.codeReview`コマンドが呼ばれた際に実行される処理を実装します。

```src/extensions.ts
async function handleCodeReview(provider: WebViewProvider): Promise<void> {
  const editor = vscode.window.activeTextEditor;
  if (!editor) {
    vscode.window.showInformationMessage("アクティブなエディタがありません。");
    return;
  }

  // 設定から API キーを取得
  const config = vscode.workspace.getConfiguration("dogReviewer");
  const apiKey = config.get<string>("apiKey");

  if (!apiKey) {
    vscode.window.showErrorMessage("APIキーが設定されていません。拡張機能の設定からAPIキーを入力してください。");
    return;
  }

  const selection = editor.selection;
  const selectedText = editor.document.getText(selection);

  // プログレスインジケータの表示中にレビューを実行
  await vscode.window.withProgress(
    {
      location: vscode.ProgressLocation.Notification,
      title: "コードをレビュー中...",
      cancellable: false,
    },
    async () => {
      provider.setImage("Progress.png");

      const genAI = new GoogleGenerativeAI(apiKey);
      const prompt = `あなたは賢いボーダーコリーです。以下のコードの問題点を犬の口調で簡潔にレビューしてください。\n\n${selectedText}`;
      const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });

      // プロンプトを送信して結果を取得
      const result = await model.generateContent(prompt);
      const response = result.response;
      const text = response.text();

      // 結果をWebViewに表示
      provider.setReviewText(text);
      provider.setImage("Success.png"); 
    }
  );
}
```
アクティブなエディタから選択中のテキストを取得し、`withProgress`で進捗インジケータを表示しながら処理を行っています。
犬にコードレビューしてもらう必要があるので、`prompt`として「犬の口調で簡潔にレビューしてください」という指示とともにコードを渡します。

## WebViewの表示
```src/extensions.ts
class WebViewProvider implements vscode.WebviewViewProvider {
  private _view?: vscode.WebviewView;
  private _reviewText: string = "";
  private _currentImage: string = "Wendy.jpg";

  constructor(private readonly context: vscode.ExtensionContext) {}

  // WebViewの初期化処理
  resolveWebviewView(webviewView: vscode.WebviewView) {
    this._view = webviewView;

    webviewView.webview.options = {
      enableScripts: true,
      localResourceRoots: [this.context.extensionUri],
    };

    this.updateWebviewContent();
  }
  
  // WebViewを更新する
  private updateWebviewContent() {
    if (!this._view) {
      return;
    }
    
    // _currentImageで指定した画像ファイルへのURIを取得
    const imageUri = this._view.webview.asWebviewUri(
      vscode.Uri.joinPath(
        this.context.extensionUri,
        "media",
        this._currentImage
      )
    );
    
    // CSSファイルへのURIを取得
    const cssUri = this._view.webview.asWebviewUri(
      vscode.Uri.joinPath(this.context.extensionUri, "media", "style.css")
    );

    // WebViewに表示するHTML
    this._view.webview.html = `<!DOCTYPE html>
          <html lang="ja">
          <head>
              <meta charset="UTF-8">
              <link rel="stylesheet" href="${cssUri}">
          </head>
          <body>
              <div class="image-container">
                  <img src="${imageUri}" style="max-width: 100%; height: auto;">
              </div>
              <div class="review-container">${this._reviewText}</div>
          </body>
          </html>`;
  }

  // Geminiから取得したコードレビューをWebViewに反映する
  public setReviewText(text: string) {
    this._reviewText = text;
    this.updateWebviewContent();
  }

  // 表示画像を更新する
  public setImage(imageName: string) {
    this._currentImage = imageName;
    this.updateWebviewContent();
  }
}
```
`WebviewViewProvider`を利用して、サイドバーに愛犬の画像とレビュー結果を表示します。
`_reviewText`や`_currentImage`の変更に合わせて`updateWebviewContent`を呼び出すことで、WebViewを更新しています。
注意点として、`asWebviewUri()`を使わないと、ディレクトリ内の画像やCSSファイルに直接アクセスできません。

## 動作確認
`F5`を押して拡張機能をデバッグ実行します。
新しいVSCodeウィンドウが立ち上がり、サイドバーに愛犬の画像が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3936046/0dc8ecf3-b532-0db2-cee0-18adcb04cd1d.png)

次に、拡張機能の設定でGemini APIキーを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3936046/c47cbb82-64a4-0003-880d-8cb8d474db57.png)

適当なファイルを開き、コードを選択した状態でショートカットメニューから「コードレビュー」を実行します。

生成中は画像が切り替わり・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3936046/9d131d77-0c60-e490-6533-9a9de5dc931e.png)

数秒後、レビュー文が返ってきました。
ちゃんと犬っぽい口調でコードの解説と問題点の指摘をしてくれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3936046/f38a71c6-6de6-e2bc-3054-2c6a46489a19.png)

## まとめ
以上で完成です。お疲れ様でした！🎉
VSCodeの拡張機能は公式のドキュメントや[サンプル](https://github.com/microsoft/vscode-extension-samples/tree/main)がとても充実しているので、比較的簡単に作ることができます。
皆さんもぜひ作ってみてください！