特定の場合のみ違うことをするコマンドを作る

たとえば C-k (kill-line) は普通、現在位置から行末を kill して改行が残るんだけど、行頭でやったら改行も含めて kill する(1行まるっと kill)にしたい。という場合。

よくあるやり方は、そういうコマンドを作って置き換えるパターン。xyzzyの音 - 編集 にあるようなの。

今回これとはちょっと違うやり方をやりたかったので、ごちゃごちゃやってみた。

  • いくつかのキーで特定の場合に違うことをさせたい。
  • 複数のモードで使いたい。
  • 特定の場合以外では、普通に動いてて欲しい。

という感じ。

まず minor-mode を作る。

;;;; Keymap

(defvar *hoge-mode-keymap* nil)

(unless *hoge-mode-keymap*
  (let ((kmap (make-sparse-keymap)))
    (define-key #\C-k 'kill-whole-line-or-original)
    (setf *hoge-mode-keymap* kmap)))


;;;; Minor mode

(defvar-local hoge-mode nil)

(defun hoge-mode (&optional (arg nil sv))
  (interactive "p")
  (ed::toggle-mode 'hoge-mode arg sv)
  (if 'hoge-mode
    (set-minor-mode-map *hoge-mode-keymap*)
    (unset-minor-mode-map *hoge-mode-keymap*))
  (update-mode-line t))
(pushnew '(hoge-mode . "Hoge") *minor-mode-alist* :key #'car)

マイナーモードのキーマップを使うだけ。そのキーマップは C-kkill-whole-line-or-original というコマンドを割り当ててるだけ。

んで、コマンドがこんなの。

(defun kill-whole-line-or-original (&optional lines)
  (interactive "*p")
  (if (bolp)
    (let ((point (point))
          (lines (cond ((or (null lines)
                            (<= lines 1))
                         0)
                       (t
                         (- arg 1)))))
      (kill-region point
                   (progn
                     (forward-line lines)
                     (goto-eol)
                     (forward-char)
                     (point))))
    (call-interactively (original-command *last-command-char*))))

実装はxyzzyの音 - 編集 をパクらせてもらった。 要は (bolp) だったら行末まで kill してるのだが、(bolp) ではない場合に (call-interactively (original-command *last-command-char*)) とした。

original-command はこれから実装するので置いとくとして、*last-command-char* は押されたキーに束縛されてる。 なので、押されたキーが実行するはずだったコマンドを探して call-interactively すればいいんじゃね。というのが今回の作戦であり、original-command は指定されたキーが実行するはずだったコマンドを探す関数。

;;; Modified version of original `lookup-key-command`
(defun original-command (key)
  (let ((bound (mapcar #'(lambda (x)
                           (when (and (keymapp x)
                                      (not (eql x *hoge-mode-keymap*)))
                             (lookup-keymap x key)))
                       (append (list (current-selection-keymap))
                               (minor-mode-map)
                               (list (local-keymap))
                               (list *global-keymap*)))))
    (or (find-if-not #'keymapp bound)
        (find-if #'identity bound))))

もともと xyzzy には lookup-key-command という、現在のバッファで使用してるキーマップから指定されたキーのコマンドを探し出す関数があるのだけど、そのままだとマイナーモードで上書きしたコマンド自身(今回の例だと kill-whole-line-or-original)が返ってきて再帰呼び出しになってしまうので、マイナーモードのキーマップ(*hoge-mode-keymap*)を除外して探すようにしたのがこれ。

これで (bolp) でなかったときは (original-command *last-command-char*) が見つけた kill-linecall-interactively して、普通に kill-line される(はず)。

FIXME: たぶん複数キーストロークC-x f とか)に対応してない。