leaf-defaultsというはちゃめちゃ便利キーワードをleafに追加した話

この記事は[Emacs Advent Calendar 2020](https://qiita.com/advent-calendar/2020/emacs)の16日目の記事です。投稿日時は見てはいけません()
前日はTakesxiSximadaさんの「[EmacsでPlantUMLを使う]()」、翌日はfiboさんの「[emacs -nw のコピペ事情](https://qiita.com/fibo/items/174898ffc447542f7786)」でした。

今回、[leaf](https://github.com/conao3/leaf.el)という自作パッケージに新しいキーワードを追加したので、その紹介をするものです。
(スター200越えました&#x2026;! やる気に影響するので、よさげなツールだなぁと思っていただければ、ぜひスターを頂ければと&#x2026;!)


# leafの紹介

leafを使っていらっしゃる方も、use-packageからの移行を悩んでおられる方も、素のElispでEmacsの設定をされている方もおられるかと思います。

まず、leafがどのようなメリットを提供するのか軽く紹介します。

-   統一されたフォーマットでのEmacsパッケージ設定
    -   パッケージ設定に係る「イディオム」を隠蔽し、簡潔でメンテナンスしやすくなる
-   パッケージの設定が一つのS式になり、一目で設定の区切りを認識できる
-   キーワードを(比較的)簡単に追加でき、leafを自分で拡張できる
-   use-packageのおせっかい?な挙動や文法上の問題を修正している。
-   Emacs-jpでleafについての質問が日本語で出来る。

とりあえず5点挙げました。素のElispで管理されている方には全ての点が、既にuse-packageで書かれている方はleafに移行することによって後半の3点のメリットを享受できるようになります。

今回は「キーワードを(比較的)簡単に追加でき、leafを自分で拡張できる」の点を使って `leaf-defaults` という機能を追加したので、その紹介をしたいと思います。


# leaf-defaults


## きっかけ

`leaf` は確かにイディオムを隠蔽してくれるが、「それ以上」が欲しい。
具体的には **各パッケージのおすすめ設定** を特殊なキーワードを指定したら **勝手に設定してくれる** 機能を入れられないだろうか。
何なら各ユーザーのおすすめ設定をどこかにアップロードしてもらって、特殊なキーワードから指定したら **自動でダウンロードする** 機能とかどうだろうか。と考えたのがきっかけでした。

元はleaf-defaultsという別パッケージにして配布する予定でした。具体的にいつ、この機能を思いついていたのかは忘れましたが、どうやら[2年前にはレポジトリを作っていた](https://github.com/conao3/leaf-defaults.el)ようでした。

ただ、Emacsの設定をアップロード/ダウンロードするとかは結構(というかかなり)セキュリティに問題がある気がしたので断念した結果、その特殊なキーワードを既存の[leaf-keywords](https://github.com/conao3/leaf-keywords.el)に入れるということで当初の目的の8割ぐらいを実現したものになります。

(こういうきっかけがあるので、この記事や他で言及するときは「leaf-defauts」として言及しますが、特にそういう名前のパッケージがあるわけではないです。leaf-keywordsの中に実装された一つのキーワードです。)


## ユースケース

私はEmacsを使いながら、Emacsのパッケージを趣味で書いていますし、気に入ったパッケージがあればその紹介記事を書いたりすることもあります。「[2020年代のEmacs入門 - Emacs-jp](https://emacs-jp.github.io/tips/emacs-in-2020.html)」とかは分散していたおすすめ入門パッケージを一つにした記事ですね。

この記事、書く方は、まぁ順当に大変なのですが、読む方も結構大変だと思います。
実際、私が最近新しい環境に放り込まれたので、この記事を見ながらEmacsの最小限のセットアップをしたのですが、コードブロックが分かれていてコピペが非常に面倒でした。

leaf-deafultsを使えば、このように書けます!

```el
(leaf leaf-keywords-defaults
  :require t
  :config
  (leaf-keywords-defaults-basic-dependency)
  (leaf-keywords-defaults--leaf-defaults/base))
```

これで「[2020年代のEmacs入門 - Emacs-jp](https://emacs-jp.github.io/tips/emacs-in-2020.html)」の設定の8割方を自分のEmacsに反映することができます!

さらに「[ddskk-posframe: ddskkツールチップposframeフロントエンド](https://emacs-jp.github.io/packages/ddskk-posframe)」という紹介記事には冒頭に設定例が書いてある例が多いですが、このような場合、

```el
(leaf skk :defaults t)
(leaf posframe :defaults t)
(leaf ddskk-posframe :defaults t)
```

と書くことで設定が完了します! (この記事の場合、 `ddskk-posframe` が `skk` と `posframe` に依存しているので3つのS式に分かれてますが、普通の記事だと1つのS式になるはず)

## 仕組み

leaf-defaultsは設定を隠蔽しているだけなので、設定の本体があるはずです。あります。[leaf-keywords.el/leaf-keywords-defaults.el](https://github.com/conao3/leaf-keywords.el/blob/6e0d407768e1adf7953fbd20e8b91ad5ecfbfd5c/leaf-keywords-defaults.el)です。このファイルは[leaf-manager](https://github.com/conao3/leaf-manager.el)を使って半自動で編集されている、私が管理している設定ファイルです。

さらっと見ると `:convert-defaults` という見慣れないキーワードがあります。まずこれを説明します。

(マクロ展開に拙作の[ppp]()を使います。ローカルで結果を確認したい方はインストールしてもらえればと。
さらに結果を簡潔にするために `(setq leaf-expand-minimally t)` しています。)


### :convert-defaultsキーワード

leafはマクロなので、leafが受け取ったS式を **自由に変換して** Emacsに解釈させることができます。普段は個人用の設定を書くために使うものなので、生成するS式の順序を変更したり、イディオムを生成するだけです。

普段のleaf。 `:doc` などのドキュメントキーワードの引数を消したり、 `:emacs>=` キーワードからバージョン判定式を生成したり、 `:bind` から `autoload` 式を生成したりなどしています。

```el
(ppp-macroexpand
 (leaf company
   :doc "Modular text completion framework"
   :emacs>= 24.3
   :ensure t
   :blackout t
   :leaf-defer nil
   :bind ((company-active-map
           ("C-n" . company-select-next)
           ("C-p" . company-select-previous))
          (company-search-map
           ("C-n" . company-select-next)
           ("C-p" . company-select-previous)))
   :custom ((company-tooltip-limit . 12)
            (company-idle-delay . 0))
   :global-minor-mode global-company-mode))
;;=> (prog1 'company
;;     (unless (fboundp 'global-company-mode)
;;       (autoload #'global-company-mode "company" nil t))
;;     (unless (fboundp 'company-select-next)
;;       (autoload #'company-select-next "company" nil t))
;;     (unless (fboundp 'company-select-previous)
;;       (autoload #'company-select-previous "company" nil t))
;;     (when (version<= "24.3" emacs-version)
;;       (leaf-handler-package company company nil)
;;       (leaf-keys
;;        ((company-active-map :package company
;;                             ("C-n" . company-select-next)
;;                             ("C-p" . company-select-previous))
;;         (company-search-map :package company
;;                             ("C-n" . company-select-next)
;;                             ("C-p" . company-select-previous))))
;;       (customize-set-variable 'company-tooltip-limit 12 "Customized with leaf in company block")
;;       (customize-set-variable 'company-idle-delay 0 "Customized with leaf in company block")
;;       (global-company-mode 1)
;;       (with-eval-after-load 'company
;;         (blackout 'company-mode nil))))
```

しかし、もっと大きく結果を変えることもできます。 `:convert-defaults` キーワードを指定するとこうなります。

```el
(ppp-macroexpand
 (leaf company
   :convert-defaults t
   :doc "Modular text completion framework"
   :emacs>= 24.3
   :ensure t
   :blackout t
   :leaf-defer nil
   :bind ((company-active-map
           ("C-n" . company-select-next)
           ("C-p" . company-select-previous))
          (company-search-map
           ("C-n" . company-select-next)
           ("C-p" . company-select-previous)))
   :custom ((company-tooltip-limit . 12)
            (company-idle-delay . 0))
   :global-minor-mode global-company-mode))
;;=> (prog1 'company
;;     (defun leaf-keywords-defaults--leaf/company nil
;;       "Default config for leaf/base."
;;       (unless (fboundp 'global-company-mode)
;;         (autoload #'global-company-mode "company" nil t))
;;       (unless (fboundp 'company-select-next)
;;         (autoload #'company-select-next "company" nil t))
;;       (unless (fboundp 'company-select-previous)
;;         (autoload #'company-select-previous "company" nil t))
;;       (when (version<= "24.3" emacs-version)
;;         (leaf-handler-package company company nil)
;;         (leaf-keys
;;          ((company-active-map :package company
;;                               ("C-n" . company-select-next)
;;                               ("C-p" . company-select-previous))
;;           (company-search-map :package company
;;                               ("C-n" . company-select-next)
;;                               ("C-p" . company-select-previous))))
;;         (customize-set-variable 'company-tooltip-limit 12 "Customized with leaf in company block")
;;         (customize-set-variable 'company-idle-delay 0 "Customized with leaf in company block")
;;         (global-company-mode 1)
;;         (with-eval-after-load 'company
;;           (blackout 'company-mode nil)))))
```

なにが変わっているでしょうか。

そうですね。leafが生成したS式全体が `defun` で囲われており、以前は **S式の実行** だったものが、 **関数の宣言** に変わりました。

この仕組みにより設定集を `leaf-keywords` に添付しても各ユーザーにとっては単に関数が宣言されているだけで、その中の設定は反映されることはありません。

`:convert-defaults` に `t` ではなくシンボルを渡した場合、そのシンボル名を使って関数が定義されます。

```el
(ppp-macroexpand
 (leaf company
   :convert-defaults t
   :global-minor-mode global-company-mode))
;;=> (prog1 'company
;;     (defun leaf-keywords-defaults--leaf/company nil
;;       "Default config for leaf/company."
;;       (unless (fboundp 'global-company-mode)
;;         (autoload #'global-company-mode "company" nil t))
;;       (global-company-mode 1)))

(ppp-macroexpand
 (leaf company
   :convert-defaults conao3
   :global-minor-mode global-company-mode))
;;=> (prog1 'company
;;     (defun leaf-keywords-defaults--conao3/company nil
;;       "Default config for conao3/company."
;;       (unless (fboundp 'global-company-mode)
;;         (autoload #'global-company-mode "company" nil t))
;;       (global-company-mode 1)))
```

最後はこの自動生成した関数を呼び出す仕組みを作るだけです。


### :defaultsキーワード

`:defaults` キーワードは `:convert-defaults` キーワードで生成された関数を実行するS式を生成するキーワードです。これまでに書いたように `t` やシンボルを与えることができます。

```el
(ppp-macroexpand-all
 (leaf company :defaults nil))
;;=> (prog1 'company)

(ppp-macroexpand-all
 (leaf company :defaults t))
;;=> (prog1 'company
;;     (leaf-keywords-defaults--leaf/company))

(ppp-macroexpand-all
 (leaf company :defaults conao3))
;;=> (prog1 'company
;;     (leaf-keywords-defaults--conao3/company))
```

`nil` はキーワード無効化、 `t` はleafが提供している設定を反映、 シンボルはユーザー名を想定しており、 `conao3` だとconao3が提供している設定を反映することになります。


### パッケージグループ機能

[leaf-keywords-defaults](https://github.com/conao3/leaf-keywords.el/blob/6e0d407768e1adf7953fbd20e8b91ad5ecfbfd5c/leaf-keywords-defaults.el)には多くのパッケージ設定が登録される予定です。パッケージを一つずつ反映するのではなく、パッケージ設定例をまとめた「パッケージグループ」のようなものを定義できたら便利ではないでしょうか。

ということで、[ここ](https://github.com/conao3/leaf-keywords.el/blob/6e0d407768e1adf7953fbd20e8b91ad5ecfbfd5c/leaf-keywords-defaults.el#L111-L112)で `base` パッケージグループを定義しています。これまでも例示していた `(leaf-keywords-defaults--leaf-defaults/base)` を実行すると、どうやら下記のパッケージ群が設定されることが分かります。

-   cus-edit
-   cus-start
-   delsel
-   files
-   paren
-   simple
-   startup
-   flycheck
-   company

(このパッケージ群は変わる可能性があります、が、ほとんど必須と言って良いパッケージのみ登録する予定です。
例えばhelmとivyについては各ユーザーの好みがあると思うので、baseパッケージグループには(現状では)入りません。)

## 拡張

Emacsユーザーは拡張性を重視します。leaf-defaultsも拡張しやすいように設計されています。
leafが提供する設定例を変えたい場合、色々な方法で変えることができます。

`:convert-deafults` で生成されるのは「関数」であることに注意すれば下記の方法があります。

1.  leafの設定キーワードを使う
    
    `:defaults` は `:when` や `:if` などの条件分岐キーワードの後に展開されます。
    ということで、普通に `:config` キーワードや `:custom` キーワードを使うことで提供されている設定例から少し変えることができます。
    
    ```el
    (leaf company
      :defaults t
      :custom ((company-idle-delay . 0)))
    ```

2.  アドバイスで変える
    
    関数なのでアドバイスをかけられます。
    
    ```el
    (leaf company
      :preface
      (defun my/advice--leaf-keywords-defaults--leaf/company (fn &rest args)
        (apply fn args)
        (setq company-idle-delay 0))
      :advice (:around leaf-keywords-defaults--leaf/company
                         my/advice--leaf-keywords-defaults--leaf/company))
    
    (leaf company :defaults t)                ; :defaults より :advice の方が遅いので注意
    ```

3.  上書きで変える
    
    関数なので、同名で定義してしまえば新しい定義で上書きできます。
    
    ```el
    (leaf company
      :convert-defaults t                   ; leafの設定例を上書き
      :custom ((company-idle-delay . 0))
      :global-minor-mode global-company-mode)
    
    (leaf company :defautls t)              ; 上書きした設定を読み込み
    ```

4.  PRで変える
    
    [leaf-keywords](https://github.com/conao3/leaf-keywords.el)にPRすればデフォルトの設定例を変更できます!()


# まとめ

leaf-defaultsについて紹介しました。2年前から構想はしていたものの、形になるまで結構な時間がかかったパッケージになりました。

これでEmacsパッケージ紹介記事がより読み易くなればいいなと思っています。

もちろん、紹介記事を書けば `:defaults` が使えるわけではありません。パッケージ紹介記事を書き、 `:defaults` キーワードで読者に設定させてみたいなという奇特な方がいらっしゃいましたら、設定例を[leaf-keywords](https://github.com/conao3/leaf-keywords.el)にPRしてもらえればと思いますー!
(当然、PRされた設定例はleaf公認としてリリースするので、レビューが入ります。)