🔣

Vimで記号や演算子をサクッと補完する

2024/08/23に公開

この記事はVim駅伝の2024-08-23の記事です。
前回の記事はとんとんぼさんのIdeaVimの設定方法です。
次回の記事はstaticWagomUさんのstatusline隠すとかっこよくなることに気づいてしまった...です。


演算子がホームポジションから遠い問題

プログラミング言語では、各々の処理を行うため、さまざまな記号の組み合わせを使用します。

たとえば、Vim scriptで使われる演算子には次のようなものがあります。

..=
=>
=~#
!=?

筆者が最近勉強しているGleamでは、以下のような演算子が頻出です。

->
|>
<>
<-

打ちづらくないですか??

一般的な配列のキーボードであれば、ホームポジション付近にはアルファベットが集中しているでしょう。
タッチタイピングに慣れた方でも、!=?->といった記号列を流れるように打つのは難しいのではないでしょうか。少なくとも筆者はintと打つように!=?と打つことはできません。各々の記号が離れている上にShiftキーの切り替えもする必要があります。手がしんどいです。

ということで、使いたい記号をサクッと出せるよう、補完を自分で定義することにしました。

記号専用の補完リストを定義

「特定のキーを押すと記号一覧が出てきて選択できる」というのを考えました。
起動キーは何でもいいのですが、押しやすく、影響の少なそうな,にしました。

vimrc
" filetypeごとに使いたい記号を設定
autocmd Filetype vim let b:symbol_list = ['..=', '=>', '=~#', '!=?']
autocmd Filetype gleam let b:symbol_list = ['->', '|>', '<>', '<-', '_ ->']

" 補完候補を表示する関数
function! s:symbol_cmp() abort
  " col('.')-1を使うことでカーソル直前の`,`を消しつつ補完する
  call complete(col('.')-1, b:symbol_list)
endfunction

" リストがあれば補完スタート なければ普通に`,`を入力
inoremap <expr> , exists('b:symbol_list')
      \ ? ',<cmd>call <sid>symbol_cmp()<cr>'
      \ : ','

,で起動し、以下のように補完候補が表示されます。
選択すればその記号に置換され、入力を続ければ,がそのまま活かされます。


gleamの例

,で展開して<c-n>/<c-p>で選択する程度であれば、指を大きく開く必要はありません。
だいぶ楽になりました。

なお、b:symbol_listがない場合は補完展開せずに通常の,にフォールバックします。
この条件分岐は以下の記事と同じ手法です。

https://zenn.dev/vim_jp/articles/f480fbad7572b7

補完候補をダイレクトに選択

例では演算子を数個しか示していませんが、候補が増えると上下移動で選択していくのも大変です。表示された候補をダイレクトに選択できると便利です。
最近追加されたKeyInputPreを使ってこれを実現してみました。

補完リストに'menu'キーを加え、選択用の文字を表示します。

vimrc
" ファイルタイプごとの補完候補の定義自体は変更なし
autocmd Filetype vim let b:symbol_list = ['..=', '=>', '=~#', '!=?']
autocmd Filetype gleam let b:symbol_list = ['->', '|>', '<>', '<-', '_ ->']

" 補完候補を表示する関数
function! s:symbol_cmp() abort
  " 補完指定用のキー 順番は要検討
  let triggers = 'hjklnmfdsavcuiorew'->split('\zs')

  " b:symbol_listとtriggersを組み合わせて補完リストを構築
  " (都度mapするのは効率が悪いかも?)
  " :help complete-items も参照
  let comp_list = b:symbol_list->copy()
        \ ->map({i, v -> {'word': v, 'menu': get(triggers, i, '')}})

  " ↑ を使って補完発動
  call complete(col('.')-1, comp_list)

  " ここからautocmd追加
  augroup symbolselect
    autocmd!
    " 候補展開中にキーが押されたときにs:symbol_select()を実行
    autocmd KeyInputPre * call s:symbol_select()
    " ↑が永続しないようモード変更時にautocmdを削除
    autocmd ModeChanged * autocmd! symbolselect
  augroup END
endfunction

function! s:symbol_select() abort
  " 現在の補完のリストは以下の式で取得できる
  let items = complete_info(['items']).items

  " 候補リストのmenuと入力されたキー(v:char)を照合
  let idx = indexof(items, {_,v -> get(v, 'menu', '') ==# v:char})

  " menuと対応するキーが押されていたら…
  if idx >= 0
    " カーソル直前の`,`を削除
    let v:char = "\<bs>"
    " v:charには1文字しか入れられないのでwordの入力にはfeedkeysを使う
    call feedkeys(items[idx].word, 'ni')
  endif
endfunction

" 起動用のキーマップは変更なし
inoremap <expr> , exists('b:symbol_list')
      \ ? ',<cmd>call <sid>symbol_cmp()<cr>'
      \ : ','

これで補完リストから直接選択するような動きができます。


gleamの例

autocmd KeyInputPreが有効なのは候補リストが出ている間だけなので、補完後には影響が残りません。詳しくはModeChangedmode()のヘルプをご覧ください(補完完了に伴いモード変更が発生します)。

類似手法との比較

  • inoremap ,h ->と異なり、2キー目の入力待ちが発生しません。
  • inoabbrev ,h ->と異なり、スペースなどを入力しなくても,hだけで展開されます。
  • 上記2種と異なり、リストが表示されるので展開用のキーを忘れても問題ありません。

注意点

  1. menuに設定したキーが別のキーにマッピングされていた場合(例:inoremap h H)はv:char自体が切り替わっているため照合できず動作しません。
  2. ,hのような文字列が入力できなくなります。これはマッピング等でも同じですが。, <bs>hであれば可能です。起動キーは選択用のキーと連続で入力しないものを使ってください。

Discussion