A quick function to delete whitespace in Lisp programs
I’ve recently found myself constantly introducing – and then deleting – whitespace when writing Lisp. A quick bit of Emacs hacking fixed it.
I’ve started using paredit as a structure editor for Lisp programs. Once I got used to the movement commands it’s greatly speeded-up my editing. However, when I’m editing functions, I find that I introduce a lot of extraneous whitespace when I’m moving forms around. Mostly this comes when I leave myself space at the end of a function to add something later, or when I delete or move code around.
Now deleting whitespace is hardly the most time-consuming of tasks, especially as paredit won’t let you accidentally remove a bracket in the process. But in keeping with the Emacs philosophy of always automating everything that even slightly annoys you, I started wondering: can I delete whitespace more effectively? Specifically, can I delete all the whitespace from a closing bracket back to the code that it closes, in one command?
paredit has a function that sort of does what I had in mind:
(defun paredit-delete-leading-whitespace ()
;; This assumes that we're on the closing delimiter already.
(save-excursion
(backward-char)
(while (let ((syn (char-syntax (char-before))))
(and (or (eq syn ?\ ) (eq syn ?-)) ; whitespace syntax
;; The above line is a perfect example of why the
;; following test is necessary.
(not (paredit-in-char-p (1- (point))))))
(delete-char -1))))
There are a couple of problems, though. The first is that it isn’t interactive, so it can’t be bound to a key. It also won’t cross a line boundary, whereas a lot of my code seems to end up with blank lines in it. (Maybe I should just get tidier….)
My first attempt corrected these two shortcomings:
(defun sd/paredit-delete-whitespace ()
(interactive)
(save-excursion
(while (let ((syn (char-syntax (char-before))))
(and (or (eq syn ?\ )
(eq syn ?-)
(eq (line-beginning-position) (point)))
(not (paredit-in-char-p (1- (point))))))
(delete-char -1))))
The syn
variable gets the syntactic character class of the
character before point. Space and dash characters denote whitespace,
interchangeably. The third test checks whether we’re at the start of
a line, and the fourth checks that we’re not in the middle of a
character specification.
Of course, solving the original problem was never going to be enough. I immediately realised that it’d be better to delete whitespace both forwards and backwards, so that placing the cursor anywhere in a block of whitespace would remove it all.
As I did this I started wondering about those tests for being at the
beginning or end of a line. This seems like a thing one would
naturally want to do, but it requires an explicit comparison. Or
does it? – no, of course it doesn’t, obviously there are functions for that, bolp
and eolp
respectively. The end result was:
(defun sd/paredit-delete-whitespace ()
(interactive)
(save-excursion
(while (let ((syn (char-syntax (char-after))))
(and (or (eq syn ?\ )
(eq syn ?-)
(eolp)))
(not (paredit-in-char-p (1+ (point))))))
(forward-char))
(while (let ((syn (char-syntax (char-before))))
(and (or (eq syn ?\ )
(eq syn ?-)
(bolp)))
(not (paredit-in-char-p (1- (point))))))
(delete-char -1))))
The structure is the same, and the function will traverse over lines the other direction too. Binding it to a key made it operational:
(define-key paredit-mode-map
(kbd "C-M-<backspace>") #'sd/paredit-delete-whitespace)
In code like this, with point at the |
:
(list 1 2
|
)
pressing C-M-<backspace>
deletes all that annoying whitespace:
(list 1 2)
Success! And I was happy for about five minutes, until I tested against this:
(list 1 2 3
|
4 5)
and ended up with:
(list 1 2 34 5)
Disaster! Well, not exactly, but annoying: if the whitespace happens not to be closed by a bracket, there’s a danger of combining characters together and changing the code’s behaviour. Not part of the original problem specification, of course, but definitely undesirable.
Fortunately there’s a solution: teach the function some more Lisp. Specifically, if after we’ve deleted the whitespace we’re not looking at a closing bracket, we need to insert a space to avoid clashing symbols together. (That’s how little Lisp we need the function to know: or, to put it another way, how much Lisp’s lack of complicated syntax simplifies manipulating its code.) We can check for brackets the same way we checked for whitespace, using a character’s syntax class. Adding this logic, plus some documentation and comments, gave:
(defun sd/paredit-delete-whitespace ()
"Delete all whitespace around point.
Whitespace from point to the next non-whitespace symbol, and from point
back to the first non-whitespace symbol, is deleted. If doing so would
accidentally merge values then a single space is inserted. It is safe
to use this function within strings.
The implementation is based on `paredit-delete-leading-whitespace' but
is interactive, will cross line boundaries, and understands enough Lisp
to avoid accidents (hopefully)."
(interactive)
(save-excursion
;; move forward to the next non-whitespace symbol
(while (let ((syn (char-syntax (char-after))))
(and (or (eq syn ?\ ) ;; whitespace syntax classes
(eq syn ?-)
(eolp)) ;; line end
(not (paredit-in-char-p (1+ (point))))))
(forward-char))
;; delete whitespace back from current position
(while (let ((syn (char-syntax (char-before))))
(and (or (eq syn ?\ )
(eq syn ?-)
(bolp) ;; line start
(not (paredit-in-char-p (1- (point))))))
(delete-char -1))
;; if the current character isn't a closing bracket, and
;; we're not in a string, add a space so we don't accidentally
;; combine two numbers, symbols, strings, or whatever
(if (not (or (eq (char-syntax (char-after)) ?\))
(paredit-in-string-p)))
(insert " "))))
And finally I was happy. I won’t be surprised if I now discover that this functionality is built-in to paredit, or somewhere else in Emacs – but I won’t be upset either. It’s been a good learning experience.