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