VScode拡張機能開発 入門~公開まで


## はじめに

情報系の学生やエンジニアがよく使用するエディタ Visual Stdio Code
今回はその拡張機能を開発していきたいと思います。vscode の拡張機能を作れると制作物のアイデアの幅が広がる、エンジニア向けサービスが作りやすいといった利点があります。ちなみにこれは勉強会で使用した資料です。
## 前提条件

vscode,Node.jsがインストールされている

## 拡張機能の雛形を作る

npm パッケージ yo,generator-code をグローバルインストールします
npm
`npm install -g yo generator-code`
yarn
`yarn global add yo generator-code`

インストールできたら拡張機能の雛形を作ります。
`yo code`

What type of extension do you want to create?  
ここでは TypeScript を選択します。  
What's the name of your extension? ()  
拡張機能の名前を入力します。ここでは famous-saying-k19074(好きな名前) にします
What's the identifier of your extension?  
そのまま enter  
What's the description of your extension?  
そのまま enter  
Initialize a git repository?  
Yes  
Bundle the source code with webpack?  
No
? Which package manager to use?  
npm

作成が終わったら(自分が作った名前)に移動  
`cd vscode-study`  
vscode を開きます  
`code .`  
下記の画像のようになっていれば OK

<font color="orange">※ワークスペースに追加する方法だとデバッグできないケースがあるので $code .で開くこと</font>  
![ひな形画像](https://i.gyazo.com/9a9cede03af4e79ef6ec230fc33fc73f.png)

## デバッグする

作られた時点で Hello world を表示する機能が既に書かれているのでこれをデバッグする。左にあるサイドバーにあるデバッグボタンを押してデバッグすると新しい vscode のウィンドウが表示される。
[![Image from Gyazo](https://i.gyazo.com/73da7b7d2226cb774208bd2730505619.gif)](https://gyazo.com/73da7b7d2226cb774208bd2730505619)

新しく開かれた vscode ウィンドウで control+shift+P でコマンドパレットを開き Hello world と入力します。右下にメッセージが表示されればデバッグ成功
[![Image from Gyazo](https://i.gyazo.com/18ff866205198570946e2f8bafbf7fa5.gif)](https://gyazo.com/18ff866205198570946e2f8bafbf7fa5)

## 簡単なコード解説

```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) {
  //拡張機能開発側vscodeのデバッグコンソールに表示される
  console.log('Congratulations, your extension "vscode-study" is now active!');

  //registerCommand(コマンド名,実行する関数)
  //コマンド名とpackage.jsonのコマンド名を一致させる必要がある
  let disposable = vscode.commands.registerCommand(
    "vscode-study.helloWorld",
    () => {
      //vscodeのAPI showInformationMessageを使用してパネルにメッセージを表示
      vscode.window.showInformationMessage("Hello World from vscode-study!");
    }
  );
  //コマンドの登録
  context.subscriptions.push(disposable);
}

// this method is called when your extension is deactivated
//拡張機能を終了した時の処理
//自動的に終わらてくれるものが多いため書くことは少ない
export function deactivate() {}
```

```json
		"contributes": {
		"commands": [
			{
                //extension.tsのregsiterCommandで登録した名前
				"command": "vscode-study.helloWorld",
                //実際に拡張機能を呼び出す時の名前
				"title": "Hello World"
			}
		]
	},
```

## 簡単な拡張機能を作ってみよう

機能を実行したら次は VSCode 勉強会と表示する拡張機能を作ってみよう

```ts
export function activate(context: vscode.ExtensionContext) {
	  //拡張機能開発側vscodeのデバッグコンソールに表示される
	console.log('Congratulations, your extension "vscode-study" is now active!');

  ...
  //メソッドの登録
	context.subscriptions.push(disposable);


//追記
    let helloOriginal = vscode.commands.registerCommand("hello.Original", () => {
    vscode.window.showInformationMessage("helloOriginal");
  });
  context.subscriptions.push(helloOriginal);
}

```

package.json に以下を追加

```json
 "activationEvents": [
    "onCommand:vscode-study.helloWorld",
     //追記
    "onCommand:hello.Original"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "vscode-study.helloWorld",
        "title": "Hello World"
      },
      //追記
      {
        "command": "hello.Original",
        "title": "call Original"
      }
    ]
  },
```

[![Image from Gyazo](https://i.gyazo.com/659f7e02f15108b3ca47fa8d46bca70d.gif)](https://gyazo.com/659f7e02f15108b3ca47fa8d46bca70d)

## 本番:一定以上の文字数を打ち込むと名言をターミナルに出力する拡張機能を作る

axios をインストール  
`npm install axios`  
`npm install -D @types/axios`

extension.ts に以下を記述

```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";
import axios, { AxiosResponse } from "axios";
import { type } from "os";

type FamousSaying = {
  auther: string;
  meigen: string;
};

export function activate(context: vscode.ExtensionContext) {
  console.log('Congratulations, your extension "vscode-study" is now active!');

  let famousSaying = vscode.commands.registerCommand(
    "famousSaying",
    async () => {
      let activeText = vscode.window.activeTextEditor;
      //初期文字数
      let firstWordCount = activeText?.document.getText().length;
      //初期閾値
      let threshold = 20;
      //閾値一覧
      let thresholdList = ["10", "20", "30", "40", "50", "100", "200"];

      //非同期処理なのでawaitで入力を待たせる必要がある
      threshold = Number(await vscode.window.showQuickPick(thresholdList));
      vscode.window.showInformationMessage("Start");

      // 編集しているファイルの変更を検知
      vscode.window.onDidChangeVisibleTextEditors(() => {
        activeText = vscode.window.activeTextEditor;
        firstWordCount = activeText?.document.getText().length;
      });

      //ファイルの書き換えを検知
      vscode.workspace.onDidChangeTextDocument(() => {
        //現在の文字数を取得
        let currentWordCount = activeText?.document.getText().length;
        //初期の文字数との差
        let diff = (currentWordCount as number) - (firstWordCount as number);

        //関数式を使う場合巻き上げはないので処理前に定義する必要がある
        const sendTerminalFamousSaying = async () => {
          try {
            const resposeData: AxiosResponse<FamousSaying[]> = await axios.get(
              "https://meigen.doodlenote.net/api/json.php"
            );
            // console.log(resposeData);

            vscode.window.activeTerminal?.sendText(
              "echo" + " " + resposeData["data"][0]["meigen"] + " > nul"
            );
          } catch {
            console.log("error");
          }
        };

        //一定の閾値を超えたら処理を実行
        if (diff > threshold) {
          sendTerminalFamousSaying();
          firstWordCount = (firstWordCount as number) + threshold;
        }

        //ファイルの文字数が初期文字数より少なくなった時に対応
        if (diff < 0) {
          firstWordCount = (firstWordCount as number) + diff;
        }
      });
    }
  );
  context.subscriptions.push(famousSaying);
}

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

コード解説
`vscode.window.showQuickPick(thresholdList)`でユーザが用意したリストから選択できる機能
`vscode.window.activeTextEditor;`で現在編集しているファイルを取得
`activeText?.document.getText().length`でファイルのテキスト文字数を取得
`vscode.window.onDidChangeVisibleTextEditors`は編集しているファイルが変わった時に起こるイベント。この時にアクティブテキストを再代入する。これをしないと編集しているファイルを切り替えた時に正常に機能しない。

`vscode.workspace.onDidChangeTextDocument`これはファイルの内容が変更された（文字を追加、文字を削除など）時に起こるイベント。この時`activeText?.document.getText().length`ファイルテキストの文字数を取得する。ファイルを開いた時に取得した`firstWordCount`と差をとる。

`sendTerminalFamouSaying`に api を叩いて返ってきたデータからターミナルに名言を出力する処理を書く

TypeScript の場合 response 型定義を行う必要がある。↓response の中身
[![Image from Gyazo](https://i.gyazo.com/9712ccd336cd1e425699823eb282c359.jpg)](https://gyazo.com/9712ccd336cd1e425699823eb282c359)
header,status などを自分で定義するのは手間がかかるので axios が提供している AxiosResponse 型のジェネリクスに response.data を流し込む。(ジェネリクスとは C#や Java にもある型を抽象化して実際に利用する時に型を確定させる機能。)
↓axios が提供している型
```ts
export interface AxiosResponse<T = unknown, D = any> {
  //ここにデータを定義する
  data: T;
  status: number;
  statusText: string;
  headers: AxiosResponseHeaders;
  config: AxiosRequestConfig<D>;
  request?: any;
}
```
こちらで定義する型
```ts
type FamousSaying = {
  auther: string;
  meigen: string;
};
```

`const resposeData: AxiosResponse<FamousSaying[]>`でそれを行う
async~await で同期処理を行なっている。await で書かれた部分はその処理が終わるまでそれ以降の処理がされない。
`vscode.window.activeTerminal?.sendText`でターミナルに文字を出力できる。

最後に`sendTerminalFamousSaying `を呼び出すタイミングを指定する
thresholdListから選択された閾値以上の文字を入力すると実行される。

### GUIとして追加
今回の拡張機能ではコマンドに加えて GUI でコマンドを叩けるようにする。
GUI にアイコンボタンを追加するには menus にプロパティを追加する必要がある。

imgディレクトリを作成する。
`mkdir img`
この中に
https://github.com/happy663/vscode-study/blob/main/img/head.svg
にあるSVGファイルをこの中に入れる


package.json

```json
  "activationEvents": [
    "onCommand:vscode-study.helloWorld",
    "onCommand:hello.Original",
    //追記
    "onCommand:famousSaying"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "vscode-study.helloWorld",
        "title": "Hello World"
      },
      {
        "command": "hello.Original",
        "title": "call Original"
      },
      {
        //追記
        "command": "famousSaying",
        "title": "call famousSaying",
        "icon": {
          "dark": "./img/head.svg",
          "light": "./img/head.svg"
        }
      }
    ],
    //追記
    "menus": {
      "editor/title": [
        {
          "when": "",
          "command": "famousSaying",
          "group": "navigation"
        }
      ]
    }
  },
```



[![Image from Gyazo](https://i.gyazo.com/79785a1eaa8c219bebe2ae3f3abaea65.gif)](https://gyazo.com/79785a1eaa8c219bebe2ae3f3abaea65)
## 拡張機能の公開

README.md を書き換える

```
# FamousSaying

## 説明

この拡張機能は一定以上の文字列を入力するとターミナルに文字を出力する拡張機能です。
```

vsce をインストールする
`npm install -g vsce`

Azure DevOps にログインする  
[https://dev.azure.com/](https://dev.azure.com/)

トークンの発行
[![Image from Gyazo](https://i.gyazo.com/981bd7d1d3052fde5aa0ed12b5ade9e8.png)](https://gyazo.com/981bd7d1d3052fde5aa0ed12b5ade9e8)

Name のところは自分の名前
[![Image from Gyazo](https://i.gyazo.com/d166cb3f2b5f1ba899dd4a2b4d5e16aa.png)](https://gyazo.com/d166cb3f2b5f1ba899dd4a2b4d5e16aa)

pulisher の作成

[https://marketplace.visualstudio.com/manage/](https://marketplace.visualstudio.com/manage/)  
create puslisher を選ぶ  
Name と ID を入力して作成する。トークンをコピペする。

package.json に publisher の項目を追記、拡張機能の名前変更することも可能

```json
  "name": "famous-saying-k19074",
  //追記
  "publisher": "toyama",
  "displayName": "famous-saying-k19074",
  "description": "",
  "version": "0.0.1",
```

ターミナルで`vsce publish`をする
事前環境で以下のエラーが出た。

```node_modules/axios/index.d.ts:93:12 - error TS2304: Cannot find name 'AbortSignal'.

93   signal?: AbortSignal;
              ~~~~~~~~~~~

Found 1 error.

 ERROR  npm failed with exit code 2
```

エラーを修正する。tsconfig.json を書き換える。

```json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "outDir": "out",
    //domを追加
    "lib": ["es6", "dom"],
    "sourceMap": true,
    "rootDir": "src",
    "strict": true /* enable all strict type-checking options
  },
  "exclude": ["node_modules", ".vscode-test"]
}
```

修正したらもう一度 `vsce publish`をする。
トークンを求められるのでさっきコピペしたトークンを貼り付ける。
The Personal Access Token verification succeeded とでれば成功

[![Image from Gyazo](https://i.gyazo.com/b91c959e5bf971dcba12c07a41415095.png)](https://gyazo.com/b91c959e5bf971dcba12c07a41415095)

## おまけ
`vsce publish minor`
バージョンを更新してデプロイできる

[この資料のgithubリポジトリ](https://github.com/happy663/vscode-study)
(よければスターお願いします）

参考  
https://code.visualstudio.com/api/references/vscode-api  
https://qiita.com/hakua-doublemoon/items/cfb638fe1ab43ca42cb8  
https://blog.yucchiy.com/2021/05/vscode-quickinput-inputbox/