⚗️

VSCodeでもmagrittrパイプをいい感じに入力するためにVSCode拡張機能の作成にチャレンジしてみる

2022/05/05に公開

R言語におけるパイプ

この記事はVSCodeでR言語プログラミングを行う際、いい感じにパイプを入力するキーボードショートカットを実装する方法について説明するものです。

R言語ではbash terminalのパイプのようなものが存在します。magrittrというパッケージにより%>%という演算子が実装されており、ごく最近リリースされたR4.1.0以降ではサードパーティーパッケージに依存しないNative pipe|>が導入されたことも話題になっています。最新のNative pipeについては私のブログにて解説させていただいておりますので、よければご参考ください。
https://excel2rlang.com/base-pipe-r420/

パイプ演算子は以下のように使用します。

library(tidyverse)

starwars %>%
    filter(height > 100) %>%
    group_by(species) %>%
    summarise(mass_mean = mean(mass))

一応Rには緩めのコード規約があり、tidyverse style guideではパイプに関して以下のルールを定めています。

  • パイプ演算子の前にはスペースを一つ置く
  • パイプ演算子の直後は改行する
  • 一番最初のパイプの後はインデントする

VSCodeでパイプのキーボードショートカットを設定する

Rstudioでは{Ctrl}+{Shift}+{m}がパイプ入力のショートカットと設定されています。このキーボードショートカットはVSCodeが生まれる前から存在しており、体に染みついてしまっています。そこで、VSCodeにもそのようなショートカットを設定したいわけですが、ググると答えがすぐに見つかります

keybindings.jsonに以下を追記すると、簡単にRstudioと同じキーボードショートカットを設定することができます。

[
    {
        "key": "Ctrl+Shift+m",
        "command": "type",
        "args": {
            "text": " %>% "
        },
        "when": "editorTextFocus && editorLangId == r"
    },
    {
        "key": "Alt+-",
        "command": "type",
        "args": {
            "text": " <- "
        },
        "when": "editorTextFocus && editorLangId == r"
    },

]

editorLangIdrmdqmdなどを加えておくと良さそうです。

VSCodeパイプショートカットを少し改良する

先ほどのキーボードショートカットを設定しておけば概ね快適にRコーディングができますが、一つ残念なのが行末の空白(スペース)があろうとなかろうと空白付きのパイプ%>%が入力されてしまう点です。Rstudioのパイプキーボードショートカットは行末に空白があるかを判断して、空白が二つ入らないような仕様となっていて、これがいい味出していたわけです。


RStudioは空白をいい感じに認識してくれる


VSCodeのキーボードショートカットだけでは空白がダブることがある

できればVSCodeでも同じような仕様のキーボードショートカットを作りたいと思ったのですが、調べてみると拡張機能として実装するよりほかなさそう😰ということでjavascriptを一切触ったことない筆者がVSCode APIを調べながら悪戦苦闘してなんとか動作するようになった話をします。

拡張機能開発環境の準備

一通りパッケージ化までを経験してから思ったのは、公式の説明が十分わかりやすく、VSCode自身のサジェスト内容でAPI仕様情報は大部分わかると感じました。拡張機能開発で困ったらAPI referenceを見ると良いと思います。

まずYour First Extensionの通り、フォルダの準備します。

# いつかにインストールしたnvmを使ってnpmバージョンを変える
nvm use v18.1.0

yarn install -g yo generator-code
yo code

# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? convertRCode
### Press <Enter> to choose default for all options below ###

# ? What's the identifier of your extension? convertRCode
# ? What's the description of your extension? VSCode R utils
# ? Initialize a git repository? Yes
# ? Bundle the source code with webpack? No
# ? Which package manager to use? yarn

# ? Do you want to open the new folder with Visual Studio Code? Open with `code`

yo codeを実行すると変な地蔵みたいなアスキーアートが登場しつつ、質問に答えながら進むと自動的に必要なファイルを含むディレクトリが作成されます。

extensions.tsを編集する

どうやらextensions.tsが拡張機能の本体のようです。VSCodeのエクスプローラー画面でextensions.tsを選択した状態でF5キーを押すと、extensions.tsを拡張機能としてロードした状態のウィンドウがポップアップします。

すると、サンプルコードとして実装されていたHello worldコマンドがコマンドパレットから実行可能な状態になっています! ちょこちょこっとtypescriptを書いてはF5でデバッグする、という流れはとても開発しやすいと思いました😲

そして公式のAPIリファレンスやらjavascriptにおける文字列の取り扱い方などを調べた結果、以下のような機能を書きました。

