Vimライクな jump/marker を実現する jumar.el の紹介

# 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` より強力.

### スクリーンショット

![jLoUv7oeW0.gif](https://qiita-image-store.s3.amazonaws.com/0/33149/3f3f1e30-da39-3544-89e4-619800d227b5.gif)

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 でなり)直接ダウンロードしてパスを通します.

```shell
$ cd /path/to
$ git clone https://github.com/kenoss/erfi
$ git clone https://github.com/kenoss/jumar
```

```emacs-lisp
(add-to-list 'load-path "/path/to/erfi/lisp")
(add-to-list 'load-path "/path/to/jumar")
```

## 試しに使ってみる

以下を評価するか `.emacs` に書いて Emacs を再起動します.

```emacs-lisp
(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 する方がいいんかな? こっちは需要ないとやらない.
- 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 の紹介でした.