branch: elpa/clojure-ts-mode
commit cca0e9f8a9f3736d503b2a87977c01f5d6b297ee
Author: Roman Rudakov <rruda...@fastmail.com>
Commit: Bozhidar Batsov <bozhi...@batsov.dev>

    Introduce more cycling refactoring commands
    
    Added:
    
    - clojure-ts-cycle-conditional
    
    - clojure-ts-cycle-not
---
 CHANGELOG.md                         |  1 +
 README.md                            |  7 +++
 clojure-ts-mode.el                   | 91 ++++++++++++++++++++++++++++++++++++
 test/clojure-ts-mode-cycling-test.el | 78 +++++++++++++++++++++++++++++++
 test/samples/refactoring.clj         |  7 +++
 5 files changed, 184 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5a6b385df2..059aa1495a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
 - [#92](https://github.com/clojure-emacs/clojure-ts-mode/pull/92): Add 
commands to convert between collections types.
 - [#93](https://github.com/clojure-emacs/clojure-ts-mode/pull/93): Introduce 
`clojure-ts-add-arity`.
 - [#94](https://github.com/clojure-emacs/clojure-ts-mode/pull/94): Add 
indentation rules and `clojure-ts-align` support for namespaced maps.
+- Introduce `clojure-ts-cycle-conditional` and `clojure-ts-cycle-not`.
 
 ## 0.3.0 (2025-04-15)
 
diff --git a/README.md b/README.md
index 59a8fa2fe5..36aa137a05 100644
--- a/README.md
+++ b/README.md
@@ -426,6 +426,11 @@ vice versa.
 - `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.
+- `clojure-ts-cycle-conditional`: Change a surrounding conditional form to its
+  negated counterpart, or vice versa (supports `if`/`if-not` and
+  `when`/`when-not`). For `if`/`if-not` also transposes the else and then
+  branches, keeping the semantics the same as before.
+- `clojure-ts-cycle-not`: Add or remove a `not` form around the current form.
 
 ### Convert collection
 
@@ -461,6 +466,8 @@ multi-arity function or macro. Function can be defined 
using `defn`, `fn` or
 | `C-c C-r {` / `C-c C-r C-{` | `clojure-ts-convert-collection-to-map`         
|
 | `C-c C-r [` / `C-c C-r C-[` | `clojure-ts-convert-collection-to-vector`      
|
 | `C-c C-r #` / `C-c C-r C-#` | `clojure-ts-convert-collection-to-set`         
|
+| `C-c C-r c` / `C-c C-r C-c` | `clojure-ts-cycle-conditional`                 
|
+| `C-c C-r o` / `C-c C-r C-o` | `clojure-ts-cycle-not`                         
|
 | `C-c C-r a` / `C-c C-r C-a` | `clojure-ts-add-arity`                         
|
 
 ### Customize refactoring commands prefix
diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el
index f69082e6bf..84aad83189 100644
--- a/clojure-ts-mode.el
+++ b/clojure-ts-mode.el
@@ -1872,6 +1872,31 @@ functional literal node."
       (clojure-ts--skip-first-child threading-sexp)
       (not (treesit-end-of-thing 'sexp 2 'restricted)))))
 
+(defun clojure-ts--raise-sexp ()
+  "Raise current sexp one level higher up the tree.
+
+The built-in `raise-sexp' function doesn't work well with a few Clojure
+nodes (function literals, expressions with metadata etc.), it loses some
+parenthesis."
+  (when-let* ((sexp-node (treesit-thing-at (point) 'sexp))
+              (beg (thread-first sexp-node
+                                 (clojure-ts--node-start-skip-metadata)
+                                 (copy-marker)))
+              (end (thread-first sexp-node
+                                 (treesit-node-end)
+                                 (copy-marker))))
+    (when-let* ((parent (treesit-node-parent sexp-node))
+                ((not (string= (treesit-node-type parent) "source")))
+                (parent-beg (thread-first parent
+                                          
(clojure-ts--node-start-skip-metadata)
+                                          (copy-marker)))
+                (parent-end (thread-first parent
+                                          (treesit-node-end)
+                                          (copy-marker))))
+      (save-excursion
+        (delete-region parent-beg beg)
+        (delete-region end parent-end)))))
+
 (defun clojure-ts--pop-out-of-threading ()
   "Raise a sexp up a level to unwind a threading form."
   (let* ((threading-sexp (clojure-ts--threading-sexp-node))
@@ -2284,6 +2309,66 @@ before DELIM-OPEN."
   (interactive)
   (clojure-ts--convert-collection ?{ ?#))
 
+(defun clojure-ts-cycle-conditional ()
+  "Change a surrounding conditional form to its negated counterpart, or vice 
versa."
+  (interactive)
+  (if-let* ((sym-regex (rx bol
+                           (or "if" "if-not" "when" "when-not")
+                           eol))
+            (cond-node (clojure-ts--search-list-form-at-point sym-regex t))
+            (cond-sym (clojure-ts--list-node-sym-text cond-node)))
+      (let ((beg (treesit-node-start cond-node))
+            (end-marker (copy-marker (treesit-node-end cond-node)))
+            (new-sym (pcase cond-sym
+                       ("if" "if-not")
+                       ("if-not" "if")
+                       ("when" "when-not")
+                       ("when-not" "when"))))
+        (save-excursion
+          (goto-char (clojure-ts--node-start-skip-metadata cond-node))
+          (down-list 1)
+          (delete-char (length cond-sym))
+          (insert new-sym)
+          (when (member cond-sym '("if" "if-not"))
+            (forward-sexp 2)
+            (transpose-sexps 1))
+          (indent-region beg end-marker)))
+    (user-error "No conditional expression found")))
+
+(defun clojure-ts--point-outside-node-p (node)
+  "Return non-nil if point is outside of the actual NODE start.
+
+Clojure grammar treats metadata as part of an expression, so for example
+^boolean (not (= 2 2)) is a single list node, including metadata.  This
+causes issues for functions that navigate by s-expressions and lists.
+This function returns non-nil if point is outside of the outermost
+parenthesis."
+  (let* ((actual-node-start (clojure-ts--node-start-skip-metadata node))
+         (node-end (treesit-node-end node))
+         (pos (point)))
+    (or (< pos actual-node-start)
+        (> pos node-end))))
+
+(defun clojure-ts-cycle-not ()
+  "Add or remove a not form around the current form."
+  (interactive)
+  (if-let* ((list-node (clojure-ts--parent-until (rx bol "list_lit" eol)))
+            ((not (clojure-ts--point-outside-node-p list-node))))
+      (let ((beg (treesit-node-start list-node))
+            (end-marker (copy-marker (treesit-node-end list-node)))
+            (pos (copy-marker (point) t)))
+        (goto-char (clojure-ts--node-start-skip-metadata list-node))
+        (if-let* ((list-parent (treesit-node-parent list-node))
+                  ((clojure-ts--list-node-sym-match-p list-parent (rx bol 
"not" eol))))
+            (clojure-ts--raise-sexp)
+          (insert-pair 1 ?\( ?\))
+          (insert "not "))
+        (indent-region beg end-marker)
+        ;; `save-excursion' doesn't work well when point is at the opening
+        ;; paren.
+        (goto-char pos))
+    (user-error "Must be invoked inside a list")))
+
 (defvar clojure-ts-refactor-map
   (let ((map (make-sparse-keymap)))
     (keymap-set map "C-t" #'clojure-ts-thread)
@@ -2306,6 +2391,10 @@ before DELIM-OPEN."
     (keymap-set map "[" #'clojure-ts-convert-collection-to-vector)
     (keymap-set map "C-#" #'clojure-ts-convert-collection-to-set)
     (keymap-set map "#" #'clojure-ts-convert-collection-to-set)
+    (keymap-set map "C-c" #'clojure-ts-cycle-conditional)
+    (keymap-set map "c" #'clojure-ts-cycle-conditional)
+    (keymap-set map "C-o" #'clojure-ts-cycle-not)
+    (keymap-set map "o" #'clojure-ts-cycle-not)
     (keymap-set map "C-a" #'clojure-ts-add-arity)
     (keymap-set map "a" #'clojure-ts-add-arity)
     map)
@@ -2322,6 +2411,8 @@ before DELIM-OPEN."
         ["Toggle between string & keyword" clojure-ts-cycle-keyword-string]
         ["Align expression" clojure-ts-align]
         ["Cycle privacy" clojure-ts-cycle-privacy]
+        ["Cycle conditional" clojure-ts-cycle-conditional]
+        ["Cycle not" clojure-ts-cycle-not]
         ["Add function/macro arity" clojure-ts-add-arity]
         ("Convert collection"
          ["Convert to list" clojure-ts-convert-collection-to-list]
diff --git a/test/clojure-ts-mode-cycling-test.el 
b/test/clojure-ts-mode-cycling-test.el
index b0d83cb5df..81eef67202 100644
--- a/test/clojure-ts-mode-cycling-test.el
+++ b/test/clojure-ts-mode-cycling-test.el
@@ -190,5 +190,83 @@
 
     (clojure-ts-cycle-privacy)))
 
+(describe "clojure-cycle-if"
+
+  (when-refactoring-with-point-it "should cycle inner if"
+    "(if this
+  (if |that
+    (then AAA)
+    (else BBB))
+  (otherwise CCC))"
+
+    "(if this
+  (if-not |that
+    (else BBB)
+    (then AAA))
+  (otherwise CCC))"
+
+    (clojure-ts-cycle-conditional))
+
+  (when-refactoring-with-point-it "should cycle outer if"
+    "(if-not |this
+  (if that
+    (then AAA)
+    (else BBB))
+  (otherwise CCC))"
+
+    "(if |this
+  (otherwise CCC)
+  (if that
+    (then AAA)
+    (else BBB)))"
+
+    (clojure-ts-cycle-conditional)))
+
+(describe "clojure-cycle-when"
+
+  (when-refactoring-with-point-it "should cycle inner when"
+    "(when this
+  (when |that
+    (aaa)
+    (bbb))
+  (ccc))"
+
+    "(when this
+  (when-not |that
+    (aaa)
+    (bbb))
+  (ccc))"
+
+    (clojure-ts-cycle-conditional))
+
+  (when-refactoring-with-point-it "should cycle outer when"
+    "(when-not |this
+  (when that
+    (aaa)
+    (bbb))
+  (ccc))"
+
+    "(when |this
+  (when that
+    (aaa)
+    (bbb))
+  (ccc))"
+
+    (clojure-ts-cycle-conditional)))
+
+(describe "clojure-cycle-not"
+
+  (when-refactoring-with-point-it "should add a not when missing"
+    "(ala bala| portokala)"
+    "(not (ala bala| portokala))"
+
+    (clojure-ts-cycle-not))
+
+  (when-refactoring-with-point-it "should remove a not when present"
+    "(not (ala bala| portokala))"
+    "(ala bala| portokala)"
+
+    (clojure-ts-cycle-not)))
+
 (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 c7547bfd16..10f12b5550 100644
--- a/test/samples/refactoring.clj
+++ b/test/samples/refactoring.clj
@@ -134,3 +134,10 @@
   ^{:bla "meta"}
   [arg]
   body)
+
+(if ^boolean (= 2 2)
+  true
+  false)
+
+(when-not true
+  (println "Hello world"))

Reply via email to