Conao3 Notes

[2019年アップデート] leaf.elで雑然としたEmacs設定ファイル「init.el」をクリーンにする

はじめに

この記事は「Emacs Advent Calendar 2019」の2日目の記事として書いたものです。昨日は私の「依存関係をスマートに解決しつつ「GitHub Actions」でCIを無料でぶん回す」でした。

まだ空きがあるので、ぜひ参加頂ければと思います!

leafについて

leaf.elはjwiwgleyさんのuse-packageを2.5年使った上で、私が感じていたストレスを解消するためにスクラッチから開発したパッケージです。Qiitaでもプレリリースとリリース時に記事を書きましたが、半年経つと新機能も増えてきます。

リリースしたときの記事に追記しようかと思いましたが、別記事にすることにしました。

leafのバージョニングについて

leaf.elのissues/pullsを見てもらえれば分かりますが、 v1.0.0 を付けたときからmasterへのcommitを禁止しており、issueを立てて、そのissueに対するPRを自分でマージする形でアップデートをしています。さらに、1つのPRで 0.0.1 バージョンが上がるようにしているので、バージョンを見るだけでどれだけアップデートされたかが分かるようになっています。

リリース記事を見ると、2019/06/15に投稿されており、この記事には2019/06/14にマージした「Treat pair which car is list in :hook, :defun etc」までの情報で書かれています。

これは :hook や :modes, :defun の分配指定の機能です。leaf.elとは/変数設定のためのキーワード/:hookの章で紹介されている、3つ目の例がちゃんと動くようにしたPRでした。(これはuse-packageでも実装されている機能なので、これをマージしないと記事として収まりが悪かった。)

(cort-deftest-with-macroexpand leaf/hook
  '(((leaf ace-jump-mode
       :hook cc-mode-hook
       :config (ace-jump-mode))
     (prog1 'ace-jump-mode
       (autoload #'ace-jump-mode "ace-jump-mode" nil t)
       (add-hook 'cc-mode-hook #'ace-jump-mode)
       (eval-after-load 'ace-jump-mode
         '(progn
            (ace-jump-mode)))))

    ((leaf ace-jump-mode
       :hook cc-mode-hook (prog-mode-hook . my-ace-jump-mode))
     (prog1 'ace-jump-mode
       (autoload #'ace-jump-mode "ace-jump-mode" nil t)
       (autoload #'my-ace-jump-mode "ace-jump-mode" nil t)
       (add-hook 'cc-mode-hook #'ace-jump-mode)
       (add-hook 'prog-mode-hook #'my-ace-jump-mode)))

    ((leaf ace-jump-mode
       :hook ((cc-mode-hook prog-mode-hook) . my-ace-jump-mode))
     (prog1 'ace-jump-mode
       (autoload #'my-ace-jump-mode "ace-jump-mode" nil t)
       (add-hook 'cc-mode-hook #'my-ace-jump-mode)
       (add-hook 'prog-mode-hook #'my-ace-jump-mode)))))

このPRでは v3.2.5 が付けられています。現在のHEADは v3.6.8 が付けられているので、単純には43件のPRがマージされたことになります。実際には外部の方のPRなどで、バージョンを上げてもらうことを言い忘れてマージしたことがあるので、少し誤差がありますが。。

leaf.el

Issue

Issueは期間中に55件ありました。

Pull request

Pull requestは期間中に45件ありました。つまりバージョン上げるのを2回見落としてます。。

leaf-keywords.el

leaf-keywords.elは外部パッケージに依存するキーワードをまとめたパッケージです。

leaf.elをEmacs本体に入れるという野望のために分けてありますが、基本的にはleaf.elとleaf-keywords.elは両方入れることを前提にしてます。

Issue

Pull request

追加された機能

Add imenu support feature

この半年の目玉機能は、間違いなくgrugrutさんに提案頂いた、leafのImenu integrationでしょう。

これをimg

こうします。img

use-packageにもあるみたいですが、どうやらバグっているとのことなので、leaf.elでしか使えません。便利すぎるので、デフォルトでオンにしてあります。

そもそもEmacsにはimenuという機能があり、 M-x imenu で起動できます。ファイルをスキャンして関数定義や変数定義などの主要な定義についてのリストを提供し、その場所へのジャンプを提供します。

私はこのPRを受けて、この機能を知り、とても便利だったので C-s を counsel-imenu にあげることにしました。

ivy の isearch 相当は swiper なのですが、最近 helm-swoop のメンテナになったので、 helm-swoop を C-S-s に割り当てて使っています。

(leaf ivy
  :ensure t
  :diminish ivy-mode
  :custom ((ivy-re-builders-alist . '((t      . ivy--regex-fuzzy)
                                      (swiper . ivy--regex-plus)))
           (ivy-use-selectable-prompt . t)

           (ivy-mode     . t)
           (counsel-mode . t))
  :init
  (leaf *ivy-requirements
    :config
    (leaf swiper
      :disabled t
      :ensure t
      :bind (([remap isearch-forward] . swiper)))
    (leaf counsel
      :ensure t
      :diminish counsel-mode
      :bind (([remap isearch-forward] . counsel-imenu)
             ("C-x C-r" . counsel-recentf)))))

