emacsの設定ファイルどうするのかというのはおそらく共通の悩み

この記事はemacs advent calendar 2012(http://qiita.com/advent-calendar/2012/emacs)の10日目の記事です。

はじめに

emacsの設定どうしてるかなどについてまとめてみました。他の人はどうしているかなど聞いてみたりしたいです。

しばらくemacsから離れていると浦島状態

最近少しだけemacsから遠ざかっていました。emacs界隈にも流行り廃りなどがありますね。しばらくemacsから離れていると、いつの間にか浦島状態に。。

新しくまたemacs環境を整備し直したいと思っていたのですが。手つかずに近い感じです。

こういう時どうしてますか?

環境改善に取り組んでみようとするものの挫折してみたり

「anythingがhelmに変わったのか。org-modeは全然追えていないけれど、便利そうなものらしいのでどんどん取り入れていこう」などとあれこれ環境改善を試みようとするわけですが。

いきなりたくさんの修正を加えると、今まで動いていた機能が動かなくなってしまったりします。emacsユーザとしては今動く環境が一番重要だったりします。

また、自作していた機能と同等の機能を持つelispが公開されていた、汎用性など考えると自作したものを置き換えてた方が良いかもしれない。。と、置き換えてみたものの微妙に細かい部分が自分の想定とは違っていてなんだか使いにくい。

途中で力つき、改善も中途半端に終わってしまい。結局、以前の環境の方が快適であったことに気づき元の環境に戻すしてしまったりすることが何度かありました。

少しずつ変えていけば良いのかも

2つの環境を比べたい

「新しい便利なelispは取り入れていきたい。一方で今使っている環境を壊したくない」こういう時に使うのはブランチですね。

もちろんgitなどのvcsを使って管理するわけですが、「2つの環境を造って使用感を試したい。」こういう時に2つの環境でemacsを立ち上げられると便利です。

.emacs.dをシンボリックリンクにする

シンボリックリンクを使います。

lrwxr-xr-x   1 nao   staff     44 10 19 02:36 .emacs.d -> /Users/podhmo/work/myprojects/emacs-sandbox/my/

.emacs.dを直接作成せずシンボリックリンクにしています。 そしてemacs-sandboxのディレクトリには、例えば以下のようなディレクトリを置いておきます。

pwd: /Users/podhmo/work/myprojects/emacs-sandbox
- my                 ;; いつも使っている.emacs.d (stable)
- experiment         ;; 移行用の.emacs.d.壊れてるかもしれない(trunk)
- bigfont            ;; 文字大きい(プレゼン用?)
- simple-python      ;; python用のとてもシンプルな環境(他の人に見せるときなど)

こうして造ったディレクトリに、.emacs.dのシンボリックリンクを張り替えることで、その時々で利用したい環境のemacsを立ち上げることができます。 (macを使い始めた時に、微妙な環境の差異をifdef的に吸収するのが面倒だったのでmacというディレクトリを造ったりしました)

emacs -q -l を使ってload

いちいち.emacs.dを張り替えることもめんどくさくなる時がありますね。そういう時は"-q -l”を使って直接立ち上げます。 -q は --no-init-fileの略でデフォルトの.emacs.d以下のファイルを読み込まないようにします。 -l は --loadの略でこちらは指定したファイルを起動時に読み込みます。

幾つかの便利関数

あとは、普通にemacsの設定を書いていくだけですが。幾つか自作した関数を冒頭に置いておく事が多いです。 例えば、以下のような関数を定義しています。

(defun current-directory ()
  (if load-file-name
      (file-name-directory load-file-name)
    default-directory))

(defun define-many-keys (key-map key-table)
  (loop for (key . cmd) in key-table
        do (define-key key-map (read-kbd-macro key) cmd)))

;; 注: 後で出てくるpackage+.elという自作のpackageに依存
(defmacro* require-and-fetch-if-not (package &key (filename nil) (noerror t) (installed-package nil) (url nil))
  (let ((pname (gensym)))
    `(or (require ,package ,filename t)
         (let ((,pname (or ,installed-package ,package))
               (my:package-install-url ,url))
           (package-install ,pname)
           (require ,package ,filename t)))))

current-directory

これは、読み込み時と起動後で現在位置のディレクトリの情報を取得方法の違いを吸収する関数です。 例えば、package.elの設定などに使ったりします。

;;; package.elが必要
(named-progn package-management
  (require 'package)
  (setq package-user-dir (concat (current-directory) "3rdparty"))
  (load "package+")
  
  (named-progn marmalade
    (add-to-list 'package-archives '("marmalade" . "http://marmalade-repo.org/packages/"))
    (add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/"))
    ;; get available packages
    (package-initialize)
    (unless package-archive-contents
      (package-refresh-contents))
    ;; (package-list-packages)
    )) 

あ、named-prognはただのコメントが書けるprognです。

(defmacro named-progn (name &rest body)
  (declare (indent 1))
  `(progn ,@body))

define-many-keys

これはキーバインド登録するのに便利な関数です。たしかemacsテクニックバイブルにも載っていたような気がします。

(define-many-keys global-map
  '(("<hiragana-katakana>" . anything)
    ("C-c C-;" . anything-occur*)
    ("C-c C-:" . anything-vcs-project)
    ("M-x" . anything-M-x)
    ("C-x b" . anything-buffers+)
    ("M-y" . anything-show-kill-ring)
    ("C-j C-j" . anything-bm-list*)
    ("C-j j" . bm-toggle)
    ))))
    )

