Skip to content

キーバインド構造化補助ライブラリ、 keyset をリリースしました

MELPA に登録されているので、 package.el からインストールできます。

これは何?

keyset は emacs のキーバインドの構造化を補助するための簡単なライブラリです。 キーシーケンスを抽象化して、できる限り設定ファイルにキーバインドが 直書きされるのを避けるために作られました。また、キーシーケンスの定義を レイアウト毎に切り替えることができるようにしてあるため、 qwerty 用と dvorak 用で別々のキーシーケンスを用意することが可能です。

きっかけ

keyset を作るきっかけは dvorak 配列への移行を考えたことでした。 自分は evil を使っているのこともあって、 hjkl によるカーソル移動を どうするかは問題でした。できれば dvorak でも似たような配置で 使いたかったのです。

しかしながら、個人的に設定したキーバインドの中には hjkl にならったものもあり、仮にそれらも dvorak 用に変更したとしても、 今度は qwerty を使わざるを得ない状況に出くわした時に 痛い目を見るのは明らかでした。

そのために、キーシーケンスを抽象化できて、なおかつ qwerty や dvorak で 切り替えができるようなライブラリが必要でした。

使い方

keyset はユーザーが動作を予想しやすいよう、なるべく単純な作りになっています。 基本的に、使用するのは keyset-defkeykeyset-key の二つです。

簡単な使い方

まずは keyset-defkey です。これはその名の通りキーシーケンスを定義します。 また、 defkey となっていますが実は関数です。

