Vim の abbrev を使いこなしてみる

2020/12/22に公開

この記事は Vim advent calendar 2020 の 22 日目の記事です。
昨日は tamago324 さんによる記事「『普通の Vim』を『ゲーミング Vim』に変える魔法のプラグイン」でした。
明日は wordijp さんによる記事「実行結果の出力先をモードレスウィンドウとして起動したVimにする」です。

はじめに

Vim に abbreviation (短縮入力)の機能があることをご存知ですか?キーマップやコマンドのユーザ定義は知っていても abbreviation についてはよく知らない、という方も多いかもしれません。
abbreviation は Vim の中でも影の薄そうな機能です。マッピングやユーザ定義コマンドについては様々な解説記事を見かけますが、abbreviation についての記事はさほど見かけません。

今日は、そんな少し不遇な abbreviation の使い道を紹介します。

abbreviation とは

Vim の abbreviation を一言でいうと、「単語を自動展開する機能」です。予めルールを決めておけば、トリガーとなる単語を打つだけで特定の文字列へと自動的に展開してくれます。Vim のヘルプにこんな例が載っていました。

.vimrc
iab ms Microsoft

これを設定ファイルに書くかコマンドライン上で実行すれば、 ms というキーワードに反応する abbreviation が登録されたことになります。その状態で挿入モード中に ms というキーワードを打てば、自動的に Microsoft に展開されるようになります。

おお!便利!…かな?

短縮入力の新規登録・一覧表示・解除を行うコマンドは以下のとおりです。

モード 登録 一覧表示 解除
INSERT :iabbrev {lhs} {rhs} :iabbrev :iunabbrev
Command-line :cabbrev {lhs} {rhs} :cabbrev :cunabbrev
INSERT/REPLACE/Command-line :abbreviate {lhs} {rhs} :abbreviate :unabbreviate

先程の例に登場した iab というコマンドは iabbrevInsert-mode abbreviation)というコマンドの省略記法です。つまり、以下のように書いても効果は変わりません。

.vimrc
iabbrev ms Microsoft

流石に "ab" では何の略か分かりにくいので、以下は省略しない記法で統一して書きます。

その他にも abbreviation に関するコマンド・決まりごとは色々あります。詳細は Vim のヘルプ (:help abbreviations) を参照してください。

よくある abbreviation の使い道

タイプミスの修正

「特定の単語を自動で変換してくれる」という性質を活かし、タイプミスの自動修正に応用するというものです。

単純な修正

よくやらかしがちなタイプミスというのは、誰にでもあるでしょう。たとえば、私はよく Python を書くとき import 文を書こうと思ってよく improt とタイプミスしてしまいます。同じミスなのにいちいち全て手動で同じ修正を行うのは、プログラマにとって苦痛ですよね。
退屈なことは Vim にやらせるべきです。

.vimrc
iabbrev improt import

もし Python のファイルにしか適用したくないのであれば、ファイルタイプを Python に限定するのがよいでしょう。

.vimrc
augroup my_vimrc
  autocmd FileType python iabbrev <buffer> improt import
augroup END

このように、 <buffer> を付ければ特定のバッファに限って abbreviation を適用することができます。

タイプミスはバッファ内だけでなく、コマンドライン中でも起きます。たとえば、私は :w コマンドで保存するときに間違えて :w] と打ってしまいます。 ] というファイルを意図せず作ってしまうのは Vimmer として格好がつきません(?)。:cabbrev コマンドを使って未然に防ぎましょう。

.vimrc
cabbrev w] w

これで ] という謎ファイルがいつのまにか作成されるのを防ぐことができます。素晴らしい!

高度な修正

…と言いたいところですが、実は上で紹介した w] の定義には少し問題があります。

  1. :cabbrev/ コマンドによる検索文字列の入力でも有効になるため、 w] という文字列を検索したいときに面倒になる
  2. それ以外でも、コマンドの途中に w] が入ると w に置換されてしまう可能性がある

