VisualStudioCode拡張婆婆

# はじめに
　この記事では、今流行りの湯婆婆をVSCodeの拡張機能で実装してみます。
　[Javaで湯婆婆を実装してみる](https://qiita.com/Nemesis/items/c7192a7c510788d2cba2) という偉大な元ネタにインスパイアされた記事です。ぜひ元記事もご覧いただければと思います。
　以降の実装・動作確認は、下記の環境で行っています。

- Windows10
- git bash (git version 2.27.0.windows.1)
- Visual Studio Code (最新)
- nodist 0.9.1
- node 14.6.0
- npm 6.14.8

# やりたいこと
　Visual Studio Code というエディタ上で、湯婆婆をします。
　より具体的には、次のような処理を行います。

1. 「契約書だよ。そこに名前を書きな。」と出力し、ユーザの入力$Name$を待つ
2. $Name$からランダムな1文字$Char$を抽出する。
3. 「フン。$Name$というのかい。贅沢な名だねぇ。」を出力
4. 「今からお前の名前は$Char$だ。いいかい、$Char$だよ。分かったら返事をするんだ、$Char$！！」を出力

# 出来たもの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52542/76c8cf6a-cf1c-d42f-496d-b1b4a25fd20b.png)
![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52542/1412aa56-ebb6-313f-775e-a45e738cb046.png)
（おまけ）
![image (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52542/a5ed4afe-a95a-7e57-ad58-056c688e6958.png)

# 事前準備：VSCode拡張機能の開発環境を整える
　開発環境の構築については、Qiita内に良い記事が複数ありますので、ページ最下部の参考文献を是非ご覧ください。
　とはいえ完全に丸投げするのは一記事としてアレなので、簡単にやることを載せておきます。

　まず、[ここ](https://github.com/nullivex/nodist/releases)からnodistをインストールしましょう。
　ここで注意点として、インストールするパスに空白が含まれていると後々エラーを吐きます。デフォルトのパスだと`C:\Progam Files`以下なのでアウトです。気を付けましょう。

　無事にインストール出来たら、git bashを立ち上げ以下のコマンドを順に実行していきます。


```sh
# nodistが入っているか確認
nodist -v

# nodeを入れてnpmが動くようにする
nodist + v12.19.0
nodist v12.19.0
nodist npm match
npm -v
npm i     # npm ERR!が出たらアウト

# Yeomanという雛形つくるツールを入れる
npm i -g yo generator-code --save
yo --version

# 作業ディレクトリを置きたいディレクトリに移動
cd ~/desktop

# 作る
yo code
（対話的な処理）
cd 作ったディレクトリ
code .
```

　VSCodeが起動すれば成功です。
　`F5`キーでデバッグモードに入ると、作成中の拡張機能が入ったVSCodeが新しく起動します。
　そこで`Ctrl+Shift+P`でコマンドパレットを開き、`Helllo World`というコマンドを実行してみましょう。
　すると、右下に![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52542/ef3fcd9f-5209-0845-2139-2f22d38397e7.png)
というポップアップが出現します。

　これが、雛形コードにデフォルトで実装されている拡張機能です。
　では、ここから湯婆婆にしていきましょう。

# 湯婆婆の実装
　簡単な拡張機能を作るのに触る必要のあるファイルは2つです。
　まずは`package.json`から。

### Package.json
```json:package.json
{
	"name": "VSCobaba",
	"displayName": "VSCobaba",
	"description": "",
	"version": "0.0.1",
	"engines": {
		"vscode": "^1.50.0"
	},
	"categories": [
		"Other"
	],
	"activationEvents": [
		"*"
	],
	"main": "./out/extension.js",
	"scripts": {
		"vscode:prepublish": "npm run compile",
		"compile": "tsc -p ./",
		"lint": "eslint src --ext ts",
		"watch": "tsc -watch -p ./",
		"pretest": "npm run compile && npm run lint",
		"test": "node ./out/test/runTest.js"
	},
	"devDependencies": {
		"@types/vscode": "^1.50.0",
		"@types/glob": "^7.1.3",
		"@types/mocha": "^8.0.0",
		"@types/node": "^12.11.7",
		"eslint": "^7.9.0",
		"@typescript-eslint/eslint-plugin": "^4.1.1",
		"@typescript-eslint/parser": "^4.1.1",
		"glob": "^7.1.6",
		"mocha": "^8.1.3",
		"typescript": "^4.0.2",
		"vscode-test": "^1.4.0"
	}
}
```

　拡張機能の情報や、拡張機能を動かすために必要な機能・情報がまとまっています。
　だいたいはデフォルトのままですが、最低限`"activationEvents"`は変更する必要があります。
　ここに書いた条件に合致したときに拡張機能がアクティブになります。

　今回は~~面倒だったので~~VSCode起動時に自動で動くようにしたかったので、`"*"`と書いています。


### extension.ts

　拡張機能の本体です。

```typescript:extension.ts
import * as vscode from 'vscode';

const myCommandId = 'plcherrim.VSCobaba';

let nameTag: vscode.StatusBarItem;
let inputNameDialogOption: vscode.InputBoxOptions = {
	ignoreFocusOut: true,
    prompt: "契約書だよ。そこに名前を書きな。",
}
let yourName = "";

export function activate({ subscriptions }: vscode.ExtensionContext) {
	subscriptions.push(vscode.commands.registerCommand(myCommandId, () => {
		vscode.window.showInformationMessage(`${yourName} 「は、はいっ！」`);
	}));

	nameTag = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
	nameTag.command = myCommandId;
	subscriptions.push(nameTag);

	vscode.window.showInputBox(inputNameDialogOption).then(value => {
		if (!value) {
			return vscode.commands.executeCommand('workbench.action.closeWindow');
		}
		let nameChars = [...value];
		
		yourName = nameChars[Math.floor(Math.random() * nameChars.length)];
		let decideNameDialogOption: vscode.InputBoxOptions = {
			ignoreFocusOut: true,
			prompt: `フン。${value}というのかい。贅沢な名だねぇ。今からお前の名前は${yourName}だ。いいかい、${yourName}だよ。分かったら返事をするんだ、${yourName}！！`,
			value: "は、はいっ！"
		}
		vscode.window.showInputBox(decideNameDialogOption).then(value => {
			nameTag.text = `返事をするんだ、${yourName}！！`;
			nameTag.show();
		});
	});
}
```

　特別難しい処理はありません。入力を受け取れる`InputBox`を2回表示しているだけです。
　色々なウィザードがあるため、拡張機能を自作する際は、[VSCode API](https://code.visualstudio.com/api/references/vscode-api)を適宜眺めることをお勧めします。

# 名無しさん問題
　湯婆婆には、「名前として空白文字列を入力すると例外を送出してバグる」というレギュレーションがあります。元ネタの筆者さん曰く、「湯婆婆がクラッシュとか面白そうじゃないですか。」とのこと。確かに風情を感じます。

　しかしながら、本実装では例外を送出していません。普通に例外を吐いても拡張機能が落ちるだけであまり面白くなかったためです。
　本実装では代わりに、空白の名前であれば即座にVSCodeを終了するようにしています。
　可能であれば、エラーでVSCode自体が落ちてくれれば面白かったのですが、能動的にVSCodeをエラー落ちさせられませんでした（有識者の方はぜひご教授ください）。


# 𠮷田さん問題
　さて、実は湯婆婆界隈には**「𠮷田さん問題」**と呼ばれる問題があります。
　以下の画像をご覧ください。
![image (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52542/872bfaa4-367d-8dcb-28c9-4a28a6f959d2.png)

　`𠮷`の字が文字化けしてしまっています。
　この事象の再現方法は簡単で、`let str = "𠮷田";` とした後に、`str[0]`を参照するだけです。

　実は`𠮷`の字をはじめとしたいくつかの文字はサロゲートペアと呼ばれる、1文字を表すのに2文字分の符号を用いている特殊な文字なのです。そのため普通に添え字でアクセスしようとすると、符号の半分だけを読みとってしまい、このような問題が発生します。

　湯婆婆が文字化けするのは怖いので、何らかの対処を入れる必要があります。
　といっても、実は上記のコードではすでに対策が入っており、実行しても文字化けすることはありません。

　該当部分はこちらになります。

```typescript
	let nameChars = [...value];	
	yourName = nameChars[Math.floor(Math.random() * nameChars.length)];
```


　ここでは`value`が名前（＝𠮷田）です。
　`value[0]`をとってしまうと文字化けするので、スプレッド構文という記法を用い、サロゲートペアを考慮しつつ1文字ずつ分割して配列に分けています。
　`split()`で分割すると、サロゲートペアを解釈してくれないため要注意です。

# おわりに
　湯婆婆と入力するのに「ゆばばばば」と入れなければならず、そのたびに微妙な気持ちになるなどしました。


# 参考文献
### 湯婆婆
[Javaで湯婆婆を実装してみる](https://qiita.com/Nemesis/items/c7192a7c510788d2cba2)

### VSCode拡張の作り方
[Visual Studio Code - Your First Extension (microsoft)](https://code.visualstudio.com/api/get-started/your-first-extension)
[Visual Studio Code はじめての拡張機能開発](https://qiita.com/rma/items/8c53077d1355ab8fa4c6)
[【拡張機能の作り方】Visual Studio Code（VSCode）のスニペット作成が面倒だったので、自作の拡張機能をインストールした。
](https://qiita.com/sfp_waterwalker/items/b2cbdbe1119ea46ab25e)
###サロゲートペアの話
[文字列を1文字ずつ配列化（サロゲートペアを考慮）](https://qiita.com/sounisi5011/items/aa2d747322aad4850fe7)