Skip to content

Commit 1dae2e2

Browse files
committed
Introduce cycle privacy refactoring command
1 parent 3569c90 commit 1dae2e2

File tree

5 files changed

+235
-8
lines changed

5 files changed

+235
-8
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [#88](https://github.com/clojure-emacs/clojure-ts-mode/pull/88): Introduce `clojure-ts-unwind` and `clojure-ts-unwind-all`.
1414
- [#89](https://github.com/clojure-emacs/clojure-ts-mode/pull/89): Introduce `clojure-ts-thread`, `clojure-ts-thread-first-all` and
1515
`clojure-ts-thread-last-all`.
16+
- [#90](https://github.com/clojure-emacs/clojure-ts-mode/pull/90): Introduce `clojure-ts-cycle-privacy`.
1617

1718
## 0.3.0 (2025-04-15)
1819

README.md

+14-7
Original file line numberDiff line numberDiff line change
@@ -376,20 +376,26 @@ following customization:
376376

377377
### Threading macros related features
378378

379-
`clojure-thread`: Thread another form into the surrounding thread. Both
379+
`clojure-ts-thread`: Thread another form into the surrounding thread. Both
380380
`->>`/`some->>` and `->`/`some->` variants are supported.
381381

382-
`clojure-unwind`: Unwind a threaded expression. Supports both `->>`/`some->>`
382+
`clojure-ts-unwind`: Unwind a threaded expression. Supports both `->>`/`some->>`
383383
and `->`/`some->`.
384384

385-
`clojure-thread-first-all`: Introduce the thread first macro (`->`) and rewrite
386-
the entire form. With a prefix argument do not thread the last form.
385+
`clojure-ts-thread-first-all`: Introduce the thread first macro (`->`) and
386+
rewrite the entire form. With a prefix argument do not thread the last form.
387387

388-
`clojure-thread-last-all`: Introduce the thread last macro and rewrite the
388+
`clojure-ts-thread-last-all`: Introduce the thread last macro and rewrite the
389389
entire form. With a prefix argument do not thread the last form.
390390

391-
`clojure-unwind-all`: Fully unwind a threaded expression removing the threading
392-
macro.
391+
`clojure-ts-unwind-all`: Fully unwind a threaded expression removing the
392+
threading macro.
393+
394+
### Cycling things
395+
396+
`clojure-ts-cycle-privacy`: Cycle privacy of `def`s or `defn`s. Use metadata
397+
explicitly with setting `clojure-ts-use-metadata-for-defn-privacy` to `t` for
398+
`defn`s too.
393399

394400
### Default keybindings
395401

@@ -400,6 +406,7 @@ macro.
400406
| `C-c C-r u` / `C-c C-r C-u` | `clojure-ts-unwind` |
401407
| `C-c C-r f` / `C-c C-r C-f` | `clojure-ts-thread-first-all` |
402408
| `C-c C-r l` / `C-c C-r C-l` | `clojure-ts-thread-last-all` |
409+
| `C-c C-r p` / `C-c C-r C-p` | `clojure-ts-cycle-privacy` |
403410

404411
### Customize refactoring commands prefix
405412

clojure-ts-mode.el

+45-1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ current sexp."
160160
:safe #'booleanp
161161
:type 'boolean)
162162

163+
(defcustom clojure-ts-use-metadata-for-defn-privacy nil
164+
"If nil, `clojure-ts-cycle-privacy' will use (defn- f []).
165+
166+
If t, it will use (defn ^:private f [])."
167+
:package-version '(clojure-ts-mode . "0.4.0")
168+
:safe #'booleanp
169+
:type 'boolean)
170+
163171
(defcustom clojure-ts-align-reader-conditionals nil
164172
"Whether to align reader conditionals, as if they were maps."
165173
:package-version '(clojure-ts-mode . "0.4")
@@ -1480,6 +1488,21 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
14801488
"map_lit" "ns_map_lit" "vec_lit" "set_lit")
14811489
"A regular expression that matches nodes that can be treated as lists.")
14821490

1491+
(defun clojure-ts--defun-node-p (node)
1492+
"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)))))
1505+
14831506
(defconst clojure-ts--markdown-inline-sexp-nodes
14841507
'("inline_link" "full_reference_link" "collapsed_reference_link"
14851508
"uri_autolink" "email_autolink" "shortcut_link" "image"
@@ -1490,7 +1513,8 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
14901513
`((clojure
14911514
(sexp ,(regexp-opt clojure-ts--sexp-nodes))
14921515
(list ,(regexp-opt clojure-ts--list-nodes))
1493-
(text ,(regexp-opt '("comment"))))
1516+
(text ,(regexp-opt '("comment")))
1517+
(defun ,#'clojure-ts--defun-node-p))
14941518
(when clojure-ts-use-markdown-inline
14951519
(markdown-inline
14961520
(sexp ,(regexp-opt clojure-ts--markdown-inline-sexp-nodes))))))
@@ -1991,6 +2015,23 @@ value is `clojure-ts-thread-all-but-last'."
19912015
(interactive "P")
19922016
(clojure-ts--thread-all "->> " but-last))
19932017

2018+
(defun clojure-ts-cycle-privacy ()
2019+
"Make a definition at point public or private."
2020+
(interactive)
2021+
(if-let* ((node-at-point (treesit-node-at (point) 'clojure t))
2022+
(defun-node (treesit-parent-until node-at-point 'defun t)))
2023+
(save-excursion
2024+
(goto-char (treesit-node-start defun-node))
2025+
(search-forward-regexp (rx "def" (* letter) (? (group (or "-" " ^:private")))))
2026+
(if (match-string 1)
2027+
(replace-match "" nil nil nil 1)
2028+
(goto-char (match-end 0))
2029+
(insert (if (or clojure-ts-use-metadata-for-defn-privacy
2030+
(not (string= (match-string 0) "defn")))
2031+
" ^:private"
2032+
"-"))))
2033+
(user-error "No defun at point")))
2034+
19942035
(defvar clojure-ts-refactor-map
19952036
(let ((map (make-sparse-keymap)))
19962037
(keymap-set map "C-t" #'clojure-ts-thread)
@@ -2001,6 +2042,8 @@ value is `clojure-ts-thread-all-but-last'."
20012042
(keymap-set map "f" #'clojure-ts-thread-first-all)
20022043
(keymap-set map "C-l" #'clojure-ts-thread-last-all)
20032044
(keymap-set map "l" #'clojure-ts-thread-last-all)
2045+
(keymap-set map "C-p" #'clojure-ts-cycle-privacy)
2046+
(keymap-set map "p" #'clojure-ts-cycle-privacy)
20042047
map)
20052048
"Keymap for `clojure-ts-mode' refactoring commands.")
20062049

@@ -2012,6 +2055,7 @@ value is `clojure-ts-thread-all-but-last'."
20122055
(easy-menu-define clojure-ts-mode-menu map "Clojure[TS] Mode Menu"
20132056
'("Clojure"
20142057
["Align expression" clojure-ts-align]
2058+
["Cycle privacy" clojure-ts-cycle-privacy]
20152059
("Refactor -> and ->>"
20162060
["Thread once more" clojure-ts-thread]
20172061
["Fully thread a form with ->" clojure-ts-thread-first-all]

test/clojure-ts-mode-cycling-test.el

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
;;; clojure-ts-mode-cycling-test.el --- Clojure[TS] Mode: cycling things tests -*- lexical-binding: t; -*-
2+
3+
;; Copyright (C) 2025 Roman Rudakov
4+
5+
;; Author: Roman Rudakov <[email protected]>
6+
7+
;; This program is free software; you can redistribute it and/or modify
8+
;; it under the terms of the GNU General Public License as published by
9+
;; the Free Software Foundation, either version 3 of the License, or
10+
;; (at your option) any later version.
11+
12+
;; This program is distributed in the hope that it will be useful,
13+
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
;; GNU General Public License for more details.
16+
17+
;; You should have received a copy of the GNU General Public License
18+
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
20+
;;; Commentary:
21+
22+
;; The code is adapted from `clojure-mode'.
23+
24+
;;; Code:
25+
26+
(require 'clojure-ts-mode)
27+
(require 'buttercup)
28+
(require 'test-helper "test/test-helper")
29+
30+
(describe "clojure-ts-cycle-privacy"
31+
32+
(when-refactoring-it "should turn a public defn into a private defn"
33+
"(defn add [a b]
34+
(+ a b))"
35+
36+
"(defn- add [a b]
37+
(+ a b))"
38+
39+
(clojure-ts-cycle-privacy))
40+
41+
(when-refactoring-it "should also work from the beginning of a sexp"
42+
"(defn- add [a b]
43+
(+ a b))"
44+
45+
"(defn add [a b]
46+
(+ a b))"
47+
48+
(backward-sexp)
49+
(clojure-ts-cycle-privacy))
50+
51+
(when-refactoring-it "should use metadata when clojure-use-metadata-for-privacy is set to true"
52+
"(defn add [a b]
53+
(+ a b))"
54+
55+
"(defn ^:private add [a b]
56+
(+ a b))"
57+
58+
(let ((clojure-ts-use-metadata-for-defn-privacy t))
59+
(clojure-ts-cycle-privacy)))
60+
61+
(when-refactoring-it "should turn a private defn into a public defn"
62+
"(defn- add [a b]
63+
(+ a b))"
64+
65+
"(defn add [a b]
66+
(+ a b))"
67+
68+
(clojure-ts-cycle-privacy))
69+
70+
(when-refactoring-it "should turn a private defn with metadata into a public defn"
71+
"(defn ^:private add [a b]
72+
(+ a b))"
73+
74+
"(defn add [a b]
75+
(+ a b))"
76+
77+
(let ((clojure-ts-use-metadata-for-defn-privacy t))
78+
(clojure-ts-cycle-privacy)))
79+
80+
(when-refactoring-it "should also work with pre-existing metadata"
81+
"(def ^:dynamic config
82+
\"docs\"
83+
{:env \"staging\"})"
84+
85+
"(def ^:private ^:dynamic config
86+
\"docs\"
87+
{:env \"staging\"})"
88+
89+
(clojure-ts-cycle-privacy))
90+
91+
(when-refactoring-it "should turn a private def with metadata into a public def"
92+
"(def ^:private config
93+
\"docs\"
94+
{:env \"staging\"})"
95+
96+
"(def config
97+
\"docs\"
98+
{:env \"staging\"})"
99+
100+
(clojure-ts-cycle-privacy))
101+
102+
(when-refactoring-it "should turn a public defmulti into a private defmulti"
103+
"(defmulti service-charge (juxt account-level :tag))"
104+
105+
"(defmulti ^:private service-charge (juxt account-level :tag))"
106+
107+
(clojure-ts-cycle-privacy))
108+
109+
(when-refactoring-it "should turn a private defmulti into a public defmulti"
110+
"(defmulti ^:private service-charge (juxt account-level :tag))"
111+
112+
"(defmulti service-charge (juxt account-level :tag))"
113+
114+
(clojure-ts-cycle-privacy))
115+
116+
(when-refactoring-it "should turn a public defmacro into a private defmacro"
117+
"(defmacro unless [pred a b]
118+
`(if (not ~pred) ~a ~b))"
119+
120+
"(defmacro ^:private unless [pred a b]
121+
`(if (not ~pred) ~a ~b))"
122+
123+
(clojure-ts-cycle-privacy))
124+
125+
(when-refactoring-it "should turn a private defmacro into a public defmacro"
126+
"(defmacro ^:private unless [pred a b]
127+
`(if (not ~pred) ~a ~b))"
128+
129+
"(defmacro unless [pred a b]
130+
`(if (not ~pred) ~a ~b))"
131+
132+
(clojure-ts-cycle-privacy))
133+
134+
(when-refactoring-it "should turn a private definline into a public definline"
135+
"(definline bad-sqr [x] `(* ~x ~x))"
136+
137+
"(definline ^:private bad-sqr [x] `(* ~x ~x))"
138+
139+
(clojure-ts-cycle-privacy))
140+
141+
(when-refactoring-it "should turn a public definline into a private definline"
142+
"(definline ^:private bad-sqr [x] `(* ~x ~x))"
143+
144+
"(definline bad-sqr [x] `(* ~x ~x))"
145+
146+
(clojure-ts-cycle-privacy))
147+
148+
(when-refactoring-it "should turn a private defrecord into a public defrecord"
149+
"(defrecord Person [fname lname address])"
150+
151+
"(defrecord ^:private Person [fname lname address])"
152+
153+
(clojure-ts-cycle-privacy))
154+
155+
(when-refactoring-it "should turn a public defrecord into a private defrecord"
156+
"(defrecord ^:private Person [fname lname address])"
157+
158+
"(defrecord Person [fname lname address])"
159+
160+
(clojure-ts-cycle-privacy)))
161+
162+
(provide 'clojure-ts-mode-cycling-test)
163+
;;; clojure-ts-mode-cycling-test.el ends here

test/samples/refactoring.clj

+12
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,19 @@
6666

6767
(->> (map square (filter even? [1 2 3 4 5])))
6868

69+
(-> (dissoc (assoc {} :key "value") :lock))
70+
6971
(deftask dev []
7072
(comp (serve)
7173
(cljs (lala)
7274
10)))
75+
76+
(def my-name "Roma")
77+
78+
(defn say-hello
79+
[]
80+
(println "Hello" my-name))
81+
82+
(definline bad-sqr [x] `(* ~x ~x))
83+
84+
(defmulti service-charge (juxt account-level :tag))

0 commit comments

Comments
 (0)