VimのYAMLのシンタックスハイライトを改善してGitHub Actionsのワークフローファイルでハイライトが壊れにくくしました

GitHub ActionsのワークフローにはYAMLファイルを使いますが、Vimシンタックスハイライトがうまく効かなくて困ることがよくありました。 Actionsでは複数行にわたる文字列に複雑なシェルスクリプトを書くことが多いのですが、 その中の一部がYAMLのフロースカラースタイルの文字列として認識されてしまい、ハイライトが壊れることがあるのです。

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Hello world!" | cut -f1 -d" "
          echo "This line is broken!"
      - env:
          test: ${{ inputs.* | join(' ') }}
        run: |
          echo 'This line is broken!'

はてなブログも全く同じ問題があることから、中ではVimシンタックスハイライトが使われているようです。 私の修正がブログのシステムに取り込まれると、このエントリーも更新できなくなりますが…

参考: YAMLの用語

参考: YAMLの用語
ブロックスカラースタイル: |
  これはブロックスカラースタイルの
  文字列です (改行を維持しない > もあるよ)
フロースカラースタイル:
  "これはダブルクォートされた
  フロースカラースタイルの
  文字列です (シングルクォートもあるよ)"
プレインスタイル: これは、
  クォートのないフロースカラースタイル、
  つまりプレインスタイルの文字列です

ブロックスカラースタイルのコードの中のシェルスクリプトとしての文字列に色がつくのは便利な気もしますが、YAMLのフロースカラースタイルの文字列は改行も含めるなど自由度が高いので、ブロックスタイルの文字列の中でフロースタイルの解釈をするのはおかしいですね。 また、プレインスタイル文字列の中のシングルクォートの解釈もおかしいです (上のjoinの例)。

GitHub Actionsをよく触るようになってからYAMLシンタックスハイライトが壊れることが多くなったので、重い腰を上げてVimYAMLシンタックスハイライトを改善しました (色々とあって取り込まれてからブログを書くまで時間がかかってしまいました…)。

github.com

このパッチの中でも、特に重要なのは以下の部分です。

syn match yamlBlockScalarHeader '[|>]\%([1-9][+-]\|[+-]\?[1-9]\?\)\%(\s\+#.*\)\?$' contained
            \ contains=yamlComment nextgroup=yamlBlockString skipnl
syn region yamlBlockString start=/^\z(\s\+\)/ skip=/^$/ end=/^\%(\z1\)\@!/ contained

|->1-といったブロックスカラースタイルのヘッダーを正しく認識し、その後に続くブロックスカラースタイルの文字列を認識するようになりました。 これによって、文字列の中に複雑なシェルスクリプトを書いてもYAMLとしてのハイライトが壊れにくくなりました。

Vimシンタックス定義の詳しいドキュメントはこちらです。 ここでは軽くシンタックスを作る時の考え方を書いておこうと思います。 Vimシンタックス定義の二つの大事な考え方は、nextgroupによる状態遷移と、containsによる構文アイテムの入れ子構造です。

シンタックスハイライトの構文アイテムには、トップレベルのアイテムとそうではないものがあります。 まずはトップレベルの構文アイテムでもって、そのパターンがあればファイルのどこでも認識したいものを定義し、そこからnextgroupを使って次に続くアイテムを指定していくというのが基本的な考え方です。 例えば、YAMLだと行が /^\s*\zs-\ze\%(\s\|$\)/ にマッチした場合は基本的にファイルのどこでもリストのマーカーとして認識させたいので、トップレベルのアイテムとして定義します。 トップレベルのアイテムがあることで、ファイルの途中からでも構文を認識できるようになっています。

多くのプログラミング言語(PietやBefungeといった例外を除く)の文法は、木構造で表現できます。 つまり構文要素の隣接関係と包含関係によって表現できるということです。 これらに対応するのが、nextgroupcontainsです。

隣接関係とは、functionというキーワードの後には識別子が続き、さらに開き括弧、引数、閉じ括弧、ブロックの開始が続くといった感じです。 Vimシンタックス定義では、nextgroupというオプションで次に続く構文アイテムを指定することができます。 ここにはトップレベルではないアイテム、containedというオプションを指定したアイテムを指定します。 実際に書いていくと同じアイテムたちを何度も指定したくなりますが、そのような場合はsyntax clusterを使うと便利です。

トップレベルのアイテムとnextgroupを使えばある程度の文法は表現できるのですが、これだけでは表現できない文法もあります。 例えば、"1 ${2 + 3} 4"みたいな文字列補間は、隣接関係だけでは表現できません。 他にも、文字列の中の\uXXXXのようなエスケープシーケンスとか、コメントの中のTODOとか、そういうものに色をつけたいことがあります。 こういった包含関係を表現するために、containsというオプションで構文アイテムの入れ子構造を表現します。 状態遷移と入れ子構造を組み合わせることで、Vimシンタックス定義は複雑な構文に対応しているのです。

今回、YAMLシンタックスハイライトを改善したことで、Vimシンタックス定義の仕組みを改めて自分の中で整理できました。 Vimシンタックスを改善したい人に、この記事が参考になれば幸いです。 個人的には、よく編集するGitHub Actionsのワークフローファイルでのハイライトを改善できましたし、積年のいくつかの問題 (#8234#10730#11517) を一気に解決できたので、とても満足しています。 それでは、また。