Vimから外部コマンドを呼び出してバッファをフォーマットする
この記事はVim駅伝の2024-08-07の記事です。
前回の記事はPeacockさんのVolar (Vueの言語サーバー)をnvim-lspconfigとNixで使うための設定です。
次回の記事はArc CosineさんのChromeOSでNeovim環境を構築するです。
Vimは標準でフィルタ機能(:h filter
)を備えています。標準入出力を扱うシェルコマンドを利用して現在のバッファを書き換える機能です。
たとえば、deno fmt
を使うと、:%!deno fmt --ext=json -
でjsonファイルをフォーマットすることができます。
複雑な設定が要らず便利なのですが、これの問題点は、フォーマッタが(構文不備などで)エラーを出力した場合、現在のバッファがエラーメッセージで書き換えられてしまうことです。
ということで、エラー対応を加えた関数を作成して使っています。
コード
ほとんどmattn/vim-sqlfmtを参考にしました。
function! fmt#run(cmd) abort
" フォーマット範囲取得 Visual modeでは`<cmd>`で呼び出される想定
let [firstline, lastline] = [1, '$']
if mode() == 'v'
let [firstline, lastline] = [line('.'), line('v')]
if firstline > lastline
let [firstline, lastline] = [lastline, firstline]
endif
endif
" バッファの内容をstdinから渡してフォーマット実行
let buf = join(getline(firstline, lastline), "\n")
let lines = systemlist(a:cmd, iconv(buf, &encoding, 'utf-8'))
" エラーが発生したら表示 バッファは変更せず終了
if v:shell_error != 0
echohl WarningMsg
for line in lines
" ターミナル用のカラーコードは除去
echomsg substitute(line, '\e[[0-9]\{-1,}m', '', 'g')
endfor
echohl None
return
endif
" バッファの書き換え・カーソル位置の復帰
defer setpos('.', getcurpos())
call append(lastline, lines)
silent! execute $'{firstline},{lastline}delete _'
endfunction
上記のコードをfmt.vim
として保存し、runtimepath
に登録すると使えるようになります。
call fmt#run('deno fmt --ext=json -')
のように呼ぶとバッファ全体をフォーマットします。
使い方
筆者の設定はこんな感じです。関数の呼び出しを<space>p
にマッピングしています。
autocmdでファイルタイプ別のフォーマットコマンドを設定し、コマンドの設定のないファイルでは=
を使ってインデントの調整を行っています。
augroup my_fmt
autocmd Filetype typescript,typescriptreact,javascript,javascriptreact,markdown,json,jsonc
\ let b:fmt_cmd = $'deno fmt --ext {expand("%:e")} -'
autocmd Filetype nix let b:fmt_cmd = 'alejandra -q -'
" ファイルタイプごとの設定が続く…
augroup END
nnoremap <expr> <space>p exists('b:fmt_cmd')
\ ? '<cmd>call fmt#run(b:fmt_cmd)<cr>'
\ : 'mzgg=G`z'
xnoremap <expr> <space>p exists('b:fmt_cmd')
\ ? '<cmd>call fmt#run(b:fmt_cmd)<cr>'
\ : '=gv'
範囲フォーマットの注意点
範囲指定はマッピング経由で呼ぶことを想定しているので、'<,'>call
で使うことはできません。
また、フィルターコマンドからは選択した部分しか見えないので、その範囲だけで構文エラーのない状態になっている必要があります。
関連
defer
でカーソル位置を保存しておく手法は以下の記事でも紹介しました。
<cmd>
マッピングでrangeを取得する手法は以下の記事でも紹介しました。当時書いたものとは少し変わっていますが。
Discussion