Skip to content

Commit cca0e9f

Browse files
rrudakovbbatsov
authored andcommitted
Introduce more cycling refactoring commands
Added: - clojure-ts-cycle-conditional - clojure-ts-cycle-not
1 parent 83d1aed commit cca0e9f

File tree

5 files changed

+184
-0
lines changed

5 files changed

+184
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- [#92](https://github.com/clojure-emacs/clojure-ts-mode/pull/92): Add commands to convert between collections types.
1919
- [#93](https://github.com/clojure-emacs/clojure-ts-mode/pull/93): Introduce `clojure-ts-add-arity`.
2020
- [#94](https://github.com/clojure-emacs/clojure-ts-mode/pull/94): Add indentation rules and `clojure-ts-align` support for namespaced maps.
21+
- Introduce `clojure-ts-cycle-conditional` and `clojure-ts-cycle-not`.
2122

2223
## 0.3.0 (2025-04-15)
2324

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,11 @@ vice versa.
426426
- `clojure-ts-cycle-privacy`: Cycle privacy of `def`s or `defn`s. Use metadata
427427
explicitly with setting `clojure-ts-use-metadata-for-defn-privacy` to `t` for
428428
`defn`s too.
429+
- `clojure-ts-cycle-conditional`: Change a surrounding conditional form to its
430+
negated counterpart, or vice versa (supports `if`/`if-not` and
431+
`when`/`when-not`). For `if`/`if-not` also transposes the else and then
432+
branches, keeping the semantics the same as before.
433+
- `clojure-ts-cycle-not`: Add or remove a `not` form around the current form.
429434

430435
### Convert collection
431436

@@ -461,6 +466,8 @@ multi-arity function or macro. Function can be defined using `defn`, `fn` or
461466
| `C-c C-r {` / `C-c C-r C-{` | `clojure-ts-convert-collection-to-map` |
462467
| `C-c C-r [` / `C-c C-r C-[` | `clojure-ts-convert-collection-to-vector` |
463468
| `C-c C-r #` / `C-c C-r C-#` | `clojure-ts-convert-collection-to-set` |
469+
| `C-c C-r c` / `C-c C-r C-c` | `clojure-ts-cycle-conditional` |
470+
| `C-c C-r o` / `C-c C-r C-o` | `clojure-ts-cycle-not` |
464471
| `C-c C-r a` / `C-c C-r C-a` | `clojure-ts-add-arity` |
465472

466473
### Customize refactoring commands prefix

clojure-ts-mode.el

+91
Original file line numberDiff line numberDiff line change
@@ -1872,6 +1872,31 @@ functional literal node."
18721872
(clojure-ts--skip-first-child threading-sexp)
18731873
(not (treesit-end-of-thing 'sexp 2 'restricted)))))
18741874

