Adding emacs command key-bindings and help functionality to org-mode

| categories: orgmode, emacs | tags: | View Comments

The documentation of functions in emacs allows you to put some light markup into function doc strings that will render as the key sequence required to run the command when you look up the help on the function. I would like to have something like that in org-mode. You can look up the key-binding to a command like this:

(substitute-command-keys "\\[org-agenda]")
C-c a

We are going to explore a way to recognize the syntax shown above, change its appearance to alert us that we are looking at an emacs command, add a tooltip, and make it clickable to open the documentation, and s (super) clickable to find the function code. Font lock is the tool we will use for this. Basically, we need a regular expression to match the syntax, and a function to find the next instance, and put some properties on the matched text.

I made a video (https://www.youtube.com/watch?v=VLUMW0sR4Vk ) showing what this post is all about. It isn't easy to see in the post ☺.

Here we use the `rx' library to build up a regular expression for this. It is a bit easier to document than a raw regexp. Since we are matching \ in the pattern, there are some obligatory escaping \ characters in there too. All we need is to integrate this into font-lock. We define a function that will move the point to the end of the next match, and put properties on the match. We will go ahead and make the text clickable so we can access documentation and code easily. The tooltip will show the key-binding to run the command.

(require 'rx)

(defvar elisp-symbol-keybinding-re
  (rx
   ;; opening \\[
   (eval "\\[")
   ;; one or more characters that are not ]
   (group (one-or-more (not (any "]"))))
   ;; The closing ]
   "]")
"Regexp for an elisp command keybinding syntax. \\[some-command]
Regexp group 1 matches `some-command'.")

(defun match-next-keybinding (&optional limit)
  "Move point to the end of the next expression matching
`elisp-symbol-keybinding-re', and put properties on the match
that shows the key sequence. Non-bound commands are not
fontified."
  (when (and (re-search-forward
              elisp-symbol-keybinding-re
              limit t)
             (fboundp (intern (match-string 1))))
    (let* ((beg (match-beginning 0))
           (end (match-end 0))
           (s (match-string 0))
           (command (match-string 1))
           (describe-func `(lambda ()
                    "Run `describe-function' on the command."
                    (interactive)
                    (describe-function (intern ,command))))
           (find-func `(lambda ()
                     "Run `find-function' on the command."
                     (interactive)
                     (find-function (intern ,command))))
           (map (make-sparse-keymap)))

      ;; this is what gets run when you click on it.
      (define-key map [mouse-1] describe-func)
      (define-key map [s-mouse-1] find-func)
      ;; Here we define the text properties
      (add-text-properties
       beg end
       `(local-map ,map
         mouse-face highlight
         help-echo ,(format
                     "%s\n\nClick for documentation.\ns-mouse-1 to find function."
                     (substitute-command-keys s))
         keybinding t)))))

Let's go ahead and make syntax for `some-command' too. This one seems simple enough we just write a regexp for it.

(defun match-next-emacs-command (&optional limit)
  "Move point to the end of the next expression matching
`this-syntax', and put a tooltip on the match
that shows the key sequence. Works on commands and variables."
  (when (and (re-search-forward
              "`\\([^']+\\)'"
              limit t)
             (or (boundp (intern (match-string 1)))
                 (fboundp (intern (match-string 1)))))
    (let* ((beg (match-beginning 0))
           (end (match-end 0))
           (s (match-string 0))
           (command (match-string 1))
           (describe-func
            `(lambda ()
               "Run `describe-function/variable' on the command."
               (interactive)
               (cond ((fboundp (intern ,command))
                      (describe-function (intern ,command)))
                     ((boundp (intern ,command))
                      (describe-variable (intern ,command))))))
           (find-func `(lambda ()
                     "Run `find-function' on the command."
                     (interactive)
                     (find-function (intern ,command))))
           (map (make-sparse-keymap)))

      ;; this is what gets run when you click on it.
      (define-key map [mouse-1] describe-func)
      (define-key map [s-mouse-1] find-func)
      ;; Here we define the text properties
      (add-text-properties
       beg end
       `(local-map ,map
         mouse-face highlight
         help-echo ,(format
                     "%s\n\nClick for documentation.%s"
                     (if (fboundp (intern command))
                         (substitute-command-keys (format "\\[%s]" command))
                       "Variable")
                     (if (fboundp (intern command))
                         "\ns-mouse-1 to find function."
                       ""))
         keybinding t)))))

Now we need a way to turn them on and off. We do that here with a minor mode.

(define-minor-mode emacs-keybinding-command-tooltip-mode
  "Fontify on emacs keybinding syntax. Adds a tooltip for
keybinding, and make the command clickable to get to the
documentation."
  :lighter " KB"
  (if emacs-keybinding-command-tooltip-mode
      ;; turn them on
      (font-lock-add-keywords
       nil
       '((match-next-keybinding 1 font-lock-constant-face)
         (match-next-emacs-command 1 font-lock-constant-face)))
    ;; turn them off
    (font-lock-remove-keywords
     nil
     '((match-next-keybinding 1 font-lock-constant-face)
       (match-next-emacs-command 1 font-lock-constant-face))))
  (font-lock-fontify-buffer))

Here we turn it on:

(emacs-keybinding-command-tooltip-mode -1)

Here are some sample uses. You can use \\[org-toggle-latex-overlays] to toggle latex overlays.

You can use \\[org-ref-helm-insert-cite-link] to insert citations.

That more or less does it! I don't know if this is the canonical way to do this, but it works nicely here. You can also use overlays, but I found them a little confusing because they are not editable, and you have to toggle the minor mode to see them. Here we have unobtrusive tooltips. One downside is these won't export in any fashion in org-mode since it is not part of the syntax. It might be a good idea to adjust `font-lock-extra-managed-props' for this

It works for this syntax too: `helm', which is also commonly used in doc strings. This should be pretty handy in org-mode documents about Emacs!

Copyright (C) 2015 by John Kitchin. See the License for information about copying.

org-mode source

Org-mode version = 8.2.10

blog comments powered by Disqus