Wednesday, January 6, 2010

Emacs: Filtered Buffer Switching

Emacs:  Filtered Buffer Switching


There are a lot of different ways to switch buffers.  A lot.  I use four myself:
  1. Toggle between the two most recently used buffers
  2. Choose buffer by name
  3. Choose buffer from a list
  4. Flip sequentially through buffers
One thing I want in common between these is for them to all use a common subset of buffers.  For example, I don't want buffers that start with '*' considered when switching unless, say, the buffer is "*scratch*".  Many packages allow you to filter the buffer list using a function, so let's make one.

I'll use three variables to do the filtering: 1) Buffer name regex's that should always be shown, 2) buffer name regex's that should never be shown (always overrides never), and 3) a boolean for if dired buffers should be shown.  The last one is because I use dired a lot and sometimes I want to see those buffers and sometimes I don't.  I'll write the function in an "ignore this buffer" way since the packages I use work that way, but it's easy enough to invert the sense if you want:

(defvar my-bs-always-show-regexps '("\\*\\(scratch\\|info\\|grep\\|compilation\\)\\*")
  "*Buffer regexps to always show when buffer switching.")
(defvar my-bs-never-show-regexps '("^\\s-" "^\\*" "TAGS$")
  "*Buffer regexps to never show when buffer switching.")
(defvar my-ido-ignore-dired-buffers t
  "*If non-nil, buffer switching should ignore dired buffers.")

(defun my-bs-str-in-regexp-list (str regexp-list)
  "Return non-nil if str matches anything in regexp-list."
  (let ((case-fold-search nil))
    (catch 'done
      (dolist (regexp regexp-list)
        (when (string-match regexp str)
          (throw 'done t))))))

(defun my-bs-ignore-buffer (name)
  "Return non-nil if the named buffer should be ignored."
  (or (and (not (my-bs-str-in-regexp-list name my-bs-always-show-regexps))
           (my-bs-str-in-regexp-list name my-bs-never-show-regexps))
      (and my-ido-ignore-dired-buffers
           (save-excursion
             (set-buffer name)
             (equal major-mode 'dired-mode)))))

This is set up to ignore all buffers that start with a space or '*', except for scratch, info, grep, and compilation buffers.  Dired buffers are also ignored.

The function to toggle between the two most recently used buffers is easy enough:

(defun my-bs-toggle ()
  "Toggle buffers, ignoring certain ones."
  (interactive)
  (catch 'done
    (dolist (buf (buffer-list))
      (unless (or (equal (current-buffer) buf)
                  (my-bs-ignore-buffer (buffer-name buf)))
        (switch-to-buffer buf)
        (throw 'done t)))))

I use ido to switch buffers by name:

(setq ido-ignore-buffers '(my-bs-ignore-buffer))

I like bs for getting a list of buffers to choose from:

(setq bs-configurations
      '(("all" nil nil nil nil nil)
        ("files" nil nil nil (lambda (buf) (my-bs-ignore-buffer (buffer-name buf))) nil)))
(setq bs-cycle-configuration-name "files")

This sets up two bs configurations, one that shows all the buffers and one that only shows my subset.  Somewhat off-topic, I like bs but not the default look ... too much information.  Here's my simplified version:

(setq bs-mode-font-lock-keywords
  (list
   ; Headers
   (list "^[ ]+\\([-M].*\\)$" 1 font-lock-keyword-face)
   ; Boring buffers
   (list "^\\(.*\\*.*\\*.*\\)$" 1 font-lock-comment-face)
   ; Dired buffers
   '("^[ .*%]+\\(Dired.*\\)$" 1 font-lock-type-face)
   ; Modified buffers
   '("^[ .]+\\(\\*\\)" 1 font-lock-warning-face)
   ; Read-only buffers
   '("^[ .*]+\\(\\%\\)" 1 font-lock-variable-name-face)))

(setq bs-attributes-list
      (quote (("" 2 2 left bs--get-marked-string)
              ("M" 1 1 left bs--get-modified-string)
              ("R" 2 2 left bs--get-readonly-string)
              ("" 2 2 left "  ")
              ("Mode" 16 16 left bs--get-mode-name)
              ("" 2 2 left "  ")
              ("Buffer" bs--get-name-length 30 left bs--get-name))))

Finally, to flip sequentially through buffers (like Alt-Tab in a window manager) I use iflipb:

(setq iflipb-boring-buffer-filter 'my-bs-ignore-buffer)

Now no matter how I switch buffers I get a consistent set to choose from, and if I want to change that set I can do it in one place.