smartparens
smartparens copied to clipboard
sp-backward-delete-word causes an error `args-out-of-range` when the kill ring is empty
Expected behavior
There should be no error when calling sp-backward-delete-word when the kill ring is empty.
Actual behavior
An error happened and it took me into recursive edit.
Steps to reproduce the problem
- Clean the kill ring using this function
(defun clean-kill-ring ()
"Use this to clean password after pasting it from a clipboard."
(interactive)
(setq kill-ring nil)
(garbage-collect))
- Call
sp-backward-delete-word. - Call
sp-backward-delete-wordagain. The error will happen this second time we call it.
Backtraces if necessary (M-x toggle-debug-on-error)
Debugger entered--Lisp error: (args-out-of-range 0 0)
get-text-property(0 yank-handler nil)
kill-append(#("aaa " 0 3 (fontified t)) t)
kill-region(6 3)
sp-backward-kill-symbol(1 t)
sp-backward-delete-symbol(1 t)
sp-backward-delete-word(1)
funcall-interactively(sp-backward-delete-word 1)
call-interactively(sp-backward-delete-word nil nil)
command-execute(sp-backward-delete-word)
Environment & version information
smartparensversion: 20211101.1101- Active
major-mode:lisp-interaction-mode - Smartparens strict mode: nil
- Emacs version (
M-x emacs-version): GNU Emacs 27.2 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.24.27, cairo version 1.17.4) of 2021-03-27 - Starterkit/Distribution: Vanilla
- OS: gnu/linux
I believe I know why this happens. I've had a related issue with deletion unexpectedly adding to the kill-ring. The sp-delete-* commands try to avoid touching the kill-ring by let-binding it, then delegating to kill-region.
On repeated deletes/kills the kill-region call chain eventually ends up in a call to add-to-history: (add-to-history 'kill-ring string kill-ring-max t). add-to-history looks up kill-ring's value with symbol-value, which bypasses lexical binding. So it ends up unexpectedly mangling the actual global kill-ring despite the let binding.
This is the same root cause, I believe, for #1040 and #1097. The best way to solve it is not clear. I have found saving (car kill-ring) and then actually setting it afterwards to work, but it seems drastic.
(let ((kill-ring-saved-car (car kill-ring)))
(sp-kill-symbol arg word)
(if (null kill-ring-saved-car)
(setq kill-ring nil)
(setcar kill-ring kill-ring-saved-car))
(Note I am writing this based on Emacs 27; I have not looked at other versions' kill-region implementations.)
I'm still on E26 and I can't find the code you are refering to. Can you tell me what is the function/file where the add-to-history call is found? I wonder why the implementation seemingly changed.
Sure thing! Here's the call chain that I see:
kill-region checks whether the last command was also a kill, and if so calls kill-append:
https://github.com/emacs-mirror/emacs/blob/emacs-27/lisp/simple.el#L4764-L4766
kill-append constructs the new kill ring value and calls kill-new:
https://github.com/emacs-mirror/emacs/blob/emacs-27/lisp/simple.el#L4648-L4650
kill-new, if its replace argument is non-nil, calls add-to-history:
https://github.com/emacs-mirror/emacs/blob/emacs-27/lisp/simple.el#L4624
Where symbol-value gets the dynamic kill-ring value:
https://github.com/emacs-mirror/emacs/blob/emacs-27/lisp/subr.el#L1975
I will try to compare with the emacs-26 branch later this evening.
I know I'm the devil for screenshotting code, but yea, here's the difference

I'm not sure what the best way to address this is. If you have a suggested or preferred approach, @Fuco1, I would be happy to try to come up with a patch.
-snips something that was my fault-
I'm in the middle of updating my emacs configuration, I followed the steps in OP and could not replicate in emacs 28.1. But i also remember encountering issues with various commands when the kill-ring was empty back then.
In the end I made my own version of sp-backward-delete-word in one oy my libraries which just uses vanilla emacs commands:
`(defun dd-delete-word (&optional arg) "Delete characters forward until encountering the end of a word. With argument ARG, do this that many times." (interactive "p") (delete-region (point) (progn (forward-word (or arg 1)) (point))))
(defun dd-backward-delete-word (&optional arg) "Delete characters backward until encountering the beginning of a word. With argument ARG, do this that many times." (interactive "p") (dd-delete-word (- (or arg 1))))`
Thanks, but your commands don't skip delimiters as smartparens does. With point at the beginning of '((foo bar) baz), smartparens forward delete produces
'(( bar) baz)
as opposed to dd-delete-word's
bar) baz)
I believe this is fixed by #1169 as I can no longer reproduce this and related kill/delete issues. I've added a test case for this issue so hopefully there won't be regressions.
The fix is working great, thank you, @Fuco1 !