Recently I found myself killing and yanking more often than ever.
One problem with that is that the capitalization and punctuation of what I have yanked is often wrong. As a (pretty stupid) example, I might see the sentence “This is bad.”, and I might want to write another one: “He thinks that this is bad, and he’s right.” If I use kill-sentence (M-k) or backward-kill-sentence (C-x DEL), I have the whole This is bad. in the kill ring, but what I need to yank is this is bad (with lower-case “t” and without the period), so I need to make both changes after yanking. After several dozen such cases I thought, this is Emacs, I shouldn’t be doing this manually!
As is often the case, coding a feature where a text is transformed in a given way is pretty easy, but finding a good UI is not. I figured, however, that I could make it so pressing C-y for the second time could trigger the transformation. Yanking the same thing twice in a row does not happen very often, and if I really need to do it, I can just press C-y C-g C-y.
First, let’s enumerate the possible transformations. Making the first letter lower- or upper-case is the obvious one. Putting the whole yanked text in quotation marks is another. Stripping punctuation from its end is the last one I can think of. For now, I’ll just implement lower-casing the first letter and stripping punctuation.
If I were to code that several years ago, I’d use a hydra. Now that Emacs has transient built-in, I’ll try to use it – these days I tend to prefer built-in facilities over external packages. But first things first – let’s start with making C-y do something else than usual when pressed twice in a row. I could just advise yank (and if you read my blog regularly, you know I like advice!), but in this case, I don’t think this is the best idea. I have no idea how often yank is called from Elisp, and whenever it is, I don’t want to risk my code firing in such situations. So, I’ll choose the boring route and define my own command which I will then bind to C-y. This has one problem, though: in Org mode, where I might need this whole feature, C-y is bound to org-yank instead. For now, I’ll just ignore it and see whether I actually need my feature in Org mode, too; if so, I’ll just write an Org version of my enhanced yank and that’s it.
(defun mbork-yank-or-transform (&optional arg)
"Call `yank' or `mbork-transform-region-transient'.
The decision depends on whether `last-command' was `yank'."
(interactive "*P")
(if (eq last-command 'yank)
(mbork-transform-region-transient)
(yank arg)))
Now comes a harder part – defining the transient. I had a few issues with that. One was that I’ve never defined transients before, and that it was quite difficult for me to wrap my head around its manual. Luckily, by combining the manual, the Transient showcase, interrogating an LLM and trial-and-error, I was able to come up with a solution which actually worked:
(defvar mbork-transform-region-overlay nil
"An overlay used by `mbork-transform-region-transient'.")
(defun mbork-transform-region-make-overlay ()
"Highlight the region using `mbork-transform-region-overlay'."
(setq mbork-transform-region-overlay
(make-overlay (region-beginning) (region-end) nil nil t))
(overlay-put mbork-transform-region-overlay
'face
'secondary-selection))
(defun mbork-transform-region-delete-overlay ()
"Delete `mbork-transform-region-overlay'."
(remove-hook 'transient-exit-hook
#'mbork-transform-region-delete-overlay)
(delete-overlay mbork-transform-region-overlay))
(transient-define-prefix mbork-transform-region-transient ()
"Apply one or more transformations to the region."
[[("." "Remove punctuation"
mbork-transform-region-remove-punctuation
:transient t)
("l" "Lower-case the first letter"
mbork-transform-region-lowercase-first
:transient t)]
[("RET" "Quit"
ignore
:transient nil)]]
(interactive "*")
(add-hook 'transient-exit-hook
#'mbork-transform-region-delete-overlay)
(mbork-transform-region-make-overlay)
(transient-setup 'mbork-transform-region-transient))
I have a feeling that it’s not ideal – for example, exiting via ignore seems hackish, not to mention the hook stuff (which I needed to make sure the overlay is properly deleted, no matter whether the user exists via RET or C-g), but it works. The only thing remaining is to define the actual transformations – and that’s (of course) the easiest part, it’s just normal Elisp:
(defun mbork-transform-region-remove-punctuation ()
"Remove punctuation from the end of the region."
(interactive "*")
(save-excursion
(goto-char (1- (region-end)))
(when (looking-at-p "[.,!?]")
(delete-char 1))))
(defun mbork-transform-region-lowercase-first ()
"Make the first character of the region lowercase."
(interactive "*")
(downcase-region (region-beginning) (1+ (region-beginning))))
(By the way, the obligatory plug is that if “just normal Elisp” sounds like magic to you, and if you’d like to learn how you can easily modify Emacs to suit your needs like what I’m doing in this post, the best go-to source is the late Robert J. Chassell’s Introduction to Programming in Emacs Lisp – and if that is not enough for you, then my book about Emacs Lisp is envisioned as a next step after that. While it doesn’t cover transients, it consists of three progressively more complex features coded from scratch to production quality, with detailed explanations. Check it out!)
Now let’s just bind C-y to my command and I’m done!
# Don't do it like this. (bind-key "C-y" #'mbork-yank-or-transform text-mode-map)
Well, almost done. It turns out (rather expectedly) that Org mode is also derived from Text mode, so this overrides its normal org-yank binding, too. (Of course, if I ever need my command in Org mode, too, I’ll just write a similar mbork-org-yank-or-transform and bind it there.) Happily, there is a simple solution:
(bind-key [remap yank] #'mbork-yank-or-transform text-mode-map)
This way my command replaces any keybinding normally mapped to yank, including, for instance, S-<insert> etc., but leaves org-yank alone.
And that’s all there is to it. The only thing that’s left is that I’ll probably extend the transformation set over time – but that is another story.