外部依存のある VS Code Extension を公開する
Qiita 初投稿です。
この記事に必要な範囲で自己紹介すると、
- ふだん VS Code を使う
- ノートは Markdown で書く
といった感じです。
RedMine を使う機会があって、Markdown で書いたテキストを Textile に変換したいな1、と思い、VS Code Extension を自作してみました。Extension の名前は Markdown2Textile です。
Extension をつくってみる、公開する、という話は 公式の Getting Started にとても分かりやすく書かれています。この記事では、マーケットプレイスで公開するにあたって、インストールから使用までのハードルを下げるために工夫したことを書きます。
Markdown2Textile について
機能
- Markdown ファイルでテキストを選択する。
- コンテキストメニューで「Copy as Textile」を選択する。
- クリップボードに Textile に変換されたテキストがコピーされている!
メモ
- Markdown から Textile への変換は Pandoc が使える。
- Pandoc まわりのライブラリは Python が充実している(主観)。
- 例えば、変換のロジックをカスタマイズしたいなら pandocfilters というパッケージがある。
- VS Code Extension (Node.js) から Python を使いたいなら python-shell というパッケージがある。
いつか上の項目に関する記事も書けたらいいなと思っています。
ここから本題です。
VS Code Extension, TypeScript, Python の基本知識を前提とした内容になっていると思います。また、コードは元々のソースから切り取ったものなので、単独で動作する保証が無いことをご了承ください。
外部依存のある Extension を導入しやすくする
困ったことに Markdown2Textile は VS Code の外にある Pandoc や Python に依存しています2。ドキュメントに dependencies は自分でインストールしてください、と書いておいても良いのですが、もう少し優しい方法はないかな?と思い、下記の点を調べてみることにしました。
- Pandoc をインストールする方法
- Python の Interpreter を選ぶ方法
- Python のパッケージをインストールする方法
Pandoc をインストールする方法
公式の Install Pandoc に OS ごとのインストール方法が載っていますが、パッケージマネージャの有無とかにも注意する必要があるので、自動化スクリプトを書くのは面倒な気がします。
そこで、Python が既にインストールされていると仮定して、pip
でインストールできないかな?と考えてみます。pip search pandoc
で調べてみると、py-pandoc というパッケージを見つけました。
pyenv で新しい Python 環境をつくって、py-pandoc をインストールしてみます3。
$ pyenv install 3.7.3
$ pyenv global 3.7.3
$ pip install py-pandoc
$ ls ~/.pyenv/versions/3.7.3/bin
2to3 easy_install-3.7 idle3.7 pip pydoc python python3-config python3.7-gdb.py pyvenv
2to3-3.7 idle pandoc pip3 pydoc3 python-config python3.7 python3.7m pyvenv-3.7
easy_install idle3 pandoc-citeproc pip3.7 pydoc3.7 python3 python3.7-config python3.7m-config
py-pandoc を使えば、Interpreter のあるディレクトリに pandoc
がインストールされるようです。Pandoc が見つからなかった場合、Python のパッケージとしてインストール可能であることが分かりました4。
Python の Interpreter を選ぶ方法
Microsoft の Python Extension では、Python: Select Interpreter
というコマンドで Python の Interpreter が選択できるようになっています。これを利用して、Python が version 2 だったときに version 3 の Interpreter を選んでもらう、というようなことを実現したいと思います5。
まず、package.json
に Python Extension に依存することを明示しておきます。
"extensionDependencies": [
"ms-python.python"
],
Python Extension がインストールされていれば、その config にアクセスして Interpreter のパスを取得できるようになります。
function getPythonPath(): string {
const pyConfiguration = vscode.workspace.getConfiguration("python", null);
// fallback to System Python
return pyConfiguration.get<string>("pythonPath", "/usr/bin/python");
}
この config には Python のバージョン情報が含まれていなかったので、自分で調べる必要がありました6。バージョンチェックは TypeScript 側ではなく、python-shell を利用して Python にやってもらいます。
# check_version.py
import sys
if sys.version_info[0] != 3:
print("Python must be version 3")
type PyShellReturn = Thenable<string | Error | undefined>;
function checkPythonVersion(): PyShellReturn {
const pyshell = new PythonShell(
"/path/to/check_version.py", { pythonPath: getPythonPath() });
return communicateWithPython(pyshell)
.then(undefined, (error: string | Error) => {
if (typeof error === "string") {
return vscode.window.showWarningMessage(error);
} else {
throw error;
}
});
}
function communicateWithPython(pyshell: PythonShell): PyShellReturn {
return new Promise((resolve, reject) => {
pyshell.on("message", (message: string) => { reject(message); });
pyshell.end((error: Error) => {
if (error) { reject(error); }
else { resolve(); }
});
});
}
上のコードでは、Python のバージョンチェックに引っ掛かると、vscode.window.showWarningMessage
でメッセージが表示されます。このタイミングで Python Extension の Python: Select Interpreter
を実行すれば、ユーザに Interpreter を選択してもらえます。
Python Extension のソースにある package.json
の commands
という項目を調べると、python.setInterpreter
が Python: Select Interpreter
コマンドの実体であることが分かります。vscode.commands.executeCommand
の引数を "python.setInterpreter"
にすれば、プログラム内から Python: Select Interpreter
を実行することができます。
上のコードの vscode.window.showWarningMessage
を下記の関数に置き換えることにより、エラーパネルの "Select Python Interpreter" ボタンを押してから Interpreter を変更できるようになりました。vscode.window.showWarningMessage
と vscode.commands.executeCommand
が Thenable なことがポイントだと思います。
function selectCompatiblePython(message: string): PyShellReturn {
return vscode.window.showWarningMessage(message, "Select Python Interpreter")
.then((item: string | undefined) => {
if (item === "Select Python Interpreter") {
return vscode.commands.executeCommand("python.setInterpreter");
} else {
throw new Error("Failed to change incompatible Python");
}
})
.then(() => {
// Python 3 を選択していれば、下記の関数は正常に resolve する。
return checkPythonVersion();
});
}
Python のパッケージをインストールする方法
Python モジュールのチェックとインストールは以下のように実装しました。
Python 側で Pandoc の存在チェックに pypandoc
を使用しています。そのため、pypandoc
インストールされていなかった場合、2 回目のチェックが必要です(ダサい)。Pandoc が見つからなかった場合、py-pandoc
をインストールします。ちなみに、モジュールのインポート失敗時に ModuleNotFoundError
になるのは Python 3.6 以降らしいです。
# check_modules.py
missing_dependencies = []
try:
import pandocfilters
except ModuleNotFoundError:
missing_dependencies.append("pandocfilters")
try:
import pypandoc
try:
pypandoc.get_pandoc_version()
except OSError:
missing_dependencies.append("py-pandoc")
except ModuleNotFoundError:
missing_dependencies.append("pypandoc")
try:
import pyperclip
except ModuleNotFoundError:
missing_dependencies.append("pyperclip")
if len(missing_dependencies) > 0:
print(", ".join(missing_dependencies))
今回は標準出力無しを正常終了の証としているので、capture_output=True
とする必要があります。
# install_via_pip.py
import sys
import subprocess
subprocess.run(
[sys.executable, "-m", "pip", "install"] + sys.argv[1:],
capture_output=True)
TypeScript 側では、check_modules.py
から見つからなかったパッケージの名前を文字列で受け取り、それを分割して install_via_pip.py
の引数として渡しています。
function checkPythonModules(): PyShellReturn {
const pyshell = new PythonShell(
"/path/to/check_modules.py", { pythonPath: getPythonPath() });
return communicateWithPython(pyshell)
.then(undefined, (error: string | Error) => {
if (typeof error === "string") {
return installPythonModules(error);
} else {
throw error;
}
});
}
function installPythonModules(message: string): PyShellReturn {
return vscode.window.showWarningMessage(
`Missing Python modules: ${message}`, "Install Missing Dependencies")
.then((item: string | undefined) => {
if (item === "Install Missing Dependencies") {
const pyshell = new PythonShell(
"/path/to/install_via_pip.py", {
pythonPath: getPythonPath(),
args: message.split(", "),
});
return communicateWithPython(pyshell);
} else {
throw new Error("Failed to install missing Python modules");
}
})
.then(() => {
// Pandoc のチェックに pypandoc を使っている
// はじめに pypandoc が無かった場合を考えて再チェックする
return checkPythonModules();
});
}
テストを書く
公式の Getting Started に従って Extension をつくると、すでに Mocha というテストフレームワークが準備されています。Node.js 側では PythonShell を mock して、、、Python 側は別にテストを準備して、、、とかやりたかったのですが、今回は時間が無かったので、vscode-arduino を参考にして、Extension とそのコマンドの存在確認だけ実装してみます。
const EXTENSION_ID = "irisTa56.markdown2textile";
const EXTENSION_COMMANDS = [
"md2tt.convertText",
"md2tt.selectPythonInterpreter",
];
const activateExtension = (): Thenable<undefined> => {
return new Promise((resolve, reject) => {
const extension = vscode.extensions.getExtension(EXTENSION_ID);
if (typeof extension === "undefined") { reject(); }
else {
if (extension.isActive === false) {
extension.activate().then(() => { resolve(); });
} else {
resolve();
}
}
});
};
suite("Markdown2Textile: Extension Tests", () => {
test("should be present", () => {
assert.ok(vscode.extensions.getExtension(EXTENSION_ID));
});
test("should be able to register md2tt commands", () => {
activateExtension().then(() => {
vscode.commands.getCommands(true).then((commands) => {
const foundCommands = commands.filter((value) =>
EXTENSION_COMMANDS.indexOf(value) >= 0 || value.startsWith("md2tt.")
);
assert.equal(foundCommands.length, EXTENSION_COMMANDS.length);
});
});
});
});
ハマりポイントは、Extension を activate してから then
で繋がないとコマンドが見つからなかったところです。このテストで activate に時間を要していることが分かり、改善点が一つ明らかになりました。また、activate の最中に外部の Python を使っているため、このテストは外部依存しています。PythonShell を mock して改善してみたいです(いつか)。
またの機会にちゃんとしたテストの記事を書けたらいいなと思います。
マーケットプレイスに公開する
公式なチュートリアルがあります。
まずは Azure DevOps にログインします。GitHub のアカウントが使えます。
あとはチュートリアル通りに進めていけば大丈夫!
まとめ
- Markdown2Textile。
- Python を使った VS Code Extension をつくれる。
- VS Code から出ずに Python のパッケージをインストールする仕組みをつくれる。
- 時間があれば microsoft/vscode-python = Python Extension をしっかり読みたい。もっと良い方法があるはず。
-
RedMine を Markdown 記法に切り替えることもできますが、Textile で書かれた過去データを変換してくれる訳ではないようなので、RedMine 側の設定変更は非現実的だと考えています。 ↩
-
Python への依存を無くし、代わりに Node.js のパッケージを使うほうが正しいかもしれません。しかし、Pandoc まわりは Python のほうが人気みたいなので、今回は Python を使いました。 ↩
-
System の Python を使った場合、権限が無いとか言われそうですが、今回は気にしないことにします。 ↩
-
Markdown2Textile は version 3 の Python のみサポートしています。 ↩
-
Python Extension 自体は Python のバージョン情報を持っているみたいですが、取得方法が分かりませんでした。 ↩