export function activate(context: vscode.ExtensionContext) {
	
    let _ = vscode.commands.registerCommand('transformrcode.addmagrittrpipe', function() {
        
        const editor = vscode.window.activeTextEditor;
        
        if (editor) {
            // 現在の行rangeを取得
            let currentline = editor.document.lineAt(editor.selection.active.line).range;
            // 現在の行のstringsを取得
            let currentstring = editor.document.lineAt(editor.selection.active.line).text;
            
            if (currentstring.endsWith(" ")) {
                let result = currentstring+"%>%";
                editor.edit(edit => edit.replace(currentline, result))
                .then(success => {
                    var position = editor.selection.end;
                    editor.selection = new vscode.Selection(position, position);
                });
            } else {
                let result = currentstring+" %>%";
                editor.edit(edit => edit.replace(currentline, result))
                .then(success => {
                    var position = editor.selection.end;
                    editor.selection = new vscode.Selection(position, position);
                });
            }
        }
    });
}

リファレンスからはパッと見よくわからなかったのが、どうやってカーソル位置の行の文字列を取得するかということと、どうやって表の文字列を変更するかということでした。また、一通り実装した機能が動作を確認した後、入力した%>%が選択された状態になってしまうため、どうやって文字列置換の後に選択を解除するかという点も難しかったように感じます。

肝心の空白認識ですが、 が一つあるか否かで条件分岐する単純な仕組みで書きました。typescriptの基礎も学んでいない私は変数の取り扱いをまるで理解していないためとても無駄で冗長的なコードを書いている気がしますが、まあ今はよしとします👋

さらに、package.jsonにも以下のようにコマンドを定義するようです。試した感じ、コマンドパレットではcontributes.commands.titleに記述してある文字列にfuzzy searchがかかるようでした。

	"activatVonEvents": [
        "onCommand:transformrcode.addmagrittrpipe"
	],
	"contributes": {
		"commands": [
            {
                "command": "transformrcode.addmagrittrpipe",
                "title": "add magrittr pipe"
            }
		]
	},

パッケージ化してインストールする

最後に公式の"publishing extension"を参考にしつつ書いたスクリプトをパッケージ化してインストールしてみます。

yarn install -g vsce
yarn vsce package

このとき、ろくに作者名やライセンス等の記述をしなかったので「ええんか?」などといっぱい聞かれますが、yyyyと進んでいきます。するとtransformrcode-0.0.1.vsixというパッケージ化されたファイルが出力されるので、右クリック=>拡張機能のVSIXのインストールでインストール完了です!

キーボードショートカットを定義する

ここまでの操作でコマンドパレットから%>%を入力することができるようになっていますが、最後の仕上げとしてキーボードショートカットを定義します。
VSCodeのkeybindings.jsonに以下の内容を入力すればかゆいところに手が届くパイプショートカットの完成です!🦚

    {
        "key": "ctrl+shift+m",
        "command": "transformrcode.addnativepipe",
        "when": "editorTextFocus"
    },


空白があってもダブらなくなった!

おまけ: Native pipe |> も実装しておく

先ほどのextensions.tsに以下のようなものを書き加えておくことで、native pipe|>も入力可能にしておきました。

export function activate(context: vscode.ExtensionContext) {
	
    let __ = vscode.commands.registerCommand('transformrcode.addnativepipe', function() {
        
        const editor = vscode.window.activeTextEditor;
        
        if (editor) {
            // 現在の行rangeを取得
            let currentline = editor.document.lineAt(editor.selection.active.line).range;
            // 現在の行のstringsを取得
            let currentstring = editor.document.lineAt(editor.selection.active.line).text;
            
            if (currentstring.endsWith(" ")) {
                let result = currentstring+"|>";
                editor.edit(edit => edit.replace(currentline, result))
                .then(success => {
                    var position = editor.selection.end;
                    editor.selection = new vscode.Selection(position, position);
                });
            } else {
                let result = currentstring+" |>";
                editor.edit(edit => edit.replace(currentline, result))
                .then(success => {
                    var position = editor.selection.end;
                    editor.selection = new vscode.Selection(position, position);
                });
            }
        }
    });
}

あとはkeybindings.jsonで実行するコマンドを変更するなどすれば臨機応変にパイプの種類を変更することができます💥

おわりに

というわけで、以上がVSCode拡張機能を作っていい感じにパイプを入力させるようにした方法でした。

この1~2年はすっかりRからPythonへとデータ解析の軸足を移していたのですが、社内でRをレクチャーするようになったのを機にRへ再入門しました。今回少しだけ触れたnative pipe|>が新たに実装されたり、torchやkerasがRでも使えるようになったりと、まだまだRも進歩しているのだなあと感じます🤔

それでは!この記事が誰かの役に立ちますように🌟

(お知らせ) 初心者向けのRブログ始めました
https://excel2rlang.com

Discussion