1875+
(defun clojure-ts--raise-sexp ()
1876+
"Raise current sexp one level higher up the tree.
1877+
1878+
The built-in `raise-sexp' function doesn't work well with a few Clojure
1879+
nodes (function literals, expressions with metadata etc.), it loses some
1880+
parenthesis."
1881+
(when-let* ((sexp-node (treesit-thing-at (point) 'sexp))
1882+
(beg (thread-first sexp-node
1883+
(clojure-ts--node-start-skip-metadata)
1884+
(copy-marker)))
1885+
(end (thread-first sexp-node
1886+
(treesit-node-end)
1887+
(copy-marker))))
1888+
(when-let* ((parent (treesit-node-parent sexp-node))
1889+
((not (string= (treesit-node-type parent) "source")))
1890+
(parent-beg (thread-first parent
1891+
(clojure-ts--node-start-skip-metadata)
1892+
(copy-marker)))
1893+
(parent-end (thread-first parent
1894+
(treesit-node-end)
1895+
(copy-marker))))
1896+
(save-excursion
1897+
(delete-region parent-beg beg)
1898+
(delete-region end parent-end)))))
1899+
18751900
(defun clojure-ts--pop-out-of-threading ()
18761901
"Raise a sexp up a level to unwind a threading form."
18771902
(let* ((threading-sexp (clojure-ts--threading-sexp-node))
@@ -2284,6 +2309,66 @@ before DELIM-OPEN."
22842309
(interactive)
22852310
(clojure-ts--convert-collection ?{ ?#))
22862311

2312+
(defun clojure-ts-cycle-conditional ()
2313+
"Change a surrounding conditional form to its negated counterpart, or vice versa."
2314+
(interactive)
2315+
(if-let* ((sym-regex (rx bol
2316+
(or "if" "if-not" "when" "when-not")
2317+
eol))
2318+
(cond-node (clojure-ts--search-list-form-at-point sym-regex t))
2319+
(cond-sym (clojure-ts--list-node-sym-text cond-node)))
2320+
(let ((beg (treesit-node-start cond-node))
2321+
(end-marker (copy-marker (treesit-node-end cond-node)))
2322+
(new-sym (pcase cond-sym
2323+
("if" "if-not")
2324+
("if-not" "if")
2325+
("when" "when-not")
2326+
("when-not" "when"))))
2327+
(save-excursion
2328+
(goto-char (clojure-ts--node-start-skip-metadata cond-node))
2329+
(down-list 1)
2330+
(delete-char (length cond-sym))
2331+
(insert new-sym)
2332+
(when (member cond-sym '("if" "if-not"))
2333+
(forward-sexp 2)
2334+
(transpose-sexps 1))
2335+
(indent-region beg end-marker)))
2336+
(user-error "No conditional expression found")))
2337+
2338+
(defun clojure-ts--point-outside-node-p (node)
2339+
"Return non-nil if point is outside of the actual NODE start.
2340+
2341+
Clojure grammar treats metadata as part of an expression, so for example
2342+
^boolean (not (= 2 2)) is a single list node, including metadata. This
2343+
causes issues for functions that navigate by s-expressions and lists.
2344+
This function returns non-nil if point is outside of the outermost
2345+
parenthesis."
2346+
(let* ((actual-node-start (clojure-ts--node-start-skip-metadata node))
2347+
(node-end (treesit-node-end node))
2348+
(pos (point)))
2349+
(or (< pos actual-node-start)
2350+
(> pos node-end))))
2351+
2352+
(defun clojure-ts-cycle-not ()
2353+
"Add or remove a not form around the current form."
2354+
(interactive)
2355+
(if-let* ((list-node (clojure-ts--parent-until (rx bol "list_lit" eol)))
2356+
((not (clojure-ts--point-outside-node-p list-node))))
2357+
(let ((beg (treesit-node-start list-node))
2358+
(end-marker (copy-marker (treesit-node-end list-node)))
2359+
(pos (copy-marker (point) t)))
2360+
(goto-char (clojure-ts--node-start-skip-metadata list-node))
2361+
(if-let* ((list-parent (treesit-node-parent list-node))
2362+
((clojure-ts--list-node-sym-match-p list-parent (rx bol "not" eol))))
2363+
(clojure-ts--raise-sexp)
2364+
(insert-pair 1 ?\( ?\))
2365+
(insert "not "))
2366+
(indent-region beg end-marker)
2367+
;; `save-excursion' doesn't work well when point is at the opening
2368+
;; paren.
2369+
(goto-char pos))
2370+
(user-error "Must be invoked inside a list")))
2371+
22872372
(defvar clojure-ts-refactor-map
22882373
(let ((map (make-sparse-keymap)))
22892374
(keymap-set map "C-t" #'clojure-ts-thread)
@@ -2306,6 +2391,10 @@ before DELIM-OPEN."
23062391
(keymap-set map "[" #'clojure-ts-convert-collection-to-vector)
23072392
(keymap-set map "C-#" #'clojure-ts-convert-collection-to-set)
23082393
(keymap-set map "#" #'clojure-ts-convert-collection-to-set)
2394+
(keymap-set map "C-c" #'clojure-ts-cycle-conditional)
2395+
(keymap-set map "c" #'clojure-ts-cycle-conditional)
2396+
(keymap-set map "C-o" #'clojure-ts-cycle-not)
2397+
(keymap-set map "o" #'clojure-ts-cycle-not)
23092398
(keymap-set map "C-a" #'clojure-ts-add-arity)
23102399
(keymap-set map "a" #'clojure-ts-add-arity)
23112400
map)
@@ -2322,6 +2411,8 @@ before DELIM-OPEN."
23222411
["Toggle between string & keyword" clojure-ts-cycle-keyword-string]
23232412
["Align expression" clojure-ts-align]
23242413
["Cycle privacy" clojure-ts-cycle-privacy]
2414+
["Cycle conditional" clojure-ts-cycle-conditional]
2415+
["Cycle not" clojure-ts-cycle-not]
23252416
["Add function/macro arity" clojure-ts-add-arity]
23262417
("Convert collection"
23272418
["Convert to list" clojure-ts-convert-collection-to-list]

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

+78
Original file line numberDiff line numberDiff line change
@@ -190,5 +190,83 @@
190190

191191
(clojure-ts-cycle-privacy)))
192192

193+
(describe "clojure-cycle-if"
194+
195+
(when-refactoring-with-point-it "should cycle inner if"
196+
"(if this
197+
(if |that
198+
(then AAA)
199+
(else BBB))
200+
(otherwise CCC))"
201+
202+
"(if this
203+
(if-not |that
204+
(else BBB)
205+
(then AAA))
206+
(otherwise CCC))"
207+
208+
(clojure-ts-cycle-conditional))
209+
210+
(when-refactoring-with-point-it "should cycle outer if"
211+
"(if-not |this
212+
(if that
213+
(then AAA)
214+
(else BBB))
215+
(otherwise CCC))"
216+
217+
"(if |this
218+
(otherwise CCC)
219+
(if that
220+
(then AAA)
221+
(else BBB)))"
222+
223+
(clojure-ts-cycle-conditional)))
224+
225+
(describe "clojure-cycle-when"
226+
227+
(when-refactoring-with-point-it "should cycle inner when"
228+
"(when this
229+
(when |that
230+
(aaa)
231+
(bbb))
232+
(ccc))"
233+
234+
"(when this
235+
(when-not |that
236+
(aaa)
237+
(bbb))
238+
(ccc))"
239+
240+
(clojure-ts-cycle-conditional))
241+
242+
(when-refactoring-with-point-it "should cycle outer when"
243+
"(when-not |this
244+
(when that
245+
(aaa)
246+
(bbb))
247+
(ccc))"
248+
249+
"(when |this
250+
(when that
251+
(aaa)
252+
(bbb))
253+
(ccc))"
254+
255+
(clojure-ts-cycle-conditional)))
256+
257+
(describe "clojure-cycle-not"
258+
259+
(when-refactoring-with-point-it "should add a not when missing"
260+
"(ala bala| portokala)"
261+
"(not (ala bala| portokala))"
262+
263+
(clojure-ts-cycle-not))
264+
265+
(when-refactoring-with-point-it "should remove a not when present"
266+
"(not (ala bala| portokala))"
267+
"(ala bala| portokala)"
268+
269+
(clojure-ts-cycle-not)))
270+
193271
(provide 'clojure-ts-mode-cycling-test)
194272
;;; clojure-ts-mode-cycling-test.el ends here

test/samples/refactoring.clj

+7
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,10 @@
134134
^{:bla "meta"}
135135
[arg]
136136
body)
137+
138+
(if ^boolean (= 2 2)
139+
true
140+
false)
141+
142+
(when-not true
143+
(println "Hello world"))

0 commit comments

Comments
 (0)