MayaをベースにVSCode拡張を自作する

これは[Maya Advent Calendar 2019](https://qiita.com/advent-calendar/2019/maya) 2日目の記事です。


# はじめに
普段はInteliJ suiteでプログラミングしますが、VSCodeもよく使います。
標準でも高機能ですが、カスタマイズするほど便利になるので、拡張機能を多く入れがちです。

Mayaの他にDCCツールに特化した拡張機能も色々入れるのですが、似た機能のエクステンションが
多く、プロジェクトと拡張の間の「痒いところ」を埋める必要性を感じることがよくあります。
AtomやSublimeでよく遭遇した問題ですが、エディタ本体のアップデートが早いと不具合を起こす
エクステンションが出て、作業が一時ストップすることもしばしばです。(最近は安定傾向です)

継続的なメンテナンスとチームのボトムアップ、TA目線の自分に使いやすいツールが欲しくて、
Mayaを開発軸に据えた自前エクステンションを作ってみることにしました。導入系記事は既に
いろんな所で公開されています。VSCodeAPIとnpmモジュールを組み合わせて、実践に即した
ノウハウを一度共有してみようと思います。

作品や所属で要求は細かく異なります。環境に適応したエクステンションを自作できるのが目標です。
本稿は、CEDEC落選ネタとして供養する記事です。

ここまで読んだけれど、興味が薄かったり忙しい人のために優れたエクステンションが既にあります。
併せて参考にして下さい。[^1]

<blockquote class="twitter-tweet"><a href="https://twitter.com/pythonvscode/status/1181696709589524481"/></blockquote>
(内容には関係ないのですが、jupyter notebook みたいなエクステンションも頑張れば自作できます。)


## 開発の準備
開発を始める前の準備です。

<img width="780" alt="スクリーンショット 0001-11-16 2.12.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48607/0b7b4e5a-777f-d513-8c91-5347ec3e8b71.png">

### 環境を整備する
Node.js をインストールした後、関連パッケージを npm から落とします。

```bash
$ npm install -g yo code-generator vsce
```

`yo` は vscode に限らず office や chrome拡張の起点となるパッケージです。`vsce`は 
marketplaceで配信、ローカル環境での共有に使うので合わせて導入します。普段の開発時は`-g`の
オフ推奨です。

VSCodeAPI は分かりやすいです。npmを組み合わせればだいたいのことは実現できます。
実際に作った感想として

- 単機能で作成するなら`Javascript`
- 統合機能としてメンテナンスするなら`Typescript`

ぐらいな温度感です。エクステンションの言語設定等は`yo`で分岐出来ます。

### 構造を理解する
最初から最後まで一貫して３つのファイルを弄ることになります。
https://code.visualstudio.com/api/get-started/extension-anatomy

1. package.json
2. extension.js(ts)
3. settings.json

`package.json` はエクステンションの中核です。コマンド、キーマップ、コンテクストメニューは
全てここに登録します。機能を拡張するに連れて管理が煩雑になりがちです。

`extension.js` はコマンドのactivateとdeactivateを担当します。簡単なエクステンション制作なら
これだけで完結しますが、最近のエクステンションはIDE規模になるものも多くてモジュール分割は
必須です。

`settings.json` は標準、エクステンションを問わず、設定を外部化したものです。後述します。



## エクステンション
通常の言語開発においては、以下の３点がサポートされると開発はスムーズです。

- Intellisense ("python.autoComplete.extraPaths"にパスを渡す)
- シンタックスハイライト (MELとかUSDとかのパッケージ)
- リモートデバッグ (`ptvsd`か`pydevd`をmayaからアタッチ)

上記に加えて、自作する中で特に有用だった実装項目は3点です。

### コマンドラインアプリをコマンド化
プロダクションユースのツールには、コマンドラインインターフェースが大体準備されています。
batファイルで呼び出すのも良いですが、これをコマンドパレットから呼び出せると幅が広がります。

Node.js 標準の`child_process`で簡単にエクステンションに内包することが出来ます。
VSCodeの選択テキストや作業パスを可変引数にして渡すことで振る舞いを変えることが出来ます。

```javascript:extension.js
const vscode = require('vscode');
const process = require('child_process');

function activate(context) {
	var disposable = vscode.commands.registerCommand('hoge', () => {
		child_process.exec('maya_run.bat')
	});
	context.subscriptions.push(disposable);
}
exports.activate = activate;

function deactivate() {
	
}
exports.deactivate = deactivate;
```

<details><summary>コマンドを登録できるのはエクスプローラーとエディター、ショートカットの3つです。</summary><div>

```json:package.json
{
	"commands": [
		{
			"command": "hoge",
			"title": "hoge fuga",
			"category": "myExtension"
		}
	],
	"menus": {
		"explorer/context": [
			{
				"command": "hoge",
				"group": "myExtension"
			}
		],
		"editor/context": [
			{
				"command": "hoge",
				"group": "myExtension"
			}
		]
	},
	"keybindings": [
		{
			"command": "hoge",
			"key": "alt+shift+m",
			"when": "editorTextFocus"
		}
	]
}
```
</div></details>

#### (補足)インタープリタの統合 
拡張を書かなくてもTerminal に`mayapy`を指定してフラグを渡せば、
VSCodeから`mayapy`環境を起動することが出来ます。[^2]

```json:settings.json
{
	"terminal.integrated.shell.windows": "C:\\Program Files\\Autodesk\\Maya2019\\bin\\mayapy.exe",
	"terminal.integrated.shellArgs.windows": ["-i", "-c", "import maya.standalone; maya.standalone.initialize('python')"]
}
```

powershellも使うしWSLも使うみたいな方にターミナルが占拠されるのは苦しいので、
シェルと継続的にやり取りする必要がある場合は Terminal オブジェクトを作ります。

```javascript:extension.js
vscode.window.createTerminal("mayapy", "C:\\Program Files\\Autodesk\\Maya2019\\bin\\mayapy.exe", ["-i", "-c", "import maya.standalone; maya.standalone.initialize('python')"]);
```

Houdini や Blender など、アプリに付属するPythonインタプリターにパスを渡せば、
簡易ディスパッチャとしてVSCodeで同時処理することも簡単です。


### 他のエクステンションの拡張
巨大なエクステンションを自前実装するのは、骨の折れる車輪の再発明です。
中にはコードを読んでもどうやって実装するのかよくわからないエクステンションもあります。
`WorkspaceConfiguration` と `executeCommand` の２つを使って内容を書き換えると楽です。

#### workspace の動的な書き換え
ユーザー設定は個人で異なる事が多いので、エクステンション側から動的に書き換えることができると
多人数開発でのレギュレーション維持に役立ちます。

maya.cmdsで書かれたモジュールを見ていると、スクリプトエディタで書かれている都合上、
タブ・シフトの混在、80文字オーバー、キーワード引数１文字などコーディング規約を守っていません。

さすがに80字制限とショートネームフラグの制約はスクリプトだと厳しいので、
`flake8`を使うときには以下の設定をよく使っています。

```json:settings.json
{
    "python.linting.flake8Enabled": true,
    "python.linting.flake8Args": ["--ignore=E501,E741"],
    "python.formatting.autopep8Args": ["--ignore=E501,E741"],
}
```

このsettings.jsonをチーム内で共有するのも一つの手段ですが、エクステンションから書くことが出来ます。
ms-python.pythonのデバッガやコードフォーマッターをハックするほうが、自前実装より楽です。

```typescript:extension.js
const lint = vscode.workspace.getConfiguration("python.linting");
const format = vscode.workspace.getConfiguration("python.formatting");
var configName = "flake8Enabled";
var setAsGlobal = lint.inspect(configName).workspaceValue == undefined;

lint.update(configName, true, setAsGlobal);
lint.update("flake8Args", ["--ignore=E501,E741"], setAsGlobal);
format.update("autopep8Args", ["--ignore=E501,E741"], setAsGlobal);

// reload して settings を反映
vscode.commands.executeCommand("workbench.action.reloadWindow");
```

自前のvsixやconfigurationをホットリロードする時は上記のコマンドを使います。

#### 拡張側から任意のコマンドの実行
コマンドパレットで呼び出すコマンドは`executeCommand`で指定して呼び出すことができます。
単独で使うよりも前後で処理を少しカスタマイズする際に便利です。

```javascript:extension.js
vscode.commands.executeCommand("python.execInTerminal");
```

導入済みエクステンションを含めたコマンド一覧は、keymapとエクステンションのページの
Contributionsタブから見つけることができます。


### 複数の選択肢をユーザーに提供
SHOTGUN や Jira などチケット管理システムをMayaと連携させて管理することも多いと思います。
今回のケースでは Redmine を使います。npm を探せば公開されたモジュールがすぐ見つかるので、
エクステンションの機能拡張も簡単です。

`npm install node-redmine`

操作に応じてコマンドを拡張するのもひとつの手段ですが、ステータスバーやQuickPickから
選択項目を広げると、一気にソレっぽくなります。
Node.JSではRESTのGETは非同期に行われるので、Promiseで確保しつつエディタ内に表示します。

```javascript:extension.js
const Redmine = require('node-redmine');
const redmine = new Redmine(hostName, config);

getIssue() {
    return new Promise((resolve, reject) => {
        redmine.get_issue_by_id(this.id, {}, (error, data) => {
            if (error) return reject(error);
                return resolve(data.issue.status.name);
            }
        )
    });
}

getIssue().then((issues) => {
	var results = issues.map(issue => {
		const icon = {
			"新規": "$(check)", "作業中": "$(pencil)", "完了": "$(thumbsup)"
		}[issue.status.name];
		return `${icon} ${issue.id} ${issue.subject}`;
	});

	vscode.window.showQuickPick(results, { placeHolder: "割り当てるタスクを選択して下さい。" }).then(item => {
		...
	});
});

```

StatusBarとQuickpickItem上では[GitHub icon](https://code.visualstudio.com/api/references/icons-in-labels)が使えます。
説明が長くなりがちな issue に対して視認性を上げる用途におすすめです。


### 最後に
MarketPlaceに公開しない場合でも`vsce`を使ってエクステンションパッケージしておくと、何かと便利です。
`vsce package名`で作成することが出来ます。

2大ゲームエンジンがVSCodeをサポートしている例を引くまでもなく、ここで紹介した以外にも
可能性のある機能がたくさんあります。オリジナルの拡張を作って、プロジェクト運用で使ってみてください。

[^1]: https://marketplace.visualstudio.com/items?itemName=FXTD-Odyssey.mayapy
[^2]: https://knowledge.autodesk.com/ja/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2019/JPN/Maya-Scripting/files/GUID-83799297-C629-48A8-BCE4-061D3F275215-htm.html