Skip to content

Commit a1fdc69

Browse files
rrudakovbbatsov
authored andcommitted
Introduce clojure-ts-add-arity refactoring command
1 parent edf0d32 commit a1fdc69

6 files changed

+584
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [#90](https://github.com/clojure-emacs/clojure-ts-mode/pull/90): Introduce `clojure-ts-cycle-privacy`.
1717
- [#91](https://github.com/clojure-emacs/clojure-ts-mode/pull/91): Introduce `clojure-ts-cycle-keyword-string`.
1818
- [#92](https://github.com/clojure-emacs/clojure-ts-mode/pull/92): Add commands to convert between collections types.
19+
- [#93](https://github.com/clojure-emacs/clojure-ts-mode/pull/93): Introduce `clojure-ts-add-arity`.
1920

2021
## 0.3.0 (2025-04-15)
2122

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,13 @@ set. The following commands are available:
411411
- `clojure-ts-convert-collection-to-vector`
412412
- `clojure-ts-convert-collection-to-set`
413413

414+
### Add arity to a function or macro
415+
416+
`clojure-ts-add-arity`: Add a new arity to an existing single-arity or
417+
multi-arity function or macro. Function can be defined using `defn`, `fn` or
418+
`defmethod` form. This command also supports functions defined inside forms like
419+
`letfn`, `defprotol`, `reify` or `proxy`.
420+
414421
### Default keybindings
415422

416423
| Keybinding | Command |
@@ -427,6 +434,7 @@ set. The following commands are available:
427434
| `C-c C-r {` / `C-c C-r C-{` | `clojure-ts-convert-collection-to-map` |
428435
| `C-c C-r [` / `C-c C-r C-[` | `clojure-ts-convert-collection-to-vector` |
429436
| `C-c C-r #` / `C-c C-r C-#` | `clojure-ts-convert-collection-to-set` |
437+
| `C-c C-r a` / `C-c C-r C-a` | `clojure-ts-add-arity` |
430438

431439
### Customize refactoring commands prefix
432440

clojure-ts-mode.el

Lines changed: 184 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,10 @@ literals with regex grammar."
757757
"Return non-nil if NODE is a Clojure list."
758758
(string-equal "list_lit" (treesit-node-type node)))
759759

760+
(defun clojure-ts--vec-node-p (node)
761+
"Return non-nil if NODE is a Clojure vector."
762+
(string-equal "vec_lit" (treesit-node-type node)))
763+
760764
(defun clojure-ts--anon-fn-node-p (node)
761765
"Return non-nil if NODE is a Clojure function literal."
762766
(string-equal "anon_fn_lit" (treesit-node-type node)))
@@ -1471,6 +1475,27 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
14711475
(fill-paragraph justify)))
14721476
t))
14731477

1478+
(defun clojure-ts--list-node-sym-text (node &optional include-anon-fn-lit)
1479+
"Return text of the first child of the NODE if NODE is a list.
1480+
1481+
Return nil if the NODE is not a list or if the first child is not a
1482+
symbol. Optionally if INCLUDE-ANON-FN-LIT is non-nil, return the text
1483+
of the first symbol of a functional literal NODE."
1484+
(when (or (clojure-ts--list-node-p node)
1485+
(and include-anon-fn-lit
1486+
(clojure-ts--anon-fn-node-p node)))
1487+
(when-let* ((first-child (clojure-ts--node-child-skip-metadata node 0))
1488+
((clojure-ts--symbol-node-p first-child)))
1489+
(clojure-ts--named-node-text first-child))))
1490+
1491+
(defun clojure-ts--list-node-sym-match-p (node regex &optional include-anon-fn-lit)
1492+
"Return TRUE if NODE is a list and its first symbol matches the REGEX.
1493+
1494+
Optionally if INCLUDE-ANON-FN-LIT is TRUE, perform the same check for a
1495+
function literal."
1496+
(when-let* ((sym-text (clojure-ts--list-node-sym-text node include-anon-fn-lit)))
1497+
(string-match-p regex sym-text)))
1498+
14741499
(defconst clojure-ts--sexp-nodes
14751500
'("#_" ;; transpose-sexp near a discard macro moves it around.
14761501
"num_lit" "sym_lit" "kwd_lit" "nil_lit" "bool_lit"
@@ -1490,18 +1515,16 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
14901515

14911516
(defun clojure-ts--defun-node-p (node)
14921517
"Return TRUE if NODE is a function or a var definition."
1493-
(and (clojure-ts--list-node-p node)
1494-
(let ((sym (clojure-ts--node-child-skip-metadata node 0)))
1495-
(string-match-p (rx bol
1496-
(or "def"
1497-
"defn"
1498-
"defn-"
1499-
"definline"
1500-
"defrecord"
1501-
"defmacro"
1502-
"defmulti")
1503-
eol)
1504-
(clojure-ts--named-node-text sym)))))
1518+
(clojure-ts--list-node-sym-match-p node
1519+
(rx bol
1520+
(or "def"
1521+
"defn"
1522+
"defn-"
1523+
"definline"
1524+
"defrecord"
1525+
"defmacro"
1526+
"defmulti")
1527+
eol)))
15051528

