2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VSCodeの機能拡張を作った

Last updated at Posted at 2024-05-03

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

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

image.png

[VScode Extension Guide]

開発

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

環境構築

> 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という名前のコマンドが定義されます。

extension.js
const module_name = "QuickJS_ESP32_Console";

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

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

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

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

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

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

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();

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

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

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

image.png

入力ダイアログ

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

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

image.png

選択ダイアログ

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

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

コマンドの実行

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

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

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

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

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

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

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

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

ソースコード

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に定義します。

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

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

image.png

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

公開

> npx vsce login <publisher name>

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

ちなみに、package.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

以上

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?