✍️

emacs-reformatterで色々なフォーマッターを使う(Biome, PHP-CS-Fixer, Prettier等)

2024/04/16に公開

備忘録メモ

Emacsで使えるemacs-reformatter(reformatter.el)を調べたところ、様々なプログラミング言語のフォーマッターに対応出来そうだったので使ってみることにしました。

https://github.com/purcell/emacs-reformatter

インストール

私はleaf.elを使っているので、インストールは下記のようにinit.elに追記しました。

(leaf reformatter
  :straight t)

straightとuse-packageの人はこうすればいいかな?

(straight-use-package 'reformatter)

Biomeで使う場合

TypeScriptではBiomeをフォーマッターとして使ってみます。

emacs-reformatterのデフォルトの動作は、標準入力で整形前のソースコードを渡して、標準出力として整形済みのソースコードを受け取るということのようなので、まずはコマンドラインで単体ファイルをフォーマットする際の使い方をおさらいしておきます。

$ cat (ファイルパス) | biome format --stdin-file-path (ファイルパス)

次にreformatter-defineというマクロを使い、フォーマッターコマンドの実行を定義します。

(reformatter-define biome-format ;; 第1引数はわかりやすい自由な名前
  :program "biome" ;; コマンド。PATHが通っていること(後述のadd-node-modules-pathを使っても良い)
  :args `("format" "--stdin-file-path" ,(buffer-file-name)) ;; オプション引数のリスト、実行時に評価される
  :lighter " BiomeFmt") ;; minor-modeとして使う場合の、モードラインに表示する名前(先頭にスペースが必要)

:args引数はフォーマット実行時に評価されるので、バッククオートのリストで関数や変数を渡すことができます。

このマクロを実行すると第1引数に渡した値を取って、自動的に2つの関数(コマンド)と、1つのminor-modeが定義されます。

  • *-buffer 関数
  • *-region 関数
  • *-on-save-mode モード

上記の例ですと、

  • biome-format-buffer 関数
  • biome-format-region 関数
  • biome-format-on-save-mode モード

が定義されます。

自動的に定義されたbiome-format-on-save-modeは、before-save時に指定通りのコマンド実行して整形済みのコードをバッファに反映してくれます。

typescript-mode起動時にbiome-format-on-save-modeを起動するようにすれば良さそうです。

(add-hook 'typescript-mode-hook 'biome-format-on-save-mode)

;; leaf.elやuse-packageの場合はまとめて書けばいい
;;
;; (leaf typescript-mode
;;  :straight t
;;  :config
;;  (reformatter-define biome-format
;;    :program "biome"
;;    :args `("format" "--stdin-file-path" ,(buffer-file-name))
;;    :lighter " BiomeFmt")
;;  :hook ((typescript-mode-hook . biome-format-on-save-mode)))

PHP-CS-Fixerで使う場合

PHP-CS-Fixerコマンドは整形したいPHPファイルのファイル名を指定する使い方で、標準入力/標準出力を使用するやり方ではありません。

$ ./vendor/bin/php-cs-fixer fix --show-progress=none (PHPファイル名)

そのためemacsのbefore-save-hookとは相性が悪いのですが、emacs-reformatterはそのあたりも考慮に入れているようで、自動的に保存前の(編集中の)内容で一時的なテンポラリファイルを作って、PHP-CS-Fixerにテンポラリファイルのパスを渡す、という動作にすることができます。

追記 2024-04-28: .php-cs-fixer.dist.phpが正しく読み込まれなかったのを修正しました

;; PHP-CS-Fixerの設定ファイルを指定するオプションを生成する関数
(defun my/php-mode-php-cs-fixer-config-option ()
  (let ((path (locate-dominating-file buffer-file-name ".php-cs-fixer.dist.php")))
    (if path (concat "--config=" (expand-file-name ".php-cs-fixer.dist.php" path))
      "")))

(reformatter-define php-cs-fixer-format
  :program "php-cs-fixer"
  :stdin nil ;; 整形前コードを標準入力で渡さない場合はnil
  :stdout nil ;; 整形済みコードを標準出力から受け取らない場合はnil
  :args `("fix" "--show-progress=none" ,(my/php-mode-php-cs-fixer-config-option) ,input-file) ;; input-file変数には拡張子なしの一時ファイルのパスが格納されている(この場合は、:input-fileキーワード引数で指定したファイルパスが格納されている)
  :input-file (reformatter-temp-file) ;; reformatter-temp-file関数は編集中の内容でテンポラリファイルを作って、その拡張子付きファイルパスを返却する関数
  :lighter " PHP-CS-Fixer")

キーワード引数:stdin:stdoutはそれぞれ、標準入力/標準出力を使用しないためnilにします。
:argsのリストで使用できるinput-fileには自動的に拡張子なしテンポラリファイルのパスが格納さているようです。

あえて拡張子付きのパスを渡したい場合は、:input-file引数にテンポラリファイルを作成する関数(reformatter-temp-file)を指定します。

composer.elを使って各プロジェクトの./vendor/binにインストールされたPHP-CS-Fixerを使うようにします。
https://github.com/emacs-php/composer.el

(defun my/php-mode-hook ()
  ;; copmoser.elのcomposer-get-bin-dir関数を使い、各プロジェクトの./vendor/binをexec-path変数に追加すると便利
  ;; https://github.com/emacs-php/composer.el
  (let ((bin-dir (composer-get-bin-dir)))
    (setq-local exec-path (cons bin-dir exec-path))
    (php-cs-fixer-format-on-save-mode)))

(add-hook php-mode-hook 'my/php-mode-hook)

PrettierかBiomeかを自動で判別する

Prettierで使う場合は、下記のようにして、(add-hook 'typescript-mode-hook 'prettier-format-on-save-mode)とすればいいでしょう。

(reformatter-define prettier-format
  :program "prettier"
  :args `("--stdin-filepath" ,(buffer-file-name))
  :lighter " PrettierFmt")

しかし、そうすると一番上で定義したbiome-format-on-save-modeと被ってしまうので、各コマンドがインストールされているかで判断します。

add-node-modules-pathを使って各プロジェクトの./node_modules/.binにインストールされたコマンドを優先的に使うようにします。
https://github.com/codesuki/add-node-modules-path

(defun my/ts-mode-hook ()
  ;; add-node-modules-pathを使い、各プロジェクトの./node_modules/.binをexec-path変数に自動追加
  ;; https://github.com/codesuki/add-node-modules-path
  (add-node-modules-path)

  ;; biomeがインストール済みならbiomeでフォーマット
  ;; prettierがインストール済みならprettierでフォーマット
  ;; そうでなければLSPでフォーマット
  (cond ((executable-find "biome")
         (biome-format-on-save-mode))
        ((executable-find "prettier")
         (prettier-format-on-save-mode))
        (t (add-hook 'before-save-hook 'eglot-format-buffer nil t))))

;;(add-hook 'typescript-mode-hook 'biome-format-on-save-mode)
;;(add-hook 'typescript-mode-hook 'prettier-format-on-save-mode)
(add-hook 'typescript-mode-hook 'my/ts-mode-hook)

以上です。

Discussion