Evil: EmacsをVimのごとく使う - 拡張編

Roads? Where we're going we don't need roads.

Back to the Future

Evilの真髄はその拡張性です. 本稿では主にチュートリアルを通して, Evilを拡張する方法を解説します.

Evilの拡張

Evilはもともと拡張性を考慮して設計されています. 最小限のコア部分がEvilの本体だとすると, ステート, コマンド, オペレータ, モーション, テキストオブジェクトなどEvilの具体的な機能はすべて拡張機能として実装されているとも言えます.

これら拡張機能を定義するための機構はあらかじめコアに備わっていて, evil-define-functionalityといった名前のマクロが用意されています. たとえば, evil-define-state, evil-define-command, evil-define-operator, evil-define-motion, evil-define-text-objectといった具合です.

あとは, 必要に応じたEmacs Lispの知識と, Evilの内部的な動作のための補助関数についての知識さえあれば, 簡単にEvilのプラグインを作成できます. このようにして作られた拡張機能の例としてもっとも優れているのは, Evilそのものの機能のソースコードです. 最初のうちは, evil-commands.elなどを参考にするのがよいでしょう.

このあとの節では, Evilの拡張機能を定義する方法をチュートリアル形式で解説していきます.

例1: コメントアウトオペレータ

オペレータを自作する例として, コメントアウト/アンコメントするためのオレータを定義してみましょう. オペレータは範囲選択なしに用いられた場合は, 直後にモーションを伴って, 最初のカーソル位置からモーション終了後の位置までの範囲に, 何らかの操作を施すものです. 選択範囲が与えられた場合は, 文字・行・矩形の選択種別に応じて操作を施します.

Evilでは, (evil-define-operator name (beg end ...) ...)の形でオペレータを定義できます. beg, endには選択範囲の開始位置と終了位置が渡されます. 後続のモーションの扱いはとくに明示的にやる必要はなく, モーション終了後に決定された選択範囲が渡されます.

単純な選択範囲のコメントアウト

Emacsでは, comment-or-uncomment-regionという関数で, 特定の範囲をコメントアウト/アンコメントできます. 選択種別について気にしなければ, これをevil-define-operatorの中で呼び出すだけで, オペレータの定義が完成します.

(evil-define-operator evil-comment-or-uncomment-region (beg end)
  "Comment out text from BEG to END."
  (interactive "<r>")
  (comment-or-uncomment-region beg end))

名前はevil-comment-or-uncomment-regionとしました. interactiveに渡している<r>は, このオペレータの引数として選択範囲を受け取ることを表していて, 範囲が選択されていないときにはinteractiveの行を解釈する途中でモーションの入力を自動的に待ちます.

矩形選択範囲のコメントアウト

上の定義でも, 文字選択(v)や行選択(V)の場合はうまくいきますが, 矩形選択(C-v)された範囲をうまくコメントアウトすることができません. 矩形選択の場合は, 選択範囲に含まれる部分を行ごとに分割して操作を適用する必要があります. たとえば, 以下の文章の|の間が選択範囲だとします.

Assume that the range
between | in this text
is seleced. If the
selection is blockwise,
then each line of the
selected text is
restricted to the
columns between |.

矩形選択では色つきで示した部分が選択範囲ですが, 実際にはbegendには|の位置が渡ってくるだけなので, 矩形選択の場合は行ごとに列の範囲を限定しながら操作を適用する必要があります. 矩形選択範囲を行ごとの範囲に変換して操作を適用できるように, evil-apply-on-blockという内部関数が用意されています.

選択種別を受け取れるようにinteractive<R>を渡すようにして, 矩形選択のときにはevil-apply-on-blockを使うようにすると以下のようになります.

