clojure-ts-mode icon indicating copy to clipboard operation
clojure-ts-mode copied to clipboard

paredit-kill freezes when killing the last form in the buffer

Open alexander-yakushev opened this issue 8 months ago • 6 comments

Expected behavior

When standing at the beginning of a form that is the last one in the buffer and pressing C-k (which is bound to paredit-kill when Paredit is enabled), the form should be killed like any other form.

Actual behavior

Emacs freezes and spins CPU at 100%. Pressing C-g gets it out of this, but the form still remains.

Steps to reproduce the problem

Go to any Clojure file, enable Paredit, perform paredit-kill on the last form, e.g.:

Image

Environment & Version information

  • MacOS Sequioia 15.3.2
  • Emacs 30.1.50 (built from https://github.com/jdtsmith/emacs-mac)
  • Latest clojure-ts-mode from unstable MELPA

clojure-ts-mode version

clojure-ts-mode 0.4.0-snapshot (package: 20250415.804)

tree-sitter-clojure grammar version

Not sure.

alexander-yakushev avatar Apr 15 '25 17:04 alexander-yakushev

Looks like a Emacs bug (or paredit bug).

it's only reproducible if there are one or more empty lines after the last expression. In normal clojure-mode when forward-sexp is called, the job is delegated to forward-sexp-default-function, which moves point to the end of the buffer if point is currently after the last sexp:

(defun forward-sexp-default-function (&optional arg)
  "Default function for `forward-sexp-function'."
  (goto-char (or (scan-sexps (point) arg) (buffer-end arg)))
  (if (< arg 0) (backward-prefix-chars)))

In clojure-ts-mode the job is done by treesit-forward-sexp, which doesn't move point to the end of the buffer, but keep it after the last closing paren of the last sexp.

When paredit-kill is called, at some point the function paredit-forward-sexps-to-kill calls forward-sexp until it reaches the end of the buffer:

(defun paredit-forward-sexps-to-kill (beginning eol)
  (let ((end-of-list-p nil)
        (firstp t))
    ;; Move to the end of the last S-expression that started on this
    ;; line, or to the closing delimiter if the last S-expression in
    ;; this list is on the line.
    (catch 'return
      (while t
        ;; This and the `kill-whole-line' business below fix a bug that
        ;; inhibited any S-expression at the very end of the buffer
        ;; (with no trailing newline) from being deleted.  It's a
        ;; bizarre fix that I ought to document at some point, but I am
        ;; too busy at the moment to do so.
        (if (and kill-whole-line (eobp)) (throw 'return nil))
        (save-excursion
          (paredit-handle-sexp-errors (forward-sexp)
            (up-list)
            (setq end-of-list-p (eq (point-at-eol) eol))
            (throw 'return nil))
          (if (or (and (not firstp)
                       (not kill-whole-line)
                       (eobp))
                  (paredit-handle-sexp-errors
                      (progn (backward-sexp) nil)
                    t)
                  (not (eq (point-at-eol) eol)))
              (throw 'return nil)))
        (forward-sexp)
        (if (and firstp
                 (not kill-whole-line)
                 (eobp))
            (throw 'return nil))
        (setq firstp nil)))
    end-of-list-p))

I'm not sure on which level this issue should be fixed, ideally treesit-forward-sexp should be fully compatible with forward-sexp-default-function, so maybe we should report it to Emacs bug tracker, I doubt though, that the fix will be installed to Emacs-30.

rrudakov avatar Apr 15 '25 20:04 rrudakov

Yeah, I think this is an Emacs bug. Those are still quite common, when it comes to TreeSitter unfortunately.

bbatsov avatar Apr 15 '25 20:04 bbatsov

I've just checked, and it's not reproducible on the latest Emacs master. We use correct treesit-things-settings in clojure-ts-mode and forward-sexp-function is set to forward-sexp-list, which behaves in a compatible way with forward-sexp-default-function.

I was planning to report this to Emacs bug tracker, but now it's not necessary.

rrudakov avatar Apr 16 '25 11:04 rrudakov

@rrudakov Let's just add a note about this in the Caveats then.

bbatsov avatar Apr 16 '25 11:04 bbatsov

OK, will do. I tried to come up with some advice function to fix it for Emacs-30, but I couldn't do it quickly.

rrudakov avatar Apr 16 '25 11:04 rrudakov

@alexander-yakushev could you please try to add this to your init file and check if it solves the issue?

(defun treesit-fix (orig-fn &optional arg)
  (when (not (funcall orig-fn arg))
    (goto-char (buffer-end arg))))

(advice-add 'treesit-forward-sexp :around #'treesit-fix)

rrudakov avatar Apr 16 '25 11:04 rrudakov