特に 1 は重大です。 w] を検索しようと /w]<CR> と打ったと思ったら、いつの間にか w を検索していた…なんてことがあったら、頭がどうにかなりそうですね。

しかし、ご安心を。この程度であれば、定義を少し複雑にするだけで回避できます。

.vimrc
cabbrev <expr> w] (getcmdtype() ==# ":" && getcmdline() ==# "w]") ? "w" : "w]"

先程と異なり、 cabbrev コマンドの直後に <expr> オプションが指定されています。これは :map コマンドにおける <expr> オプションと同様、「後ろの式を評価して、得られる文字列を用いる」ということを表します。つまり、 (getcmdtype ... "w]" という文字列にそのまま置き換えるのではなく、(getcmdtype ... "w]" という式を評価して得られる文字列に置き換えます。

(getcmdtype() ==# ":" && getcmdline ==# "w]") ? "w" : "w]" は Vim script で書かれた式ですが、以下のことさえ知っておけば Vim script に馴染みがなくてもおよそ意味がとれるはずです。

  • getcmdtype(): 現在のコマンドラインの種類を取得する関数。コマンドラインの種類には以下のものがある。

    • ::w:q など、通常の Ex コマンド)
    • / (前方検索するときに出るやつ)

    種類についての詳細は :help cmdwin-char参照。

  • getcmdline(): 現在のコマンドラインに書かれている文字列を取得する関数。

  • ==#: 文字列の比較演算子。右辺と左辺が等しければ真 (1)、そうでなかえれば偽 (0) に評価される。

  • &&: いわゆる論理積。

  • «expr» ? «expr-true» : «expr-false»: いわゆる三項演算子。条件 «expr» が真のときは «expr-true» が、偽のときは «expr-false» が評価される。
    詳しくは :help 41.3参照。

つまり、「通常の Ex モード中で、今現在 w] とだけ書かれている場合は w]w に展開する。それ以外は w] のままにする」という規則で展開します。したがって、検索文字列入力時や別のコマンドの引数となっている場合に誤反応してしまうのを防げるというわけです。

もう一つの具体例として、VISUAL モードに入った状態で保存しようと :w<CR> のように叩くと

:'<,'>w

のように「範囲指定された状態での :w コマンド」が起動し、「部分的に保存したければ ! をつけろ」みたいな趣旨のエラーが出てしまいます(設定によっては「部分的に保存しますか?」というダイアログが出る場合もあるものの、これはこれで面倒くさい)。実際にファイルを部分的に保存したくなることは滅多にないでしょうから、これも abbreviation を使って抑止してしまいましょう。先ほどと同じ要領で、

