EmacsにVimの1文字移動コマンドの実装してみる(Emacsトラノマキ版にundo機能などを追加)

Vimにはf{char}という便利なコマンドがあるとの事で、fの後に任意の文字を打つとそこまでカーソルが移動する。[Enter]キーを押す必要も無いので確かに便利。いわゆる一文字検索。これをEmacsでより便利に実装してみるという話。
目次
元ネタ
元ネタはSoftWare Design誌掲載の下記記事。
Vimの動作確認
f{char} | 指定文字までカーソルを前方移動 |
F{char} | 指定文字までカーソルを後方移動 |
; | 一文字検索を繰り返し (検索のリピート) |
, | 一文字検索を逆に繰り返し (検索のundo) |
※ 実践Vimによるとt{char},T{char}もある。
なお実際に試してみるとこの動きは行をまたげない模様。なので現在の行でのカーソル移動である。この辺は設定で回避できるのかもしれないけれど調べていない。
Emacsで実装してみる
現在行だけの縛りは無くして、現在のバッファ上を自由に動けるようにする。
これについては元ネタで既に実装済。
リピート系の実装
実践Vimによるといきすぎる事がよくあるとの事なので、undo的な動作も実装したい。
f{char}コマンドと;コマンドはとても強力な組み合わせであり(snip)。ただし、カーソルがいきつく先がどこかがハッキリしない。結果、;キーをとにかく連打してしまいやすい。そして、いきすぎてしまう。
キーの省略
例えばキーバインドは何でも構わないけれど前方一文字検索に "H-f"、後方一文字検索に "H-b"、一文字検索リピートに "H-]"、一文字検索undoに "H-[" と割り当てると4つのキーバインドの定義が必要。これは勿体無いしやや面倒。
そこで前方一文字検索に "H-f"、後方一文字検索に "H-b"。加えて一文字検索リピートにも "H-f"、一文字検索undoにも "H-b" という形で検索とリピートのキーを同一にしたい。こうすれば2つのキーバインドで済むし、指の移動も少なくなりそう。
実際の実装
というわけで元ネタをベースにこんな感じで実装してみました。検索とリピートは同じキーで利用可能です。
<span style="color: #7C7C7C;">;; </span><span style="color: #7C7C7C;">------------------------------------------------------------------------ </span><span style="color: #7C7C7C;">;; </span><span style="color: #7C7C7C;">一文字検索の設定 </span><span style="color: #7C7C7C;">;; </span><span style="color: #7C7C7C;">http://dev.ariel-networks.com/wp/documents/aritcles/emacs/part16 </span><span style="color: #7C7C7C;">;; </span><span style="color: #7C7C7C;">------------------------------------------------------------------------ </span><span style="color: #8c8c8c;">(</span><span style="color: #96CBFE;">defvar</span> <span style="color: #c5c8c6; background-color: #1d1f21;">last-search-char</span> nil<span style="color: #8c8c8c;">)</span> <span style="color: #8c8c8c;">(</span><span style="color: #96CBFE;">setq</span> search-clear-idle-time 1<span style="color: #8c8c8c;">)</span> <span style="color: #7C7C7C;">;; </span><span style="color: #7C7C7C;">update: </span><span style="color: #7C7C7C;">;; </span><span style="color: #7C7C7C;">20171208: ignore-case対応 </span><span style="color: #7C7C7C;">;; </span><span style="color: #7C7C7C;">search-forward/backwardとrepeatを統合 </span><span style="color: #7C7C7C;">;; </span><span style="color: #7C7C7C;">アイドルタイマーを追加 </span><span style="color: #8c8c8c;">(</span><span style="color: #96CBFE;">defun</span> <span style="color: #FFD2A7;">search-forward-repeat-with-char</span> <span style="color: #93a8c6;">(</span>arg<span style="color: #93a8c6;">)</span> <span style="color: #93a8c6;">(</span><span style="color: #96CBFE;">interactive</span> <span style="color: #8AE234;">"p"</span><span style="color: #93a8c6;">)</span> <span style="color: #93a8c6;">(</span><span style="color: #96CBFE;">cl-case</span> arg <span style="color: #b0b1a3;">(</span>4 <span style="color: #97b098;">(</span><span style="color: #96CBFE;">let</span> <span style="color: #aebed8;">(</span><span style="color: #b0b0b3;">(</span>char <span style="color: #90a890;">(</span>read-key <span style="color: #8AE234;">"Move to Char: "</span><span style="color: #90a890;">)</span><span style="color: #b0b0b3;">)</span><span style="color: #aebed8;">)</span> <span style="color: #aebed8;">(</span><span style="color: #96CBFE;">when</span> <span style="color: #b0b0b3;">(</span>char-equal <span style="color: #90a890;">(</span>char-after <span style="color: #a2b6da;">(</span>point<span style="color: #a2b6da;">)</span><span style="color: #90a890;">)</span> char<span style="color: #b0b0b3;">)</span> <span style="color: #b0b0b3;">(</span>forward-char<span style="color: #b0b0b3;">)</span><span style="color: #aebed8;">)</span> <span style="color: #aebed8;">(</span>search-forward <span style="color: #b0b0b3;">(</span>string char<span style="color: #b0b0b3;">)</span> nil t<span style="color: #aebed8;">)</span> <span style="color: #aebed8;">(</span>backward-char<span style="color: #aebed8;">)</span> <span style="color: #aebed8;">(</span><span style="color: #96CBFE;">setq</span> last-search-char char<span style="color: #aebed8;">)</span><span style="color: #97b098;">)</span><span style="color: #b0b1a3;">)</span> <span style="color: #b0b1a3;">(</span>1 <span style="color: #97b098;">(</span><span style="color: #96CBFE;">if</span> last-search-char <span style="color: #aebed8;">(</span><span style="color: #96CBFE;">progn</span> <span style="color: #b0b0b3;">(</span><span style="color: #96CBFE;">when</span> <span style="color: #90a890;">(</span>char-equal <span style="color: #a2b6da;">(</span>char-after <span style="color: #9cb6ad;">(</span>point<span style="color: #9cb6ad;">)</span><span style="color: #a2b6da;">)</span> last-search-char<span style="color: #90a890;">)</span> <span style="color: #90a890;">(</span>forward-char<span style="color: #90a890;">)</span><span style="color: #b0b0b3;">)</span> <span style="color: #b0b0b3;">(</span>search-forward <span style="color: #90a890;">(</span>string last-search-char<span style="color: #90a890;">)</span> nil t<span style="color: #b0b0b3;">)</span> <span style="color: #b0b0b3;">(</span>backward-char<span style="color: #b0b0b3;">)</span><span style="color: #aebed8;">)</span> <span style="color: #aebed8;">(</span><span style="color: #96CBFE;">progn</span> <span style="color: #b0b0b3;">(</span><span style="color: #96CBFE;">let</span> <span style="color: #90a890;">(</span><span style="color: #a2b6da;">(</span>char <span style="color: #9cb6ad;">(</span>read-key <span style="color: #8AE234;">"Move to Char: "</span><span style="color: #9cb6ad;">)</span><span style="color: #a2b6da;">)</span><span style="color: #90a890;">)</span> <span style="color: #90a890;">(</span><span style="color: #96CBFE;">if</span> <span style="color: #a2b6da;">(</span>char-equal <span style="color: #9cb6ad;">(</span>char-after <span style="color: #8c8c8c;">(</span>point<span style="color: #8c8c8c;">)</span><span style="color: #9cb6ad;">)</span> char<span style="color: #a2b6da;">)</span> <span style="color: #a2b6da;">(</span>forward-char<span style="color: #a2b6da;">)</span><span style="color: #90a890;">)</span> <span style="color: #90a890;">(</span>search-forward <span style="color: #a2b6da;">(</span>string char<span style="color: #a2b6da;">)</span> nil t<span style="color: #90a890;">)</span> <span style="color: #90a890;">(</span>backward-char<span style="color: #90a890;">)</span> <span style="color: #90a890;">(</span><span style="color: #96CBFE;">setq</span> last-search-char char<span style="color: #90a890;">)</span><span style="color: #b0b0b3;">)</span><span style="color: #aebed8;">)</span><span style="color: #97b098;">)</span><span style="color: #b0b1a3;">)</span><span style="color: #93a8c6;">)</span> <span style="color: #93a8c6;">(</span>run-with-idle-timer search-clear-idle-time nil '<span style="color: #b0b1a3;">(</span>lambda <span style="color: #97b098;">()</span> <span style="color: #97b098;">(</span><span style="color: #96CBFE;">setq</span> last-search-char nil<span style="color: #97b098;">)</span><span style="color: #b0b1a3;">)</span><span style="color: #93a8c6;">)</span> <span style="color: #8c8c8c;">)</span> <span style="color: #8c8c8c;">(</span><span style="color: #96CBFE;">defun</span> <span style="color: #FFD2A7;">search-backward-repeat-with-char</span> <span style="color: #93a8c6;">(</span>arg<span style="color: #93a8c6;">)</span> <span style="color: #93a8c6;">(</span><span style="color: #96CBFE;">interactive</span> <span style="color: #8AE234;">"p"</span><span style="color: #93a8c6;">)</span> <span style="color: #93a8c6;">(</span><span style="color: #96CBFE;">cl-case</span> arg <span style="color: #b0b1a3;">(</span>4 <span style="color: #97b098;">(</span><span style="color: #96CBFE;">let</span> <span style="color: #aebed8;">(</span><span style="color: #b0b0b3;">(</span>char <span style="color: #90a890;">(</span>read-key <span style="color: #8AE234;">"Move backward to Char: "</span><span style="color: #90a890;">)</span><span style="color: #b0b0b3;">)</span><span style="color: #aebed8;">)</span> <span style="color: #aebed8;">(</span>search-backward <span style="color: #b0b0b3;">(</span>string char<span style="color: #b0b0b3;">)</span> nil t<span style="color: #aebed8;">)</span> <span style="color: #aebed8;">(</span><span style="color: #96CBFE;">setq</span> last-search-char char<span style="color: #aebed8;">)</span><span style="color: #97b098;">)</span><span style="color: #b0b1a3;">)</span> <span style="color: #b0b1a3;">(</span>1 <span style="color: #97b098;">(</span><span style="color: #96CBFE;">if</span> last-search-char <span style="color: #aebed8;">(</span>search-backward <span style="color: #b0b0b3;">(</span>string last-search-char<span style="color: #b0b0b3;">)</span> nil t<span style="color: #aebed8;">)</span> <span style="color: #aebed8;">(</span><span style="color: #96CBFE;">progn</span> <span style="color: #b0b0b3;">(</span><span style="color: #96CBFE;">let</span> <span style="color: #90a890;">(</span><span style="color: #a2b6da;">(</span>char <span style="color: #9cb6ad;">(</span>read-key <span style="color: #8AE234;">"Move backward to Char: "</span><span style="color: #9cb6ad;">)</span><span style="color: #a2b6da;">)</span><span style="color: #90a890;">)</span> <span style="color: #90a890;">(</span>search-backward <span style="color: #a2b6da;">(</span>string char<span style="color: #a2b6da;">)</span> nil t<span style="color: #90a890;">)</span> <span style="color: #90a890;">(</span><span style="color: #96CBFE;">setq</span> last-search-char char<span style="color: #90a890;">)</span><span style="color: #b0b0b3;">)</span><span style="color: #aebed8;">)</span><span style="color: #97b098;">)</span><span style="color: #b0b1a3;">)</span><span style="color: #93a8c6;">)</span> <span style="color: #93a8c6;">(</span>run-with-idle-timer search-clear-idle-time nil '<span style="color: #b0b1a3;">(</span>lambda <span style="color: #97b098;">()</span> <span style="color: #97b098;">(</span><span style="color: #96CBFE;">setq</span> last-search-char nil<span style="color: #97b098;">)</span><span style="color: #b0b1a3;">)</span><span style="color: #93a8c6;">)</span> <span style="color: #8c8c8c;">)</span>
使い方
- 検索とリピートは同じコマンドです
- 一旦検索すると検索した文字が保存されるのでリピート可能です
- 保存された検索文字は指定のアイドル時間経過後に消去されます
- 検索文字が保存されている状態でprefix付きで実行すれば強制的に検索文字を指定可能です。
M-x search-forward-repeat-with-char | 指定文字までカーソルを前方移動 |
---|---|
M-x search-backward-repeat-with-char | 指定文字までカーソルを後方移動 |
M-x search-forward-repeat-with-char | 一文字検索を前方に繰り返し |
M-x search-backward-repeat-with-char | 一文字検索を後方に繰り返し |
保存した文字はデフォルトでは1秒のアイドル時間後に消去されます。アイドル時間を調整したい時は下記パラメーターで。
<span style="color: #8c8c8c;">(</span><span style="color: #96CBFE;">setq</span> search-clear-idle-time 1<span style="color: #8c8c8c;">)</span>
またC-u M-x search-forward-repeat-with-char でアイドル時間内でも強制的にプロンプトから文字の再指定が可能です。
また僕はこんな感じでキーバインドを設定しています。view-mode時に利用するのも便利。こちらはf{char}、b{char}で移動できる。この辺はお好みで。
<span style="color: #8c8c8c;">(</span><span style="color: #96CBFE;">setq</span> mac-right-option-modifier 'hyper<span style="color: #8c8c8c;">)</span> <span style="color: #8c8c8c;">(</span><span style="color: #96CBFE;">bind-keys</span> <span style="color: #93a8c6;">(</span><span style="color: #8AE234;">"H-b"</span> . search-backward-repeat-with-char<span style="color: #93a8c6;">)</span> <span style="color: #93a8c6;">(</span><span style="color: #8AE234;">"H-f"</span> . search-forward-repeat-with-char<span style="color: #93a8c6;">))</span> <span style="color: #8c8c8c;">(</span><span style="color: #96CBFE;">bind-keys</span> <span style="color: #DAD085;">:map</span> view-mode-map <span style="color: #93a8c6;">(</span><span style="color: #8AE234;">"b"</span> . search-backward-repeat-with-char<span style="color: #93a8c6;">)</span> <span style="color: #93a8c6;">(</span><span style="color: #8AE234;">"f"</span> . search-forward-repeat-with-char<span style="color: #93a8c6;">)</span><span style="color: #8c8c8c;">)</span>
ダウンロード
下記からソースをダウンロードしてload-pathの通った所に置いて (require 'search-one-word) としても利用可能です。
https://www.inabamasaki.com/arc/search-one-word.el
# シェル $ cd ~/emacs.d/elisp $ wget https://www.inabamasaki.com/arc/search-one-word.el ;; Emacs <span style="color: #8c8c8c;">(</span><span style="color: #96CBFE;">require</span> '<span style="color: #99CC99;">search-one-word</span><span style="color: #8c8c8c;">)</span>
追記 (2017.12.19)
機能強化しました。