branch: elpa/clojure-mode
commit 7d3c0c16e4aa14a051b393c249f0f4d307a2c74d
Author: yuhan0 <[email protected]>
Commit: GitHub <[email protected]>
Add refactoring command for converting #() shorthand to (fn ...) (#601)
---
CHANGELOG.md | 1 +
clojure-mode.el | 64 +++++++++++++++++++++++++
test/clojure-mode-promote-fn-literal-test.el | 72 ++++++++++++++++++++++++++++
3 files changed, 137 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45e2557..0a08b4b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
* Allow additional directories, beyond the default `clj[sc]`, to be correctly
formulated by `clojure-expected-ns` via new `defcustom` entitled
`clojure-directory-prefixes`
* Recognize babashka projects (identified by the presence of `bb.edn`).
+* [#601](https://github.com/clojure-emacs/clojure-mode/pull/601): Add new
command `clojure-promote-fn-literal` for converting #() function literals to
`fn` form
### Changes
diff --git a/clojure-mode.el b/clojure-mode.el
index 599a312..0c782b4 100644
--- a/clojure-mode.el
+++ b/clojure-mode.el
@@ -263,6 +263,8 @@ The prefixes are used to generate the correct namespace."
(define-key map (kbd "C--") #'clojure-toggle-ignore)
(define-key map (kbd "_") #'clojure-toggle-ignore-surrounding-form)
(define-key map (kbd "C-_") #'clojure-toggle-ignore-surrounding-form)
+ (define-key map (kbd "P") #'clojure-promote-fn-literal)
+ (define-key map (kbd "C-P") #'clojure-promote-fn-literal)
map)
"Keymap for Clojure refactoring commands.")
(fset 'clojure-refactor-map clojure-refactor-map)
@@ -284,6 +286,7 @@ The prefixes are used to generate the correct namespace."
["Toggle #_ ignore form" clojure-toggle-ignore]
["Toggle #_ ignore of surrounding form"
clojure-toggle-ignore-surrounding-form]
["Add function arity" clojure-add-arity]
+ ["Promote #() fn literal" clojure-promote-fn-literal]
("ns forms"
["Insert ns form at the top" clojure-insert-ns-form]
["Insert ns form here" clojure-insert-ns-form-at-point]
@@ -2769,6 +2772,67 @@ With a numeric prefix argument the let is introduced N
lists up."
(interactive)
(clojure--move-to-let-internal (read-from-minibuffer "Name of bound symbol:
")))
+;;; Promoting #() function literals
+(defun clojure--gather-fn-literal-args ()
+ "Return a cons cell (ARITY . VARARG)
+ARITY is number of arguments in the function,
+VARARG is a boolean of whether it takes a variable argument %&."
+ (save-excursion
+ (let ((end (save-excursion (clojure-forward-logical-sexp) (point)))
+ (rgx (rx symbol-start "%" (group (? (or "&" (+ (in "0-9")))))
symbol-end))
+ (arity 0)
+ (vararg nil))
+ (while (re-search-forward rgx end 'noerror)
+ (when (not (or (clojure--in-comment-p) (clojure--in-string-p)))
+ (let ((s (match-string 1)))
+ (if (string= s "&")
+ (setq vararg t)
+ (setq arity
+ (max arity
+ (if (string= s "") 1
+ (string-to-number s))))))))
+ (cons arity vararg))))
+
+(defun clojure--substitute-fn-literal-arg (arg sub end)
+ "ARG is either a number or the symbol '&.
+SUB is a string to substitute with, and
+END marks the end of the fn expression"
+ (save-excursion
+ (let ((rgx (format "\\_<%%%s\\_>" (if (eq arg 1) "1?" arg))))
+ (while (re-search-forward rgx end 'noerror)
+ (when (and (not (clojure--in-comment-p))
+ (not (clojure--in-string-p)))
+ (replace-match sub))))))
+
+(defun clojure-promote-fn-literal ()
+ "Convert a #(...) function into (fn [...] ...), prompting for the argument
names."
+ (interactive)
+ (when-let (beg (clojure-string-start))
+ (goto-char beg))
+ (if (or (looking-at-p "#(")
+ (ignore-errors (forward-char 1))
+ (re-search-backward "#(" (save-excursion (beginning-of-defun)
(point)) 'noerror))
+ (let* ((end (save-excursion (clojure-forward-logical-sexp)
(point-marker)))
+ (argspec (clojure--gather-fn-literal-args))
+ (arity (car argspec))
+ (vararg (cdr argspec)))
+ (delete-char 1)
+ (save-excursion (forward-sexp 1) (insert ")"))
+ (save-excursion
+ (insert "(fn [] ")
+ (backward-char 2)
+ (mapc (lambda (n)
+ (let ((name (read-string (format "Name of argument %d: "
n))))
+ (when (/= n 1) (insert " "))
+ (insert name)
+ (clojure--substitute-fn-literal-arg n name end)))
+ (number-sequence 1 arity))
+ (when vararg
+ (insert " & ")
+ (let ((name (read-string "Name of variadic argument: ")))
+ (insert name)
+ (clojure--substitute-fn-literal-arg '& name end)))))
+ (user-error "No #() literal at point!")))
;;; Renaming ns aliases
diff --git a/test/clojure-mode-promote-fn-literal-test.el
b/test/clojure-mode-promote-fn-literal-test.el
new file mode 100644
index 0000000..07b5dba
--- /dev/null
+++ b/test/clojure-mode-promote-fn-literal-test.el
@@ -0,0 +1,72 @@
+;;; clojure-mode-promote-fn-literal-test.el --- Clojure Mode: convert fn
syntax -*- lexical-binding: t; -*-
+
+;; This file is not part of GNU Emacs.
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for clojure-promote-fn-literal
+
+;;; Code:
+
+(require 'clojure-mode)
+(require 'buttercup)
+
+(describe "clojure-promote-fn-literal"
+ :var (names)
+
+ (before-each
+ (spy-on 'read-string
+ :and-call-fake (lambda (_) (or (pop names) (error "")))))
+
+ (when-refactoring-it "should convert 0-arg fns"
+ "#(rand)"
+ "(fn [] (rand))"
+ (clojure-promote-fn-literal))
+
+ (when-refactoring-it "should convert 1-arg fns"
+ "#(= % 1)"
+ "(fn [x] (= x 1))"
+ (setq names '("x"))
+ (clojure-promote-fn-literal))
+
+ (when-refactoring-it "should convert 2-arg fns"
+ "#(conj (pop %) (assoc (peek %1) %2 (* %2 %2)))"
+ "(fn [acc x] (conj (pop acc) (assoc (peek acc) x (* x x))))"
+ (setq names '("acc" "x"))
+ (clojure-promote-fn-literal))
+
+ (when-refactoring-it "should convert variadic fns"
+ ;; from https://hypirion.com/musings/swearjure
+ "#(* (`[~@%&] (+))
+ ((% (+)) % (- (`[~@%&] (+)) (*))))"
+ "(fn [v & vs] (* (`[~@vs] (+))
+ ((v (+)) v (- (`[~@vs] (+)) (*)))))"
+ (setq names '("v" "vs"))
+ (clojure-promote-fn-literal))
+
+ (when-refactoring-it "should ignore strings and comments"
+ "#(format \"%2\" ;; FIXME: %2 is an illegal specifier
+ %7) "
+ "(fn [_ _ _ _ _ _ id] (format \"%2\" ;; FIXME: %2 is an illegal specifier
+ id)) "
+ (setq names '("_" "_" "_" "_" "_" "_" "id"))
+ (clojure-promote-fn-literal)))
+
+
+(provide 'clojure-mode-convert-fn-test)
+
+
+;;; clojure-mode-promote-fn-literal-test.el ends here