Boosting Which Func Mode

Which Func Mode shows the name of the current “tag” in the mode line. A tag is whatever the current major mode adds to its imenu index, e.g. function names in Emacs Lisp, class declarations in Python, or section names in Markdown.

By default, Which Func Mode just dumps the entire tag name at the end of the mode line, after the entire list of minor modes, which is neither particularly sophisticated nor particularly visible. When I redesigned my mode line, I already moved it to a more prominent place in my mode line, by changing the position of mode-line-misc-info.

In this post I’ll show you how to boost the tag name itself with some nifty tricks.

Customising the appearance of Which Func Mode

Which Func Mode exposes its mode line format in the variable which-func-format, which holds a standard Mode Line Format. The default value takes the tag name from the internal variable which-func-current and adds some standard text properties to specify the key map (for mouse support) and faces.

We’ll just copy the standard value, but replace which-func-current with our own function:

(setq which-func-format
        (:propertize (:eval (lunaryorn-which-func-current))
                     local-map ,which-func-keymap
                     face which-func
                     mouse-face mode-line-highlight
                     help-echo "mouse-1: go to beginning\n\
mouse-2: toggle rest visibility\n\
mouse-3: go to end")

We call our custom function lunaryorn-which-func-format to obtain the actual tag name. That’s where the magic will happen.

Truncating the tag name

Some IMenu tags can be really, really long. Let’s make some fun with nested classes in Python:

class Spam:

    class With:

        class Eggs:

            def bake(self):
                def doit():

            return doit

The box indicates the position of the point. In this situation Which Func Mode will show Spam.With.Eggs.bake.doit in the mode line. That’s a long name, which takes a lot of space, especially on small displays.

Let’s fix this, by truncating tag names to a maximum of 20 characters:

(require 'subr-x)

(defun lunaryorn-which-func-current ()
  (if-let (current (gethash (selected-window) which-func-table))
      (truncate-string-to-width current 20 nil nil "…")

This function takes the current tag name from which-func-table which caches the current tag for each window, because computing the imenu index may be expensive depending on the major mode and the buffer size. If there is a current tag, we truncate it to 20 characters at the end, replacing trailing text with an ellipsis. Otherwise we just return the standard “unknown” string.

We are using the if-let macro from subr-x in this function, which is only available as of Emacs 24.4. For earlier Emacs versions you can either replace it with a nested let/if, or use the -if-let macro from the popular dash.el library.

We truncate the tag name at the end, because that’s where the “nearest” part of the tag name appears. We have a good chance to see that in the buffer anyway, so it’s better to omit this part if we have little space, and preserve “farther away” parts of the tag name.

Mode-specific truncation

This approach is a little primitive and won’t always yield good results. We can do better than that, by taking the current major mode into account when truncating.

For instance, in Emacs Lisp we typically prefix global symbols with the name of the defining library, as a poor man’s namespace system. Now, typically we know what file we are in, and we can see the file name and the buffer name in the mode line anyway, so it’s really redundant in the tag name. Let’s remove it from the tag name.

First we define a function to find the current “namespace”, which returns the buffer file name, if any, unless the file name refers to init.el. In this case we return the “namespace” used for functions in init.el, e.g. lunaryorn in this example:

(defun lunaryorn-current-namespace ()
  "Determine the namespace of the current file."
  (when-let (filename (buffer-file-name))
    (if (string= (file-truename filename) (file-truename user-init-file))
        "lunaryorn"                       ; The “namespace” of my init
      (file-name-base filename))))

when-let is from subr-x, too, so everything said before about if-let applies to it as well. On Emacs 24.3 and earlier you need to replace it with a nested when/let or with -when-let from dash.el.

With this function, we can now extend lunaryorn-which-func-current:

(defun lunaryorn-which-func-current ()
  "Determine the name of the current function."
  (if-let (current (or (gethash (selected-window) which-func-table)))
       (pcase major-mode
          (let ((namespace (lunaryorn-current-namespace)))
            (if (and namespace
                     (string-prefix-p namespace current 'ignore-case))
                (concat "…" (substring current (length namespace)))
         (_ current))
       20 nil nil "…")

We use pcase to dispatch on the major-mode of the current buffer. In emacs-lisp-mode, we obtain the namespace using the function we defined before and remove it from the tag name. For all other cases, we just return the tag name without modifications. The result is then truncated to 20 characters as before.

Mode line with truncated which-function for Python Mode

That’s much better than the default. As you can see my mode line features the insanely awesome nyan cat, like any other decent folk’s mode line does.