Skip to content

Introduce cycle privacy refactoring command #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [#88](https://github.com/clojure-emacs/clojure-ts-mode/pull/88): Introduce `clojure-ts-unwind` and `clojure-ts-unwind-all`.
- [#89](https://github.com/clojure-emacs/clojure-ts-mode/pull/89): Introduce `clojure-ts-thread`, `clojure-ts-thread-first-all` and
`clojure-ts-thread-last-all`.
- [#90](https://github.com/clojure-emacs/clojure-ts-mode/pull/90): Introduce `clojure-ts-cycle-privacy`.

## 0.3.0 (2025-04-15)

Expand Down
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,20 +376,26 @@ following customization:

### Threading macros related features

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

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

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

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

`clojure-unwind-all`: Fully unwind a threaded expression removing the threading
macro.
`clojure-ts-unwind-all`: Fully unwind a threaded expression removing the
threading macro.

### Cycling things

`clojure-ts-cycle-privacy`: Cycle privacy of `def`s or `defn`s. Use metadata
explicitly with setting `clojure-ts-use-metadata-for-defn-privacy` to `t` for
`defn`s too.

### Default keybindings

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

### Customize refactoring commands prefix

Expand Down
46 changes: 45 additions & 1 deletion clojure-ts-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ current sexp."
:safe #'booleanp
:type 'boolean)

(defcustom clojure-ts-use-metadata-for-defn-privacy nil
"If nil, `clojure-ts-cycle-privacy' will use (defn- f []).

If t, it will use (defn ^:private f [])."
:package-version '(clojure-ts-mode . "0.4.0")
:safe #'booleanp
:type 'boolean)

(defcustom clojure-ts-align-reader-conditionals nil
"Whether to align reader conditionals, as if they were maps."
:package-version '(clojure-ts-mode . "0.4")
Expand Down Expand Up @@ -1480,6 +1488,21 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
"map_lit" "ns_map_lit" "vec_lit" "set_lit")
"A regular expression that matches nodes that can be treated as lists.")

(defun clojure-ts--defun-node-p (node)
"Return TRUE if NODE is a function or a var definition."
(and (clojure-ts--list-node-p node)
(let ((sym (clojure-ts--node-child-skip-metadata node 0)))
(string-match-p (rx bol
(or "def"
"defn"
"defn-"
"definline"
"defrecord"
"defmacro"
"defmulti")
eol)
(clojure-ts--named-node-text sym)))))

(defconst clojure-ts--markdown-inline-sexp-nodes
'("inline_link" "full_reference_link" "collapsed_reference_link"
"uri_autolink" "email_autolink" "shortcut_link" "image"
Expand All @@ -1490,7 +1513,8 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
`((clojure
(sexp ,(regexp-opt clojure-ts--sexp-nodes))
(list ,(regexp-opt clojure-ts--list-nodes))
(text ,(regexp-opt '("comment"))))
(text ,(regexp-opt '("comment")))
(defun ,#'clojure-ts--defun-node-p))
(when clojure-ts-use-markdown-inline
(markdown-inline
(sexp ,(regexp-opt clojure-ts--markdown-inline-sexp-nodes))))))
Expand Down Expand Up @@ -1991,6 +2015,23 @@ value is `clojure-ts-thread-all-but-last'."
(interactive "P")
(clojure-ts--thread-all "->> " but-last))

(defun clojure-ts-cycle-privacy ()
"Make a definition at point public or private."
(interactive)
(if-let* ((node-at-point (treesit-node-at (point) 'clojure t))
(defun-node (treesit-parent-until node-at-point 'defun t)))
(save-excursion
(goto-char (treesit-node-start defun-node))
(search-forward-regexp (rx "def" (* letter) (? (group (or "-" " ^:private")))))
(if (match-string 1)
(replace-match "" nil nil nil 1)
(goto-char (match-end 0))
(insert (if (or clojure-ts-use-metadata-for-defn-privacy
(not (string= (match-string 0) "defn")))
" ^:private"
"-"))))
(user-error "No defun at point")))

(defvar clojure-ts-refactor-map
(let ((map (make-sparse-keymap)))
(keymap-set map "C-t" #'clojure-ts-thread)
Expand All @@ -2001,6 +2042,8 @@ value is `clojure-ts-thread-all-but-last'."
(keymap-set map "f" #'clojure-ts-thread-first-all)
(keymap-set map "C-l" #'clojure-ts-thread-last-all)
(keymap-set map "l" #'clojure-ts-thread-last-all)
(keymap-set map "C-p" #'clojure-ts-cycle-privacy)
(keymap-set map "p" #'clojure-ts-cycle-privacy)
map)
"Keymap for `clojure-ts-mode' refactoring commands.")

Expand All @@ -2012,6 +2055,7 @@ value is `clojure-ts-thread-all-but-last'."
(easy-menu-define clojure-ts-mode-menu map "Clojure[TS] Mode Menu"
'("Clojure"
["Align expression" clojure-ts-align]
["Cycle privacy" clojure-ts-cycle-privacy]
("Refactor -> and ->>"
["Thread once more" clojure-ts-thread]
["Fully thread a form with ->" clojure-ts-thread-first-all]
Expand Down
163 changes: 163 additions & 0 deletions test/clojure-ts-mode-cycling-test.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
;;; clojure-ts-mode-cycling-test.el --- Clojure[TS] Mode: cycling things tests -*- lexical-binding: t; -*-

;; Copyright (C) 2025 Roman Rudakov

;; Author: Roman Rudakov <[email protected]>

;; 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 <https://www.gnu.org/licenses/>.

;;; Commentary:

;; The code is adapted from `clojure-mode'.

;;; Code:

(require 'clojure-ts-mode)
(require 'buttercup)
(require 'test-helper "test/test-helper")

(describe "clojure-ts-cycle-privacy"

(when-refactoring-it "should turn a public defn into a private defn"
"(defn add [a b]
(+ a b))"

"(defn- add [a b]
(+ a b))"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should also work from the beginning of a sexp"
"(defn- add [a b]
(+ a b))"

"(defn add [a b]
(+ a b))"

(backward-sexp)
(clojure-ts-cycle-privacy))

(when-refactoring-it "should use metadata when clojure-use-metadata-for-privacy is set to true"
"(defn add [a b]
(+ a b))"

"(defn ^:private add [a b]
(+ a b))"

(let ((clojure-ts-use-metadata-for-defn-privacy t))
(clojure-ts-cycle-privacy)))

(when-refactoring-it "should turn a private defn into a public defn"
"(defn- add [a b]
(+ a b))"

"(defn add [a b]
(+ a b))"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a private defn with metadata into a public defn"
"(defn ^:private add [a b]
(+ a b))"

"(defn add [a b]
(+ a b))"

(let ((clojure-ts-use-metadata-for-defn-privacy t))
(clojure-ts-cycle-privacy)))

(when-refactoring-it "should also work with pre-existing metadata"
"(def ^:dynamic config
\"docs\"
{:env \"staging\"})"

"(def ^:private ^:dynamic config
\"docs\"
{:env \"staging\"})"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a private def with metadata into a public def"
"(def ^:private config
\"docs\"
{:env \"staging\"})"

"(def config
\"docs\"
{:env \"staging\"})"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a public defmulti into a private defmulti"
"(defmulti service-charge (juxt account-level :tag))"

"(defmulti ^:private service-charge (juxt account-level :tag))"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a private defmulti into a public defmulti"
"(defmulti ^:private service-charge (juxt account-level :tag))"

"(defmulti service-charge (juxt account-level :tag))"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a public defmacro into a private defmacro"
"(defmacro unless [pred a b]
`(if (not ~pred) ~a ~b))"

"(defmacro ^:private unless [pred a b]
`(if (not ~pred) ~a ~b))"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a private defmacro into a public defmacro"
"(defmacro ^:private unless [pred a b]
`(if (not ~pred) ~a ~b))"

"(defmacro unless [pred a b]
`(if (not ~pred) ~a ~b))"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a private definline into a public definline"
"(definline bad-sqr [x] `(* ~x ~x))"

"(definline ^:private bad-sqr [x] `(* ~x ~x))"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a public definline into a private definline"
"(definline ^:private bad-sqr [x] `(* ~x ~x))"

"(definline bad-sqr [x] `(* ~x ~x))"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a private defrecord into a public defrecord"
"(defrecord Person [fname lname address])"

"(defrecord ^:private Person [fname lname address])"

(clojure-ts-cycle-privacy))

(when-refactoring-it "should turn a public defrecord into a private defrecord"
"(defrecord ^:private Person [fname lname address])"

"(defrecord Person [fname lname address])"

(clojure-ts-cycle-privacy)))

(provide 'clojure-ts-mode-cycling-test)
;;; clojure-ts-mode-cycling-test.el ends here
12 changes: 12 additions & 0 deletions test/samples/refactoring.clj
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,19 @@

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

(-> (dissoc (assoc {} :key "value") :lock))

(deftask dev []
(comp (serve)
(cljs (lala)
10)))

(def my-name "Roma")

(defn say-hello
[]
(println "Hello" my-name))

(definline bad-sqr [x] `(* ~x ~x))

(defmulti service-charge (juxt account-level :tag))