🧞

Vimでspaceなしで展開できるabbrevっぽいものを作る

2023/08/09に公開

この記事はVim駅伝の2023-08-09の記事です。
前回の記事はigrepさんのVimの検索で、都合がいいときだけ大文字小文字を区別するです。
次回の記事はstaticWagomUさんのsandwichを使ったら案の定便利だったはなしです。


Vimで少し文字数の多いコマンドを入力するとき、あるいは似た形のコマンドを頻繁に使用する(置換など)ときに、 abbreviation (短縮入力)機能を使うと便利です。
しかしこの機能は、英単語の入力が想定されているようで、spaceやenterで単語区切りが発生したときに初めて変換が実行されます。コマンドラインでは区切りを待たずに即変換されたほうが便利だと感じました。

そこで、spaceなどを入力しなくても即変換される仕組みを作りました。

" {{{ AbbrevCmd
" expand abbreviations immediately in command-line
let s:abbrev_cmds = {}
function! s:abbrev_cmd(raw_rhs, cmd_lhs, ...) abort
  " generate rhs of cnoremap
  const rhs = a:raw_rhs ? "'<c-u>'.." .. join(a:000, ' ') :
        \ "'<c-u>" .. substitute(join(a:000, ' '), "'", "''", 'g') .. "'"

  " pick the last character of lhs
  " example: mes -> me & s
  const lhs_except_last = slice(a:cmd_lhs, 0, -1)
  const lhs_last = slice(a:cmd_lhs, -1)

  " save rhs to handle same lhs_last character
  if !has_key(s:abbrev_cmds, lhs_last)
    let s:abbrev_cmds[lhs_last] = {}
  endif
  let s:abbrev_cmds[lhs_last][':' .. lhs_except_last] = rhs

  " make string that represents key-value pair of dict
  const rhs_list = map(items(s:abbrev_cmds[lhs_last]), {_,val -> printf("'%s':%s", val[0], val[1])})

  " execute cnoremap
  const fmt = "cnoremap <expr> %s get({%s},getcmdtype()..getcmdline(),'%s')"
  execute printf(fmt, lhs_last, join(rhs_list, ','), lhs_last)
endfunction
command! -nargs=+ -bang AbbrevCmd call s:abbrev_cmd(<bang>0, <f-args>)
" }}}

使用する際は上記をvimrcなどにコピーして読み込んでください。

使い方

AbbrevCmdコマンドは最初の引数が変換元文字列、第二以降の引数が変換先文字列です。
!を付けると第二以降の引数を式として扱い、変数などを利用できます。map-<expr>のイメージです。

例えば次のように使います。

AbbrevCmd mes messages
AbbrevCmd! ss '%s/' .. @/ .. '//g<Left><Left>'

これは、以下のマッピングコマンドを実行します[1]
cnoremap <expr> s get({':me':'<C-U>messages',':s':'<C-U>'..'%s/' .. @/ .. '//g<Left><Left>',},getcmdtype()..getcmdline(),'s')

これにより、コマンドモードでsを入力した場合、すでにmeが入力されていればmessagesに、すでにsが入力されていれば%s/(最後の検索文字列)/(ここにカーソル)/gに、それぞれ変換されます。それ以外の場合であれば普通にsの入力となります。

結果的に、コマンドラインで:mesまで入力したタイミングで:messagesが展開されます。通常のabbreviationsとは異なり、spaceやenterを押す必要はありません。

付記

  • 変換元文字列にrangeを使うことはできません
  • buffer-mapには未対応です
  • マッピング末尾に<cr>を入れれば展開だけでなく実行までされます
  • インサートモード用にも作れると思いますが、コマンドラインモードよりも多様な文字列が入力されうるため、暴発の危険が大きそうです

参考

abbreviationを活用すること、dictを使ったコマンドを生成して実行することは以下の記事にインスパイアされました。

https://zenn.dev/monaqa/articles/2020-12-22-vim-abbrev

サンプルで使った:ssは以下の記事が元ネタです。

https://zenn.dev/vim_jp/articles/2023-06-30-vim-substitute-tips

脚注
  1. 1回目はmesだけがマッピングされ、2回目にmesとssがマッピングされます。最後の実行が有効になります ↩︎

Discussion