jumar.el の紹介
これは何?
この記事は Emacs Advent Calendar 2014 (http://qiita.com/advent-calendar/2014/emacs) の5日目の記事
です.
「今年作ったものについて書く予定です」と書いていたのですが, 丁度作っていたものが思ったよりも
ぎりぎりになったので遅くなってしまいました. 日付が変わるまでに投稿できていればいいのですが.
(スクリーンショットなどは撮っている暇がないので多分追記するでしょう.)
4日目は kaz-yos さんによる ESS+R使いがClojureで遊びたくて作ったeval-in-repl.el (C-RETでREPL評価)
の紹介 でした. (http://qiita.com/kaz-yos/items/3cf663d67f69d3cc2fc2)
6日目はmhayashi1120 さんの予定です.
jumar.el
Jumar (じゅまーる, と発音するらしい) は Vim の marker と jump をエミュレートすることを目的として開発
された Emacs 拡張です. 普通に前後にジャンプすることに加え, Vim の :jumps
の代替として Helm
を使ってマーカーを表示し, インタラクティブにマーカーを覗く, ジャンプする, 削除するなどが可能になって
います.
大体終わりましたが, まだ開発段階です.
GitHub で公開しています: https://github.com/kenoss/jumar
三行で説明
- 新しいマーカーオブジェクト
jumar:jumarker
は自動で管理され kill したバッファに対しても有効に. - マークリングに代わって
jumar:tree
を使い, デフォルトでDWINにマーカーのツリーとリストを操作できる. - (オプショナルで) Helm でマーカーの集合を可視化できる.
:jumps
より強力.
スクリーンショット
GIF: http://g.recordit.co/jLoUv7oeW0.gif
ビデオ版: http://recordit.co/jLoUv7oeW0
インストール
Jumar (https://github.com/kenoss/jumar) は以下を必要とします:
- Emacs 24 (またはそれ以降のversion)
- ERFI (https://github.com/kenoss/erfi)
まだ MELPA には登録されていません. 数日中に申請する予定です.
使うには ERFI と jumar を(git clone なり zip でなり)直接ダウンロードしてパスを通します.
$ cd /path/to
$ git clone https://github.com/kenoss/erfi
$ git clone https://github.com/kenoss/jumar
(add-to-list 'load-path "/path/to/erfi/lisp")
(add-to-list 'load-path "/path/to/jumar")
試しに使ってみる
以下を評価するか .emacs
に書いて Emacs を再起動します.
(require 'jumar)
(require 'jumar-dwin)
(require 'helm) ; if you use visualizer.
;; If one needs highlight the line after jump.
(require 'erfi-emacs)
(add-hook 'jumar-post-jump-hook 'erfi-emacs-hl-turn-on-until-next-command)
;; Initialization
(jumar-dwin-use-preconfigured-scheme 'list+history)
(jumar-init)
;; As you like.
(define-key global-map (kbd "C-'") 'jumar-dwin-add-marker)
(define-key global-map (kbd "C-\"") 'jumar-dwin-jump-current)
(define-key global-map (kbd "C-,") 'jumar-dwin-jump-backward)
(define-key global-map (kbd "C-.") 'jumar-dwin-jump-forward)
(define-key global-map (kbd "C-x C-'") 'helm-jumar-dwin-jumarkers)
;; As you need. Advise jump commands, like `find-tag' and `gtags-find-tag',
;; to add jumarker before/after jump.
(jumar-dwin-advise-jump-command-to-add-jumarker 'find-tag)
; (jumar-dwin-advise-jump-command-to-add-jumarker 'elisp-slime-nav-find-elisp-thing-at-point)
試しに以下を評価してみましょう. 10個のマーカーを登録します.
(save-excursion
(goto-char (window-start))
(dotimes (i 10)
(call-interactively 'jumar-dwin-add-marker)
(forward-line 2)))
ここで C-x C-'
(helm-jumar-dwin-jumarkers
) すると登録されたマーカーが Helm で表示されます.
上下移動して Enter を押すとそのマーカーにジャンプします. C-z
(helm-execute-persistent-action
)
で Helm を保ったままそのマーカーを覗きます. ジャンプすると >
マークが移動していることに気づく
でしょう. これはカレントなマーカーを表す印です. (取り敢えず上の "Jumarkers in list" だけに
注目しておきましょう.)
Helm を使わずにジャンプすることもできます. C-.
(jumar-dwin-jump-forward
) で >
のついた一つ
後ろのマーカーへ. C-,
(jumar-dwin-jump-backward
) で一つ前へ. C-'
(jumar-dwin-add-marker
)
で新しいマーカーを登録できます. ジャンプは異なるバッファにマーカーがある場合, バッファを
切り替えます.
Helm の各行の情報は左から, カレントを表すマーク, ブランチの情報, マーカーのある行, 列, バッファ,
その位置のバッファの内容が表示されます.
Jumar はマーカーが存在するバッファが生きているかどうかを自動で判別してうまいことやりくりします.
他のどうでもいいバッファに切り替えてマーカーを登録してから kill-buffer
してみましょう.
この状態で Helm を見るとカレントマーカーの部分に Killed buffer
と表示されているはずです.
これにジャンプしようとするとバッファを開き直し, そのバッファの全てのマーカーを復元しようとします.
(復元は Helm を通じて開き直したときだけではなく他のコマンドによって開き直されたときにもなされます.)
デフォルトでは jumar-dwin-jump-forward
や jumar-dwin-jump-backward
は先頭と最後の unavailable
なマーカーはスキップして次の有効なマーカーにジャンプします.
Jumar はデフォルトで二つのマーカーの集合を使用します. ツリーとリストです. (どちらも構造としては
jumar:tree
ですが.) DWINコマンド群はこれらをどのように使うかを jumar-dwin-action-control-alist
変数によって判断します. この変数によって挙動を逐一変えることもできますが, 通常は
jumar-dwin-use-preconfigured-scheme
で用意された設定を使えば十分でしょう. 用意されている用法は
以下の通りです.
引数 | 説明 |
---|---|
'list-main |
リストを主に使い, ツリーは補助. |
'tree-main |
ツリーを主に使い, リストは補助. |
'list-only |
リストのみ使う. |
'tree-only |
ツリーのみ使う. |
'list+history |
'list-main と似ているが, ツリーを全履歴保持のためのリストとして使う. |
例えば 'list+history
の場合, jumar-dwin-add-marker
はツリーとリストの両方に追加,
jumar-dwin-jump-forward
などは常にリストの方を参照します. ジャンプ系コマンドは C-u
付きで
呼び出すと使っていない方の集合を参照します. (普段リストを参照していればツリーを参照する.)
コマンドまとめ(上の設定で):
キー | コマンド | 説明 |
---|---|---|
C-' |
jumar-dwin-add-marker |
マーカーを追加する |
C-" |
jumar-dwin-jump-current |
カレントマーカーへジャンプ |
C-, |
jumar-dwin-jump-backward |
カレントマーカーの一つ前へジャンプ |
C-. |
jumar-dwin-jump-forward |
カレントマーカーの一つ後ろへジャンプ |
C-x C-' |
helm-jumar-dwin-jumarkers |
Helm で表示 |
Helm で有効なコマンド:
キー | コマンド |
C-i (helm-select-action )したときの説明 |
説明 |
---|---|---|---|
Enter |
helm-jumar-jump/set-current |
Jump to marker | 選択されたマーカーをカレントにしてジャンプ |
C-z |
helm-jumar-persistent-action |
現在行にあるマーカーを覗く | |
C-u C-z |
helm-jumar-persistent-action |
現在行にあるマーカーを削除 | |
C-c d |
helm-jumar-delete-nodes-below |
Delete markers below | 現在行から下にあるマーカーを削除 |
C-c k |
helm-jumar-delete-marked-nodes |
Delete marker(s) | マークされたマーカーを削除 |
M-f |
helm-jumar-forward-branch |
Forward branch | ブランチの切り替え(後) |
M-b |
helm-jumar-backward-branch |
Backward branch | ブランチの切り替え(前) |
C-r |
helm-jumar-run-recenter |
選択されたノードを中心にして再表示 |
helm-jumar-forward-branch
と helm-jumar-backward-branch
はブランチを切り替えるためのコマンド
です. ツリーは Helm で表示する際, 選択された子を辿ったパンくずリストの形にされています.
行数の左側が |
でないもの(-+
とか -+-
とか)はブランチを複数持っています.
('tree-main
でカレントマーカーが最後以外のときに jumar-dwin-add-marker
するか,
jumar-add-marker
や jumar-add-marker*
を使うとブランチが出現します.)
pop-tag-mark
などの置き換え
上記の様に (jumar-dwin-advise-jump-command-to-add-jumarker 'find-tag)
とすれば find-tag
したときにマーカーを記録できます. (連続した tag jump のときは最小限の記録をします.)
M-*
(pop-tag-mark
) の代りに使えるようになります. pop-tag-mark
とは違い一度元の場所に戻った
後でもジャンプした先とを行き来できます.
(どうでもよさそうなコメント: pop-tag-mark
はマークリングを使っているのではく,
マーカーのリングを使っていて ring-remove
するのでマーカーは本当に pop される.)
作った経緯や内部の詳細など, という体の与太話
作ろうと思った経緯と既存の解法
従来の Emacs の jump/marker 機能は貧弱でした. 使われている構造はマークリングで,
push と pop しかできず, バッファを横断するマーカーは一応あれどもマーカーが差すバッファが存在しない
場合はエラーを吐くという代物でした. Vim ですら(という言い方は Vim に対して失礼ですが)
:jumps
によってマーカーのリストの中身の情報を見ることができ, バッファが閉じられている場合は
自動で開き直してくれるのに, です. ソースコードを読むのにタグジャンプは必須ですが,
pop-tag-mark
を僕は上手く扱えませんでした. また, タグジャンプ以外にも「このファイルのあの辺りも
弄らないといけないけどここに戻ってきたい」という需要がありました. この用途には Emacs 側としては
「C-@
(set-mark-command
) と C-x C-x
(exchange-point-and-mark
) 使えばいい」という意見を
ググった結果から感じとりましたが, transient-mark-mode
の下では C-@
一発ではなく C-@ C-@
を使わねばならず, なおかつこのマークが他のコマンド群(例えば kill-line
)によって頻繁に変更されるため
信用ならないという欠点がありました.
(これらのコマンド群にはハードコードされていてオフにすることができない.)
(まぁ mark-ring やそれを暗黙のうちに変更する基本的なコマンド群は simple.el
で定義されている
わけだからしゃーないっちゃしゃーないのだが何故もちっとリッチなマーカーを誰も作ろうとしなかったのか.)
(いや mark-ring を cycle していくコマンドを定義すれば C-x C-x
でアクセスできないという状況は
回避できるけどさ, おれはマーカーの集合を弄るタイミングを自分で制御したいんじゃ.)
(因みに単方向限定ではあるけど set-mark-command
を C-u
付きで呼び出せば cycle していける.
set-mark-command-repeat-pop
を t
にすればなお良し.)
この問題に対する既存の解法をまとめておきます:
- デフォルトの(バッファローカル, グローバル)マークリングを使う.
もしくは marker とring.el
などを組み合せて適当なコマンドを作る.
(因みに Emacs の mark と marker は完全に別物: http://www.fan.gr.jp/~ring/doc/elisp_20/elisp_31.html) - Evil の
C-o
/C-i
. 現状ではバッファローカルのみ.
https://github.com/bling/evil-jumper を取り込むという話が上っている. (Persistency 有り.)
(https://bitbucket.org/lyro/evil/issue/42/c-o-across-buffers-after-c-broken)
ただし, 見た感じでは Vim の per-window jump-list をエミュレートしようとしていてオプショナルに
していないおかげでelscreen.el
などの window をまるっと挿げ替える系の拡張の存在下では
上手く動かなそう. (試してないけど. elscreen 側で hash-table をまるっと入れ替えればなんとかなりそう
ではあるけど,window-configuration-change-hook
が実行されるタイミングとかちゃんと見て
window-configuration-change-hook
からフックが呼び出される順番をしっかり保証しないとだめなんじゃ
なかろうか.)
(そういえば window-local variable っぽいものが同様に hash-table を使えば作れるという話を小耳に
挟んだけど同じ問題があると思う. Emacs が window 毎にカーソルの色変えられないのもこういう問題がある
のかな.) - Icicles が buffer-local/global mark ring へのアクセサを提供しているようだ.
http://stackoverflow.com/questions/3393834/how-to-move-forward-and-backward-in-emacs-mark-ring
Icicles を使っていないので使用感はわからない. ただset-mark-command
とは違い双方向に飛べるようだ.
marker を使うわけではないけど移動系コマンドは他にも色々ある. 大体は buffer-local だけど:
-
goto-chg.el
過去に変更した箇所に飛ぶ. undo/redo しない undo/redu みたいな感じ. -
point-undo.el
ポイントの移動を全て記録しておいて辿れるようにしてある.
(どうでもよさそうなコメント: point-undo.el
, 記録しているポイントのリストに長さの制限がなくて
移動する毎にリストが伸びていくので長時間作業してるとメモリ食いそう. 実際はどうなんだろ.)
実装するに当たって気を配った点と内部の詳細
- buffer が kill されたときにも有効な marker object を作る.
jumar:jumarker
がそれに当たります. これは別に何てことはなくて,kill-buffer-hook
,
before-revert-hook
,find-file-hook
でバッファの生存状態を監視してバッファが生きているときは
ただの Emacs の marker object の wrapper, 死んでいるときはバッファのファイル名やポイントを保持
するだけです. それらのデータから元の marker を正確に「復元」することは kill する前の
バッファの内容と開き直したファイルの内容が一致する場合に限られるので, オプション変数
jumar-revive-marker-function
でより良い復元をできるように余地を残しています. 例えば,
少しファイルの内容が変更されただけであれば, 元の marker 付近のテキストと一致する場所を,
開き直したファイルの元々 marker があったポイント付近で探せばいいでしょう.
ただこういう smart な復元は(多分) Vim ではなされていない気がしますし, 自分に必要ないので
単純な復元しかデフォルトでは提供していません. - jumarker のための集合.
jumar:tree
です. わぁい Parametric polymorphism, あかり Parametric polymorphism 大好き.
参考にしたのはundo-tree.el
です. 可視化をちゃんと用意してやれば履歴にツリー使えるじゃん, と.
元々はツリーしか提供する予定はなく, ツリーをブランチなしで使うことによって Vim のエミュレーション
を提供する予定でした. 結局, 使ってるうちにツリーは普通のユーザーにとって難しすぎるのではと思い
元々の Vim のリストに戻ってツリーを補助として使う用に. ただ DWIN の scheme に'list+history
を用意しちゃったのでもうツリーじゃなくていいんじゃないかな...
('list+history
でも難しい人は'list-only
を使おう!)
因みに後述するけど per-window/per-screen jump-list を実装することを意識している.
あと unit test のためという名目で content は jumarker でなくてもいいようになっている. - ツリービューワとしての Helm の利用
元々 visualizer を自前で書いていたのだけれど, 機能を足していくうちに Helm に近付いていく
気がしたのでもう Helm でいいじゃん, と. ただ Helm は「候補を出して選択させる」インターフェース
であって, 常に表示しておく(e2wm.el
のガジェットみたいな)用途には全く使えないのでもしかしたら
復活するやもしらん. 常に表示しておく用途が現状では想像できないけど.
あとこれを使ってundo-tree.el
の visualizer を Helm に乗せることも可能だと思う.
(helm-jumar:make-cadidates
の部分をごにょればいい気がするけど, そこまでundo-tree.el
の
visualizer に不満はないので結局書かないパターン.)
1800行近くあっていまだに開発版かよ
- buffer-local markers や smart filtering
- buffer-local なマーカーだけをワンキーで filtering して 登録順/位置順 をトグルできるようにする.
Helm を介さないときにも「位置順で次のマーカー」とか...は DWIN 考えるのめんどくさそうだ. - 現状では Helm による filtering はツリーの場合 breadcrumb list 限定なので
検索かけるときだけツリー全体をリストに flatten する方がいいんかな? こっちは需要ないとやらない.
- buffer-local なマーカーだけをワンキーで filtering して 登録順/位置順 をトグルできるようにする.
- Vim みたいな persistency と register
需要待ち. 個人的には Vim みたいな使い方で Emacs を使う場合は emacsclient 使うだろうから特に
何もしなくても persistent だし, 特定のファイルやその中の特定の位置に素早くアクセスしたいなら
bookmark.el
とかbm.el
とか使った方がいいんじゃないかなーと思う. Helm interface あるし. - Evil 向けのコマンド
そのうち書く. - per-window/per-screen jump-list
どうやら Vim は window 毎に jump-list を保持しているらしい. (上の Evil のリンクを参照.)
(vimmer は未来に生きてるな.) Elscreen の screen 毎に jump-list を保持することは可能で,
それがやりやすい作りにしたつもりではある. ただ persistency との兼ね合いがあるので
どっちも実装する場合はリストの方だけ挿げ替えるようにしてツリーの方だけ persistent,
とかにすると思う.
per-window は色々問題があるからあんまり考えたくない.- 上にも書いたように window-local な変数なんて幻想なんや.
- Elscreen との統合がきつい. 絶対想定外のケースが発生する.
- そもそも現時点では per-window な jump-list の需要がよくわからん. Elscreen ではいかんのか?
terminal で emacsclient で使ってるという場合に Elscreen 使うのが(例えば使えるキーが少ないから)
めんどい, というのはあるのかも. でもその場合は tmux なり screen なり使って window 切って
emacsclient すればいいのでは. (server.el
の内部構造知らんけど)対応する側としては
per-window よりは楽そう.
まだあった気もする.
要望やバグ報告は Twitter @keno_ss か GitHub の issue か,
Lingr の Emacs 部屋にいるときに言ってもらえれば.
蛇足: Helm の (DISPLAY . REAL) pairs 問題
現状では, ツリーもリストも内部的には jumar:tree
で, マーカーの削除やブランチの変更を行なうため
Helm の candidate list には (DISPLAY . REAL) の組を, REAL に jumar:node
を入れて使っている.
Helm にはこの用な REAL の使用はドキュメントされておらず, 名前からして使えるはずと思っていた
のだけれど一箇所だけ問題があった. 複数のマーカーを削除しようとしたときにエラーが起こる.
問題は REAL を string=
で比較している一箇所だけ(helm-jumar:with-temporary-patch
でごにょってた
箇所).
Helm の最新版では equal
を使うようになっている. (https://github.com/emacs-helm/helm/issues/706)
どうなるかなと思ったけどパッチ取り込まれて良かった.
そういうわけで, 複数削除を使いたい人は Helm の最新版を使ってください.
(doubly-linked tree のノードの比較だから無限ループになっちゃうと思ったけど Emacs の eqaul
はまず
eq
するのね. docstring には書いてないけど GNU Emacs Lisp Reference Manual には載ってる.
http://www.gnu.org/software/emacs/manual/html_node/elisp/Equality-Predicates.html)
終わりに
走り書きになりましたが, 新しい jump/marker 環境を提案する Jumar の紹介でした.