.vimrc
cabbrev <expr> w (getcmdtype() ==# ":" && getcmdline() ==# "'<,'>w") ? "\<C-u>w" : "w"

とすればよいですね。条件に合致したときは <C-u> キーを叩いてコマンドラインを一度まっさらな状態にすることで、望みの挙動を実現しています。

このように、 Vim script の式に基づいて動的に展開文字列を設定できる :abbrev <expr> は非常に強力です。もし abbreviation の副作用(展開すべきでないときに展開してしまう現象)に悩むことがあれば、 <expr> を用いて抑止できないか考えてみるとよいかもしれません。

特定のキーワードの大文字化

もっぱら SQL で用いられる印象。私の観測範囲では SQL 以外で必要になったことがありません。以下のように特定のキーワードを abbreviation として登録しておけば、自動で大文字になります。

.vimrc
augroup my_vimrc
  autocmd FileType sql iabbrev <buffer> select SELECT
  autocmd FileType sql iabbrev <buffer> from FROM
  autocmd FileType sql iabbrev <buffer> where WHERE
  autocmd FileType sql iabbrev <buffer> order ORDER
  autocmd FileType sql iabbrev <buffer> by BY
  " 以下略
augroup END

地味に便利ですね!Shiftキーを押す回数がぐんと減るため、小指の健康に貢献してくれそうです。
また、SQL のキーワードに一律で上のような abbrev を付与してくれる Vim プラグインもすでに存在します。普段から SQL を書く人は使ってみると良いかもしれません。

https://github.com/jsborjesson/vim-uppercase-sql

実装から分かる通り、内部で :iabbrev コマンドを使用しています。ただしこちらも <expr> を使って少し高度なことをしており、文字列やコメントの中では自動で大文字にならないよう配慮されています。

短縮入力としての使い道

さて、今度は原点に立ち返り、 abbreviation を短縮入力として使うことを考えてみましょう。

とはいいつつも、INSERT モードで短縮入力を行うために iabbrev が使われることはさほどありません。バッファ上の短縮入力ならプレースホルダなどの機能が充実している UltiSnips などのスニペットプラグインのほうが使いやすいことが多いからです[1]

では、Commane-line モードの方はどうでしょうか?

コマンドライン中で短縮入力を行う手段

長ったらしいコマンドを短いストローク数で打ち込む方法は主に3つあります[2]

  1. コマンドラインの補完を用いる
  2. 短いコマンドを独自で定義する
  3. abbreviation を用いる

コマンド実行操作そのものを特定のキーにマッピングしてしまう手もありますが、今回はあくまでコマンドラインモードに入る前提で工夫することにします。

1つ目の補完を用いる方法は最も手軽であり、ユーザが何も設定しなくとも候補を選ぶだけでよい優れた手法です。しかし補完候補が多い場合はさしてストローク数の削減にはならないことも多く、「定型化した何度も使うコマンドを一瞬で呼び出したい」という用途にはあまり向いていません。

2つ目は設定ファイルで独自にコマンドを定義してしまう方法です。たとえば、 CocConfigCocCommand snippets.editSnippets という「長ったらしいもののよく使う」コマンド [3] があったとして

.vimrc
command! CC CocConfig
command! Cs CocCommand snippets.editSnippets

のように定義する方法です。このように、ユーザ定義したコマンド名は基本的に大文字で始まるものでなければなりません。打ちやすいのは明らかに小文字のコマンドなのですが、そのままでは定義できないのです。悔しいですね。

.vimrc
" E183: ユーザー定義コマンドは英大文字で始まらなければなりません
command! cc CocConfig

cabbrev を用いた短縮入力

そこで :cabbrev です。 :cabbrev を使えば、コマンドライン上での短縮入力を手軽に定義することができます。

先程の例同様、CocConfigCocCommand snippets.editSnippets を楽に書く方法を考えてみましょう。:cabbrev を用いて短縮入力を実現するには以下のようにします。

.vimrc
cabbrev cc CocConfig
cabbrev cs CocCommand snippets.editSnippets

こうすると :cc と打ってエンターキーを押せば、自動的に :CocConfig<CR> に展開されて :CocConfig コマンドが実行されます。お手軽ですね!

先程紹介した :w] コマンドと同様の工夫も入れてみると、以下のように書くことでより副作用の少ない便利な abbreviation となります。

