Vimのexpressionレジスタ「=」で計算結果を挿入する
コード中の数値をちょっと計算したいだけなのに、電卓アプリを開いて結果をコピペしていました。Vimには式を評価してその場に挿入できる特殊なレジスタがあり、これを使えばウィンドウを切り替える必要がなくなります。
ただし"=は、他のレジスタと同じ「文字列の入れ物」ではありません。抱えているのはテキストではなく式そのもので、使うたびに評価し直されます。基本の打ち方から、型ごとの挿入のされ方、コマンドラインでの振る舞い、式が失敗したときの後始末までを、Vim 9.1.1752とNeovim 0.12.4で実際に打って確かめた結果とあわせて整理します。確認はどちらもプラグインを読まない状態(vim -Nu NONEとnvim -u NONE)で行いました。
目次(8項目)
| キー | これだけ覚える |
|---|---|
| Ctrl-r = | インサートモードで式を評価して挿入 |
"= + p | ノーマルモードで式を評価して挿入 |
| @{レジスタ} | 式の中で他のレジスタの値を使う |
| Ctrl-f | 式の入力中に押すと履歴のウインドウが開く |
基本の使い方
インサートモードでCtrl-r =を押すと、画面下に式を入力できる状態になります。式を入力してEnterを押すと、その計算結果がカーソル位置に挿入されます。
Total: │ ← インサートモード
12*8<Enter>
Total: 96
画面下に出てくる入力欄は、見た目どおりコマンドラインの一種です。Ctrl-wで単語ごとに消す、↑で履歴をたどるといった編集操作がそのまま効きます。式を打っている間はバッファにまだ何も起きておらず、Enterを押した瞬間に初めて結果が流し込まれます。
Escで抜けると式は破棄され、何も挿入されません。手元では行末がTotal: (の状態でCtrl-r =を押し、1+1と入力してからEsc、続けて閉じ括弧を打ちました。行はTotal: ()になり、抜けたあとも挿入モードの続きから打てています。式を打ち間違えても編集の流れが切れないので、気軽に呼び出せます。ノーマルモードでは"=のあとにpで貼り付ける形になります。Ctrl-rに続けて押せるキーの一覧はレジスタの貼り付け方にまとめています。
"=12*8<Enter>p
「=」はテキストを持たないレジスタ
名前付きレジスタや番号付きレジスタが文字列を抱えているのに対して、"=が抱えているのは式の文面です。公式ヘルプもレジスタとしては例外だと断っています。
This is not really a register that stores text, but is a way to use an expression in commands which use a register.
この性質は代入して試すとはっきりします。ヘルプが read-write と書いているとおり、:letで式を入れておくことができます。
:let @= = "'L' . line('.')" " 式そのものを入れておく
:echo @= " 'L' . line('.') と返る(L2 ではない)
読み出しても評価結果ではなく式の文面がそのまま返ってきます。値が決まるのは貼り付けた瞬間です。上の状態で2行目にカーソルを置いて"=と打ち、式を入力せずにEnter、続けてpを押すとL2が入りました。同じ手順を4行目で繰り返すとL4が入ります。式が保存された瞬間ではなく、使った瞬間のカーソル位置で評価されているわけです。:registersで一覧を出しても"=の行には40+2のような式の文面が並び、他のレジスタのように結果の文字列が入っているわけではありません。この表示はVimでもNeovimでも同じでした。
式を入力せずにEnterを押すと直前の式を使い回す、という挙動もここから来ています。:let @= = "40+2"を実行してからCtrl-r =Enterと押したところ、手元では42が挿入されました。同じ計算を離れた場所へ何度も差し込むときに効きます。
式の中では@aのような記法で他のレジスタの中身も読めます。レジスタaに数値をヤンクしておいてから@a * 2と書けば、その2倍が入ります。レジスタの種類ごとの役割分担はレジスタ全般の一覧で整理しています。
評価結果の型で挿入のされ方が変わる
| 評価結果の型 | 挿入される内容 |
|---|---|
| 数値・浮動小数点数 | 自動的に文字列へ変換されて挿入 |
| リスト | 各要素が1行ずつになり、末尾にも改行が付く |
| 辞書 | string()相当の文字列表現がそのまま挿入される({'a': 1}のような見た目になる) |
| 関数参照(Funcref) | エラー(E729)になる |
結果はいったん文字列に直されますが、直し方が型によって違います。手元で[1,2,3]を評価したところ、3行が入ったうえに空行が1行余りました。これはリストに固有の話ではなく、文字列が改行で終わっていると行単位の貼り付けとして扱われるという規則の表れです。
:let @= = "\"NEW\\n\""のように改行で終わる文字列を入れてpを押すと、カーソル行の下に新しい行として入りました。改行を外した"NEW"では、カーソルの次の位置に押し込まれる文字単位の貼り付けになります。辞書はキーを整列した{'a': 1}という文字列になり、エラーにならないぶん意図しない挿入に気づきにくくなります。関数参照だけはE729: Using a Funcref as a Stringで止まりました。
数値では割り算に注意が必要です。整数どうしの/は切り捨てになるので、3/2も24/16も結果は1になります。小数が欲しいときは片方を浮動小数で書きます。
Ctrl-r =24/16<Enter> " 1(整数どうしなので切り捨て)
Ctrl-r =24.0/16<Enter> " 1.5
Ctrl-r =printf('%.2f', 24.0/16)<Enter> " 1.50(桁を指定する)
浮動小数はそのまま入れると桁が丸められます。1.0/3は0.333333までしか入らず、1000000.0*1000は1.0e9という指数表記になりました。この2つはVim 9.1.1752とNeovim 0.12.4で同じ結果でした。桁をそろえたいときや指数表記を避けたいときはprintf()を挟むのが確実で、printf('%.0f', 1000000.0*1000)なら1000000000が入ります。詳細な仕様は公式ヘルプ(:h registers)で確認できます。
"=が持っているのはテキストではなく式です。Ctrl-r =(インサートモード)でも"=(ノーマルモード)でも、評価はキーを押したその場で走ります。
コマンドラインでは結果がいったん手元に戻る
expressionレジスタはコマンドラインモードでも同じキーで呼び出せますが、結果の行き先が違います。インサートモードでは結果がそのままバッファへ入るのに対し、コマンドラインでは入力中の行に文字として並ぶだけで、実行はされません。
item 1からitem 3までが並ぶファイルで、検索の/を押してからCtrl-r =でprintf('item %d', 3)と入力し、Enterを押してみました。画面下は/item 3という表示のまま止まり、もう一度Enterを押して初めて検索が走り、カーソルが3行目へ移りました。式を組み立ててから実行前に目で確かめられるので、置換のように失敗が怖いコマンドと相性がよくなっています。
行に分かれない点は覚えておくと事故を減らせます。:を押してから[1,2]を評価すると、コマンドラインには1^@2^@と表示されました。改行はNUL文字として1行の中に並ぶので、リストを返す式をコマンドラインで使うことはまずありません。
置換の中で式を使いたいときは、"=ではなく:s側の入口を使います。置換文字列の先頭に\=を書くと、残りがVim scriptの式として扱われます。この記法はレジスタとは無関係で、:sの置換文字列の中だけで通じる約束事です。
:%s/\d\+/\=submatch(0)*10/ " item 1 → item 10
:%s/^/\=line(".").": "/ " 各行の頭に行番号を付ける
この2つは入口が違うだけでなく、評価される回数も違います。Ctrl-r =が評価するのは押した1回きりですが、\=はマッチのたびに評価されます。3行のファイルで2つ目のコマンドを流すと各行の頭が1: 、2: 、3: となり、行ごとにline('.')が計算し直されていることが確認できました。マッチした文字列をsubmatch()で受け取れるのも\=側だけの利点です。
いまの状態を材料にして計算する
式レジスタが実用になるのは、いまバッファにある数値を使って計算するときです。ビジュアルモードで数値を選び、cで消してからCtrl-r =を押すと、消した値が無名レジスタに入っているので式の材料にできます。
v で数値を選ぶ
c " 消して挿入モードへ(消した値は " に入る)
Ctrl-r = @" * 2<Enter> " 消した値を2倍して挿入
単純な増減ならCtrl-aのほうが速いので、この形が効くのは掛け算や割り算、単位の変換のようにCtrl-aでは届かない計算です。式の中ではVim scriptの関数も使えるため、カーソル位置や外部コマンドの出力まで材料にできます。
Ctrl-r =line('.')<Enter> " 現在の行番号
Ctrl-r =repeat('-', 40)<Enter> " 区切り線を40文字
Ctrl-r =trim(system('date'))<Enter> " 外部コマンドの出力を1行で
system()を使うときにtrim()で包んでいるのは、前の節の規則があるからです。外部コマンドの出力は改行で終わるため、そのまま入れると余分な行が生まれます。手元でsystem('echo hi')を評価するとhiのあとに空行が残り、trim(system('echo hi'))ではhiだけが入りました。repeat()のほうは区切り線や桁合わせの空白を数えずに作れるので、覚えておくと出番があります。
ファイル名と日付もよく使う材料ですが、こちらは別に記事があります。expand()の修飾子でファイル名を削り出す書き方は読み取り専用レジスタの使い道に、strftime()の書式指定は日付を挿入する方法にまとめました。
Neovimではluaeval()を式の中で呼べます。Ctrl-r =に続けてluaeval('2^10')と打つと1024が入りました。一方でmacOSに最初から入っているVim 9.1.1752は-luaでビルドされているため、同じ式は使えません。Luaで書いた処理をその場で呼びたいときはNeovim側の利点になります。
CSSのpx値をremに変換する計算を毎回電卓アプリでやっていましたが、expressionレジスタでCtrl-r =24.0/16<Enter>のように打てばその場で結果が挿入されると知ってから、ウィンドウを切り替える手間がまるごと消えました。地味ですが、積み重なると無視できない時短になりました。連番を振りたいときにline('.')を組み合わせる応用も覚えてからは、単純な四則演算以上の場面でも頼るようになっています。
履歴から呼び戻す、式が失敗したとき
式の入力欄はコマンドラインの一種なので、専用の履歴が残ります。Ctrl-r =を押したあと↑を押せば、前に打った式を呼び戻せます。手元で12*8、100/7、strftime("%Y")の順に評価してから↑を2回押すと、strftime("%Y")、100/7と遡れました。同じ計算を少しだけ変えて何度も使うときは、打ち直すより速く済みます。
長い式は履歴のウインドウで直すほうが確実です。Ctrl-r =のあとにCtrl-fを押すと[Command Line]という名前のウインドウが開き、過去の式が=付きで並びます。通常の編集操作で書き換えてからEnterで実行できます。ノーマルモードから開くq:にあたる操作は式の履歴には用意されておらず、q=を押しても手元では何も起きませんでした。ウインドウそのものの使い方はコマンドラインウインドウの使い方で扱っています。
式が失敗したときは何も挿入されません。未定義の変数を書くとE121: Undefined variable: nosuchvarが表示され、Enterでメッセージを閉じると挿入モードの続きから打てました。バッファに残るのは途中まで打った文字だけで、失敗した式の分は入りません。ただしエラーは画面の下に一瞬出るだけなので、長い式ほど原因を見失いやすくなります。組み立てに自信がないうちは:echoで結果を確かめてから使うと安全です。
まとめ
expressionレジスタは、レジスタの形をした式の評価窓口です。文字列を溜め込まないぶん使うたびに値が決まり、カーソル位置や外部コマンドの出力まで材料にできます。結果の行き先がインサートモードとコマンドラインで違うことと、整数どうしの割り算が切り捨てになることの2点を押さえておけば、電卓アプリを開く回数はかなり減ります。他のレジスタとの役割分担はレジスタ全般の一覧で確かめられます。