require-and-fetch-if-not

これは、結構キモいかもしれないです。requireを試してみて存在しなかったらネットにアクセスして取りにいくという関数です。 デフォルトではmelpaやmarmaladeから探します。urlを直接指定することで直接ファイルを取ってくる事もできます(ディレクトリには対応していません)

一時期、3rdpartyのライブラリは自分で管理するリポジトリに追加しないみたいなポリシーを持っていた時に造りました。

;; melpaなどから
(require-and-fetch-if-not 'anything)

;; ファイルを直接
(named-progn keyboad-settings
  (named-progn key-chord
    (require-and-fetch-if-not 'key-chord :url "http://www.emacswiki.org/emacs/download/key-chord.el")
    (setq key-chord-two-keys-delay 0.02)
    (key-chord-mode 1)

以下のようなpackage+.elに require-and-fetch-if-not 依存している関数が定義されてます。

;; package+.el

;;; from url
;; (let ((my:package-install-url "https://....<package-name>.el"))
;;   (package-install '<package-name>))
;;; from package list
;; (package-install '<package-name>

(require 'package)

;; dont-use, directly
(defvar my:package-install-url nil)
(defvar my:local-package-list nil)

;; custom variable 
(defvar my:local-package-sync-p t)

;; utility
(defmacro* my:package--with-work-buffer (url &rest body)
  "inspired by `package--with-work-buffer'"
  (declare (indent 1))
  (let ((buf (gensym)))
    `(let ((,buf  (url-retrieve-synchronously ,url)))
       (with-current-buffer ,buf
         (progn 
           (package-handle-response)
           (re-search-forward "^$" nil 'move)
           (forward-char)
           (delete-region (point-min) (point)))
         ,@body)
       (kill-buffer ,buf))))

(defun my:package--find-version ()
  (save-excursion
    (progn (goto-char (point-min))
           (re-search-forward "Version: *" nil t 1)
           (or (thing-at-point 'symbol) "0.0"))))

;; functions
(defun my:package-install-from-url (url name &optional version description requires)
  (let ((version version))
    (my:package--with-work-buffer url
      (unless version
        (setq version (my:package--find-version)))
      (package-unpack-single name version (or description "no description")
                             requires))
    ;; Try to activate it.
    (add-to-list 'load-path (package--dir name version))
    (when my:local-package-sync-p
      (add-to-list 'my:local-package-list (list name version))
      (my:local-package-store-save))))

;; local package
(defun my:local-package-store--create (fname &optional forcep)
  (when (or (not (file-exists-p fname)) forcep)
    (with-current-buffer (find-file-noselect fname)
      (insert "nil")
      (save-buffer))))
  
(defun my:local-package-store-fname ()
  (concat package-user-dir "/.local-package.list"))

(defun my:local-package-store-load ()
  (let ((fname (my:local-package-store-fname)))
    ;; if not found. create store file
    (my:local-package-store--create fname)
    (let ((buf (find-file-noselect fname)))
      (prog1 (read (with-current-buffer buf (buffer-string)))
        (kill-buffer buf)))))

(defun my:local-package-store-save ()
  (let ((fname (my:local-package-store-fname)))
    (with-current-buffer (find-file-noselect fname)
      (erase-buffer)
      (insert (prin1-to-string my:local-package-list))
      (save-buffer)
      (kill-buffer))))

(defun my:local-package-initialize ()
  (loop for (name version) in (my:local-package-store-load)
        do (add-to-list 'load-path (package--dir name version))))

;; advices
(defadvice package-install (around from-url-dispatch last (name) activate)
  (cond (my:package-install-url
         (let ((name (if (symbolp name) (symbol-name name) name)))
           (my:package-install-from-url my:package-install-url name)))
        (t ad-do-it)))

(defadvice package-initialize (after local-package-initialize activate)
  (my:local-package-initialize))

まとめ

  1. emacs界隈をwatchしていない。次第に浦島状態に
  2. 流行に追いつこうとするも、一気に取り入れようとして挫折したことがあった
  3. 使い心地を試しながら少しずつ取り入れていきたい(利用したいケースによって環境を分けたいときあったりする) .emacs.dはシンボリックリンク
  4. 幾つか便利関数を定義してたり

本当はもう少し作業をしていて

  • emacsの環境を造るscaffold的な機能(project template的な)を造っていたり
  • start-processでミニマムな設定の子emacs的なものを起動して、そこで直行した機能の設定を作成してみようとか
    • 複数の環境をanything(helm)的なUIで選択したいなーとか
  • 直行した機能を併合して新しい環境を造りたい
    • 依存関係などを考慮したいし。作成した環境には直接含めないような個人的な設定は別に持っておきたいとか
  • 設定をplugin化したい

などと考えたりしてたのですが作業が間に合いませんでした。

ちなみに設定をplugin化したいというのは以下のようなイメージ

(defun python-mode-setup ()
  (python-plugin:quick-run)
  (python-plugin:virtual-env)
  (python-plugin:ffap-import)
  (python-plugin:auto-complete)
  (python-plugin:scaffold)
  (python-plugin:anything-help))

(add-hook 'python-mode-map 'python-mode-setup)
;;各pluginに依存関係あるのでその定義しないとまずい。

これのためにこんなものをつくったりはしてた。

http://pod.hatenablog.com/entry/2012/10/19/225820