Monday, March 22, 2010

Writing Emacs Commands That Work on Regions or the Entire Buffer

Some Emacs commands come in pairs: one for regions and one for the entire buffer (print-region / print-buffer, eval-region / eval-buffer, ispell-region / ispell-buffer, etc.), others have a command only for regions and require a C-x h to act on the entire buffer (downcase/upcase-region, many of the compile commands, and so on). But does it really have to be like that?

Consider this code snippet from a nice post on the excellent EMACS-FU:

1:  (defun djcb-count-words (&optional begin end)
2:    "count words between BEGIN and END (region); if no region defined, count words in buffer"
3:    (interactive "r")
4:    (let ((b (if mark-active begin (point-min)))
5:        (e (if mark-active end (point-max))))
6:      (message "Word count: %s" (how-many "\\w+" b e))))

This little gem is full of geeky goodness. The actual word counting happens on line 6—a nice implementation due to Rudolf Olah—but the important part for us happens on lines 3–5. Using (interactive "r") causes the point and mark to be passed to the function, smallest first. Thus if there is an active region, the parameters begin and end will specify its boundaries. If no region is defined—indicated by mark-active being nil—the function uses the beginning and end of the buffer as the boundaries for the how-many function. Otherwise it uses the region boundaries as specified in the begin and end parameters.

I really like this trick and I used it in several of my functions for a long time. Then one day I got an error: Emacs informed me that the mark was not set and therefore the (interactive "r") failed. This doesn't happen very often; you usually see it when you've just opened the file and haven't done anything to cause the mark to be set yet. Still, it was annoying so I wrote a macro to do the same thing without having to worry about whether the mark was set or not.

 1:  ;; Macro to take care of (interactive "r") with no mark set problem
 2:  (defmacro with-region-or-buffer (args &rest body)
 3:    "Execute BODY with BEG and END bound to the beginning and end of the
 4:  current region if one exists or the current buffer if not.
 5:  This macro replaces similar code using (interactive \"r\"), which
 6:  can fail when there is no mark set.
 8:  \(fn (BEG END) BODY...)"
 9:    `(let ((,(car args) (if mark-active (region-beginning) (point-min)))
10:           (,(cadr args) (if mark-active (region-end) (point-max))))
11:       ,@body))
13:  ;; Indent it properly
14:  (put 'with-region-or-buffer 'lisp-indent-function 1)

The \(fn (BEG END) BODY...) on line 8 provides a correctly formatted prototype of the macro for the documentation. Line 14 tells Emacs how to indent elisp code that uses the with-region-or-buffer macro.

Now if we write:

(with-region-or buffer (b e)
  (message "Word count: %s" (how-many "\\w+" b e)))

it will expand to

(let ((b (if mark-active (region-beginning) (point-min)))
      (e (if mark=active (region-end) (point-max))))
  (message "Word count: %s" (how-many "\\w+" b e)))

and count the number of words in the region, if there is one, or the entire buffer otherwise.

No comments:

Post a Comment