15061529
(defconst clojure-ts--markdown-inline-sexp-nodes
15071530
'("inline_link" "full_reference_link" "collapsed_reference_link"
@@ -1727,19 +1750,23 @@ Forms between BEG and END are aligned according to
17271750

17281751
;;; Refactoring
17291752

1753+
(defun clojure-ts--parent-until (pred)
1754+
"Return the closest parent of node at point that satisfies PRED."
1755+
(when-let* ((node-at-point (treesit-node-at (point) 'clojure t)))
1756+
(treesit-parent-until node-at-point pred t)))
1757+
1758+
(defun clojure-ts--search-list-form-at-point (sym-regex &optional include-anon-fn-lit)
1759+
"Return the list node at point which first symbol matches SYM-REGEX.
1760+
1761+
If INCLUDE-ANON-FN-LIT is non-nil, this function may also return a
1762+
functional literal node."
1763+
(clojure-ts--parent-until
1764+
(lambda (node)
1765+
(clojure-ts--list-node-sym-match-p node sym-regex include-anon-fn-lit))))
1766+
17301767
(defun clojure-ts--threading-sexp-node ()
17311768
"Return list node at point which is a threading expression."
1732-
(when-let* ((node-at-point (treesit-node-at (point) 'clojure t)))
1733-
;; We don't want to match `cond->' and `cond->>', so we should define a very
1734-
;; specific regexp.
1735-
(let ((sym-regex (rx bol (* "some") "->" (* ">") eol)))
1736-
(treesit-parent-until node-at-point
1737-
(lambda (node)
1738-
(and (or (clojure-ts--list-node-p node)
1739-
(clojure-ts--anon-fn-node-p node))
1740-
(let ((first-child (treesit-node-child node 0 t)))
1741-
(clojure-ts--symbol-matches-p sym-regex first-child))))
1742-
t))))
1769+
(clojure-ts--search-list-form-at-point (rx bol (* "some") "->" (* ">") eol) t))
17431770

17441771
(defun clojure-ts--delete-and-extract-sexp ()
17451772
"Delete the surrounding sexp and return it."
@@ -1874,9 +1901,7 @@ With universal argument \\[universal-argument], fully unwinds thread."
18741901
(n)
18751902
(1)))
18761903
(if-let* ((threading-sexp (clojure-ts--threading-sexp-node))
1877-
(sym (thread-first threading-sexp
1878-
(treesit-node-child 0 t)
1879-
(clojure-ts--named-node-text))))
1904+
(sym (clojure-ts--list-node-sym-text threading-sexp t)))
18801905
(save-excursion
18811906
(let ((beg (thread-first threading-sexp
18821907
(treesit-node-start)
@@ -1962,9 +1987,7 @@ cannot be found."
19621987
(interactive "p")
19631988
(if-let* ((threading-sexp (clojure-ts--threading-sexp-node))
19641989
((clojure-ts--threadable-p threading-sexp))
1965-
(sym (thread-first threading-sexp
1966-
(treesit-node-child 0 t)
1967-
(clojure-ts--named-node-text))))
1990+
(sym (clojure-ts--list-node-sym-text threading-sexp t)))
19681991
(let ((beg (thread-first threading-sexp
19691992
(treesit-node-start)
19701993
(copy-marker)))
@@ -2032,6 +2055,135 @@ value is `clojure-ts-thread-all-but-last'."
20322055
"-"))))
20332056
(user-error "No defun at point")))
20342057

2058+
(defun clojure-ts--node-child (node predicate)
2059+
"Return the first child of the NODE that matches the PREDICATE.
2060+
2061+
PREDICATE can be a symbol representing a thing in
2062+
`treesit-thing-settings', or a predicate, like regexp matching node
2063+
type, etc. See `treesit-thing-settings' for more details."
2064+
(thread-last (treesit-node-children node t)
2065+
(seq-find (lambda (child)
2066+
(treesit-node-match-p child predicate t)))))
2067+
2068+
(defun clojure-ts--node-start-skip-metadata (node)
2069+
"Return NODE start position optionally skipping metadata."
2070+
(if (clojure-ts--metadata-node-p (treesit-node-child node 0 t))
2071+
(treesit-node-start (treesit-node-child node 1))
2072+
(treesit-node-start node)))
2073+
2074+
(defun clojure-ts--add-arity-internal (fn-node)
2075+
"Add an arity to a function defined by FN-NODE."
2076+
(let* ((first-coll (clojure-ts--node-child fn-node (rx bol (or "vec_lit" "list_lit") eol)))
2077+
(coll-start (clojure-ts--node-start-skip-metadata first-coll))
2078+
(line-parent (thread-first fn-node
2079+
(clojure-ts--node-child-skip-metadata 0)
2080+
(treesit-node-start)
2081+
(line-number-at-pos)))
2082+
(line-args (line-number-at-pos coll-start))
2083+
(same-line-p (= line-parent line-args))
2084+
(single-arity-p (clojure-ts--vec-node-p first-coll)))
2085+
(goto-char coll-start)
2086+
(when same-line-p
2087+
(newline-and-indent))
2088+
(when single-arity-p
2089+
(insert-pair 2 ?\( ?\))
2090+
(backward-up-list))
2091+
(insert "([])\n")
2092+
;; Put the point between square brackets.
2093+
(down-list -2)))
2094+
2095+
(defun clojure-ts--add-arity-defprotocol-internal (fn-node)
2096+
"Add an arity to a defprotocol function defined by FN-NODE."
2097+
(let* ((args-vec (clojure-ts--node-child fn-node (rx bol "vec_lit" eol)))
2098+
(args-vec-start (clojure-ts--node-start-skip-metadata args-vec))
2099+
(line-parent (thread-first fn-node
2100+
(clojure-ts--node-child-skip-metadata 0)
2101+
(treesit-node-start)
2102+
(line-number-at-pos)))
2103+
(line-args-vec (line-number-at-pos args-vec-start))
2104+
(same-line-p (= line-parent line-args-vec)))
2105+
(goto-char args-vec-start)
2106+
(insert "[]")
2107+
(if same-line-p
2108+
(insert " ")
2109+
;; If args vector is not at the same line, respect this and place each new
2110+
;; vector on a new line.
2111+
(newline-and-indent))
2112+
;; Put the point between square brackets.
2113+
(down-list -1)))
2114+
2115+
(defun clojure-ts--add-arity-reify-internal (fn-node)
2116+
"Add an arity to a reify function defined by FN-NODE."
2117+
(let* ((fn-name (clojure-ts--list-node-sym-text fn-node)))
2118+
(goto-char (clojure-ts--node-start-skip-metadata fn-node))
2119+
(insert "(" fn-name " [])")
2120+
(newline-and-indent)
2121+
;; Put the point between sqare brackets.
2122+
(down-list -2)))
2123+
2124+
(defun clojure-ts--letfn-defn-p (node)
2125+
"Return non-nil if NODE is a function definition in a letfn form."
2126+
(when-let* ((parent (treesit-node-parent node)))
2127+
(and (clojure-ts--list-node-p node)
2128+
(clojure-ts--vec-node-p parent)
2129+
(let ((grandparent (treesit-node-parent parent)))
2130+
(string= (clojure-ts--list-node-sym-text grandparent)
2131+
"letfn")))))
2132+
2133+
(defun clojure-ts--proxy-defn-p (node)
2134+
"Return non-nil if NODE is a function definition in a proxy form."
2135+
(when-let* ((parent (treesit-node-parent node)))
2136+
(and (clojure-ts--list-node-p node)
2137+
(string= (clojure-ts--list-node-sym-text parent) "proxy"))))
2138+
2139+
(defun clojure-ts--defprotocol-defn-p (node)
2140+
"Return non-nil if NODE is a function definition in a defprotocol form."
2141+
(when-let* ((parent (treesit-node-parent node)))
2142+
(and (clojure-ts--list-node-p node)
2143+
(string= (clojure-ts--list-node-sym-text parent) "defprotocol"))))
2144+
2145+
(defun clojure-ts--reify-defn-p (node)
2146+
"Return non-nil if NODE is a function definition in a reify form."
2147+
(when-let* ((parent (treesit-node-parent node)))
2148+
(and (clojure-ts--list-node-p node)
2149+
(string= (clojure-ts--list-node-sym-text parent) "reify"))))
2150+
2151+
(defun clojure-ts-add-arity ()
2152+
"Add an arity to a function or macro."
2153+
(interactive)
2154+
(if-let* ((sym-regex (rx bol
2155+
(or "defn"
2156+
"letfn"
2157+
"fn"
2158+
"defmacro"
2159+
"defmethod"
2160+
"defprotocol"
2161+
"reify"
2162+
"proxy")
2163+
eol))
2164+
(parent-def-node (clojure-ts--search-list-form-at-point sym-regex))
2165+
(parent-def-sym (clojure-ts--list-node-sym-text parent-def-node))
2166+
(fn-node (cond
2167+
((string= parent-def-sym "letfn")
2168+
(clojure-ts--parent-until #'clojure-ts--letfn-defn-p))
2169+
((string= parent-def-sym "proxy")
2170+
(clojure-ts--parent-until #'clojure-ts--proxy-defn-p))
2171+
((string= parent-def-sym "defprotocol")
2172+
(clojure-ts--parent-until #'clojure-ts--defprotocol-defn-p))
2173+
((string= parent-def-sym "reify")
2174+
(clojure-ts--parent-until #'clojure-ts--reify-defn-p))
2175+
(t parent-def-node))))
2176+
(let ((beg-marker (copy-marker (treesit-node-start parent-def-node)))
2177+
(end-marker (copy-marker (treesit-node-end parent-def-node))))
2178+
(cond
2179+
((string= parent-def-sym "defprotocol")
2180+
(clojure-ts--add-arity-defprotocol-internal fn-node))
2181+
((string= parent-def-sym "reify")
2182+
(clojure-ts--add-arity-reify-internal fn-node))
2183+
(t (clojure-ts--add-arity-internal fn-node)))
2184+
(indent-region beg-marker end-marker))
2185+
(user-error "No suitable form to add an arity at point")))
2186+
20352187
(defun clojure-ts-cycle-keyword-string ()
20362188
"Convert the string at point to a keyword, or vice versa."
20372189
(interactive)
@@ -2141,6 +2293,8 @@ before DELIM-OPEN."
21412293
(keymap-set map "[" #'clojure-ts-convert-collection-to-vector)
21422294
(keymap-set map "C-#" #'clojure-ts-convert-collection-to-set)
21432295
(keymap-set map "#" #'clojure-ts-convert-collection-to-set)
2296+
(keymap-set map "C-a" #'clojure-ts-add-arity)
2297+
(keymap-set map "a" #'clojure-ts-add-arity)
21442298
map)
21452299
"Keymap for `clojure-ts-mode' refactoring commands.")
21462300

@@ -2155,6 +2309,7 @@ before DELIM-OPEN."
21552309
["Toggle between string & keyword" clojure-ts-cycle-keyword-string]
21562310
["Align expression" clojure-ts-align]
21572311
["Cycle privacy" clojure-ts-cycle-privacy]
2312+
["Add function/macro arity" clojure-ts-add-arity]
21582313
("Convert collection"
21592314
["Convert to list" clojure-ts-convert-collection-to-list]
21602315
["Convert to quoted list" clojure-ts-convert-collection-to-quoted-list]

0 commit comments

Comments
 (0)