📝

:%!xxx-fmtをいい感じにスクリプトでやる

2025/03/05に公開

この記事はVim駅伝の2025-03-05の記事です。
前回の記事はkawarimidollさんの略しすぎて別の単語になってしまったVimのコマンドなにこれクイズでした。私はdebuなのに:debu使ったことがありませんでした。
次回の記事はstaticWagomUさんのvim駅伝を2年間書き続けることで得られたものの予定です。継続してるのえらい。

Vimでは:range!という機能を使うことにより、外部コマンドでフィルターをかけられます。これを使って :%!xxxfmt - のように標準入出力を使ったフォーマットコマンドでフォーマットをかけるという使い方ができます。
これはシンプルでよいのですが、フォーマットをかけた際にカーソルがファイルの先頭に戻る[1]、エラーが起きた際にファイル全体が置き換わる(アンドゥしてもカーソルが戻らないので結構不便)という問題があります。
Vim scriptでも同じことをよりよい方法で行えるため、私はちょっとしたスクリプトを書いて使っています。本記事ではそのスクリプトを紹介して解説します。

function! vimrc#feat#format#execute(cmd) abort
  let result = systemlist(a:cmd, getline(1, '$'))
  if v:shell_error != 0
    for l in result
      echoerr l
    endfor
    return
  endif
  let view = winsaveview()
  call deletebufline('%', 1, '$')
  call setline(1, result)
  call winrestview(view)
endfunction

まず、最初の行はバッファ全体を取得してコマンドに流し込み結果を取得というのをバッファを壊さずに行っています。[2]
コマンドだと問答無用でバッファが書き換わりますが、スクリプトだと v:shell_error でコマンドの返り値を取得できます。
コマンドが異常終了している場合、resultはエラーメッセージとみなせるので各行ごとに:echoerrで出力します。[3]
正常終了している場合は、まずwinsaveview()を使い、画面の表示情報(どこまでスクロールしてるか、カーソルがどこにあるか)を保存します。
その上でdeletebufline()setline()でバッファをコマンドの結果に置き換えた後、winrestview()でカーソル位置などを元に戻します。

後は各ftpluginに nnoremap <buffer> mf <Cmd>call vimrc#feat#format#execute('nixfmt')<CR> のように書いて使っています。

数分で書いた割に結構便利に使えています。

脚注
  1. ファイル全体が書き換えられるため ↩︎

  2. systemlist()は雑にコマンドを実行してくれて入力もいい感じに扱ってくれますが、シェルの影響を受けるためあまりポータブルとは言えません。筆者はconfig.fishにうっかりechoを書いてしまい出力に混ざるというやらかしをしてしまったことがあります。 ↩︎

  3. 結合して出力すると改行が^@で表示されます ↩︎

GitHubで編集を提案

Discussion