VSCodeの機能拡張を作った

今回は、Visual Studio Code(VSCode)の機能拡張を作っていきます。
ESP32で動くJavascript実行環境を作っていまして、普段使っているVSCodeからそのJavascriptの実装とアップロードができるようにしたかったのがモチベーションです。

コマンドとしては、5つ作ります。また、VSCodeの画面の左下のステータスバーに、1つのテキストのボタンと3つのアイコンのボタンを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a568e711-65ec-2dbe-5551-62ed951fa87d.png)

[VScode Extension Guide]

https://code.visualstudio.com/api

# 開発

・環境構築
・ひな型作成
・コマンドの実装
・ステータスバーのボタンの実装
・メッセージトーストの表示
・入力ダイアログ
・選択ダイアログ
・コマンドの実行
・クリップボードへのコピー

## 環境構築

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

## ひな型作成

適当なフォルダで以下を実行します。

```
> yo code
```

いろいろ聞いていますが、適当に入力します。
言語としてTypescriptでもいいですがJavascriptが簡単なのでそちらを選びました。

```
> 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 (JavaScript)
? What's the name of your extension? test
? What's the identifier of your extension? test
? What's the description of your extension?
? Enable JavaScript type checking in 'jsconfig.json'? No
? Initialize a git repository? Yes
? Which package manager to use? npm

Writing in C:\hogehoge\test...
   create test\.vscode\extensions.json
   create test\.vscode\launch.json
   create test\test\extension.test.js
   create test\.vscodeignore
   create test\.gitignore
   create test\README.md
   create test\CHANGELOG.md
   create test\vsc-extension-quickstart.md
   create test\jsconfig.json
   create test\extension.js
   create test\package.json
   create test\.vscode-test.mjs
   create test\.eslintrc.json

Changes to package.json were detected.
```

これにより、extension.jsやpackage.jsonなど、必要なファイルが自動生成されます。

## コマンドの実装

async function activate(context) に実装します。
以下の場合、QuickJS_ESP32_Console.code_uploadという名前のコマンドが定義されます。

```js:extension.js
const module_name = "QuickJS_ESP32_Console";

	var disposable = vscode.commands.registerCommand(module_name + '.code_upload', async function () {

// ここに処理内容を実装する

	});
	context.subscriptions.push(disposable);
```

## ステータスバーのボタンの実装

textのところに普通の文字列を指定してもいいですし、$くくりでアイコンを表示することもできます。

https://code.visualstudio.com/api/references/icons-in-labels

このボタンをクリックすると、commandで指定したコマンドが実行されます。

```js:extension.js
	var myButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, defaultPriority);
	myButton.text = "$(repo-push)";
	myButton.tooltip = "コードをダウンロードします。";
	myButton.command = module_name + ".code_download";
	myButton.show();
```

## メッセージトーストの表示

処理中に、右下にトーストを表示させる場合です。

```js:extension.js
vscode.window.showInformationMessage('アップロードしました。');
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/99bd064f-f86c-1840-dbc9-21353e0b62ec.png)

## 入力ダイアログ

ユーザに何か文字を入力してもらうためのダイアログを表示します。

```js:extension.js
		const ipaddress = await vscode.window.showInputBox({
			title: '対象デバイスのIPアドレスを指定してください。',
			value: targetIp
		});
		if (ipaddress) {
// 入力されたときの処理
		}else{
			vscode.window.showInformationMessage('キャンセルされました。');
		}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/bf6f791f-b3b7-1a26-c26e-8be6e97ee913.png)

## 選択ダイアログ

ユーザにいずれかを選択してもらいます。

```js:extension.js
		const selected = await vscode.window.showQuickPick(
				['Upload', 'Download', 'Restart', 'Setting'],
				{ placeHolder: 'コマンドを選択して下さい。' });
		switch(selected){
			case "Upload": /* 何かの処理 */; break;
			case "Download": /* 何かの処理 */; break;
			case "Restart": /* 何かの処理 */; break;
			case "Setting": /* 何かの処理 */; break;
		}
``` 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/99821b15-75d7-8b05-66c7-53648ff4562b.png)

## コマンドの実行

