Here’s a quick rant, kinda.
I’ve become a huge fan of structural editing recently. My structural editing journey was maybe a bit unusual, but I’ve finally settled on Smartparens for quite some time. Originally I was introduced to structural editing by Parinfer mode which had some Paredit functionality, so it got me interested. Then I’ve been using Parinfer-rust for some time, but when I got the job in Clojure, I’ve realized that it has a major problem of generating too much noise in the Git in commits because it needs to reformat the whole buffer to work, which is way too aggressive, especially when working with multiple people. I’ve already written a small article about parentheses, and various structural editing solutions in this blog, but I’ve actually changed my mind regarding Paredit style editing since that time. Learning some additional shortcuts and chords was not as bad as I’ve thought it would be.
So I’ve switched to Paredit, but it had some bugs related to Clojure, and I’ve replaced it with Smartparens with Paredit keybindings, and I was basically (mostly) satisfied ever since. But recently I’ve read Mastering Emacs (great book) and decided to greatly simplify my Emacs config. I was tempted to replace Smartparens with Paredit because it felt simpler and more focused, but I’ve figured out that it breaks the tempo1. I’ve tried to fix most shortcomings of Paredit, but it felt too clunky, and I’ve dropped the idea, since Smartparens doesn’t have such problems, so I’ve returned to it. Funnily enough, the sole reason I wanted to replace Smartparens with Paredit was that I felt that the configuration is way too complicated, but in the end, with all these fixes, Paredit’s configuration was even longer than the one I had for Smartparens.
So here are some problems with Paredit that I’ve noticed after reading Mastering Emacs:
Most commands that should accept negative (or just an arbitrary prefix argument) due to being mapped on a key that usually accepts it, completely ignore it instead.
M-- M-dthat acts as
M-backspacein most editing modes, just ignores the negative argument in Paredit. I’ve managed to fix that with the following advice, but I’m not sure that every such command can be fixed this way, as there might be not counterpart command. This aspect of modifying commands with the (negative) argument became really important to me, because I’ve started using more motion-related commands, and Paredit basically prevented me from becoming more efficient.
(defvar aorst--paredit-reversable-commands '((paredit-forward-delete . paredit-backward-delete) (paredit-forward-kill-word . paredit-backward-kill-word)) "Alist of `paredit-mode' command and their backward motion equivalents.") (defun aorst/paredit-support-negative-arg (orig-fn &rest args) "Find ORIG-FN in `aorst--paredit-reversable-commands' variable, and apply it's counterpart, when the `current-prefix-arg' is negative." (if (eq current-prefix-arg '-) (let (current-prefix-arg) (if-let ((f (cdr (assoc this-command aorst--paredit-reversable-commands)))) (apply f args) (if-let ((f (car (rassoc this-command aorst--paredit-reversable-commands)))) (apply f args) (apply orig-fn args)))) (apply orig-fn args))) (dolist (f (flatten-list aorst--paredit-reversable-commands)) (advice-add f :around #'aorst/paredit-support-negative-arg))
Transient mark mode deletion feature that was added in Emacs 24 is purposely ignored by Paredit.
This again can be fixed with advice, and actually works reliably enough, making me only wonder why Paredit maintainers decided to put censored swearing in the comment instead of implementing the feature itself. Paredit has everything needed to do this, and yet chose to ignore the feature. It’s easy to fix this with advice, but again, Smartparens does respect Emacs design decisions. Here’s the code I’ve wrote to fix this:
(defvar aorst--paredit-delete-region-functions '(paredit-forward-delete paredit-backward-delete) "List of `paredit-mode' functions that should support tmm region deletion.") (defvar aorst--paredit-kill-region-functions '(paredit-forward-kill-word paredit-backward-kill-word) "List of `paredit-mode' functions that should support tmm region killing.") (defun aorst/paredit-fix-tmm (orig-fn &rest args) "Allow deleting/killing a region if expression is balanced." (if (and transient-mark-mode mark-active) (cond ((memq this-command aorst--paredit-delete-region-functions) (paredit-delete-region (region-beginning) (region-end))) ((memq this-command aorst--paredit-kill-region-functions) (paredit-kill-region (region-beginning) (region-end))) (t (apply orig-fn args))) (apply orig-fn args))) (dolist (f (append aorst--paredit-delete-region-functions aorst--paredit-kill-region-functions)) (advice-add f :around #'aorst/paredit-fix-tmm))
In the end, after simplifying my config, I’ve completely turned off the transient mark mode, as it is really not as important as I’ve thought. So maybe this is why Paredit authors didn’t bothered to implement this feature, but I still think it’s not an excuse not to support it, some users may rely on it. And I still occasionally enable
C-space C-spaceshortcut, so support for this feature is still important to me.
Point position is wrong after using splice commands in inferior-lisp type REPLs and any other REPLs I’ve tried so far.
This actually looks like a bug, but I’ve also noticed that not a lot of people use structural editing in the REPL (maybe because of that?). It’s a minor inconvenience, however, I’ve had zero problems with Smartparens strict mode enabled in REPLs so far. Which is very important to me, as I often write code directly in the REPL buffer, so I’d like to have all my structural editing features. Sadly, I wasn’t able to fix this myself.
All this may sound like I’m praising the Smartparens here, but it’s actually not true. Smartparens has a lot of its own quirks and problems, that I had to deal with. It is by no means a silver bullet, but in the current state it’s good enough, I guess.
My dream package would be something based off Tree-sitter (or any other blazingly fast parser) because it actually understands the structure of the code unlike Smartparens, which uses the regex-based search for that.
Yes, regex can be used for implementing structural editing, and it works to some degree for languages other than Lisps, as shown by Smartparens, but it’s not near as accurate, as it would be with a proper parser.
I’ve tried my best at implementing an algorithm that implements a backward syntax parser for Elixir to make Smartparens better understand the
do: keyword, as it doesn’t require matching
end keyword, which broke the parser.
It kinda works, but still breaks on some combination of symbols in the code, so it’s not nearly as good as a solution with a proper parser.
However even with its quirks, Smartparens with the Paredit compatibility layer, and respecting of Emacs editing model, makes it overall a better Emacs citizen than Paredit. Yes, it’s somewhat clunkier to configure, but in the end, you have more EMACSy structural editing than you get with Paredit itself, and I actually don’t really understand why is that, as Paredit is older, and was used by Emacs hackers for quite a long time before Smartparens appeared. If anyone knows the story behind that, please let me know!
If you’re interested to try out Smartparens, here’s my configuration, that makes it work similarly to Paredit in most aspects, so you don’t have to re-learn your muscle memory. I’m suggesting you to try out Smartparens for yourself, ans see if these features I’ve talked about are really worth the switch. It was for me for sure.
Tempo is the term from the Mastering Emacs book, which basically means that you don’t need to release Ctrl or Meta keys for some chord progressions, like when using
M-- M-dinstead of
C-- M-d. ↩︎