2022-09-17 Safe killing with Emacs

Some time ago Samuel Wales had a very interesting question on the Emacs mailing list. He sometimes kills some portion of text to yank it elsewhere, but gets distracted and forgets about the yanking. This way, the text is lost. He basically wants to make the “kill-yank” cycle atomic so that you never end up in the intermediate step where the information is lost.

It is obviously very difficult (maybe even impossible) to implement this in Emacs in full generality – what if you follow C-y with an M-y and the yanked text gets lost again? what if you undo yanking? etc. – but something can be done for sure. It seems to be a handy convention that every killing command in Emacs eventually calls kill-region, and that the function responsible for yanking is, well, yank. (In most modes it is bound to C-y, but Org-mode, for example, binds C-y to org-yank. Fortunately, it eventually calls yank anyway. Not all modes’ versions of yank do that, though – for example, lispy-yank does not.)

So, one way of solving our problem is to advise kill-region and yank. What I basically need is for kill-region to act as copy-region-as-kill (this is the function called by M-w – well, it actually calls kill-ring-save, but copy-region-as-kill does the heavy lifting) and then remember the text copied, so that our advised yank will know what to delete.

(define-minor-mode safe-kill-mode
  "Toggle Safe Kill mode.
In this mode, `C-w' highlights the killed text instead of
deleting it, and `C-y' deletes it.  This way, if you kill a text
but forget to yank it, it stays where it were.

Note: this is just a proof-of-concept and there are many ways to
trick this mode and actually lose the killed text, so please take
\"safe\" with a grain of salt."
  :global t
  :lighter " SafeKill"
  (if safe-kill-mode
      (progn
	(advice-add 'kill-region :around #'kill-region-safely)
	(advice-add 'yank :after #'delete-on-yanking))
    (advice-remove 'kill-region #'kill-region-safely)
    (advice-remove 'yank #'delete-on-yanking)
    (when (overlayp safe-kill-overlay)
      (delete-overlay safe-kill-overlay)
      (setq safe-kill-overlay nil))))

(defvar safe-kill-overlay nil
  "An overlay used by `safe-kill-mode' to highlight the \"killed\" text.")

(defun kill-region-safely (beg end &optional region orig-function)
  "Mark the region to be killed, but don't delete it."
  (if (not region)      
      (funcall orig-function beg end region)
    (copy-region-as-kill beg end region)
    (when (overlayp safe-kill-overlay)
      (delete-overlay safe-kill-overlay))
    (setq safe-kill-overlay (make-overlay (region-beginning) (region-end)))
    (overlay-put safe-kill-overlay 'face 'shadow)))

(defun delete-on-yanking (&optional arg)
  "Delete the previously \"killed\" text when yanking."
  (when (and (not arg)
	     (overlayp safe-kill-overlay))
    (with-current-buffer
	(overlay-buffer safe-kill-overlay)
      (delete-region (overlay-start safe-kill-overlay)
		     (overlay-end safe-kill-overlay)))
    (delete-overlay safe-kill-overlay))
  (setq safe-kill-overlay nil))

Please note that this is a proof-of-concept and the word “safe” is an exaggeration – I can’t even think of numerous ways you could be tricked into false feeling of security by this code. Still, the fact that it took me less than an hour to code this (it would be much faster if not for some research I did until I settled on the idea of advice) is yet another testament to the unbelievable flexibility of Emacs.

Also, let me mention that if you want to learn to code things like that yourself, probably the best resource to start with is the late Robert J. Chassell’s excellent An introduction to programming in Emacs Lisp. And if you want to go further, my own book on Emacs Lisp is a possible next step after that.

CategoryEnglish, CategoryBlog, CategoryEmacs