Jan 05, 2014

Emacs tabbar tuning

I like tabbar-mode and the buffer cycling options it provides. I can cycle between buffers in a single group (especially useful when cycling scope is limited to tabs) and between different groups. This makes it much easier to manage many open buffers within different contexts (such as projects).

(require 'tabbar)
(tabbar-mode t)
(setq tabbar-cycle-scope 'tabs)

(global-set-key (kbd "s-{") 'tabbar-backward-group)
(global-set-key (kbd "s-}") 'tabbar-forward-group)
(global-set-key (kbd "s-[") 'tabbar-backward)
(global-set-key (kbd "s-]") 'tabbar-forward)


The grouping defaults are reasonable (by major mode name), but could be better. We'll use the default tabbar-buffer-groups-function from tabbar.el as a basis. This section contains the entire function budy.

(defun my-tabbar-buffer-groups ()
"Return the list of group names the current buffer belongs to.
Return a list of one element based on major mode."

*Process* group

The default grouping separates "process" buffers into their own group. This is a problem with flycheck, for example. When flycheck does its thing the buffer briefly becomes a "process" buffer, causing tabbar to yank it between groups. This makes for a very jittery tab bar. Let's fix that by removing the "Process" group altogether:

   (setq my-group-by-project nil)
    ;; ((or (get-buffer-process (current-buffer))
    ;;      ;; Check if the major mode derives from `comint-mode' or
    ;;      ;; `compilation-mode'.
    ;;      (tabbar-buffer-mode-derived-p
    ;;       major-mode '(comint-mode compilation-mode)))
    ;;  "Process"
    ;;  )

OK, better. Now our flychecked files stay grouped by major mode.

    ((member (buffer-name)
             '("*scratch*" "*Messages*"))
    ((eq major-mode 'dired-mode)
    ((memq major-mode
           '(help-mode apropos-mode Info-mode Man-mode))
    ((memq major-mode
             rmail-edit-mode vm-summary-mode vm-mode mail-mode
             mh-letter-mode mh-show-mode mh-folder-mode
             gnus-summary-mode message-mode gnus-group-mode
             gnus-article-mode score-mode gnus-browse-killed-mode))

Grouping by project

While working on multiple projects the tab bar quicly becomes unwieldy. I prefer to group mine by "project name:mode name" instead. I use projectile for getting the name of the project.

Also, for small projects I like to be able to group by project name alone. For this I defined a simple toggle variable my-group-by-project.

     ;; Return `mode-name' if not blank, `major-mode' otherwise.
     (let ((group 
            (if (and (stringp mode-name)
                     ;; Take care of preserving the match-data because this
                     ;; function is called when updating the header line.
                     (save-match-data (string-match "[^ ]" mode-name)))
              (symbol-name major-mode))))
       (if (projectile-project-p)
           (if my-group-by-project
             (format "%s:%s" (projectile-project-name) group))

Performance tuning

tabbar-mode calls tabbar-buffer-groups-function A LOT -- for each open buffer for every single keystroke. Since projectile-project-name is not a super fast function this will slow Emacs down. Assuming we never want buffers to change groups, we could quasi-memoize this function and cache group names per every project. We can do this with a pseudo-closure.

(defun my-cached (func)
  "Turn a function into a cache dict."
  (lexical-let ((table (make-hash-table :test 'equal))
                (f func))
    (lambda (key)
      (let ((value (gethash key table)))
        (if value
          (puthash key (funcall f) table))))))

;; evaluate again to clear cache
(setq cached-ppn (my-cached 'my-tabbar-buffer-groups))

(defun my-tabbar-groups-by-project ()
  (funcall cached-ppn (buffer-name)))

(setq tabbar-buffer-groups-function 'my-tabbar-groups-by-project)

Now, tabbar will be fetching a cached group name for existing buffers. To wipe the cache we recreate the closure by reevaluating.

Finally, wire the toggle (we must clear the cache for regrouping to take effect):

(defun my-toggle-group-by-project ()
  (setq my-group-by-project (not my-group-by-project))
  (message "Grouping by project alone: %s"
           (if my-group-by-project "enabled" "disabled"))
  (setq cached-ppn (my-cached 'my-tabbar-buffer-groups)))