(leaf helm
  :ensure t
  ;; :require helm-config
  :config
  (leaf helm-swoop
    :load-path `,(locate-user-emacs-file "site-lisp/helm-swoop")
    :custom (helm-swoop-pre-input-function
             . (lambda ()
                 (if mark-active
                     (buffer-substring-no-properties (mark) (point))
                   "")))
    :bind ((helm-swoop-map   ("C-s" . helm-multi-swoop-all-from-helm-swoop))
           ("C-S-s" . helm-swoop)
           ("C-c f" . hydra-helm-swoop/body))))

目玉機能その2です。機密情報をEmacsの外に保存して、動的に復号し、Emacsの中で使う auth-sources のleafキーワードです。

この機能についてはちょっと説明が必要なので、別記事にし てAdvent Calenarの穴埋めをし ます。

Add leaf-key-bindlist visualization

目玉機能その3です。leafの :bind や :bind* で設定したキーバインドのリストを表示します。もしEmacsデフォルトのキーバインドを上書きしている場合は上書き前の関数名も表示されます。

これも、もともとuse-packageに実装されている describe-personal-bindings の移植です。

しかし、use-packageでは単にbufferにフォーマットされた文字列を表示するだけですが、 leaf-key-describe-bindings ではEmacsビルドインの表データ表示ライブラリ、 tabulated-list を使用して表示します。

img

それによって各列での昇順、降順のソートなどができることで、use-packageのそれより使いやすく/見やすくなっていると思います。

Add leaf-expand-minimally variable

leaf-emapand-minimally という変数を追加しました。これは、use-packageにもある use-package-expannd-minimally の移植です。移植といいつつ、機能だけの移植でどうやって実装されているかまで見ていません。

leafの leaf-expand-minimally の実装は leaf-expand-minimally-suppress-keywords に指定されたキーワードに対して、優先度最大で、 nil を指定するというものです。

今のところ leaf-expand-minimally-suppress-keywords には :leaf-protect のみが指定されているので、有効にした場合は:leaf-protectだけが無効化されます。

(defmacro p (form)
  "Output FORM processed `macroexpand-1' and `pp'."
  `(progn
     (pp (macroexpand-1 ',form))
     nil))
;;=> p

(p (leaf ace-window
     :ensure t
     :bind (("M-o a w" . ace-window))))
;;=> (prog1 'ace-window
;;     (leaf-handler-leaf-protect ace-window
;;       (unless
;;           (fboundp 'ace-window)
;;         (autoload #'ace-window "ace-window" nil t))
;;       (declare-function ace-window "ace-window")
;;       (leaf-handler-package ace-window ace-window nil)
;;       (leaf-keys
;;        (("M-o a w" . ace-window)))))

(let ((leaf-expand-minimally t))
  (p (leaf ace-window
       :ensure t
       :bind (("M-o a w" . ace-window)))))
;;=> (prog1 'ace-window
;;     (unless
;;         (fboundp 'ace-window)
;;       (autoload #'ace-window "ace-window" nil t))
;;     (declare-function ace-window "ace-window")
;;     (leaf-handler-package ace-window ace-window nil)
;;     (leaf-keys
;;      (("M-o a w" . ace-window))))

ただ、leafの展開形を知りたいだけなら後述する leaf-expand の方が使いやすいと思います。

Add leaf expand visualization feature

img

img

M-x leaf-expand と M-x leaf-create-issue-template を実装しました。

leaf-expand は現在のポイントから上のS式をスキャンして、一番最初に見つかったleafを macroexpand-1 で展開し、新しいバッファに表示します。

leaf-create-issue-template はついでに実装したものです。展開前と展開後のleafをmd形式で新しいバッファに表示します。これをコピペすれば簡単にissueが書けるのではないかなと思います。

Add leaf-available-keywords

leaf-available-keywords を追加しました。

lispプログラムから実行すると、単に現在使用できるキーワードのリストを返し、 M-x leaf-avairable-keywords と実行するとエコーエリアに表示します。

@@ image @@

あれ、どんなキーワードが使えるんだっけ。ってときに使えるかもしれません。このコマンドを実行する前にドキュメントを見てしまいそうですが。。

Why don't :setq, :custom, etc. distribute when normalizing?

:hook や :mode 以外のキーワードの分配代入のサポートの提案です。結局、下記のキーワードの全てで分配代入をサポートしました。

:ensure :package
:hook :mode :interpreter :magic :magic-fallback :defun
:pl-setq :pl-pre-setq :pl-setq-default :pl-custom
:auth-custom :auth-pre-setq :auth-setq :auth-setq-default
:setq :pre-setq :setq-default :custom :custom-face

同じ値を複数の変数に設定するときに便利かもしれません。ただあんまり記述量変わらないし、変数名も長いので2行に渡ることもあり、私はあまり使ってません。

ただ、やっぱり :hook や :mode では便利。

Automatically require packages feature

leaf-keywords.elの追加機能です。leaf-keywords.elで使えるパッケージがインストールされていた場合、 (leaf-keywords-setup) をしたときに自動でrequireするようにしました。

この機能を使うには (leaf-keywords-setup) をする前に el-get, hydra, key-combo, smartrep, key-chord, diminish, delight のうちどれかをpackage.elなどでインストールしておく必要があります。

Remove eval-after-load statement

leaf-keywords.elの追加機能です。leaf-keywords.elによって出力されたS式は、追加パッケージがrequireされたときに発火するように eval-after-load で囲われていました。

しかしこのおせっかい機能によって、 :diminish キーワードを使用しているのに diminish をインストールし忘れているという事故が起こっていました。

そこでエイヤッと取っぱらってしまいました。エラーによってきちんとユーザーに報告した方が良いだろうという判断です。なお leaf-protect で囲われている限り、エラーは起こりますが内部のエラーは全てワーニングに変換されるので、その後のパッケージは読み込もうとします。

修正された機能

Correctly eval cdr

私は「ドット対のリスト」を指定するのが好みで、それをずっと使っていましたが、単に「ドット対」を渡したときに盛大にバグっていたことを教えて頂きました。

  • 動く

    (leaf some-mode
      :hook ((t . other-mode)))
  • 動かない

    (leaf some-mode
      :hook (t . other-mode))

leafは結構大きなDSL解析器になっていて、デバックも大変になってきています。とりあえず、テストケースは全部通ることを確認しているので、見つけ次第追加していくしかないですね。。

なお、Readmeや私が書くleafの記事で紹介しているものはテストケースから抜き出したものなので、動くことが保証されています。

Fix install code

Readmeにおいて leaf のダウンロードに失敗したら package-refresh してもう一度ダウンロードを試すようにしていましたが、通常、leafは一番最初にダウンロードされるだろうということを仮定してインストールコードを簡略化しました。

ついでに leaf-keywords.el のインストールコードも prog1 を使わない形に変えました。

  • before

    (prog1 "prepare leaf"
      (prog1 "package"
        (custom-set-variables
         '(package-archives '(("org"   . "https://orgmode.org/elpa/")
                              ("melpa" . "https://melpa.org/packages/")
                              ("gnu"   . "https://elpa.gnu.org/packages/"))))
        (package-initialize))
    
      (prog1 "leaf"
        (unless (package-installed-p 'leaf)
          (unless (assoc 'leaf package-archive-contents)
            (package-refresh-contents))
          (condition-case err
              (package-install 'leaf)
            (error
             (package-refresh-contents)       ; renew local melpa cache if fail
             (package-install 'leaf))))
    
        (leaf leaf-keywords
          :ensure t
          :config (leaf-keywords-init)))
    
      (prog1 "optional packages for leaf-keywords"
        ;; optional packages if you want to use :hydra, :el-get,,,
        (leaf hydra :ensure t)
        (leaf el-get :ensure t
          :custom ((el-get-git-shallow-clone  . t)))))
  • after

    (prog1 "leaf"
      (prog1 "install leaf"
        (custom-set-variables
         '(package-archives '(("org"   . "https://orgmode.org/elpa/")
                              ("melpa" . "https://melpa.org/packages/")
                              ("gnu"   . "https://elpa.gnu.org/packages/"))))
        (package-initialize)
        (unless (package-installed-p 'leaf)
          (package-refresh-contents)
          (package-install 'leaf)))
    
      (leaf leaf-keywords
        :ensure t
        :config
        ;; optional packages if you want to use :hydra, :el-get,,,
        (leaf hydra :ensure t)
        (leaf el-get :ensure t
          :custom ((el-get-git-shallow-clone . t)))
    
        ;; initialize leaf-keywords.el
        (leaf-keywords-init)))

Byte compile

leafを使用したinit.elをバイトコンパイルしたときにワーニングが出たので直しました。バイトコンパイルについては少しコツがあるので、また別記事にするかもしれません。

Do configure autoload if not have already set autoload

二重にautoloadされることを防止するためにautoloadする前に fboundp するようにしました。実はuse-packageの出力結果では fboundp してautoloadしていたのですが、そのチェックは本当に必要なんだろうか。と思ってチェックを外していたのです。

しかし、実際のところpackage.elによって設定された正しいautoloadをleafが上書きしていることが分かったので、設定する前にチェックするようにしました。

:hook should not autoload lambda expressions

:hook でlambda式を指定したときに、複数式だと動かないことと、変なautoloadが生成されていることの報告を受けて直しました。

この変更によって :hook で正常にlambda式を使用できるようになりました。

なお、 :bind, :bind* ではlambda式をバインドすることが現状できないので、 :preface で関数を宣言してバインドしてもらえればと思います。。

まとめ

leafはIssueやPRを放置することなく、後方互換性を保った上で精力的に機能拡張を行なっています。ぜひこの記事やリリース記事を参考にして、leafで快適なEmacs生活を送って頂ければと思います。

なお、12/18には「Emacs Advent Calendar 2019」の18日目の記事として @keita44_f4 さんによるleafの記事がでるようなので、それも参考にしていただければと思います。

最後になりますが、Patreonでご支援を頂ける方を募集しています。普段はleafなどのElispパッケージなどを中心にOSS活動をしつつ、学生をしています。ぜひよろしくお願いします。

https://www.patreon.com/conao3