編集を加速するVimのquickfix機能

この記事はVim Advent Calendar 2020の3日目の記事です。
昨日は@mira010さんのvim pluginsをインストールしてみましょうでした。

みなさんquickfixを使っていますか?
Vimのquickfix機能はgrepやmakeなどの結果を保持する専用のバッファと、それを扱うための各種コマンドからなります。
IDEには当たり前のようにあるような機能ですが、Vimの場合は他の機能と組み合わせることで編集操作を格段に効率化できます。

:grep:makeも、

  1. 外部コマンドを指定した引数で実行し、
  2. ファイル名や行番号、メッセージなどの出力を解析し、
  3. ジャンプのために使えるリストを作ってくれる

のは共通です。
このリストはquickfixリストと呼ばれるもので、:copenで専用のウィンドウが開きます。
そして<Enter>キーやダブルクリックで該当行にジャンプします。

ただデフォルトのgrepコマンドはまだしも、makeコマンドは滅多に使わないという人もいるかもしれません。
その際はgrepprgmakeprgオプションで実行する外部コマンドを任意のコマンドに変更できます。
(出力される結果が解析できない形式の場合はerrorformatなども変更する必要があります。)

grepコマンドはgit grep

" スペースはエスケープが必要
set grepprg=git\ grep\ -n\ --no-color

makeコマンドはlinterやタスクランナーなどにするとグッと使いやすくなります。

" 使用例
" :make ./...
" :make --disable-all -E staticcheck
set makeprg=golangci-lint\ run

これらの設定はvimrcに書いておいたり、簡単に切り替えられるコマンドやマッピングを用意しておいても良いでしょう。

しかし、quickfixリストは同時に複数の結果を表示することができません。
そのため、複数のquickfixリストを扱うには:colder:cnewer:chistoryを使って履歴を行き来する必要があります。
もしくはlocationリストを使います。

locationリストとはウィンドウローカルなquickfixリストのことで、コマンドのプレフィックス

だけで、quickfixリストと同じように使えます。

そのため、別のウィンドウやタブページで個別にlocationリストを開くことで複数の結果を表示できます。

さて、ここまで紹介した機能だと便利なジャンプリストでしかありません。
quickfixリストを使ってさらに効率的な編集をするには、:cnextとマクロを組み合わせて使います。

例えばgrepで絞り込んだ行の特定の文字列を置換したければ、以下の操作を(qqなどで)マクロに記録し、

:s/Before/After/
:w
:cnext

大きな数を指定して(100@q1000@qで)実行すると、quickfixリストの最後まで自動的に繰り返してくれます。
リストが指定した数より少なければそこでマクロが止まってくれるので重複実行は気にしなくて大丈夫です。

この時、さらにquickfixリストを絞り込みたくなることがあるかもしれません。 :grepの場合は正規表現を工夫しても良いですが、:packadd cfilterで使えるようになる:Cfilterでquickfixリストを絞り込んでしまうのがとても楽です。

それでも絞り込むのが難しい場合、いったん別のバッファにコピーして編集し、:cbuffer:cgetbufferでquickfixリストを読み込み直しても構いません。
(quickfixリストを:setlocal modifiableして書き換えるのはちょっと乱暴なので非推奨です。)

また、quickfixリスト自体をファイルとして保存しておき、:cfile:cgetfileで読み込み直すことも可能です。
そうするとジャンプ可能なTODOリストとしても使えるので、リストが巨大だったら少しずつ進めたり、他のVimmerと作業を分担する、なんてこともできるかもしれません。

ということで、ぜひquickfix機能を活用してみてください。

本当はより実践的な例として、最近やったことを具体的なコマンド付きで紹介できれば良かったんですが、ある事情でそのヒストリーをロストして再現環境もなく...
覚えている範囲でフワッと紹介して終わります。

目的はterraformerで生成したmonitoring_alert_policy.tfファイルのdocumentationに、フィルター付きでCloud LoggingのWeb画面に飛べるリンクを追加することでした。
そのフィルターの内容はlogging_metric.tfファイルに定義されているものを使います。

  • monitoring_alert_policy.tf(のサンプル)
resource "google_monitoring_alert_policy" "alert_policy_error" {
  display_name = "My Alert Policy(Error)"
  combiner     = "OR"
  conditions {
    display_name = "test condition"
    condition_threshold {
      filter     = "resource.type=gae_app AND metric.type=logging.googleapis.com/user/my-error-metric"
      duration   = "60s"
      comparison = "COMPARISON_GT"
      aggregations {
        alignment_period   = "60s"
        per_series_aligner = "ALIGN_RATE"
      }
      threshold_value = 0.1
      trigger {
        count = 1
      }
    }
  }

  documentation = {
    mime_type = "text/markdown"
    # TODO: ここに↓の形式でリンクを入れたい
    # - [NAME](https://console.cloud.google.com/logs/query?project=PROJECT_ID&query=FILTER)
    content = ""
  }
}
  • logging_metric.tf(のサンプル)
resource "google_logging_metric" "logging_metric_error" {
  name   = "my-error-metric"
  filter = "resource.type=gae_app AND severity>=ERROR"
  metric_descriptor {
    metric_kind = "DELTA"
    value_type  = "INT64"
  }
}

手順:

  1. :grepする
  2. :copenして、不要な行があれば除外する
  3. マクロを記録開始して、
    1. getline()substitute()で検索するmetricの名前を抽出する
    2. logging_metric.tfのバッファに移動する
    3. search()getline()substitute()を使ってfilterを取得する
    4. 元バッファに戻る
    5. documentationのcontentがある行に移動する
    6. リンクを生成して追加する
      • conditionsが複数ある場合はそれぞれのリンクを追加する
      • フィルターはbase64化してクエリパラメータにする必要がある
    7. :wで保存する
    8. :cnextする
  4. マクロの記録を終了する
  5. 1000回ほど繰り返す

実は初めの数回は関数などを使用せず、ノーマルモードコマンドで普通に編集をしていました。
ただ同じような操作を繰り返していることは薄々感じていたのと、残りの件数を見て即マクロに切り替えたという経緯があります。
そのまま続けていたら数時間はかかっていたと思いますが、ほぼ一瞬で終わらせることができました。

明日は@kaneshinさんです。