From b3cdb9dbb2516e5f70935e4f9901fd3a955e4882 Mon Sep 17 00:00:00 2001 From: yuhan0 Date: Wed, 7 Apr 2021 05:19:06 +0800 Subject: [PATCH 1/6] Convert #() fn shorthand --- clojure-mode.el | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/clojure-mode.el b/clojure-mode.el index 599a3126..b838f930 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -2769,6 +2769,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: "))) +;;; Shorthand fn conversion +(defun clojure--gather-shorthand-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-shorthand-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-convert-shorthand-fn () + "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 "#(") + (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-shorthand-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-shorthand-arg n name end))) + (number-sequence 1 arity)) + (when vararg + (insert " & ") + (let ((name (read-string "Name of variadic argument: "))) + (insert name) + (clojure--substitute-shorthand-arg '& name end))))) + (user-error "No #() shorthand at point!"))) ;;; Renaming ns aliases From 430baf6ff00fa286e9726cc45baed4fc4c3ce7f4 Mon Sep 17 00:00:00 2001 From: yuhan0 Date: Wed, 7 Apr 2021 05:20:14 +0800 Subject: [PATCH 2/6] Add tests for shorthand fn --- clojure-mode.el | 2 +- test/clojure-mode-convert-fn-test.el | 72 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 test/clojure-mode-convert-fn-test.el diff --git a/clojure-mode.el b/clojure-mode.el index b838f930..57049d9e 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -2807,7 +2807,7 @@ END marks the end of the fn expression" (when-let (beg (clojure-string-start)) (goto-char beg)) (if (or (looking-at-p "#(") - (forward-char 1) + (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-shorthand-args)) diff --git a/test/clojure-mode-convert-fn-test.el b/test/clojure-mode-convert-fn-test.el new file mode 100644 index 00000000..401a7be8 --- /dev/null +++ b/test/clojure-mode-convert-fn-test.el @@ -0,0 +1,72 @@ +;;; clojure-mode-convert-fn-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 . + +;;; Commentary: + +;; Tests for clojure-convert-shorthand-fn + +;;; Code: + +(require 'clojure-mode) +(require 'buttercup) + +(describe "clojure-convert-shorthand-fn" + :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-convert-shorthand-fn)) + + (when-refactoring-it "should convert 1-arg fns" + "#(= % 1)" + "(fn [x] (= x 1))" + (setq names '("x")) + (clojure-convert-shorthand-fn)) + + (when-refactoring-it "should convert 2-arg fns" + "#(conj (pop %1) (assoc (peek %1) %2 (* %2 %2)))" + "(fn [acc x] (conj (pop acc) (assoc (peek acc) x (* x x))))" + (setq names '("acc" "x")) + (clojure-convert-shorthand-fn)) + + (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-convert-shorthand-fn)) + + (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-convert-shorthand-fn))) + + +(provide 'clojure-mode-convert-fn-test) + + +;;; clojure-mode-convert-fn-test.el ends here From bec0de2f4476a8899ee0957814ba685e17c2ef68 Mon Sep 17 00:00:00 2001 From: yuhan Date: Tue, 16 Nov 2021 16:28:01 +0800 Subject: [PATCH 3/6] Rename convert-shorthand-fn -> promote-fn-literal --- clojure-mode.el | 16 +++++++-------- ...> clojure-mode-promote-fn-literal-test.el} | 20 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) rename test/{clojure-mode-convert-fn-test.el => clojure-mode-promote-fn-literal-test.el} (79%) diff --git a/clojure-mode.el b/clojure-mode.el index 57049d9e..518fc17b 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -2769,8 +2769,8 @@ 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: "))) -;;; Shorthand fn conversion -(defun clojure--gather-shorthand-args () +;;; 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 %&." @@ -2790,7 +2790,7 @@ VARARG is a boolean of whether it takes a variable argument %&." (string-to-number s)))))))) (cons arity vararg)))) -(defun clojure--substitute-shorthand-arg (arg sub end) +(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" @@ -2801,7 +2801,7 @@ END marks the end of the fn expression" (not (clojure--in-string-p))) (replace-match sub)))))) -(defun clojure-convert-shorthand-fn () +(defun clojure-promote-fn-literal () "Convert a #(...) function into (fn [...] ...), prompting for the argument names." (interactive) (when-let (beg (clojure-string-start)) @@ -2810,7 +2810,7 @@ END marks the end of the fn expression" (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-shorthand-args)) + (argspec (clojure--gather-fn-literal-args)) (arity (car argspec)) (vararg (cdr argspec))) (delete-char 1) @@ -2822,14 +2822,14 @@ END marks the end of the fn expression" (let ((name (read-string (format "Name of argument %d: " n)))) (when (/= n 1) (insert " ")) (insert name) - (clojure--substitute-shorthand-arg n name end))) + (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-shorthand-arg '& name end))))) - (user-error "No #() shorthand at point!"))) + (clojure--substitute-fn-literal-arg '& name end))))) + (user-error "No #() literal at point!"))) ;;; Renaming ns aliases diff --git a/test/clojure-mode-convert-fn-test.el b/test/clojure-mode-promote-fn-literal-test.el similarity index 79% rename from test/clojure-mode-convert-fn-test.el rename to test/clojure-mode-promote-fn-literal-test.el index 401a7be8..07b5dba9 100644 --- a/test/clojure-mode-convert-fn-test.el +++ b/test/clojure-mode-promote-fn-literal-test.el @@ -1,4 +1,4 @@ -;;; clojure-mode-convert-fn-test.el --- Clojure Mode: convert fn syntax -*- lexical-binding: t; -*- +;;; clojure-mode-promote-fn-literal-test.el --- Clojure Mode: convert fn syntax -*- lexical-binding: t; -*- ;; This file is not part of GNU Emacs. @@ -17,14 +17,14 @@ ;;; Commentary: -;; Tests for clojure-convert-shorthand-fn +;; Tests for clojure-promote-fn-literal ;;; Code: (require 'clojure-mode) (require 'buttercup) -(describe "clojure-convert-shorthand-fn" +(describe "clojure-promote-fn-literal" :var (names) (before-each @@ -34,19 +34,19 @@ (when-refactoring-it "should convert 0-arg fns" "#(rand)" "(fn [] (rand))" - (clojure-convert-shorthand-fn)) + (clojure-promote-fn-literal)) (when-refactoring-it "should convert 1-arg fns" "#(= % 1)" "(fn [x] (= x 1))" (setq names '("x")) - (clojure-convert-shorthand-fn)) + (clojure-promote-fn-literal)) (when-refactoring-it "should convert 2-arg fns" - "#(conj (pop %1) (assoc (peek %1) %2 (* %2 %2)))" + "#(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-convert-shorthand-fn)) + (clojure-promote-fn-literal)) (when-refactoring-it "should convert variadic fns" ;; from https://hypirion.com/musings/swearjure @@ -55,7 +55,7 @@ "(fn [v & vs] (* (`[~@vs] (+)) ((v (+)) v (- (`[~@vs] (+)) (*)))))" (setq names '("v" "vs")) - (clojure-convert-shorthand-fn)) + (clojure-promote-fn-literal)) (when-refactoring-it "should ignore strings and comments" "#(format \"%2\" ;; FIXME: %2 is an illegal specifier @@ -63,10 +63,10 @@ "(fn [_ _ _ _ _ _ id] (format \"%2\" ;; FIXME: %2 is an illegal specifier id)) " (setq names '("_" "_" "_" "_" "_" "_" "id")) - (clojure-convert-shorthand-fn))) + (clojure-promote-fn-literal))) (provide 'clojure-mode-convert-fn-test) -;;; clojure-mode-convert-fn-test.el ends here +;;; clojure-mode-promote-fn-literal-test.el ends here From 7aba6607d6d36388cd09d74062bb11b858ea8164 Mon Sep 17 00:00:00 2001 From: yuhan Date: Tue, 16 Nov 2021 16:29:50 +0800 Subject: [PATCH 4/6] Add "P" binding to refactoring map --- clojure-mode.el | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clojure-mode.el b/clojure-mode.el index 518fc17b..c3019df6 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) From a85c0792da651c219285726a9a9c2efdb0fd2654 Mon Sep 17 00:00:00 2001 From: yuhan Date: Sat, 20 Nov 2021 02:44:57 +0800 Subject: [PATCH 5/6] Add changelaog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e25574..0a08b4b5 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 From c1cc7a56295c6c51ed8175b42c55c1826b9bca52 Mon Sep 17 00:00:00 2001 From: yuhan Date: Sat, 20 Nov 2021 02:47:45 +0800 Subject: [PATCH 6/6] Add menu entry --- clojure-mode.el | 1 + 1 file changed, 1 insertion(+) diff --git a/clojure-mode.el b/clojure-mode.el index c3019df6..0c782b49 100644 --- a/clojure-mode.el +++ b/clojure-mode.el @@ -286,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]