Python の dotted path をコピーする VS Code 拡張機能を作ってみた
🔧 作ったもの
Copy Python Path という Python の dotted path (例 foo.hoge.class
)をコピーする拡張機能を作ってみました。
開発理由は、Django で unittest を実行する際に、dotted path を使うのですが、それを毎回手動で組み立てるのが面倒だったからです。
機能はこちらです。
- クラス or メソッドの定義行でコマンドを実行すると、ルートからそのクラス or メソッドまでの dotted path をクリップボードにコピーする
- クラス or メソッドの定義行以外でコマンドを実行すると、ルートからそのファイルまでの dotted path をクリップボードにコピーする
こちらからインストールできます。レビューをもらえると泣いて喜びます。
コードもこちらで公開しています。
🦾 仕組み & 工夫したところ
JS で Python コードをパースしてメソッド・クラス定義を解析
ファイルまでの相対パスだけならば VS Code の API から取得できるのですが、クラス・メソッドの定義までのパスとなると、VS Code の API から取得することはできません。
正確にクラス・メソッド名、定義位置(階層構造)を取得するためには Python コードをパースする必要がありました。
そこで今回は、 ANTLR4 ベースのパーサージェネレーターである dt-python-parser を使ってみました。
このライブラリに Python コードを与えると AST(抽象構文木)にパースしてくれ、さらに ANTLR4 ベースのメソッドで、AST をトラバースできます。
dt-python-parser を使った copy-python-path の解析部分のコードはこちらです。
// ...
/**
* Get defined symbols related to the selected rows from a python file. e.g. Class name, function name
* @param {string} text - python code
* @param {number} lineNumber - current cursor line number
* @return {array} defined symbols
*/
export const getRelatedDefinedSymbols = (text: string, lineNumber: number): string[] => {
const parser = new Python3Parser();
const tree = parser.parse(text);
const symbols: DefinedSymbol[] = [];
class MyListener extends Python3Listener {
enterClassdef(ctx: any): void {
symbols.push({
name: ctx.children[1].getText(),
line: ctx.children[0].getSymbol().line,
column: ctx.children[0].getSymbol().column,
});
}
enterFuncdef(ctx: any): void {
symbols.push({
name: ctx.children[1].getText(),
line: ctx.children[0].getSymbol().line,
column: ctx.children[0].getSymbol().column,
});
}
}
const listenTableName = new MyListener();
parser.listen(listenTableName, tree);
const symbol = symbols.filter(s => s.line === lineNumber)[0];
if (!symbol) {
return [];
}
if (symbol.column === 0) {
return [symbol.name];
}
const parentSymbolNames = getDefinedParentSymbols(symbol, symbols).map(s => s.name);
return [...parentSymbolNames, symbol.name];
};
dt-parser でクラス名・メソッド名と、その定義位置を取得しコードの構造に合わせて並び替えた配列を作っています。
コード構造に合わせた並び替えは、getDefinedParentSymbols
内にて再帰処理で行っています。
E2E テストで動作を担保
仕組みは簡単な拡張機能ですが、継続してメンテナンスしやすいように E2E テストを追加してみました。
以下の画像のようにテストを実行すると、VS Code が別プロセスで立ち上がり実際コマンドを実行してくれます。
テストコードはこちらです。
// ...
const executeCommandWithWait = async (command: string): Promise<any> => {
await sleep(500);
await vscode.commands.executeCommand(COMMAND_NAME);
await sleep(1000);
};
const COMMAND_NAME = 'copy-python-path.copy-python-path';
// example.py is following code
/*
class ClassA:
def class_a_method_a():
pass
class ClassB:
def class_b_method_a():
pass
class ClassD:
def class_d_method_a():
pass
*/
const testFileLocation = '/pythonApp/example.py';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
let editor: vscode.TextEditor;
setup(async () => {
// open folder
const fileUri = vscode.Uri.file(vscode.workspace.workspaceFolders![0].uri.fsPath + testFileLocation);
const document = await vscode.workspace.openTextDocument(fileUri);
editor = await vscode.window.showTextDocument(document);
});
test('selected class lines', async () => {
editor.selection = new vscode.Selection(new vscode.Position(0, 0), new vscode.Position(0, 0));
await executeCommandWithWait(COMMAND_NAME);
assert.strictEqual(await vscode.env.clipboard.readText(), 'pythonApp.example.ClassA');
});
test('selected method lines', async () => {
editor.selection = new vscode.Selection(new vscode.Position(1, 0), new vscode.Position(1, 0));
await executeCommandWithWait(COMMAND_NAME);
assert.strictEqual(await vscode.env.clipboard.readText(), 'pythonApp.example.ClassA.class_a_method_a');
});
test('selected nested class lines', async () => {
editor.selection = new vscode.Selection(new vscode.Position(4, 0), new vscode.Position(4, 0));
await executeCommandWithWait(COMMAND_NAME);
assert.strictEqual(await vscode.env.clipboard.readText(), 'pythonApp.example.ClassA.ClassB');
});
// ...
});
カーソル位置を移動しながら、コマンドを実行し、クリップボードへのコピー結果を検証しています。
適宜、setTimeoutを使い実行感覚を開けている所がポイントです。
実は VS Code 拡張機能のテストの方法は、ほぼほぼ情報がなくとても苦労しました。VS Code のドキュメントにも Hello World レベルの簡単なテスト記載しかなく、結局、Microsoft の出している拡張機能の実際のテストコードをみながらテストを書きました。
※ 参考になったリポジトリ
GitHub Actions での CI 環境の整備
VS Code のドキュメントに CI の情報があったので、そちらを参考に E2E テストと拡張機能のリリースの CI を GitHub Actions で組んでみました。
GitHub Actions のイメージに VS Code の起動に必要なパッケージは入っているようで、ほぼ考慮点はなく実行することができました。
以下が E2E テストの GitHub Actions です。
Mac と Linux(ubuntu)と Windows のイメージで並列してテストを実しています。
name: E2E
on:
push:
branches:
- main
jobs:
build:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'
- name: Install dependencies
run: yarn install
# xvfb is required to run vscode on linux
- name: Run test on linux
run: xvfb-run -a yarn test
if: runner.os == 'Linux'
- name: Run test on Mac and Windows
run: yarn test
if: runner.os != 'Linux'
リリースはこちらです。
name: release
on:
workflow_dispatch:
inputs:
release_type:
description: 'select release type'
type: choice
options:
- patch
- minor
- major
required: true
jobs:
build:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'
- name: Install dependencies
run: yarn install
# xvfb is required to run vscode on linux
- name: Run test on linux
run: xvfb-run -a yarn test
if: runner.os == 'Linux'
- name: Run test on Mac and Windows
run: yarn test
if: runner.os != 'Linux'
- name: Publish
if: success() && matrix.os == 'ubuntu-latest'
run: |
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
yarn run deploy -- ${{ github.event.inputs.release_type }}
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
リリースバージョンを管理するために、リリース種別をオプションで受け取る手動ワークフローとしています。
E2E テストの CI と同じ処理の実行後、リリースのスクリプトを実行しています。
リリース自体はvsceを利用しているのですが、その内部でバージョンのコミットが走るので、それ用に git config も設定しています。
おわりに
開発を思い立って数日で公開できたので、かなりスピード感をもって開発できてよかったなと思いました。今回で VS Code 拡張機能開発の基礎は学べたので、今後も色々便利な拡張機能の開発や既存の拡張機能への PR など積極的に行っていきたいです。
Discussion
これすごくほしかった拡張機能だったので、大変感謝しております!!!
CIでしっかりテストされているのも素晴らしいです👏
うおお!!ありがとうございます 🙌