Skip to content

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

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
alexander-yakushev opened this issue Apr 15, 2025 · 6 comments
Open
Labels
bug Something isn't working

Comments

@alexander-yakushev
Copy link
Member

alexander-yakushev commented Apr 15, 2025

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

clojure-ts-mode version

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

tree-sitter-clojure grammar version

Not sure.

@alexander-yakushev alexander-yakushev added the bug Something isn't working label Apr 15, 2025
@rrudakov
Copy link
Contributor

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.

@bbatsov
Copy link
Member

bbatsov commented Apr 15, 2025

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

@rrudakov
Copy link
Contributor

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.

@bbatsov
Copy link
Member

bbatsov commented Apr 16, 2025

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

@rrudakov
Copy link
Contributor

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
Copy link
Contributor

@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)

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants