• 作成:

Emacsで保存時に自動フォーマットをするようにプロジェクト設定で定める

背景

新しいHaskellプロジェクトでは様々な人が開発に参加することを想定して、 stylish-haskellなどより比較的強いコードフォーマッタであるfourmoluを自動で全適用することにしました。

CIでチェックをかけて差分があったら全部排除します。

haskell-language-serverにはfourmoluプラグインがあるので、 lsp対応のエディタならばどのエディタでもfourmoluを透過的に使うことが出来ます。

問題

VSCodeのformat-on-saveをEmacsでもやる方法 - Lambdaカクテルにも書いてあるように、 Emacsのlsp-modeには保存時に自動でフォーマットする機能がありません。

もちろんこの先の記事に書いてあるように簡単に自作することは出来るのですが、プロジェクトで管理している.dir-locals.elが個人のEmacsの設定に依存するのは避けたいです。

.dir-locals.elの設定

まず.dir-locals.elは以下のように全部内部で書いてしまうことで自動フォーマットを有効にすることは出来ます。

((nil
  . ((eval
      . (defun project-format-buffer ()
          (when (commandp #'lsp-format-buffer)
            (lsp-format-buffer))))))
 (haskell-mode
  . ((eval . (add-hook 'before-save-hook #'project-format-buffer nil t)))))

(bound-and-true-p lsp-mode)でチェックするコードを足すとeglotと使い分けている人にも対応出来るかも? そもそもeglotにも対応するべきなのですが、今の所eglotを使っている人が居ないので。

Emacsはこれを開くたびに警告を出します

これだけではEmacs側でプロジェクトのディレクトリやそれ以下のファイルを開くたびに警告が出てしまいます。何でもインラインに書けるということはwebブラウザのCookieをアップロードとかされかねないので当然の機能です。許可するのはそれぞれ簡単なのですがセキュリティ警告をすぐに消してしまう習慣がついてしまうと、本当にまずい警告が出てきた時にも実行を許可してしまうリスクが高まります。

VSCodeはこういう問題に関してプロジェクトディレクトリを信用するボタンを押す仕組みがあります。 Emacsにも似たような仕組みは警告メッセージを見る限りあるように思えます。しかし自動で書き込まれる領域に依存するのは逆に自動で壊れることにも繋がるような気がします。今回はこちらが指定する値は分かっているのでそれだけを許可するように設定したほうが良さそうです。

特定の値を許可する

init.elなどに以下のように記述して解決しました。

(add-to-list 'safe-local-eval-forms '(add-hook 'before-save-hook #'project-format-buffer nil t))
(add-to-list 'safe-local-eval-forms '(defun project-format-buffer ()
                                       (when (commandp #'lsp-format-buffer)
                                         (lsp-format-buffer))))

Claude 3.7くんに聞いたらeval部分も含めて追加するようなコードを出してきましたが、 safe-local-variable-valuesの方ではなくsafe-local-eval-formsなので、 evalは前提となっているので含めないべきですね。

個人のinit.elに依存しているように見えるるのは問題ないのか?

これなら問題ないと考えています。何故ならばまず.dir-locals.elを書いた時点で警告は出ますが動作自体はしています。

その後警告を消す方法として、

  • custom.elに書き込まれる値を再起動するときに読み込んで信頼する
  • ディレクトリを信頼する
  • 私のように実際に実行される式を信頼する

と言う方法のうちどれかを選ぶのは、プロジェクトのポリシーではなくエディタを設定する個人の責任だと考えているからです。

だからその部分はむしろ個人に委ねたいです。

lsp-modeに機能提案を出すべきだろうか

lsp-mode自体が以下のような設定変数を持っていれば問題ないのですが。

(defcustom lsp-format-buffer-on-save nil
  "Format buffer on save by lsp."
  :type 'boolean
  :group 'lsp-mode
  :safe t
  :local t)

割と基本的な機能なので実装されていないのは方針の問題なのかもしれません。一度issueなどで聞いてみるべきでしょうね。

似たようなことを議論しているissueはあるようです。 Add option to call lsp-format-buffer on save · Issue #3944 · emacs-lsp/lsp-mode