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. 7: 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)) 12: 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