.vimrc
cabbrev <expr> cc (getcmdtype() ==# ":" && getcmdline() ==# "cc") ? "CocConfig" : "cc"
cabbrev <expr> cs (getcmdtype() ==# ":" && getcmdline() ==# "cs") ? "CocCommand snippets.editSnippets" : "cs"

実は :cc:cs も標準のコマンド名と被っているので、上の例をそのまま用いる場合はそこだけ注意しましょう。上の定義を行った場合も、:␣cc␣cs のように最初に一つスペースを入れればもともと定義されていた方を問題なく使えます(getcmdline() ==# "cc" 等の条件を満たさなくなるため)。

短縮入力の利点・欠点

abbreviation には以下の利点があります。

  • 自動で展開されるため、タブ補完より手間が少ない。

  • 独自コマンドと比べて名前の制約がゆるい(英小文字からはじめてもOK)。

  • 展開後に編集することもできるため、末尾にオプションを付けるといった操作が簡単にできる。

  • コマンド履歴の検索性が上がる。

    :command! などを用いて自分専用のエイリアスを大量に定義すると、後でコマンド履歴を他人が見たときに訳の分からないことになりがち & 検索性も低くなりがち。それに対し、 :cabbrev を用いる方法では展開後のコマンドが実行履歴として保存されるため、他人からどのようなコマンドを打ったか分かりやすくなる & 検索性も上がる。

一方、以下の点に気をつけなければいけません。

  • 副作用(変なタイミングで起動してしまうこと)への注意が必要。

    少し定義は複雑になるものの、本記事で述べた方法で回避すると良い。

  • 自分で規則を覚えないといけない。

    タブ補完のように候補を出してくれないため、キーマップ同様作った規則を記憶しておく必要がある。ただし、:ab と打てばユーザ定義された短縮入力の一覧を出せる。

終わりに

最後までご覧いただきありがとうございました。おそらく、abbreviation を導入したからといって作業が劇的に効率化されるわけではないでしょう。しかし、小さな改善を気軽にたくさん積み重ねられるのも Vim というエディタの大きな魅力だと私は思います。
この記事を通じて「abbreviation、意外と使えるかも」「今まで煩わしかったこの作業が少し効率化できそう」と思ってもらえたなら幸いです。

おまけ

最後の最後に、私が行っている abbreviation 関係の設定(の一部)をまとめておきます。今の所 coc.nvimgina.vim のコマンドに対して適用してますが、今後もっと増えるかも。

.vimrc
" 範囲保存したいときは write を使おう
cnoreabbrev <expr> w ((getcmdtype() ==# ":" && getcmdline() ==# "'<,'>w") ? ("\<C-u>w") : ("w"))

" typo の達人
function! s:modify_write_typo(typo)
  exec 'cnoreabbrev <expr> ' .. a:typo .. ' ((getcmdtype() ==# ":" && getcmdline() ==# "' .. a:typo .. '")? "w" : "' .. a:typo .. '")'
endfunction

call s:modify_write_typo("w2")
call s:modify_write_typo("w]")

" abbrev の自動生成を行う
function! s:make_abbrev_rule(rules)
  let keys = uniq(sort(map(copy(a:rules), "v:val['from']")))
  for key in keys
    let rules_with_key = filter(copy(a:rules), "v:val['from'] ==# '" .. key .. "'")
    let dict = {}
    for val in rules_with_key
      if has_key(val, 'prepose')
        let dict[val['prepose'] .. ' ' .. key] = (val['to'])
      else
        let dict[key] = val['to']
      endif
    endfor
    exec 'cnoreabbrev <expr> ' .. key .. ' '
    \ .. '(getcmdtype() !=# ":")? "' .. key .. '" : '
    \ .. 'get(' .. string(dict) .. ', getcmdline(), "' .. key .. '")'
  endfor
endfunction

call s:make_abbrev_rule([
\   {'from': 'c', 'to': 'CocCommand'},
\   {'from': 'cc', 'to': 'CocConfig'},
\   {'from': 'gb', 'to': 'Gina blame'},
\   {'from': 'gc', 'to': 'Gina commit'},
\   {'from': 'gp', 'to': 'Gina push'},
\   {'from': 'gs', 'to': 'Gina status -s --opener=split'},
\   {'prepose': 'CocCommand', 'from': 's', 'to': 'snippets.editSnippets'},
\   {'prepose': 'Gina commit', 'from': 'a', 'to': '--amend'},
\ ])
脚注
  1. ちなみに、スニペット機能を持つプラグイン選びは deoppet.nvim 等の作者である Shougo さんの記事「スニペットプラグインについて 2020 年版」 が非常に参考になると思います。 ↩︎

  2. 他にも、vim-altercmdなどの特殊なプラグインを用いる方法もありますが、ここでは紹介しません。 ↩︎

  3. CocConfigcoc.nvim というプラグインの設定ファイルを開くコマンド、 CocCommand snippets.editSnippetscoc-snippets という coc.nvim の拡張機能におけるスニペット編集バッファを開くコマンドです。どちらも私にとって「キーマップとして登録したくなるほどではないが、まあまあわりとよく実行するコマンド」という立ち位置だったので例として使いました。 ↩︎

Discussion