branch: elpa/clojure-ts-mode
commit cca0e9f8a9f3736d503b2a87977c01f5d6b297ee
Author: Roman Rudakov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
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"))