• 作成:
  • 更新:

私の.emacs.dをelpaをgit管理下に入れる気の狂った管理方法からleafに移行させました

概要

ncaq/.emacs.d: my Emacs config を気の狂った管理方法からleafに移行しました.

参考文献

背景

私はelpaをgitの管理下に全てぶちこんでいました.

Emacsに詳しくない人にもわかりやすく言うと, これはnode_modulesをgit管理下にぶちこむのと同じぐらいの暴挙です.

なんでこんな気の狂った行為に出たのかと言うと, 当時Emacsを使い始めた私は高校3年生, 学生やりながら半人プログラマとして3年目. パッケージマネージャという概念もよく分からない未熟者だったからです.

更に当時はuse-packageがあったかは知らないですが少なくとも有名ではなく, 愚かで劣悪な私はcaskなどを使うメリットなどもよくわかりませんでした.

そこで選んだ手段がelpaを全部ぶちこむというもの. これで複数端末間のバージョンも保たれるしお手軽!

でも当然この方法には問題がありました.

  • アップデートや新規パッケージ導入のたびに大量のgitコミットが発生するのでアップデートが億劫になる
  • そのたびにmagitがあまりの大量コミットにものすごい遅さになってしまう(CLIでコミットしていました)
  • GitHubの検索でノイズを発生させてしまう
  • なんで導入したのかよく分からないパッケージが残ってしまう
  • 差分が大量にあるので自分自身で書いたEmacs Lispコードが良くわからなくなる
  • 他沢山(忘れた)

気の狂った管理には当然それなりの問題点があるわけですね.

というわけでまともな管理に移行することにしました. use-packageでもleafでもどちらでも良かったのですが, use-packageはインデントが崩れやすいということでインデント揃えたい派なのでleafを選択しました.

移行ログ

バイトコンパイルができなかった

(leaf leaf-keywords
      :ensure t
      :config (leaf-keywords-init))

がエラーになって原因の切り分けが出来なくて苦心していたのですが, バイトコンパイルするとエラーになるみたいですね.

バイトコンパイルはマクロを展開するからですかね.

ググってみたらやっぱり普通では出来ないようですね.

しかしleaf.elを使っている場合、そのままではバイトコンパイルできません。なお、leaf.elのReadmeでもバイトコンパイルのことには全く触れていません。バイトコンパイルキーワードとして実装されていることは紹介していますが、実際どのようにしたらいいのか書いてないのです。その方法を解説するものです。

leaf.elに依存したEmacs設定ファイル「init.el」をバイトコンパイルして爆速にする - Qiita

知らずにバイトコンパイルしてる人居るからバイトコンパイル出来ないことは触れたほうが良いような気もしますが, とにかく出来ないというわけです.

これを参考に

