開発効率を高める上でエディタの入力補完は重要な機能です。
数文字を入力しただけで目的とする単語もしくは文章の全体が入力できる効率の良さに加え、うろ覚えの状態でも入力可能な利便性、タイポを防ぐことでの品質への貢献など様々な恩恵を得られます。
Vimとしてどのような入力補完が用意されているかを網羅的に把握している訳ではありませんが、個人的な必要性に準じて調査・検討した結果として現在使用している補完方法としては以下の3つがあります。
- 辞書
- complete関数
- Abbreviations(略語)
今回はこれら入力補完に関して紹介したいと思います。
辞書
「Vimマスターへの道:画面分割とターミナル機能でTDD促進」でも触れましたが、挿入モードにおける入力補完は、ある程度文字を入力しておいて<C-n>もしくは<C-p>を入力することで、そこまでの入力文字列を含む単語を候補として選択可能にしてくれる機能です。
この時、デフォルト状態でも同時に開いているファイル内の単語などは候補になります。例えばPHPのプログラムを書いていて標準的に出てくる「public」や「function」と言った単語に関しても、それがどこかに既に書かれていれば補完対象になります。逆に言えば、初めて入力する場合は補完対象になりません。単語全体を自力で入力する必要があります。しかし、このような標準的な単語は最初から補完対象になっていてもらいたいところです。この課題を解決するのが「辞書」です。
辞書作成
まずは辞書ファイルを作成します。特に難しいことはなく、補完対象にしたい単語を羅列するのみです。
ファイル置き場としてはvimrcと同じ「~/.vim」配下とし、辞書だけでまとめておけるようにサブフォルダ「~/.vim/dict」を作成し、この配下に目的別に辞書を作成する形が良さそうです。
サンプルとしてPHPに関する辞書を「~/.vim/dict/php」として作成してみます。
public
private
protected
class
function
(続く)
辞書は作成しただけでは有効にならず、辞書を読み込む設定と読み込んだ辞書を補完対象とする設定の2つが必要です。
辞書の読み込み
辞書を読み込む設定としてvimrcに以下の内容を追記します。
augroup vimrc
autocmd!
autocmd BufNewFile,BufRead *.php set dictionary=$HOME/.vim/dict/php
augroup END
辞書の読み込み自体は以下の書式のみで実現されます。
set dictionary=$HOME/.vim/dict/php
ただ、上記の書式だけではPHP以外のファイルに関しても同辞書が読み込まれてしまいます。PHPファイル(拡張子が「.php」)を開いた場合のみ同辞書を読み込むようにする設定が以下です。
autocmd BufNewFile,BufRead *.php set dictionary=$HOME/.vim/dict/php
「BufNewFile」は新規にファイルを作成する場合、「BufRead」は既存ファイルを読み込んだ場合、くらいの意味です。
なお、上記のようにvimrc内でautocmdを無条件実行してしまうと、vimrcを再読み込みするタイミングでも同処理が実行されてしまうようです。上記辞書読み込みに関する重複実行の弊害がどの程度あるのかは良く分かりませんが、他のケースも含めたautocmd実行の基本的作法として重複実行を避けるような形を標準的に採用しておくのが良さそうです。それが以下の構造になります。
augroup vimrc
autocmd!
(各種autocmd実行)
augroup END
「augroup」ではautocmdの実行をグループ化する仕組みです。上記では「vimrc」と言う名称のグループを定義していることになりますが、グループ名称は任意です。「autocmd!」は既存のautocmdの実行結果を無効にする設定ですが、上記のようにグループ内に書くことで、同グループ内で実行されたautocmdの結果のみ無効にできます。つまり上記書式全体としては「vimrcグループとして実行されたautocmdの結果を一旦無効化しておいて再度実行し直す」と言う処理を行っている訳です。
これらの書式を組み合わせたものが、最初に示した内容になります。
辞書の補完対象化
「Vimマスターへの道:画面分割とターミナル機能でTDD促進」でも触れたように、補完候補をどこから取得してくるかは「complete」と言う変数で決まっていますが、デフォルト設定(.,w,b,u,t,i)に辞書は含まれていません。
辞書を意味する記号は「k」なので、これをcompleteに追加するようvimrcに下記設定を追記しましょう。
set complete=.,w,b,u,t,i,k
これで辞書の内容が補完対象として有効になったかと思います。
complete関数
前述した基本的入力補完では目的とする単語の一部(前方)を入力することで残り部分が補完されますが、逆に言えば単語の最初の方を正しく入力できなければ使えません。
例えば、Laravelのコレクションは多機能で、それらを使いこなせば本来複雑な処理もシンプルに書けるのですが、多機能ゆえに名称・用法を正確に覚えていない物も多く、使いたくなるたびにGoogle先生に質問する必要が生じます。この問題を単なる入力補完で解決しようとしてもスペルが分かっていないので候補自体を呼び出せなかったり、候補を呼び出せたとしても正しい用法までは分からず、結局はGoogle先生のお世話になると言うことになりがちです。
このような問題を解決する手段がcomplete関数です。
具体的な用法は以下のようになります。
nnoremap <Leader>lcol a<C-R>=MyCompleteCollection()<CR>
func! MyCompleteCollection()
call complete(col('.'), [
\ {'word': 'contains()', 'abbr': 'contains', 'info': 'contains(val|[key => val]|callback) / 指定したアイテムがコレクションに含まれているかどうかを判定'},
(その他定義)
\ ])
return ''
endfunc
上記の処理内容全体をざっくりまとめると、「Laravelコレクション機能用の補完候補をcomplete関数を使って選択・設定させる関数MyCompleteCollectionを定義し、Vim画面のノーマルモード状態で<Leader>(<Space>)+”lcol”と入力した際に同関数を呼び出すことで、設定した補完候補から選択・設定できるようにする」と言う内容になっています。
complete機能を使用するメリットは、目的特化で候補を提示できることと、補完文字列の候補だけでなく関連する補足情報まで扱えると言うことです。
complete関数の用法
complete関数の引数は、第一引数が入力対象位置で、上記のように「col(“.”)」とするとカーソル位置になります。
第二引数は候補のリストになりますが、書式的にはPHP風に言うと連想配列の配列です。候補1つに関する情報が1つの連想配列として表現され、それが候補分だけ配列になった構造になっています。
1つの候補に関する情報としてはいくつかの属性を設定できるようになっていますが、個人的に必要性を感じるのは今のところ以下の3つです。
word | 挿入される文字列(必須) |
abbr | メニュー上に表示される文字列 |
info | 補足情報 |
「word」で定義されたものが実際に挿入される文字列です。
「abbr」はメニュー上に選択候補として表示される文字列です。メニュー上に表示する文字列と実際に挿入される文字列を区別したい場合に有効です。
先に示した例では括弧の有無しか違いがありませんが、標準的構造として入力される文字列とメニュー上の選択肢を別にしておいた方が融通が利きそうと判断し、このようにしています。
「info」は補足情報ですが、本属性として設定した情報がどこに表示されるかは「completeopt」と言う変数の設定内容に依存します。デフォルトでは「preview」となっており、その場合はプレビューウインドウと言う、画面分割的な意味での別画面(私の環境では候補表示を行った画面の上部を分割する形)に設定内容が表示されます。
この情報はあくまで参考情報として表示されるのみで補完内容とは直接関係しません。言い換えれば補完時に参考にしたい情報を自由に書いておける訳です。加えてプレビューウインドウは補完操作自体が完了しても残り続けるため、引数や戻り値に関する情報などを書いておけば引き続きそれらを参考にできます。逆にプレビューウインドウを消す手間が生じるため、そこを不便に感じるようであれば付けなくても問題ありません。
complete関数の用法は上記の通りですが、これを目的ごとに独自に定義した関数内から呼び出し、この目的特化形独自関数を必要に応じて呼び出すことで用途にあった補完候補が選択できるようになる訳です。
独自関数の用法
前述したようにcomplete関数を呼び出す独自関数を定義したとして、それをVim上で使えるようにする設定が必要になります。
それが以下です。
nnoremap <Leader>lcol a<C-R>=MyCompleteCollection()<CR>
これ自体は単なるノーマルモードのマッピングです。ただし、最初に「a」とすることでいきなり挿入モードに切り替えていますが。
「<C-R>=(処理内容)<CR>」は挿入モードにおいて「処理内容」で記述した内容を実行し、その結果を反映すると言うものですが、complete関数においては選択結果の反映含めて実行してしまう模様で、独自関数自体は空文字を返しています。よって、ここでは独自関数を実行すること自体が目的(戻り値の反映は実質無意味)です。
挿入モードの操作をあえてノーマルモードの操作として定義しているのは、マッピングする独自キー操作の都合です。
挿入モードでは基本的には様々なキー入力がそのまま有効になっている必要があります。例えば何らかの操作を「a」と言う文字にマッピングしてしまうと「a」と言う文字の入力が行えなくなってしまいます(「a」を押した段階でマッピングされた操作だと解釈されてしまうため)。よって、入力として成立しにくい「コントロールキー+何らかのキー」のような操作とマッピングする必要が生じますが、必然的に押し難い操作になってしまいます。
そのため、独自キー操作として選択の自由度が高いノーマルモードとして定義しています。
この辺は完全に個人の好みです。
Abbreviations(略語)
略語は文字通り入力したい文字列に対応した短い文字列を定義し、その短い文字列の入力を以て元の長い文字列の入力と見なす操作です。
具体的には以下のような使い方ができます。
iab if if () {<CR>}
「iab」が略語定義を示すキーワードです。
次の文字列(スペースまで)が略語であり、上記例では「if」がそれに当たります。
それ以降は実際に入力されるべき文字列で、上記例では「if () {<CR>}」がそれに当たります。この例のように変換後の内容は複数単語から構成されていたり、複数行に渡ることも可能です。
実際、上記定義後に挿入モードにおいて「if<space>」と入力すると以下のように展開されます。
if () {
}<space>
「<space>」は半角スペース一文字で、元々の入力で「if<space>」のように最後にスペースを入力しているため、それが補完後にもそのまま残っています。略語の入力では、それが略語として定義された文字列を意味することを明示する必要があります。上記例で言えば「if」と入力しただけでは、例えば「iframe」のような文字列を入力しようとしている途中かもしれません。よって明確に「if」だとわかるように単語として完結させる必要がある訳です。
ちなみに半角スペース以外では記号全般でも単語の完結と見なされるようです。言い換えれば単語が継続していると見なされるのはアルファベットもしくは数字だけのようです(全て確認した訳ではありませんが)。特殊なケースは「改行」で、これは単語の切れ目を意味するような気がするのですが補完は行われず、単に「if」の後に改行されるだけです。
上記のように単語の完結を意味する1文字が必要なことは略語を用いる上で少々問題となる点です。文字通り長い単語もしくは1行内の部分的なフレーズを略語として定義したケースでは、半角スペースで略語の入力を完結(本来期待される文字列を反映)、スペース以降に続きの文字列を入力、と言う流れで不都合はありませんが、先に示したif文の例などでは最後の半角スペースは明らかに邪魔です。これを残さない方法がないかと調査してみましたが、今のところ有効な方法は見つかっていません。
総括
今回は入力補完方法を3点紹介しましたが、上記以外にもマッピングなども入力補完に使用できます。挿入モード用のマッピングなどはVimとしての操作の簡略化よりも入力自体の簡略化を目指したものが多いかもしれません。
具体例としては以下のようなものがあります。
inoremap { {}<Left>
上記は「{」と入力しただけで「}」も入力され、かつ入力位置を「{」と「}」の間に戻すと言うものです(蛇足ながら、個人的には「{」の入力後に「}」を入力する習慣がついてしまっているため逆に邪魔になり、今のところ使っていませんが)。
Vim全般に言えることですが、特に入力補完は自力で育てていく必要がある機能で、知っただけで便利になると言うものではありません。しかし見方を変えれば、工夫次第でいくらでも成長する機能であり、品質・生産性の向上に大きく貢献してくれそうな期待感もあります。
前述の「{}」の補完のように一見便利そうでも使ってみると馴染めないものもあったり、一方で全般的に慣れの問題もあるのである程度は辛抱強く使ってみる必要もあったりと、なかなかに試行錯誤を要する作業にはなると思いますが、頑張ってマイVim環境を育てていきたいと思います。