From 8d8a1a77394fd4fe17a43572cebd4fbe61db8527 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 20 Feb 2026 14:46:45 +0200 Subject: [PATCH] [Fix #649] Preserve arglist metadata when adding arity clojure-add-arity was leaving metadata annotations (^String, ^:keyword, ^{...}) stranded between the defn name and arity forms. Now metadata stays attached to its arglist inside the arity wrapper. --- CHANGELOG.md | 1 + clojure-mode.el | 43 +++++++++++++++++--- test/clojure-mode-refactor-add-arity-test.el | 42 ++++++++++++++++++- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daf08b3b..aade0642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ### Bugs fixed * [#402](https://github.com/clojure-emacs/clojure-mode/issues/402): Font-lock protocol method docstrings with `font-lock-doc-face`. +* [#649](https://github.com/clojure-emacs/clojure-mode/issues/649): Fix `clojure-add-arity` severing arglist metadata (`^String`, `^:keyword`, `^{...}`) when converting single-arity to multi-arity. * Fix typos in `clojure-mode-extra-font-locking`: `halt-when?` -> `halt-when`, `simple-indent?` -> `simple-ident?`. * Fix `doc` and `find-doc` misplaced under `clojure.core` instead of `clojure.repl` in extra font-locking. diff --git a/clojure-mode.el b/clojure-mode.el index 7ff2a16d..64137bc1 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -3225,6 +3225,22 @@ Assumes cursor is at beginning of function." (insert "[") (save-excursion (insert "])\n(" (match-string 0)))) +(defun clojure--find-arglist-metadata-start (bracket-pos) + "Return the start of metadata annotations before BRACKET-POS. +If no metadata is found, return BRACKET-POS. +Handles ^Type, ^:keyword, ^{:key val}, and chains like ^:private ^String. +Relies on ^ having prefix syntax (\\=') in the Clojure syntax table, +which makes `backward-sexp' from `[' treat `^String [' as two sexps." + (save-excursion + (goto-char bracket-pos) + (let ((result bracket-pos)) + (ignore-errors + (while (progn + (backward-sexp) + (eq (char-after) ?^)) + (setq result (point)))) + result))) + (defun clojure--add-arity-internal () "Add an arity to a function. @@ -3242,14 +3258,31 @@ Assumes cursor is at beginning of function." (save-excursion (insert "])\n("))) ((looking-back "\\[" 1) ;; single-arity fn (let* ((same-line (= beg-line (line-number-at-pos))) - (new-arity-text (concat (when same-line "\n") "(["))) + (bracket-pos (1- (point))) + (meta-start (clojure--find-arglist-metadata-start bracket-pos))) (save-excursion (goto-char end) (insert ")")) - - (re-search-backward " +\\[") - (replace-match new-arity-text) - (save-excursion (insert "])\n(["))))))) + (if (< meta-start bracket-pos) + ;; Has metadata before arglist — move it inside the arity + ;; wrapper so it stays associated with the original arglist. + ;; E.g. (defn foo ^String [x] ...) becomes: + ;; (defn foo ([]) (^String [x] ...)) + (let ((meta-text (replace-regexp-in-string + "[ \t\n\r]+" " " + (string-trim + (buffer-substring meta-start bracket-pos))))) + (goto-char meta-start) + (skip-chars-backward " \t\n") + (delete-region (point) (1+ bracket-pos)) + (insert "\n([") + (save-excursion + (insert "])\n(" meta-text " ["))) + ;; No metadata — original behavior + (let ((new-arity-text (concat (when same-line "\n") "(["))) + (re-search-backward " +\\[") + (replace-match new-arity-text) + (save-excursion (insert "])\n(["))))))))) ;;;###autoload (defun clojure-add-arity () diff --git a/test/clojure-mode-refactor-add-arity-test.el b/test/clojure-mode-refactor-add-arity-test.el index 5f1c5fb9..59824826 100644 --- a/test/clojure-mode-refactor-add-arity-test.el +++ b/test/clojure-mode-refactor-add-arity-test.el @@ -72,9 +72,47 @@ body)" "(defn foo - ^{:bla \"meta\"} ([|]) - ([arg] + (^{:bla \"meta\"} [arg] + body))" + + (clojure-add-arity)) + + (when-refactoring-with-point-it "should handle a single-arity defn with ^Type metadata" + "(defn string + ^String + |[x] + (str x))" + + "(defn string + ([|]) + (^String [x] + (str x)))" + + (clojure-add-arity)) + + (when-refactoring-with-point-it "should handle a single-arity defn with ^:keyword metadata" + "(defn fo|o + ^:private + [arg] + body)" + + "(defn foo + ([|]) + (^:private [arg] + body))" + + (clojure-add-arity)) + + (when-refactoring-with-point-it "should handle a single-arity defn with multiple metadata" + "(defn fo|o + ^:private ^String + [arg] + body)" + + "(defn foo + ([|]) + (^:private ^String [arg] body))" (clojure-add-arity))