(eval-and-compile
  (prog1 "leaf init"
    (require 'package)
    (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
    (package-initialize)
    (unless (package-installed-p 'leaf)
      (package-refresh-contents)
      (package-install 'leaf))
    (leaf leaf-keywords
      :ensure t
      (leaf-keywords-init))))

と書いてみたら成功しました. 警告も消えて良かった良かった.

でもバイトコンパイルとleafの相性悪いことが分かってきたのでもういっそのことinit-loaderやめてinit.elに書いていくことにします. バイトコンパイル自体もinit.elをやってもあまり意味がないのでやめることにしました.

自前の関数にシンボル渡す場合はafterめっちゃ書く必要がある

with-eval-after-loadと同じくleafに囲んでもrequireしないとflycheckによる警告が出るのは, defvarで読み込まれるキーワード指定で解決.

(leaf compile
  :defvar compilation-minor-mode-map compilation-mode-map compilation-shell-minor-mode-map
  :config (progn
            (ncaq-set-key compilation-minor-mode-map)
            (ncaq-set-key compilation-mode-map)
            (ncaq-set-key compilation-shell-minor-mode-map)
            ))

で警告なしに書ける.

と思ったら警告が消えるだけで普通にめっちゃエラー出る.

Warning (leaf): Error in `compile' block.  Error msg: Symbol’s value as variable is void: compilation-minor-mode-map
Warning (leaf): Error in `comint' block.  Error msg: Symbol’s value as variable is void: comint-mode-map
Warning (leaf): Error in `diff-mode' block.  Error msg: Symbol’s value as variable is void: diff-mode-map
Warning (leaf): Error in `doc-view' block.  Error msg: Symbol’s value as variable is void: doc-view-mode-map
Warning (leaf): Error in `make-mode' block.  Error msg: Symbol’s value as variable is void: makefile-mode-map
Warning (leaf): Error in `man' block.  Error msg: Symbol’s value as variable is void: Man-mode-map
Warning (leaf): Error in `pascal' block.  Error msg: Symbol’s value as variable is void: pascal-mode-map
Warning (leaf): Error in `prolog' block.  Error msg: Symbol’s value as variable is void: prolog-mode-map
Warning (leaf): Error in `rect' block.  Error msg: Symbol’s value as variable is void: rectangle-mark-mode-map
Warning (leaf): Error in `rg' block.  Error msg: Symbol’s value as variable is void: rg-mode-map

after書いていくしかないのか. パッケージ名と同じやつを書くのはなんだかダサいのだが…

leafのトップシンボルに意味をもたせたくないのはわかるが一々afterに同じ名前書いていくのは面倒だな. tを指定したら同じになるとかどうだろう.

(leaf comint    :after comint    :defvar comint-mode-map         :config (ncaq-set-key comint-mode-map))
(leaf diff-mode :after diff-mode :defvar diff-mode-map           :config (ncaq-set-key diff-mode-map))
(leaf doc-view  :after doc-view  :defvar doc-view-mode-map       :config (ncaq-set-key doc-view-mode-map))
(leaf make-mode :after make-mode :defvar makefile-mode-map       :config (ncaq-set-key makefile-mode-map))
(leaf man       :after man       :defvar Man-mode-map            :config (ncaq-set-key Man-mode-map))
(leaf pascal    :after pascal    :defvar pascal-mode-map         :config (ncaq-set-key pascal-mode-map))
(leaf prolog    :after prolog    :defvar prolog-mode-map         :config (ncaq-set-key prolog-mode-map))
(leaf rect      :after rect      :defvar rectangle-mark-mode-map :config (ncaq-set-key rectangle-mark-mode-map))
(leaf rg        :after rg        :defvar rg-mode-map             :config (ncaq-set-key rg-mode-map))

これはあまりにもダサくないか? なんかショートカットが欲しい…

とかツイートしてたら対応されました. feature#299 by conao3 · Pull Request #376 · conao3/leaf.el

これ:configより後に実行されてwith-eval-after-loadに包まれる引数があればちょうど良いのでは? :require tがあればrequireの後に実行されますが, 無ければrequireは実行されませんしね…

あとlsp-modeに対するyasnippetのように, 「このモードが有効になった時のみこの拡張を読み込みたい」 という需要はありそうな気がします. 提案してみようかな.

関数とパッケージ名が違う場合の対応

rainbow-delimitersがrainbow-delimiters-modeにしないと起動しないのにパッケージ名にmodeがついてないからスッキリhookに書けない. 公式ドキュメントのドット対を使うことで指定を変えて対処.

;; 括弧の対応を色対応でわかりやすく
(leaf rainbow-delimiters
  :ensure t
  :hook ((prog-mode-hook web-mode-hook) . rainbow-delimiters-mode-enable)
  :config (set-face-foreground 'rainbow-delimiters-depth-1-face "#586e75")) ;文字列の色と被るため変更

こういう類のものは名前を変えたleafノードを作って追加していくと綺麗になることが後でわかりました.

(leaf lsp-mode
  :ensure t
  :require t
  :defvar company-backends
  :custom (lsp-prefer-flymake . nil)    ; flycheckを優先する
  :bind (:lsp-mode-map
         ("C-c C-e" . lsp-workspace-restart)
         ("C-c C-i" . lsp-format-buffer)
         ("C-c C-n" . lsp-rename)
         ("C-c C-r" . lsp-execute-code-action)
         ("C-c C-t" . lsp-describe-thing-at-point))
  :config
  (leaf helm-lsp
    :ensure t
    :bind (:lsp-mode-map :package lsp-mode ("C-." . helm-lsp-workspace-symbol)))
  (leaf company-lsp
    :ensure t
    :require t
    :config
    (push 'company-lsp company-backends)
    (leaf yasnippet :ensure t :require t))
  (leaf lsp
    :hook
    css-mode-hook
    go-mode-hook
    haskell-mode-hook
    java-mode-hook
    python-mode-hook
    ruby-mode-hook
    scala-mode-hook
    typescript-mode-hook
    ))

のような感じです. lsp-modeはlsp-modeではなくlspを実行する必要があるのですよね.

と思っていたら add :minot-mode keyword · conao3/leaf.el@97cdb15 の変更で-modeが自動でつけ足されるので出来ないことが判明. 裏APIを使っていたようなものだったので仕方がない. 書き換えです.

代わりにeldocelisp-slime-navのように-modeサフィックスをつけなきゃならないライブラリで短く書けるようになったので, これはトレードオフの変更ですね. あちらを立てればこちらが立たずという状況です.

customの右辺に式が書けなかった

customの右辺に直接式が書けなかったから一度変数に入れて対処してしまいました. 何かやり方を間違えている? 単純な掛け算の結果の式とかも入れられなかったんですよね. マクロゆえの制約なのでしょうか?

(leaf dired
  :require t
  :preface (defvar ls-option (concat "-Fhval" (when (string-prefix-p "gnu" (symbol-name system-type)) " --group-directories-first")))
  :custom ((dired-auto-revert-buffer . t) ; diredの自動再読込
           (dired-dwim-target . t)
           (dired-isearch-filenames . t)
           (dired-listing-switches . ls-option)
           (dired-recursive-copies . 'always)  ; 聞かずに再帰的コピー
           (dired-recursive-deletes . 'always) ; 聞かずに再帰的削除
           )
  :config
  (defun dired-jump-to-current ()
    (interactive)
    (dired "."))
  (leaf wdired
    :require t
    :bind (:dired-mode-map
           ("C-o" . nil)
           ("C-p" . nil)
           ("M-o" . nil)
           ("C-^" . dired-up-directory)
           ("C-c C-p" . wdired-change-to-wdired-mode)
           )
    :config (ncaq-set-key dired-mode-map)))

マクロの制約でした. やるならバッククオートを仕込むと良さそうですね.

諦めてrequireした方が良いことが多々ある

謎の挙動や初期化順序に悩まされた場合諦めてrequireすると素直に動いてくれることがよくありました. requireしてもそんなに遅くならないのでよほど重いパッケージ以外は素直にrequireした方が良いということを教訓として取り入れました.

移行は完了しました

ドット対忘れたりして謎エラー起こしてそれが他のエラーメッセージを謎誘発させたりするのが難点かなと思いました. とは言えひとまず移行出来ました.

起動が早くなった気がします

init-loaderをやめたからなのかわかりませんが, 何故か起動が早くなった気がします.

Unused lexical variable ‘helm-ag-insert-at-point’の問題

leafとはあまり関係ないかもしれませんがleafに移行する過程で何故か出てきました.

(defun helm-do-ag-project-root-or-default-at-point ()
    (interactive)
    (let ((helm-ag-insert-at-point 'symbol))
      (helm-do-ag-project-root-or-default)))

という関数でhelm-ag-insert-at-pointがレキシカルスコープの変数なのに使われていないと警告が出ます. しかしhelm-ag-insert-at-pointdefcustomで設定された変数なのでダイナミックスコープのままなんですよね.

レキシカルバインディングが有効な場合でも、特定の変数はダイナミックにバインドされたままです。これらはスペシャル変数(special variable)と呼ばれます。defvar、defcustom、defconstで定義されたすべての変数は、スペシャル変数です(Defining Variablesを参照してください)。その他のすべての変数はレキシカルバインディングの対象になります。

Using Lexical Binding (GNU Emacs Lisp Reference Manual)

この警告があると心地よくバイトコンパイル出来ない… いやバイトコンパイルはいまさらやる気では無いのですが警告が出っぱなしというのは気持ち悪いですよね.

custom-set-variablesを使って保存しておいた変数を使うなどやってみたのですがやはり未使用変数だと警告が出ます.

悩んだ結果defvarでスペシャル変数だと注釈を付ければ良いとわかりました. 何故前は警告が出てなかったのか謎ですね.

(defun helm-do-ag-project-root-or-default-at-point ()
    (interactive)
    (defvar helm-ag-insert-at-point)
    (let ((helm-ag-insert-at-point 'symbol))
      (helm-do-ag-project-root-or-default)))

移行してから

新しいパッケージもサクッと試せるようになって見通しが良くなって最高です.

残った課題

ncaq/ncaq-emacs-utilsmoduleディレクトリ以下にgit submodulesとして置いているのですが, これもel-get機能で取得するようにした方が良いのでしょうね. ちょっと面倒そうなのとあまりメリットが無さそうでまだやってないのですが.