単純な使い方として、例えば emacs で一般的に次の行へ移るために使われる “C-n” を :next-line という名前で定義してみましょう。 keyset は (require 'keyset) としてロードされているとします。

(keyset-defkey :next-line "C-n")

これで :next-line という名前でキーシーケンスが定義されました。 また、 keyset-defkey で定義するキーシーケンスの名前には キーワード(コロンから始まるシンボル)を使用します。 キーワードを使用する理由は後述の keyset 内でのキーシーケンス表現にあります。

次に、 keyset-key を使用してキーシーケンスを取得します。 keyset-key が返すキーシーケンスは define-key などで使えるオブジェクトで、 現段階ではベクタです。これを define-key などの設定で使用します。

(define-key any-map (keyset-key :next-line) 'any-command)

以上が keyset の基本的な流れです。こうすることによってキーバインドの直書きを 避けることができます。

もう少しだけ実践的な例

実際にはもう少しややこしくなるはずです。例えば emacs で一般的に次へ移ることを 意味するのは “n” で、次の行へというのはそれに ctrl を足したものだったりします。 そこで :next として “n” を定義して、 :next-line でそれを参照するように してみましょう。

(keyset-defkey :next "n")
(keyset-defkey :next-line '(:C :next))

:next-line の定義内容がリストに変わりました。そしてその内容は、 ctrl を意味する :C と定義済みのキーシーケンスを指す :next になっています。 もうおわかりですね、そうです、これはキーシーケンス :next にモディファイアキー ctrl を付与するという意味です!あとは先程の例と同じように define-key で 使用するだけです。

また、キーシーケンスを加工することは keyset-key でも行うことができます。 :next-line の代わりに :next を用いるならば次のようにします。

(define-key any-map (keyset-key :C :next) 'any-command)

これも同じ結果になります。どちらの方法を用いるかはユーザーの自由です。 また、 keyset-key の場合はリストにまとめる必要はありません。

keyset 内でのキーシーケンス表現

keyset ではキーシーケンスを表現するために、文字列やキーワード、シンボル、 そしてそれらで構成されたリストが使用することができます。それらの意味は 次のとおりです。

文字列

文字列は kbd マクロで使用する形式のものが使えます。意味も kbd マクロの ものと同じです。

例:

  • “abc”
  • “RET”
  • “C-x C-c”
  • “<f11>”

キーワード

キーワードには二つの意味があります。 それは、モディファイアキーと定義済みキーシーケンスです。

まず、モディファイアキーですが、これを示すキーワードは あらかじめ用意されています。それがこちらです。

意味 短い名前 長い名前
Alt :A :alt
Hyper :H :hyper
Meta :M :meta
Super :s :super
Shift :S :shift
Control :C :control

また、同じモディファイアキーが重複して設定されても一つとして扱われます。

;; どちらも同じ結果になる
(keyset-key :C "n")
(keyset-key :C :C :C "n")

そして、それ以外のキーワードはすべてキーシーケンス名として解釈されます。

シンボル

シンボルは、キーシーケンスのベクタ表記で用いるものと同じ意味を持ちます。 つまり、下記のコードはどれも同じことを意味しています。

[remap command]
(kbd "<remap> <command>")
(keyset-key 'remap 'command)

リスト

リストはすべて再帰的に展開されます。したがって、ネストしていても構いません。 これは keyset のユーザーが自身でリストの展開をしなくていいようにするために 導入されています。

;; どちらも同じ結果になる
(keyset-key :C "x" :C "c")
(keyset-key '((:C) "x") :C () "c")

レイアウトについて

最初にも書いたとおり、 keyset ではレイアウト毎にキーシーケンスを 切り替えることができます。この機能を使うには keyset-defkey&rest 引数を使用し、 keyset-layout を使いたいレイアウトに設定します。

例えば、 vim の次の行に相当するものを用意したい場合には次のようになります。

;; デフォルトで "j" 、 dvorak で "h"
(keyset-defkey :vim-next-line "j" :dvorak "h")

;; dvorak を使う
(setq keyset-layout :dvorak)

;; "h" が使われる
(define-key any-map (keyset-key :vim-next-line) 'any-command)

注意点として keyset-layout の変更は keyset-key の呼び出しより前に行う 必要があります。また、レイアウト名は自由に決めることができますが、 キーワードを使うことが望ましいです。ちなみに、デフォルトのレイアウトは :default となっていて、他のレイアウト向けにキーシーケンスが 定義されていない場合はこちらのものが使用されます。

おまけ

keyset には keyset-key と同じような関数として keyset-key-string が 用意されています。これは keyset-key の戻り値を kbd 形式の 文字列にしたもので、動作確認などに活用してみてください。

長くなってしまいましたが、ここまで読んでいただきありがとうございます。

org-mode で書かれた howm メモを jekyll に出力する

org-mode を使って howm でメモをとっているのですが、 メモの閲覧はブラウザでできたらいいなぁと思いつつも org-mode のデフォルトの html 出力はちょっとさびしいと思っていました。 そこで、いい機会なので何かと耳にする jekyll を試してみることにしました。

調べてみると org-mode と jekyll を利用している方々のページが見つかったので、 それらを参考にしました。

emacs の設定

メモファイルを jekyll で扱えるようにするため、 org-publish を使用して メモファイルすべてを jekyll 用の html ファイルに変換していきます。 また、その変換には ox-jekyll.el で定義されているバックエンドを使用する 予定だったのですが、自分のメモのフォーマットに合わせたり、 pygments を利用したかったこともあって、拡張したものを用意することになりました。 それが以下になります。

(require 'cl-lib)
(require 'ox)
(require 'ox-publish)
(require 'ox-jekyll)
(require 'uuid)

(defvar my-org-howm-jekyll--src-table nil)

(defun my-org-howm-jekyll--yaml-front-matter (info)
  (let* ((headline
          (assoc 'headline
                 (cddr (plist-get info :parse-tree))))
         ;; タイトルは最初のヘッドラインを使用
         (title (or (org-element-property :raw-value headline)
                    (org-jekyll--get-option info :title)))
         (date
          (org-jekyll--get-option info :date))
         (layout
          (org-jekyll--get-option info :jekyll-layout org-jekyll-layout))
         (categories
          (org-jekyll--get-option info :jekyll-categories org-jekyll-categories))
         (published
          (org-jekyll--get-option info :jekyll-published org-jekyll-published))
         (comments
          (org-jekyll--get-option info :jekyll-comments))
         ;; 最初のヘッドラインからタグを取得
         (tags
          (mapconcat 'identity (org-element-property :tags headline) " ")))
    (unless (equal published "true")
      (setq title (concat "[PREVIEW] " title)))
    (concat
     "---"
     "\ntitle: \""    title "\""
     ;; "\"\ndate: "     date
     "\nlayout: "     layout
     "\ncategories: " categories
     "\npublished: "  published
     "\ncomments: "   comments
     "\ntags: "       tags
     "\n---\n")))

(defun my-org-howm-jekyll-template (contents info)
  (if org-jekyll-include-yaml-front-matter
      (concat
       (my-org-howm-jekyll--yaml-front-matter info)
       contents)
    contents))

(defun my-org-howm-jekyll-escape (contents)
  (replace-regexp-in-string "\\([{}%]\\)" "{% raw %}\\1{% endraw %}" contents))

(defun my-org-howm-jekyll-src-block (src-block contents info)
  (if org-jekyll-use-src-plugin
      ;; エスケープ処理を避けるためにプレースホルダーとして uuid を使用
      (let ((id (uuid-string))
            (language (org-element-property :language src-block))
            (value (my-org-howm-jekyll-escape
                    (org-remove-indentation
                     (org-element-property :value src-block)))))
        (push (cons id
                    (format "{%% highlight %s %%}\n%s{%% endhighlight %%}"
                            language
                            value))
              my-org-howm-jekyll--src-table)
        id)
    (org-export-with-backend 'html src-block contents info)))

(defun my-org-howm-jekyll-final-function (contents backend info)
  (let ((escaped (my-org-howm-jekyll-escape contents)))
    (if org-jekyll-use-src-plugin
        ;; プレースホルダーの展開
        (cl-loop for (id . src) in my-org-howm-jekyll--src-table
                 do (setq escaped
                          (replace-regexp-in-string id
                                                    src
                                                    escaped
                                                    nil
                                                    t))
                 finally (progn
                           (setq my-org-howm-jekyll--src-table nil)
                           (cl-return escaped)))
      escaped)))

(org-export-define-derived-backend 'howm-jekyll 'jekyll
                                   :translate-alist
                                   '((src-block . my-org-howm-jekyll-src-block)
                                     (template . my-org-howm-jekyll-template))
                                   :filters-alist
                                   '((:filter-final-output . my-org-howm-jekyll-final-function)))

(defun my-org-howm-jekyll-publish-to-html (plist filename pub-dir)
  (org-publish-org-to 'howm-jekyll filename ".html" plist pub-dir))

タイトルやタグの取得方法を変更して、 html 全体から liquid のマークアップとして 解釈されそうな文字を一通りエスケープするようにしました。 ただ、 pygments を使うための highlight タグはそのままにする必要があったので、 変換中はプレースホルダーとして uuid 文字列を置いて、エスケープ処理後に 展開するようにしました。ちょっと無理やりな気もしますが、他にいい方法が 思いつきませんでした。

あとは、これを使って次のような形で org-publish 用の設定を追加しました。

(add-to-list 'org-publish-project-alist
             '(("howm"
                :base-directory       "path/to/memo"
                :publishing-directory "path/to/repo/_posts"
                :jekyll-categories    "howm"
                :base-extension       "howm"
                :recursive            t
                :publishing-function  my-org-howm-jekyll-publish-to-html
                :section-numbers      nil
                :with-toc             nil
                :with-tags            nil
                :with-sub-superscript nil)))

あとは必要に応じて M-x org-publish howm とするだけです。

jekyll

jekyll の方はあまり手の込んだことをするつもりはなかったので jekyll bootstrap を使って _config.yml に次の 2 行を追加して 必要ない部分はコメントアウトしました。

highlighter: pygments
lsi: true

また、関連記事取得用に gsl と rb-gsl をインストールしました。

おわりに

これでやっと howm のメモをブラウザで閲覧できるようになりました。 関連記事はいくらかランダムな感じもありますが、個人的には十分でした。 それと、 emacs の設定ファイルも org-mode で書いているので それらも jekyll を使って見れるようにしました。ただ、このままだと不便なので そのうち改善したいところです。

helm で正規表現などを無効化する

動機

helm は絞り込みが高機能で正規表現による絞り込みはもちろん、 バッファをメジャーモードで絞り込んだりするなど情報源によっては 情報源が用意している記法を用いることで様々な絞り込みができるらしいのですが、 自分はほとんどこの機能を使ったことがありません。 便利かもしれないのですが、絞り込む文字列を増やすことで個人的には十分でした。

そこで、 helm で入力した文字列を見た目通りに扱えるようにしよう、 というのが今回の動機です。

方針

helm では情報源の match 属性にある関数が情報源に合わせた絞り込みを 行っていますが、今回はこれを無効化して helm-match-plugin に絞り込みを任せます。

helm-match-plugin

helm-match-plugin のデフォルトの状態1で絞り込みを担っている helm-mp-3-match で正規表現が無効化されるようにします。2

helm-mp-3-match ではパターン生成に helm-mp-3-get-patterns を使用しているので、この関数の戻り値を加工することで正規表現を無効化します。 helm-mp-3-get-patterns の戻り値は述語関数と正規表現の連想リストのような 構造をしているので正規表現の部分に regexp-quote を適応すればよいでしょう。3

(require 'helm-match-plugin)

(defvar my-enable-helm-without-regexp t)

(defadvice helm-mp-3-get-patterns
    (after my-helm-without-regexp activate)
  (when my-enable-helm-without-regexp
    (setq ad-return-value
          (mapcar (lambda (it)
                    (cons (car it) (regexp-quote (cdr it))))
                  ad-return-value))))

あとは情報源の絞り込みに helm-match-plugin が使われるように情報源の match 属性を削除します。また、 dont-plug 属性に helm-compile-source--match-plugin が含まれていても helm-match-plugin が使用されなくなってしまうので、これも取り除きましょう。

使い回しできるように関数として用意します。また変数を加工するときには cl-callf が便利です。

(require 'cl-lib)

(defun my-make-helm-source-without-regexp (source)
  (cl-loop for (key . value) in source
           unless (eq key 'match)
           if (eq key 'dont-plug)
           collect (cons key (remove 'helm-compile-source--match-plugin value))
           else
           collect (cons key value)))

;; 例として `helm-source-buffers-list'
(require 'helm-buffers)

(cl-callf my-make-helm-source-without-regexp helm-source-buffers-list)

helm-migemo

migemo を使用する情報源の場合、上のコードだけでは不十分なので 追加の対策をします。というのも migemo-get-pattern では 入力は文字列として処理されるので、エスケープ用の “\\” も文字として 扱われてしまうからです。当然といえば当然の動作ですね。

  • migemo-get-pattern の動作例
    (migemo-get-pattern "c\\+\\+")
    ;; => "\\(c\\s-*\\\\\\s-*+\\s-*\\\\\\s-*+\\|c\\s-*¥\\s-*+\\s-*¥\\s-*+\\)"
    

したがって、 helm-mp-3migemo-match 内では helm-mp-3-get-patterns の戻り値を加工せず migemo-get-pattern の戻り値の必要な箇所を regexp-quote で加工します。

(require 'cl-lib)
(require 'helm-migemo)

(defvar my-enable-helm-migemo-without-regexp nil)

(defadvice helm-mp-3migemo-match
    (around my-helm-without-regexp activate)
  (if my-enable-helm-without-regexp
      (let ((my-enable-helm-without-regexp nil)
            (my-enable-helm-migemo-without-regexp t))
        ad-do-it)
    ad-do-it))

(defadvice migemo-get-pattern
    (after my-helm-without-regexp activate)
  (when my-enable-helm-migemo-without-regexp
    (let ((str   ad-return-value)
          (start 0))
      (save-match-data
        (while (string-match "\\\\s-\\*\\([^[\\]\\)" str start)
          (let ((newtext (regexp-quote (match-string 1 str))))
            (setq str   (replace-match newtext t t str 1)
                  start (+ (cl-second (match-data))
                           (1- (length newtext)))))))
      (setq ad-return-value str))))

おわりに

とりあえず自分の環境では目的の動作を得ることが出来ました。 “c++” のような文字列や拡張子を入力するのがいくらか楽になりました。 既存の情報源を加工するのではなく、それを元に新しく情報源を定義したほうが 行儀がいいかもしれませんね。 絞り込みが若干もたつくようになった気もしますが、そのあたりは必要に応じて 直していこうかと思います。 そういえば、以前 “\” を使って絞りこみをしようとしたらうまく動作しなかったのは migemo が関係していたのかも…

Footnotes:

1

helm-mp-matching-methodmulti3 の状態。

2

helm-mp-3-match だけでなく helm-mp-exact-match も絞り込みを 担っていますが今回は手を付けません。

3

正確にはこれだけだと “!” によるマイナス検索が有効なままに なってしまいますが、これも今回は手を付けません。

el-init の機能紹介

前回は el-init の紹介を兼ねて簡単な使い方の説明だけでした。

今回は el-init の機能やオプションなどについて紹介したいと思います。

変数

まずは、主要な変数についてです。

el-init:load-file-regexp

el-init でロードの対象になるファイルの正規表現です。
デフォルトでは

"\\.elc?$"

になっていて、バイトコンパイルされたものを含む
emacs lisp ファイルにマッチするようになっています。

もし、el-init でロードする対象ファイルを絞りたいのであれば
この変数の値を変更すると良いでしょう。

el-init:load-directory-list

el-init でロード対象となるファイルを探索するディレクトリのリストです。
el-init:load でロードするディレクトリのサブディレクトリとして表現します。
リストの先頭にあるサブディレクトリにある設定ファイルから順にロードされます。

リストの要素はサブディレクトリ名の文字列です。
サブディレクトリ下も再帰的に探索したい場合は
先頭にサブディレクトリ名、
二番目の要素を t としたリストとして記述します。

例えば、

  • base
  • init
  • lang (再帰的に探索)

↑のサブディレクトリを指定したい場合は

("base" "init" ("lang" t))

とします。

実際には次のようなコードになるでしょう。

(setq el-init:load-directory-list '("base" "init" ("lang" t)))

el-init:load-function-compile

el-init でロードに使用する無名関数をバイトコンパイルするかどうかを制御します。
non-nil であればバイトコンパイルをし、それをロードに使用します。
バイトコンパイルする目的は無名関数の動作速度を向上させることのみです。

この無名関数は、
require のラップで説明するラッパー関数をひとまとまりにしたもので
el-init:load-function-list に多くの関数を指定している人は、
多少ロードにかかる時間が短縮されるかもしれません。

el-init:before-load-hook

el-init:load で設定ファイルがロードされる前に実行されるフックです。
あまり使われることはないかもしれません。

el-init:after-load-hook

el-init:load で設定ファイルをロードし終えた時に実行されるフックです。
ロードに失敗しても実行されます。
これもあまり使われることはないでしょう。

el-init:record

el-init でロード結果などの情報を格納する変数です。
現在は require 関数で指定するシンボルと、
そのファイルに関する情報を格納した plist との alist になっています。

言葉で示すより例を示したほうがわかりやすいかもしれないので、
例を示します。

((init-org-mode :error "Error Message"))

el-init のデフォルトではロード中にエラーがあった場合にのみ
エラーの内容が記録されるようになっているので、大抵は nil になっていると思います。

この変数へのアクセスには
今後のバージョンアップも考慮して専用の関数を用いると良いでしょう。

レコードの取得 el-init:get-record

レコードの取得には el-init:get-record を使用します。

(el-init:get-record SYMBOL &optional PROPERTY)
  • SYMBOL
    ファイル名のシンボルを指定します。
  • PROPERTY
    取得したい項目を指定します。
    オプショナル引数になっていて、何も指定がない場合は plist 全体が返されます。

例:

(el-init:get-record 'init-org-mode)
;; => (:error "Error Message")
(el-init:get-record 'init-org-mode :error)
;; => "Error Message"

ちなみに、el-init:get-record は setf 可能です。
setf を使いたい人はぜひどうぞ。

レコードの追加 el-init:add-record

レコードの追加には el-init:add-record を使用します。

(el-init:add-record SYMBOL PROPERTY VALUE)
  • SYMBOL
    ファイル名のシンボルを指定します。
  • PROPERTY
    追加する項目を示すシンボルを指定します。
  • VALUE
    項目の値を指定します。

例:

(el-init:add-record 'init-anything :loadtime 0.1)

el-init:load-function-list

el-init でロードに使用する require 関数のラッパー関数のリストです。
デフォルトではエラーを記録するための
el-init:require/record-error だけが設定されています。
詳しくは require のラップを参照してください。

require のラップ

el-init では、設定ファイルのロードに require 関数を使用すると言って来ましたが、
実は使用されるのは素の require 関数ではありません。
el-init:load-function-list に指定された複数のラッパー関数によってラップされた
require 関数がロードに使用されます。

ラッパー関数は around アドバイスや CLOS の around メソッドのようなものです。
el-init:load-function-list の先頭から順に呼び出されます。

ラッパー関数の定義

ラッパー関数を定義するには el-init:define-require マクロを使用します。

(el-init:define-require NAME &rest BODY)
  • NAME
    ラッパー関数の名前を指定します。
  • BODY
    ラッパー関数で行う処理を記述します。

el-init:define-require の BODY 部では、
暗に変数 feature, filename, noerror が導入されます。
これは require 関数の引数に対応するものです。

  • 実際は el-init:define-require マクロが defun 式に展開された時の引数です。
    気になる人は el-init:define-require の定義を見るか、macroexpand してみましょう。
    el-init:define-require は el-init-require.el に定義されています。

BODY 部の中で el-init:next 関数を呼ぶことによって
require 関数を含めた残りのラッパー関数を呼び出します。
従って、ラッパー関数内で直接 require 関数を呼ぶことはしません。

el-init:next 関数の引数は下記の通りです。
それぞれ require 関数の引数と対応しています。

(el-init:next &optional FEATURE FILENAME NOERROR)

引数を何も指定しなければ、呼び出し元のラッパー関数と同じ引数が使用されます。
CLOS の call-next-method とだいたい同じです。
引数の値を変えたい場合は、el-init:next 関数を引数付きで呼び出します。

;; noerror を t に
(el-init:next feature filename t)

せっかくなので、el-init:require/record-error の定義を見てみましょう。

el-init:require/record-error の目的は
ロード中に発生したエラーを el-init:record に記録することです。
副次的な効果として、ロード中にエラーが発生しても
他のファイルのロードは行われるようになります。

(el-init:define-require el-init:require/record-error
  (condition-case e
      (el-init:next)
    (error (el-init:add-record feature
                               :error
                               (error-message-string e)))))

condition-case でロード処理(el-init:next の呼び出し)を覆いエラーを捕まえています。
エラーが発生した場合は el-init:add-record を使って
レコードにエラーメッセージを記録します。
この時に、暗に導入された変数 feature を利用しています。

他のラッパー関数の紹介

el-init には el-init:require/record-error 以外にも
あらかじめ用意されたラッパー関数があります。
簡単にですが紹介したいと思います。
ラッパー関数は el-init-require-definitions.el に定義されているので、
気になる人はファイルを覗いてみると良いでしょう。

el-init:require/benchmark

各ファイルのロードにかかった時間、GCの回数、GCにかかった時間を記録します。
設定ファイルから設定ファイルをロードするケースは未対応なので、
現状では重複してカウントされてしまいます。

el-init:require/ignore-errors

その名の通り ignore-errors でラップします。
ロード中のエラーを無視します。
記録も何もしません。

el-init:require/system-case

init-loader の emacs の種類によってロードする設定ファイルを切替える機能の el-init 版です。
emacs の種類以外に、OS ごとの分岐も可能です。
el-init:next をわざと呼び出さないようにしてロードする設定ファイルを選んでいます。

el-init:load

el-init:load 関数の引数についても一応紹介しておきます。

(el-init:load DIRECTORY &key DIRECTORY-LIST FUNCTION-LIST COMPILE OVERRIDE)
  • DIRECTORY
    読み込み対象のディレクトリ
  • DIRECTORY-LIST, FUNCTION-LIST, COMPILE
    それぞれ指定がなければ
    el-init:load-directory-list, el-init:load-function-list, el-init:load-function-compile
    の値が使用されます。
    主にテストなどを書く場合を想定して用意したもので、
    init.el などで使うことはおすすめしません。
  • OVERRIDE
    non-nil な場合、require 関数を el-init のロードで使用する関数で一時的に置き換えます。
    ライブラリのロードなどにもラッパー関数を通したい時に使用します。
    ほとんど使うことはないかもしれません。

最後に

これで el-init の機能紹介は終わりです。
なんとなくわかってもらえたでしょうか。
el-init は、なるべくユーザが自由に機能をいじれるよう
意識して作りました(ラッパー関数のあたりなど)。
アイデアが浮かんだなら、ぜひラッパー関数を自作してみてください。
個人的には、古い elc ファイルを避けてロードしたり、
バイトコンパイルを試みるようなラッパー関数があってもいいかなと思っています。

el-init:load-function-list の tips として、
ラッパー関数の性質上、
el-init:require/system-case のような
ロードするかどうかを決めるものはリストの先頭の方に、
el-init:require/record-error のような
エラー抑制系のものはリストの末尾の方に置くといいと思います。

では。

el-init なるものを作っています

前書き

自分は Emacs を使い始めてからというもの、
いろいろな人のブログや web サイトなどを参考に
いろいろなコードを自分の設定ファイルに取り込んできました。

設定ファイルは、拡張機能やプログラミング言語、ジャンルなどによって
ファイルを分割し、init-loader によって管理してきました。

結果、分割された設定ファイルはいつの間にか 190 個を超えていて、
設定ファイルごとの依存関係や適切なロード順を
ファイル名の番号で整理するのは徐々に困難になって行きました。

また、設定ファイルをバイトコンパイルすると他の設定ファイルが
ロードされること前提で書かれているコードが警告を受けて、
重要な警告が覆い隠されてしまうことも多々ありました。

そういうことがあったので、
番号とは違う形でのロード順の決定法や、
設定ファイルごとの依存関係の明示ができるようにならないかと考え
el-init を書きました。

el-init って?

el-init とは、特定のディレクトリ下にある emacs の設定ファイルを
ロードするための emacs lisp です。

こんなかんじで使用します。

(require 'el-init)
(setq el-init:load-directory-list '("base" "pkg" ("ext" t)))

(el-init:load "~/.emacs.d/inits")

ねらい

設定ファイルの大雑把なロード順の指定と
設定ファイルごとの依存関係を明示的に表現することが主なねらいです。

設計

ロード順

段階的なロード順の指定は、ディレクトリを分けることで表現します。
el-init では段階的なロード順をロードするディレクトリのサブディレクトリとして
表現します。

  • 何よりも先にロードされて欲しい設定ファイル
  • パッケージマネージャの初期化用の設定ファイル
  • 各拡張機能用の設定ファイル

のような、大雑把なくくりを表現します。

依存関係

設定ファイルごとの依存関係を表現するために
el-init では require と provide を使用します。

各設定ファイルは provide 式を記述し、
依存する設定ファイルがある場合は、
require を使って依存する設定ファイルが読み込まれるようにします。

require と provide を使う関係上、
ロード対象のディレクトリはすべて load-path に追加されることになります。

インストール

コードは github から取得できます。

使い方

例えば ~/emacs.d/inits が次のようなディレクトリ構成になっているとします。

▾ inits
  ▾ base
      init-encode.el
  ▾ ext
    ▾ anything
        init-anything.el
    ▾ org-mode
        init-org-mode.el
        init-org2blog.el
  ▾ pkg
      init-el-get.el
      init-elpa.el

それぞれのサブディレクトリとその中にある設定ファイルの意味は次のようだとします。

ディレクトリ 設定ファイルの目的
base どの設定ファイルよりも先に読み込まれて欲しい設定ファイル群
ext 拡張機能用の設定ファイル群
pkg パッケージマネージャ用の設定ファイル群

この時、

  • まず inits/base ディレクトリ下の設定ファイルをロードし、
  • 次に、inits/pkg ディレクトリ下の設定ファイルをロードし、
  • 最後に、inits/ext ディレクトリ下(サブディレクトリ下も含む)にある設定ファイルをロードする

という動作を el-init で行いたいとします。

その場合には init.el などに次のように記述します。

;; ロードパスの設定は適宜読み替えてください
(add-to-list 'load-path "path/to/el-init")

(require 'el-init)
(setq el-init:load-directory-list '("base" "pkg" ("ext" t)))

(el-init:load "~/.emacs.d/inits")

では、詳しく見て行きましょう。

ロードパスの追加

el-init は設定ファイルローダなので、
大抵はパッケージマネージャなどの恩恵を受ける前にロードすることになるでしょう。
従って、ロードパスを手で追加します。

;; ロードパスの設定は適宜読み替えてください
(add-to-list 'load-path "path/to/el-init")

el-init をロード

require で el-init をロードします。

(require 'el-init)

ロードするサブディレクトリを指定

ロードするサブディレクトリとその順序を指定します。

(setq el-init:load-directory-list '("base" "pkg" ("ext" t)))

el-init:load-directory-list にはサブディレクトリ名を記述します。
リストの先頭にあるサブディレクトリから順にロードされます。
サブディレクトリ内を再帰的に探索して欲しい場合は

("ext" t)

のように、

(サブディレクトリ名 t)

の形で記述します。

ロード

目的のディレクトリの指定されたサブディレクトリ内のファイルを順にロードします。

(el-init:load "~/.emacs.d/inits")

この場合は、

  1. ~/.emacs.d/inits/base
  2. ~/.emacs.d/inits/pkg
  3. ~/.emacs.d/inits/ext下サブディレクトリも含む

の順で各ディレクトリ内の設定ファイルがロードされます。

設定ファイルの書き方

el-init では 依存関係を示すために、各設定ファイルに
provide 式を記述する必要があります。

とは言っても、ファイル名と同じシンボルを provide 関数に渡すだけです。

;; init-anything.elの場合
(provide 'init-anything)

これをファイル内に記述します。
それだけです。

ファイル名が他の拡張機能と被らないように注意しましょう。

anything.el x
init-anything.el o

provide 省略マクロ

el-init には provide の引数を省略することができるマクロが用意されています。
次のように記述することでファイル名を気にする必要がなくなります。

(require 'el-init)
(el-init:provide)

最後に

今回は簡単な紹介だけですが、気になることなどがあれば
コメントか github の issues@HKe7 にリプライを飛ばしてもらえるとありがたいです。

詳しい説明や el-init:load-function-list や el-init:record については
後日書こうと思います。

defpath.el を書きました

defpath

以前

で紹介したパスユーティリティのコードですが、
今見なおしてみるとなかなかひどいコードでした。
関数として書かれている上に、eval を呼び出すなんて!

当時は lisp の勉強を始めたばかりだったのであんなコードを書いてしまいましたが、
今思えば、これはマクロで十分だったなぁと…

ということで defpath.el として書き直しました。

使い方

使い方は以前と少し変わっていて、defvar と同じような形で使います。
docstring を使えるようになりました。
使い方を統一するという点で言えば
常に関数の形でパスの値を呼び出すほうがいいかと思います。

;; パスの定義
(defpath emacs.d "~/emacs.d")

;; ドキュメント付き
(defpath tmp "/tmp" "一時ディレクトリ")

;; パスの呼び出し
path/to/emacs.d          ;=> "~/emacs.d"
(path/to/emacs.d)        ;=> "/home/hkey/emacs.d"
(path/to/emacs.d "init") ;=> "/home/hkey/init"

余談

emacs lisp の中には hoge–huga みたいな形で
接頭辞との区切りにハイフンを2つ並べたものがありますが、
これは common lisp で言うところの内部シンボルのようなものなのでしょうか。

ライブラリと設定の関係の明示

設定ファイルが成長するに連れて

emacs 設定ファイルは日々成長していくものですよね?
新しい拡張機能やライブラリ(以下ライブラリで統一します)、便利なコード片など、気に入ったものを見つけたら自分の環境にインストールして、それに対する設定コードを .emacs (や .emacs.d/init.el)に書き足していく事と思います。
ライブラリが増えて設定コードが大きくなるに連れて、ある設定コードが必要としているライブラリがそのコード周辺を見ただけでは正確にわからなくなる時が多々ありました。
簡単な例としてはこんな感じです。

;; ac-slimeの設定
(add-hook 'slime-mode-hook 'set-up-slime-ac)
(add-hook 'slime-repl-mode-hook 'set-up-slime-ac)

これは ac-slime の設定の例です。
このコードは少なくとも auto-complete, slime, ac-slime を必要としています。
ですが、今の自分がそのことを知っていても、将来の自分が覚えているかどうかは怪しいところです。
僕は、ライブラリがロードされることを前提としたコードが、自分の設定ファイルに多くあることを不安に思っていました。
そこで今回は、コードとライブラリの関係を明示的にするために、僕が用意したマクロを紹介します。

用意したマクロ

今回紹介するマクロ(とその補助関数)は以下になります。

(eval-when-compile
  (require 'cl))

(defun* gen-lib-loader (liblist &optional (noerr t))
  (mapcar
   (lambda (x)
     (if (symbolp (car x))
         (destructuring-bind
             (feature &optional filename (noerror noerr)) x
           `(require ',feature ,filename ,noerror))
       (destructuring-bind
           (file &optional (noerror noerr) &rest rest) x
         `(load ,file ,noerror ,@rest))))
   (mapcar (lambda (x)
             (if (listp x) x (list x)))
           liblist)))

(defmacro with-library (liblist &rest form)
  "指定したライブラリが全て読み込み済みの時のみ`form'を実行する。"
  (declare (indent 1))
  `(when
       (and
        ,@(mapcar
           (lambda (x) `(featurep ',x))
           liblist))
     ,@form))

(defmacro with-load-library (liblist &rest form)
  "指定したライブラリを順に読み込み、すべてのライブラリの読み込みが成功した場合のみ`form'を実行する。"
  (declare (indent 1))
  `(when
       (and
        ,@(gen-lib-loader liblist))
     ,@form))

(defmacro with-lazy-library (liblist &rest form)
  "指定したライブラリが全て読み込まれた時点で`form'を実行する。"
  (declare (indent 1))
  (with-lazy-library-code-gen liblist `(progn ,@form)))

(defun with-lazy-library-code-gen (liblist form)
  (if (null liblist)
      form
    `(eval-after-load
      ,(format "%s" (car liblist))
      ',(with-lazy-library-code-gen (cdr liblist) form))))

このうち、目的のマクロは

  • with-library
  • with-load-library
  • with-lazy-library

の3つです。

with-library

このマクロは、指定したライブラリがずべてロード済みだった場合のみ、このマクロに包まれたコードを実行します。
(正確には、そのように動作するコードを生成するといったところでしょうか)
このマクロはあるライブラリ用の設定を用意するけど、そのライブラリをロードするかどうかは、他所に任せるといったものです。
ちなみに、ロード済みかどうかは featurep を使って判断します。

  • 書式
    書式は次のようになります。

    (with-library (lib1 lib2 ...)
      code ...)
    

  • 展開結果
    展開結果の例は次のようになります。

    ELISP> (macroexpand '(with-library (hoge huga)
                           (foo)
                           (bar baz)))
    (if
        (and
         (featurep 'hoge)
         (featurep 'huga))
        (progn
          (foo)
          (bar baz)))
    

with-load-library

with-library とは違い、このマクロは指定したライブラリの読み込みを試し、すべての読み込みに成功した場合のみ、このマクロに包まれたコードを実行します。
ライブラリの設定とともにライブラリの読み込みに関しても責任を持ちます。
ライブラリの読み込みには require と load が利用できるようにしてあります。
それぞれ、ライブラリをシンボルで指定した場合には require 、文字列で指定した場合には load が使用されます。
また、 require と load のオプショナル引数を使用したい場合には、ライブラリ名にオプショナル引数をつなげたリストを指定します。

  • 書式

    (with-load-library (lib1 "lib2" (lib3 "lib3-dev") ...)
      code ...)
    

  • 展開結果

    ELISP> (macroexpand '(with-load-library (hoge "huga" (piyo "piyopiyo"))
                           (foo)
                           (bar baz)))
    (if
        (and
         (require 'hoge nil t)
         (load "huga" t)
         (require 'piyo "piyopiyo" t))
        (progn
          (foo)
          (bar baz)))
    

with-lazy-library

最後に、このマクロは指定したライブラリが全て読み込まれたタイミングで、このマクロに包まれたコードを実行します。
つまるところ、ネストした eval-after-load に展開されます。
ライブラリの指定にはシンボルと文字列の両方が使えますが、どちらを用いても意味は変わりません。
ライブラリが、 load で読み込まれることを示す目印として僕は利用しています。
ライブラリが読み込まれるまでコードを保持しておく以外は、 with-library と同じような動作をします。

  • 書式

    (with-lazy-library (lib1 "lib2" ...)
      code ...)
    

  • 展開結果

    ELISP> (macroexpand '(with-lazy-library (hoge "huga")
                           (foo)
                           (bar baz)))
    (eval-after-load "hoge"
      '(eval-after-load "huga"
         '(progn
            (foo)
            (bar baz))))
    

使用例

用意したマクロを使って、冒頭に出てきたコードを書いてみると次のようになります。

(with-lazy-library (slime auto-complete)
  (with-load-library (ac-slime)
    (add-hook 'slime-mode-hook 'set-up-slime-ac)
    (add-hook 'slime-repl-mode-hook 'set-up-slime-ac)))

これで、 slime と auto-complete のロードに関しては責任を持たないけど、両方が読み込まれたならば、 ac-slime のロードとそれに関する設定を行う、という事がコード上で表現できるようになりました。

終わりに

今回の目的は、設定コードとライブラリの関係をコード上に表すことでした。
とりあえずは目的を果たせたのではないかと思います。
これで、マクロ呼び出し側から意味合いを残して、実装を抜き出すことができました。
同じような意味で書かれていた似たようなコードを統一できましたし、実装に不備があっても訂正箇所はごく僅かです。
車輪の再発明な気はしますが、これで少しは .emacs の整理が楽になるのではないでしょうか。

実際には(おまけ)

実際には、自分は前回用意した eval-after-load-compile を使用すると共に、バイトコンパイル時にだけライブラリをロードするようにして、今回紹介したマクロを使用しています。
なぜ上で紹介しなかったのかというと、実装からくる注意点がいくつか存在するからです。
参考までに、コードを載せておきますね。
(変更のない部分は省略してあります)

(defmacro with-load-library (liblist &rest form)
  "指定したライブラリを順に読み込み、すべてのライブラリの読み込みが成功した場合のみ`form'を実行する。"
  (declare (indent 1))
  `(progn
     (eval-when-compile
       ,@(gen-lib-loader liblist))
     (when
         (and
          ,@(gen-lib-loader liblist))
       ,@form)))

(defmacro with-lazy-library (liblist &rest form)
  "指定したライブラリが全て読み込まれた時点で`form'を実行する。"
  (declare (indent 1))
  `(progn
     (eval-when-compile
       ,@(gen-lib-loader liblist))
     ,(with-lazy-library-code-gen liblist `(progn ,@form))))

(defun with-lazy-library-code-gen (liblist form)
  (if (null liblist)
      form
    `(eval-after-load-compile
      ,(format "%s" (car liblist))
      ',(with-lazy-library-code-gen (cdr liblist) form))))

eval-after-load を少し改造 (その2)

仕様変更は少ないほうがいいよね、と言っておきながら早速仕様変更のお話です。

以前書いた `eval-after-load-q’ にはいくつかの狙いがありました。

  1. `eval-after-load’ の記述のしにくさを改善する。
  2. ファイル名の記述を `require’ のようにクオートしたシンボルで記述できるようにする。
  3. バイトコンパイルを行う。

この内1、2は糖衣構文を提供するもの(2に関してはわがまま成分多め)で、3は最適化に関するものと、異なる意図のコードが同居していました。

今回、emacs lisp にもコンパイラマクロを定義できること知り、また、次回示すユーティリティマクロを用意することで `eval-after-load’ を直接記述する必要がなくなったので、次のように書き直しました。

(eval-when-compile
  (require 'cl))

(defalias 'eval-after-load-compile 'eval-after-load)

(define-compiler-macro eval-after-load-compile (file form)
  (if (eq 'quote (car form))
      `(eval-after-load ,file
         '(funcall
           ,(byte-compile
             `(lambda ()
                ,(cadr form)))))
    `(eval-after-load ,file ,form)))

(defmacro eval-after-load-q (file &rest form)
  (declare (indent 1))
  `(eval-after-load-compile ,file
                            '(progn ,@form)))

eval-after-load-compile

`eval-after-load’は Emacs の標準関数なので、バイトコンパイルするかどうかを明示的にして他所に影響が出ないようにするためと、展開結果に展開前と同じ関数を含めたいので `eval-after-load-compile’ というエイリアスを用意して、もとの `eval-after-load’ と区別をします。
`eval-after-load-compile’ はクオートされたフォームが与えられた場合だけバイトコンパイル時にフォームをバイトコンパイルします。
それ以外の時は `eval-after-load’ と同じ動作をします。

ELISP> (compiler-macroexpand
        '(eval-after-load-compile "hoge"
                                  '(+ 1 2 3)))
(eval-after-load "hoge"
  '(funcall
    #[nil "\300\207"
          [6]
          1]))

ELISP> (compiler-macroexpand
        '(eval-after-load-compile "hoge"
                                  (list '+ 1 2 3)))
(eval-after-load "hoge"
  (list '+ 1 2 3))

eval-after-load-q

`eval-after-load-q’ に関しては、クオートと `progn’ を省略するだけにとどめ、 `eval-after-load-compile’ を呼ぶようになっています。
コンパイルの必要性を感じない場合は `eval-after-load’ に書き換えるといいと思います。

ELISP> (macroexpand '(eval-after-load-q "hoge"
                       (message "hello ")
                       (message "world")))
(eval-after-load-compile "hoge"
                         '(progn
                            (message "hello ")
                            (message "world")))

おわりに

というわけで、またまた `eval-after-load’ に関する話でした。
実際のところ `eval-after-load’ の中で関数でも定義しない限りバイトコンパイルによる最適化の恩恵はほとんど無いのではないかと思われます。
それでも僕がバイトコンパイルを行いたいのは、バイトコンパイル時に不正な式を見つけだせるようにしたいからです。
本当はそういう仕組みが用意されているのかもしれませんが、とりあえずこれで怪しい式に対して警告が出るようになりました。

これで、.emacs のロード中にコケるという悲しい事態をいくらか回避できるのではないでしょうか。

テーマを変更しました

このブログのテーマを Greenery から Titan に変更しました。
Greenery 結構気に入っていたんですが、
文字が若干小さくて読むのが辛いところがありました。
お気に入りのテーマではあったんですけど、やはり記事を読んでもらうことが第一なので字の読みやすい Titan にしました。

テーマを変更するにあたって、配置が以前と若干変わったかもしれませんが、
読みやすさを手に入れるためなので許して欲しいところです。

僕としては、基本的にいつものものはいつものところにあるべきで、
いたずらにそれを崩すべきではないだろうと思っています。
ウェブサイトにしろ他のものにしろ、普段利用しているものの不意な仕様変更はあまり嬉しかったことがないから、というだけなんですけどね。

今回は読みやすさのほうが重要だと思って変更しましたが、
今後もあまり色々と変更がないようにしていきたいですね。

第5回関西 Emacs に参加しました

始めて関西 Emacs に参加

twitter でその存在を知り、第3回、第4回は ust の中継を見させてもらっていたのですが、
どうしても Emacs 使いの方々に会ってみたかったし、色々とお話も聞きたいという事で、
今回半ば勢いだけで参加させて頂きました。
そのため、情報をもらいはするもののあまり返せるものがなく、
また、何かと迷惑をかけてしまったかと思います。
このことに関しては反省する限りです。

とはいえ、関西 Emacs はとても楽しませてもらいました。
そもそも30人くらいの Emacs 使いが一箇所に集まっているという事自体がすごくて、
その上発表や話を効くことができたり、議論したりと非常に刺激的で、
直接 emacs の話ができる人がまわりにいない僕にとっては夢のような空間でした。

結局最後まで顔とハンドルネーム(とアイコン)が一致しない人が多かったり(すいません)、
ほとんどお話出来なかった方もいて(自分が人見知りのせいなのがほとんど)、
多少もったいないことをしてしまいましたが、とても楽しくて貴重な時間でした
今回、勉強会素人である自分を参加させていただき、本当にありがとうございました。

帰る途中で、色々と聞きたかったことが他にもあったことに思い出したので、
次に参加することがあるときは気になることを片っ端からメモっていこうかと思います。