From 1dae2e27dedb27d660b31b6a05687bad36c6b2f8 Mon Sep 17 00:00:00 2001 From: Roman Rudakov Date: Sun, 4 May 2025 20:38:24 +0200 Subject: [PATCH] Introduce cycle privacy refactoring command --- CHANGELOG.md | 1 + README.md | 21 ++-- clojure-ts-mode.el | 46 +++++++- test/clojure-ts-mode-cycling-test.el | 163 +++++++++++++++++++++++++++ test/samples/refactoring.clj | 12 ++ 5 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 test/clojure-ts-mode-cycling-test.el diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a45d82..c8fc91b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index c7b8e40..bf14a33 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 45dcc62..a110d2f 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -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") @@ -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" @@ -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)))))) @@ -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) @@ -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.") @@ -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] diff --git a/test/clojure-ts-mode-cycling-test.el b/test/clojure-ts-mode-cycling-test.el new file mode 100644 index 0000000..d0e8130 --- /dev/null +++ b/test/clojure-ts-mode-cycling-test.el @@ -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 + +;; 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: + +;; 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 diff --git a/test/samples/refactoring.clj b/test/samples/refactoring.clj index e6f24b8..109243d 100644 --- a/test/samples/refactoring.clj +++ b/test/samples/refactoring.clj @@ -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))