コード上からコマンドを実行します。

```js:extension.js
vscode.commands.executeCommand(module_name + '.code_download')
```

## 選択中のファイルの内容の取得

作成中のコードの内容をESP32にアップロードしたかったため、選択中のファイルの内容を取得する必要がありました。

```js:extension.js
		let script;
		try{
			const editor = vscode.window.activeTextEditor;
			script = editor.document.getText();
		}catch(error){
			console.error(error);
			vscode.window.showInformationMessage('ファイルがありません。');
			return;
		}
```

## クリップボードへのコピー

テキストをクリップボードにコピーします。

```js:extension.js
vscode.env.clipboard.writeText(script);
```

# ソースコード

```js:extension.js
const vscode = require('vscode');
const Arduino = require('./Arduino');

let targetIp = null;
let arduino = null;
const module_name = "QuickJS_ESP32_Console";
const defaultPriority = 1000;

/**
 * @param {vscode.ExtensionContext} context
 */
async function activate(context) {
	console.log("activate called");

	if( !targetIp ){
		const ipaddress = await vscode.window.showInputBox({
			title: '対象デバイスのIPアドレス'
		});
		if (ipaddress) {
			targetIp = ipaddress;
		}
	}

	if( targetIp ){
		arduino = new Arduino("http://" + targetIp);
		vscode.window.showInformationMessage(`対象デバイス(${targetIp})を開きました。`);
	}

	var myButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, defaultPriority);
	myButton.text = "QuickJS_ESP32";
	myButton.command = module_name + ".command_menu";
	myButton.show();

	var myButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, defaultPriority);
	myButton.text = "$(repo-pull)";
	myButton.tooltip = "コードをアップロードします。";
	myButton.command = module_name + ".code_upload";
	myButton.show();

	var myButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, defaultPriority);
	myButton.text = "$(repo-push)";
	myButton.tooltip = "コードをダウンロードします。";
	myButton.command = module_name + ".code_download";
	myButton.show();

	var myButton = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, defaultPriority);
	myButton.text = "$(debug-restart)";
	myButton.tooltip = "対象デバイスを再起動します。";
	myButton.command = module_name + ".terminal_restart";
	myButton.show();

	var disposable = vscode.commands.registerCommand(module_name + '.code_upload', async function () {
		if( !targetIp ){
			vscode.window.showInformationMessage('対象デバイスのIPアドレスを指定してください。');
			return;
		}

		let script;
		try{
			const editor = vscode.window.activeTextEditor;
			script = editor.document.getText();
		}catch(error){
			console.error(error);
			vscode.window.showInformationMessage('ファイルがありません。');
			return;
		}
		try{
			await arduino.code_upload_main(script, false);
					
			vscode.window.showInformationMessage('アップロードしました。');
		}catch(error){
			console.error(error);
			vscode.window.showInformationMessage('アップロードに失敗しました。');
		}
	});
	context.subscriptions.push(disposable);

	disposable = vscode.commands.registerCommand(module_name + '.command_menu', async function () {
		const selected = await vscode.window.showQuickPick(
				['Upload', 'Download', 'Restart', 'Setting'],
				{ placeHolder: 'コマンドを選択して下さい。' });
		switch(selected){
			case "Upload": 	vscode.commands.executeCommand(module_name + '.code_upload'); break;
			case "Download": 	vscode.commands.executeCommand(module_name + '.code_download'); break;
			case "Restart": 	vscode.commands.executeCommand(module_name + '.terminal_restart'); break;
			case "Setting": 	vscode.commands.executeCommand(module_name + '.setting_host'); break;
		}
	});
	context.subscriptions.push(disposable);

	disposable = vscode.commands.registerCommand(module_name + '.setting_host', async function () {
		const ipaddress = await vscode.window.showInputBox({
			title: '対象デバイスのIPアドレスを指定してください。',
			value: targetIp
		});
		if (ipaddress) {
			targetIp = ipaddress;
			arduino = new Arduino("http://" + targetIp);
			vscode.window.showInformationMessage(`対象デバイス(${targetIp})を開きました。`);
		}else{
			vscode.window.showInformationMessage('キャンセルされました。');
		}
	});	
	context.subscriptions.push(disposable);

	var disposable = vscode.commands.registerCommand(module_name + '.code_download', async function () {
		if( !targetIp ){
			vscode.window.showInformationMessage('対象デバイスのIPアドレスを指定してください。');
			return;
		}

		try{
			const script = await arduino.code_download();

			vscode.env.clipboard.writeText(script);
			vscode.window.showInformationMessage('クリップボードにダウンロードしたコードをコピーしました。');
			
			// var answer = await vscode.window.showInformationMessage('コードを差し替えますがよいですか？', 'Yes', 'No');
			// console.log(answer);

			// vscode.window.activeTextEditor.edit(builder => {
			// 	const doc = vscode.window.activeTextEditor.document;
			// 	builder.replace(new vscode.Range(doc.lineAt(0).range.start, doc.lineAt(doc.lineCount - 1).range.end), script);
			// });
					
		}catch(error){
			console.error(error);
			vscode.window.showInformationMessage('ダウンロードに失敗しました。');
		}
	});	
	context.subscriptions.push(disposable);

	var disposable = vscode.commands.registerCommand(module_name + '.terminal_restart', async function () {
		if( !targetIp ){
			vscode.window.showInformationMessage('対象デバイスのIPアドレスを指定してください。');
			return;
		}

		try{
			await arduino.restart();
		}catch(error){
			console.error(error);
			vscode.window.showInformationMessage('再起動に失敗しました。');
		}
	});	
	context.subscriptions.push(disposable);
}

function deactivate() {}

module.exports = {
	activate,
	deactivate
}
```