(evil-define-operator evil-comment-or-uncomment-region (beg end type)
  "Comment out text from BEG to END with TYPE."
  (interactive "<R>")
  (if (eq type 'block)
      (evil-apply-on-block #'comment-or-uncomment-region beg end nil)
    (comment-or-uncomment-region beg end)))

実はこのままではうまくいきません. comment-or-uncomment-regionは, コメント開始記号を挿入/削除するので, 行ごとの操作を適用する途中で, 列の範囲がずれてしまうのです. Emacsには, 文字列の挿入/削除が起きても位置を正しく保存するために, マーカという仕組みがあります. まずは行ごとに限定すべき範囲に対してマーカを設定して, そのマーカで示された範囲にコメントアウト操作を適用するようにしましょう.

(defun evil-mark-on-lines (beg end lines)
  (let ((beg-marker (save-excursion (goto-char beg) (point-marker)))
        (end-marker (save-excursion (goto-char end) (point-marker))))
    (set-marker-insertion-type end-marker t)
    (setcdr lines (cons (cons beg-marker end-marker) (cdr lines)))))

(defun evil-apply-on-block-markers (func beg end &rest args)
  "Like `evil-apply-on-block' but first mark all lines and then
call the function on the marked ranges."
  (let ((lines (list nil)))
    (evil-apply-on-block #'evil-mark-on-lines beg end nil lines)
    (dolist (range (nreverse (cdr lines)))
      (let ((beg (car range)) (end (cdr range)))
        (apply func beg end args)
        (set-marker beg nil)
        (set-marker end nil)))))

(evil-define-operator evil-comment-or-uncomment-region (beg end type)
  "Comment out text from BEG to END with TYPE."
  (interactive "<R>")
  (if (eq type 'block)
      (evil-apply-on-block-markers #'comment-or-uncomment-region beg end)
    (comment-or-uncomment-region beg end)))
キーマップ

プラグインが定義するキーは, 適宜オン/オフできるとよいでしょう. そのためには, プラグイン専用のマイナーモードを定義し, そのマイナーモードが有効な場合だけキーを割り当てるのがよいでしょう.

マイナーモードは以下のように定義します. これにより, マイナーモード用のキーマップevil-operator-comment-mode-mapが自動的に作られます.

(defgroup evil-operator-comment nil
  "Comment/uncomment operator for Evil"
  :prefix "evil-operator-comment-"
  :group 'evil)
(define-minor-mode evil-operator-comment-mode
  "Buffer local minor mode of comment/uncomment operator for Evil."
  :lighter ""
  :keymap (make-sparse-keymap)
  :group 'evil-operator-comment
  (evil-normalize-keymaps))

evil-define-keyを使うと, このマイナーモードが有効で, かつ特定のステートのときだけ使えるキーを定義できます. ここでは, ノーマルステートとビジュアルステートのCキーに割り当てることにします.

(evil-define-key 'normal evil-operator-comment-mode-map
                 "C" 'evil-comment-or-uncomment-region)
(evil-define-key 'visual evil-operator-comment-mode-map
                 "C" 'evil-comment-or-uncomment-region)

このマイナーモードを常に有効にしておきたい場合のために, グローバルマイナーモードも定義しておくとよいでしょう.

(defun evil-operator-comment-mode-install () (evil-operator-comment-mode 1))
(define-globalized-minor-mode global-evil-operator-comment-mode
  evil-operator-comment-mode evil-operator-comment-mode-install
  "Global minor mode of comment/uncomment operator for Evil.")

これで, 使う側では

(global-evil-operator-comment-mode 1)

とするだけで済むようになります.

例2: 次のシンボルへ移動するモーション

モーションを自作する例として, 次のシンボルへ移動するモーションを定義してみましょう.

モーションの定義

Emacsにはもともとforward-symbol(thingatpointパッケージ)というコマンドがあり, Evilを使っていてもこのコマンドはふつうに動作します. しかし, w(evil-forward-word-begin)で単語移動するときなどは単語の先頭で止まりますが, forward-symbolはシンボルの終端(シンボルの最後の文字の次の位置)で止まってしまいます. これはちょうど, Emacsの単語移動コマンドforward-wordが単語の終端で止まるのと同じで, EmacsVimの移動コマンドの流儀の違いに由来します.

この流儀の違いを吸収するために, Evilにはevil-move-beginningという内部関数が用意されています. この関数には, 移動する回数と, Emacs流の移動関数を渡します. ただし, 移動関数は指定された回数の移動ができなかった場合(途中でバッファの先頭や末尾に到達してしまった場合)は, 残りの移動回数を返す必要があります. 残り移動回数を返すようにしたシンボル移動のコマンドを仮にevil-forward-symbolとすると, wのようにVimの流儀でシンボル移動するコマンドevil-forward-symbol-beginは以下のように定義できます.

(evil-define-motion evil-forward-symbol-begin (count)
  "Move the cursor to the beginning of the COUNT-th next symbol."
  :type exclusive
  (evil-move-beginning count #'evil-forward-symbol))

モーションの定義にはevil-define-motionマクロを使います. :typeには, オペレータを施したときに, 移動後のカーソル位置をオペレータの適用範囲に含めるかどうかを指定します. evil-forward-symbolは次のシンボルの先頭に移動するコマンドなので, 移動後のカーソル位置をオペレータの適用範囲には含めません(exclusiveを指定します).

次にevil-forward-symbolを定義しましょう. evil-forward-wordの定義を参考に, foward-symbolがバッファの端点に到達してしまったときだけnilを返すと仮定して, 以下のように定義してみました.

(defun evil-forward-symbol (&optional count)
  (setq count (or count 1))
  (let* ((dir (if (>= count 0) +1 -1))
         (count (abs count)))
    (while (and (> count 0)
                (forward-symbol dir))
      (setq count (1- count)))
    count))

実はこれはうまくいきません. なぜなら, forward-symbolforward-wordと違って, 端点に到達してもnilを返さない場合があるからです. そこで, forward-symbolの定義を少し修正したevil-forward-smbol1を定義して, そちらを使うようにしましょう.

(defun evil-forward-symbol1 (dir)
  (if (natnump dir)
      (re-search-forward  "\\(\\sw\\|\\s_\\)+" nil 'move count)
    (prog1 (re-search-backward "\\(\\sw\\|\\s_\\)+" nil 'move)
      (skip-syntax-backward "w_"))))

(defun evil-forward-symbol (&optional count)
  (setq count (or count 1))
  (let* ((dir (if (>= count 0) +1 -1))
         (count (abs count)))
    (while (and (> count 0)
                (evil-forward-symbol1 dir))
      (setq count (1- count)))
    count))

これでおおよそうまくいきますが, このままではwと違って, 空行を飛ばしてしまいます. Evilにはevil-move-empty-linesという, 次の空行まで移動するコマンドがあるので, このコマンドと, いま定義したevil-forward-symbolと, 移動距離の短い方を使えばうまくいきます. Evilには「移動距離の短い方を使う」ためのマクロevil-define-union-moveも用意されているので, これを使いましょう.

(evil-define-union-move evil-move-symbol (count)
  "Move by symbols."
  (evil-forward-symbol count)
  (evil-move-empty-lines count))

いま定義したevil-move-symbolを, evil-forward-symbolの代わりにevil-move-beginningに渡すように, evil-forward-symbol-beginを修正します.

(evil-define-motion evil-forward-symbol-begin (count)
  "Move the cursor to the beginning of the COUNT-th next symbol."
  :type exclusive
  (evil-move-beginning count #'evil-move-symbol))

同じようにして, 前のシンボルに移動するコマンドも定義できます.

(evil-define-motion evil-backward-symbol-begin (count)
  "Move the cursor to the beginning of the COUNT-th previous symbol."
  :type exclusive
  (evil-move-beginning (- (or count 1)) #'evil-move-symbol))

内部で用いるコマンドはcountが負数でもいいように定義してあったので, evil-move-beginningに渡すcountの符号を反転させるだけで, 何も新しいものは要りません.

さらに, evil-move-beginningの代わりにevil-move-endを使うと, 単語移動におけるe(evil-forward-word-end)のように, シンボルの末尾で止まるコマンドも定義できます.

(evil-define-motion evil-forward-symbol-end (count)
  "Move the cursor to the end of the COUNT-th next symbol."
  :type inclusive
  (evil-move-end count #'evil-move-symbol nil t))

(evil-define-motion evil-backward-symbol-end (count)
  "Move the cursor to the end of the COUNT-th previous symbol."
  :type inclusive
  (evil-move-end (- (or count 1)) #'evil-move-symbol nil t))

シンボルの末尾はオペレータの適用範囲に含めて欲しいので, evil-define-motionに渡す:typeinclusiveです. また, evil-move-endの呼出しでは, inclusiveな場合には第4引数で指定する必要があります.

以上で, シンボル単位の移動コマンドが定義できました. 実際には, たとえば行末のシンボルにカーソルがあるときに, 次の行の空白をオペレータの適用範囲に含めないようにしたりといった細かな調整をすべきかもしれませんが, おおまかなところとしてはこれで完成です.

テキストオブジェクト

evil-move-symbolのように, 一定の条件を満たすように作られた関数は, Evilの内部関数evil-an-object-rangeevil-inner-object-rangeを使って, そのままテキストオブジェクトに変換できます.

(evil-define-text-object evil-a-symbol (count &optional beg end type)
  "Select a symbol."
  (evil-an-object-range count beg end type #'evil-move-symbol))

(evil-define-text-object evil-inner-symbol (count &optional beg end type)
  "Select inner symbol."
  (evil-inner-object-range count beg end type #'evil-move-symbol))

ただし, evil-an-object-rangeevil-inner-object-rangeを使ってテキストオブジェクトを作る場合は, 渡す関数が次の条件を満たす必要があります.

  1. 引数としてcountのみをとる
  2. countが正のときは, オブジェクトの末尾がcount個過ぎた後の, 最初のオブジェクトの先頭に移動する(ただし, 最初からカーソルがオブジェクトの末尾にあるときは, そのオブジェクトも数える)
  3. countが負のときは, count個前のオブジェクトの先頭に移動する(ただし, 最初からカーソルがオブジェクトの先頭にあるときはそのオブジェクトは数えない)
  4. 返り値は, countで指定された回数と実際に移動できた回数の差(つまり成功時は0になる)

evil-move-symbolはこの条件を満たすように作っていたわけです.

キーマップ

定義されたコマンドを簡単なキーで使えるようにする場合, たとえば,w等に割り当てるなら次のようにします.

(define-key evil-motion-state-map (kbd ",") (make-sparse-keymap))
(define-key evil-motion-state-map (kbd ",w") #'evil-forward-symbol-begin)
(define-key evil-motion-state-map (kbd ",b") #'evil-backward-symbol-begin)
(define-key evil-motion-state-map (kbd ",e") #'evil-forward-symbol-end)
(define-key evil-motion-state-map (kbd ",ge") #'evil-backward-symbol-end)
(define-key evil-outer-text-objects-map (kbd ",w") #'evil-a-symbol)
(define-key evil-inner-text-objects-map (kbd ",w") #'evil-inner-symbol)

例3: 同じ文字の間を表すオブジェクト

evil-an-object-rangeevil-inner-object-rangeは用いずに, いちからテキストオブジェクトを自作する例として, 同じ文字の間を表すオブジェクトを定義してみましょう. vifcで文字cに囲まれた範囲を選択できるようにするのが目標です. vafcのときはcそのものも選択されるように, vifcした後にもう一度ifcした場合は選択範囲を拡大することにしましょう.

テキストオブジェクトを定義するにはevil-define-text-objectを使います. 定義されるテキストオブジェクトは引数countと, オプショナル引数beg, end, typeを取ります. オプショナル引数は, 既に選択範囲がある場合に渡されます. 実際の処理はevil-between-rangeという関数に任せることにしましょう.

(evil-define-text-object evil-a-between (count &optional beg end type)
  "Select range between a character by which the command is followed."
  (evil-between-range count beg end type t))

(evil-define-text-object evil-inner-between (count &optional beg end type)
  "Select inner range between a character by which the command is followed."
  (evil-between-range count beg end type))

evil-between-rangeの第5引数で, 入力された文字cそのものも選択範囲に含めるかどうかを指定することにしました.

単純な実装

evil-between-rangeがやるべきことは, count個前と後の文字cを見つけて, 見つけた位置を表す範囲オブジェクトを返すことです. 文字の入力を受け取るにはevil-read-key, 回数指定で文字を探すにはevil-find-char, 範囲オブジェクトをつくるにはevil-rangeを使います.

(defun evil-between-range (count beg end type &optional inclusive)
  (ignore-errors
    (let ((count (abs (or count 1)))
          (ch (evil-read-key))
          beg-inc end-inc)
      (save-excursion
        (evil-find-char (- count) ch)
        (setq beg-inc (point)))
      (save-excursion
        (backward-char)
        (evil-find-char count ch)
        (setq end-inc (1+ (point))))
      (if inclusive
          (evil-range beg-inc end-inc)
        (evil-range (1+ beg-inc) (1- end-inc))))))

文字が見つからなかった場合はエラーになるので, 全体をignore-errorsで囲っておきます. 文字を探す間のカーソル移動が後に影響しないようにsave-excursionで囲うようにもします. 負の回数には意味がないので絶対値を取ります. 後方に文字を探すときは, カーソルがその文字の上にあるときを考慮して1文字戻ってから探します. 第5引数がtのときは, 見つかった文字の位置も含めた範囲を, それ以外のときは1文字ずつ縮小した範囲を返します.

選択範囲の拡大

この定義のままだと, 常にカーソル位置から始めて文字を探しますが, begendが指定されたとき(既に範囲が選択されているとき)は, 既存の選択範囲の開始位置から前に, 終了位置から後ろに探して, 結果的に選択範囲が拡大されるようにしなければなりません. また, 選択範囲がちょうど指定の文字の間の範囲を表している場合は, すぐ外側の文字が見つかって, 選択範囲が拡大されなくなってしまうので, 特別な取り扱いが必要です. このような点を考慮して修正したのが以下のコードです.

(defun evil-between-range (count beg end type &optional inclusive)
  (ignore-errors
    (let ((count (abs (or count 1)))
          (beg (and beg end (min beg end)))
          (end (and beg end (max beg end)))
          (ch (evil-read-key))
          beg-inc end-inc)
      (save-excursion
        (when beg (goto-char beg))
        (evil-find-char (- count) ch)
        (setq beg-inc (point)))
      (save-excursion
        (when end (goto-char end))
        (backward-char)
        (evil-find-char count ch)
        (setq end-inc (1+ (point))))
      (if inclusive
          (evil-range beg-inc end-inc)
        (if (and beg end (= (1+ beg-inc) beg) (= (1- end-inc) end))
            (evil-range beg-inc end-inc)
          (evil-range (1+ beg-inc) (1- end-inc)))))))
キーマップ

できあがったテキストオブジェクトを使うためのキーを割り当てるには, evil-outer-text-objects-mapevil-inner-text-objects-mapを使います.

(define-key evil-outer-text-objects-map "f" 'evil-a-between)
(define-key evil-inner-text-objects-map "f" 'evil-inner-between)

例4: かなステート

ステートを自作する例として, ひらがな/カタカナを簡単に入力できるステートを定義してみましょう. Vimにはもともとダイグラフの機能があり, Evilもこれをサポートしているので, 入力されたキーのかな変換にはこれを使うことにしましょう.

ステートの定義

新しいステートを定義するにはevil-define-stateマクロを使います. これを使うだけで, ステート・キーマップ・フックといったものが自動的に作成されます.

(evil-define-state kana
  "Kana input state."
  :tag " <かな> "
  :cursor (bar . 2)
  :message "-- かな --"
  :enable (insert)
  (cond
   ((evil-kana-state-p)
    ;; かなステートに入ったときにすることはここに書く
    )
   (t
    ;; かなステートを抜けるときにすることはここに書く
    )))

かなステートに入ったときは, 挿入ステートのキーマップも使えるようにしました(:enableキーワード) これによってESCでノーマルステートに戻るなどの操作は自動的に有効になります. 他にどんなキーワードがあるかは, M-x describe-function RET evil-define-state REThttps://bitbucket.org/lyro/evil/src/master/evil-states.el:evil-states.elを参照して下さい. 今回は使いませんが, evil-define-stateの本体では, ステートの有効化/無効化時に実行するコードを書くことができます.

かな変換コマンド

ダイグラフそのものを表すキーが入力されたときに, ダイグラフが表す文字を挿入するコマンドevil-insert-kanaを作りましょう. たとえば,

(define-key some-map (kbd "ka") #'evil-insert-kana)

と割り当てることで, kaの入力に対して「か」が挿入されるようにします.

this-command-keys関数を使うと, 現在のコマンドを実行するために入力したキーを取得できるので, これをダイグラフとみなして文字を挿入するようにします.

(evil-define-command evil-insert-kana ()
  "Insert Kanas."
  (interactive)
  (let ((str (this-command-keys)))
    (if (= 2 (length str))
        (let ((ch (evil-digraph (list (aref str 0) (aref str 1)))))
          (if (characterp ch)
              (insert-char ch)
            (error "Invalid digraph \"%s\"" str)))
      (error "Invalid keys \"%s\"" str))))

入力されたキーが2文字でない場合, 有効なダイグラフでない場合はエラーにしています.

キーマップ

まず, 日本語のダイグラフの範囲に, evil-insert-kanaを割り当てます. 割り当てるキーマップはevil-kana-state-mapです.

(defun evil-kana-make-digraph-patterns (c1 c2)
  (let (result)
    (dolist (first (list (string c1) (upcase (string c1))))
      (dolist (second (list (string c2) (upcase (string c2))))
        (push (concat first second) result)))
    result))

(let* ((vowels '(?a ?i ?u ?e ?o))
       (consonants '(?k ?s ?t ?n ?h ?m ?y ?r ?w ?g ?z ?d ?b ?v))
       (singles '(?n)))
  (dolist (v vowels)
    (define-key evil-kana-state-map (format "%c5" v) #'evil-insert-kana)
    (define-key evil-kana-state-map (format "%c6" v) #'evil-insert-kana)
    (define-key evil-kana-state-map (upcase (format "%c5" v))
      #'evil-insert-kana)
    (define-key evil-kana-state-map (upcase (format "%c6" v))
      #'evil-insert-kana)
    (dolist (c consonants)
      (dolist (str (evil-kana-make-digraph-patterns c v))
        (define-key evil-kana-state-map (read-kbd-macro str)
          #'evil-insert-kana))))
  (dolist (s singles)
    (define-key evil-kana-state-map (format "%c5" s) #'evil-insert-kana)
    (define-key evil-kana-state-map (format "%c6" s) #'evil-insert-kana)))

ノーマルステートからC-kでかなステートに移れるようにします.

(define-key evil-normal-state-map (kbd "C-k") #'evil-kana-state)

リファレンス

ステート
(evil-define-state name doc keywords... body...)
ステートを定義します. ステート切り替えコマンドevil-name-state, ステート判別コマンドevil-name-state-p, キーマップevil-name-state-mapなどが自動的に定義されます. keywordsにキーワードを指定することで, モードラインに表示するステートのタグ, カーソル形状, フックなども設定できます. 詳しくはM-x describe-function RET evil-define-state RETを参照して下さい.
コマンド
(evil-define-command name (args...) doc keywords... body)
コマンドを定義します. キーワードには:repeat, :keep-visualなど, コマンドをEvil化するための設定を書きます. コマンドが引数をとる場合は, bodyの先頭に(interactive "code")と書きます. codeに指定する内容は引数の種類によってに決まり, evil-define-interactive-codeで定義されているものか, Emacs標準のものを指定します. evil-define-interactive-codeで定義されているものにどんなものがあるかは, evil-types.elを参照して下さい.
(evil-define-interactive-code "code" (prompt) doc keywords... body)
コマンド引数のコードを定義します. (prompt)は省略可能で, ユーザからの入力を待つ場合に指定します. promptは, interactiveで指定されたプロンプト文字列を受け取るための変数名です. キーワードはExコマンドの引数のコードを定義する場合に使います. bodyには, 実際に引数を計算するための式を書きます.
オペレータ
(evil-define-operator name (beg end &optional type args...) doc keywords... body)
オペレータを定義します. 最初の2つの引数は選択範囲の開始位置と終了位置を受け取ります. 3つ目の引数を受け取る場合は, 選択種別が渡されます. これ以外の方法で引数を受け取る場合はbodyの先頭に(interactive "code")と書きます. codeの意味はevil-define-commandと同じです. キーワードには:repeat, :motion, :move-point, :jumpなどを指定します. :keep-visual t:suppress-operator tは自動的に設定されます. bodyにはオペレータの操作を書きます.
モーション
(evil-define-motion name (count args...) doc keywords... body)
モーションを定義します. 最初の引数は繰り返し回数を受け取ります. これ以外の方法で引数を受け取る場合はbodyの先頭に(interactive "code")と書きます. codeの意味はevil-define-commandと同じです. キーワードには:type, :jumpなどを指定します. :keep-visual tは自動的に設定されます. bodyには実際の移動操作を書きます.
(evil-define-union-move name (count) moves...)
複数の移動操作のうち, 移動距離が最小のものを採用する移動操作を定義します. movesには移動操作を書きます. それぞれの移動操作は, 指定回数の移動に成功したときは0を返さなければなりません. 0以外の値を返した場合, その移動操作は採用されません.
テキストオブジェクト
(evil-define-text-object name (count &optional beg end type args...) doc keywords... body)
テキストオブジェクトを定義します. 最初の引数は繰り返し回数, 2番目以降は選択範囲を受け取ります. これ以外の方法で引数を受け取る場合はbodyの先頭に(interactive "code")と書きます. codeの意味はevil-define-commandと同じです. キーワードには:type, :extend-selectionなどを指定します. デフォルトでは:extend-selection tに設定されます. bodyには新しい選択範囲を表すオブジェクトを返す式を書きます. 選択範囲は, 選択開始位置, 終了位置で始まるリストで, 3つ目の要素がある場合は選択種別です. このリストは通常evil-range関数で作ります.
補助関数
(evil-apply-on-block func beg end pass-columns args...)
関数funcを, begからendまでの矩形範囲に適用します. funcは2引数以上の関数でなければなりません. pass-columnsnilのときはfuncの最初の2引数にはバッファ内の位置が渡され, それ以外の場合は列位置が渡されます. args...funcへの3つ目以降の引数です.
(evil-move-beginning count forward &optional backward)
次のcount番目のオブジェクトの先頭まで移動します. forwardbackwardにはオブジェクト間を移動する関数を渡します. どちらの関数も回数を引数に取ります. もしどちらかの関数が指定されなかった場合は, 指定された方の関数に符号が逆の回数を指定したものがもう片方の関数として使われます. 関数は指定された回数の移動が成功すれば0を, そうでなければ残り回数を返す必要があります.
(evil-move-end count forward &optional backward inclusive)
次のcount番目のオブジェクトの末尾まで移動します. forwardbackwardにはオブジェクト間を移動する関数を渡します. どちらの関数も回数を引数に取ります. もしどちらかの関数が指定されなかった場合は, 指定された方の関数に符号が逆の回数を指定したものがもう片方の関数として使われます. 関数は指定された回数の移動が成功すれば0を, そうでなければ残り回数を返す必要があります. カーソルをオブジェクトの末尾に移動する場合はinclusivetを指定します.それ以外の場合はカーソルはオブジェクトの末尾の次の位置に移動します.
(evil-an-object-range count beg end type forward &optional backward range-type newlines)
移動関数からオブジェクトの範囲を作ります. beg, end, typeには現在の選択範囲を指定します. forward, backwardには移動関数を指定します. 移動関数は回数を引数に取り, 指定された回数の移動が成功すれば0を, そうでなければ残り回数を返す必要があります.
(evil-inner-object-range count beg end type forward &optional backward range-type )
移動関数からオブジェクトの範囲を作ります. beg, end, typeには現在の選択範囲を指定します. forward, backwardには移動関数を指定します. 移動関数は回数を引数に取り, 指定された回数の移動が成功すれば0を, そうでなければ残り回数を返す必要があります.
(evil-range beg end &optional type properties...)
範囲オブジェクトを作ります.
(evil-read-key &optional prompt)
ユーザからのキー入力を読み取ります. promptを指定すると入力待ちの間プロンプトを表示します. Emacs標準のread-key関数と違い, evil-read-key-mapが有効になります.
(evil-with-single-undo body)
body内でのバッファの変更を, 1回で元に戻せるようにします.

おわりに

Evilの拡張方法を修得すれば, ますますEvilを自由に使うことができます. また, 拡張機能を書いていくことでEvilの内部のしくみにも詳しくなります. 一通り理解が深まったら, ぜひEvilのプラグインの開発, 本家Evilの開発への参加を目指してみて下さい.

  1. 導入編
  2. 設定編
  3. 拡張編
  4. 付録