LoginSignup
6
4

More than 3 years have passed since last update.

外部依存のある VS Code Extension を公開する

Posted at

外部依存のある VS Code Extension を公開する

Qiita 初投稿です。

この記事に必要な範囲で自己紹介すると、

  • ふだん VS Code を使う
  • ノートは Markdown で書く

といった感じです。

RedMine を使う機会があって、Markdown で書いたテキストを Textile に変換したいな1、と思い、VS Code Extension を自作してみました。Extension の名前は Markdown2Textile です。

Extension をつくってみる、公開する、という話は 公式の Getting Started にとても分かりやすく書かれています。この記事では、マーケットプレイスで公開するにあたって、インストールから使用までのハードルを下げるために工夫したことを書きます。

Markdown2Textile について

機能

  1. Markdown ファイルでテキストを選択する。
  2. コンテキストメニューで「Copy as Textile」を選択する。
  3. クリップボードに 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.jsoncommands という項目を調べると、python.setInterpreterPython: Select Interpreter コマンドの実体であることが分かります。vscode.commands.executeCommand の引数を "python.setInterpreter" にすれば、プログラム内から Python: Select Interpreter を実行することができます。

上のコードの vscode.window.showWarningMessage を下記の関数に置き換えることにより、エラーパネルの "Select Python Interpreter" ボタンを押してから Interpreter を変更できるようになりました。vscode.window.showWarningMessagevscode.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 をしっかり読みたい。もっと良い方法があるはず。

  1. RedMine を Markdown 記法に切り替えることもできますが、Textile で書かれた過去データを変換してくれる訳ではないようなので、RedMine 側の設定変更は非現実的だと考えています。 

  2. Python への依存を無くし、代わりに Node.js のパッケージを使うほうが正しいかもしれません。しかし、Pandoc まわりは Python のほうが人気みたいなので、今回は Python を使いました。 

  3. pyenv で Python をインストールするときにこの記事に助けられました。 

  4. System の Python を使った場合、権限が無いとか言われそうですが、今回は気にしないことにします。 

  5. Markdown2Textile は version 3 の Python のみサポートしています。 

  6. Python Extension 自体は Python のバージョン情報を持っているみたいですが、取得方法が分かりませんでした。 

6
4
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
6
4