Endless Parentheses

Ramblings on productivity and technical subjects.

profile for Malabarba on Stack Exchange

Define context-aware keys in Emacs

What do you do if you want to override a key only in a certain context? Take this Quotation Marks post as an example. We want to change the " key in general, but retain the regular behaviour if we’re inside a code-block. In this case the solution was to just call the old behaviour manually, but what if you’re writing a more general command and you don’t know what this “old behaviour” is?

As it turns out, it’s a little-known feature of Emacs that you can specify filter functions to determine whether a keybind should be active. We use this quite a bit in SX.el. While the syntax is far from simple, it’s very easy to copy-paste and just fill in your predicate.

First, we redefine endless/round-quotes without that ugly part that conditionally calls self-insert-command (previous version here).

(defun endless/round-quotes (italicize)
  "Insert “” and leave point in the middle.
With prefix argument ITALICIZE, insert /“”/ instead (meant
for org-mode)."
  (interactive "P")
  (if (looking-at "”[/=_\\*]?")
      (goto-char (match-end 0))
    (when italicize
      (if (derived-mode-p 'markdown-mode)
          (insert "__")
        (insert "//"))
      (forward-char -1))
    (insert "“”")
    (forward-char -1)))

Then, we define the same key as before, but instead of just passing the command we use a menu-item bound to nil. The fact that it’s a menu-item is irrelevant here, it behaves exactly like a key bound to nil (i.e., an empty keybind). However, this allows us to setup a filter that changes the keybind to endless/round-quotes if we’re not inside an org-src-block.

(define-key org-mode-map "\""
  '(menu-item "maybe-round-quotes" nil
              :filter (lambda (&optional _)
                        (unless (org-in-src-block-p)
                          #'endless/round-quotes))))

That maybe-round-quotes is just a useless name for the menu-item, and you can learn more about all of this on this manual page. For now, it suffices to say we deserve a more convenient way to use this feature.

Of course, that’s nothing a good macro can’t fix.

(defmacro endless/define-conditional-key (keymap key def
                                                 &rest body)
  "In KEYMAP, define key sequence KEY as DEF conditionally.
This is like `define-key', except the definition
\"disappears\" whenever BODY evaluates to nil."
  (declare (indent 3)
           (debug (form form form &rest sexp)))
  `(define-key ,keymap ,key
     '(menu-item
       ,(format "maybe-%s" (or (car (cdr-safe def)) def))
       nil
       :filter (lambda (&optional _)
                 (when ,(macroexp-progn body)
                   ,def)))))

Which leads to the much nicer syntax:

(endless/define-conditional-key org-mode-map "\""
                                #'endless/round-quotes
  (not (org-in-src-block-p)))

And a similar keybind for Markdown, which is a bit more of a mouth-full.

(endless/define-conditional-key markdown-mode-map "\""
                                #'endless/round-quotes
  (not (or (markdown-code-at-point-p)
           (memq 'markdown-pre-face
                 (face-at-point nil 'mult)))))
comments powered by Disqus