## コマンドの定義

コマンドは、package.jsonに定義します。

```json:package.js
  "contributes": {
    "commands": [
      {
        "command": "QuickJS_ESP32_Console.command_menu",
        "title": "QuickJS_ESP32: Menu"
      },
      {
        "command": "QuickJS_ESP32_Console.code_upload",
        "title": "QuickJS_ESP32: Upload"
      },
      {
        "command": "QuickJS_ESP32_Console.code_download",
        "title": "QuickJS_ESP32: Download"
      },
      {
        "command": "QuickJS_ESP32_Console.setting_host",
        "title": "QuickJS_ESP32: Setting"
      },
      {
        "command": "QuickJS_ESP32_Console.terminal_restart",
        "title": "QuickJS_ESP32: Restart"
      }      
    ]
  },
```

# デバッグ

VSCodeで、「実行とデバッグ」から、Run Extensionを選択すると、別のVSCodeが立ち上がります。今回の機能拡張がインストールされた状態ですので、すぐにコマンドを実行できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/7a4dac69-e5f6-6fac-4496-9a19cdf92cd5.png)

コマンドの実行は、Ctrl＋Shift＋Pを押して、検索のところに、QuickJSと入れると、コマンドの選択肢が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/e4e8958e-ae76-4109-15cc-d1c034fe1abb.png)

もちろん、もとのVSCode上でブレイクポイントで処理を止めることもできます。

# 公開

```
> npx vsce login <publisher name>
```

実際には上記の前にパブリッシャーIDを取得したりアクセストークンを生成したりする必要がありますが、詳細は以下を参考させていただきました。ただし、画面が結構変わってるので注意です。

https://qiita.com/tkts_knr/items/92a15a9fe7475418b333

ちなみに、package.jsonに以下のような文字を入れる必要がありました。ご自身の環境に合わせてください。

```json:package.json
  "publisher": "poruruba",
  "repository": {
    "type": "git",
    "url": "https://github.com/poruruba/QuickJS-ESP32-Console.git"
  },
```

また、LICENSEファイルも置く必要がありました。(GitHubに置いたときに作られたものを使いました)

# 参考

ESP32で動くJavascript実行環境
・https://poruruba.booth.pm/items/3735175
・https://github.com/poruruba/QuickJS_ESP32_IoT_Device_M5Unified

作成した機能拡張
・https://marketplace.visualstudio.com/items?itemName=poruruba.QuickJS-ESP32-Console
・https://github.com/poruruba/QuickJS-ESP32-Console

以上