Skip to content

Commit 3569c90

Browse files
rrudakovbbatsov
authored andcommitted
Introduce threading refactoring commands
1 parent 4bdd7f2 commit 3569c90

6 files changed

+524
-27
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
- Consistent indentation with regular forms.
1212
- Support for automatic aligning forms.
1313
- [#88](https://github.com/clojure-emacs/clojure-ts-mode/pull/88): Introduce `clojure-ts-unwind` and `clojure-ts-unwind-all`.
14+
- [#89](https://github.com/clojure-emacs/clojure-ts-mode/pull/89): Introduce `clojure-ts-thread`, `clojure-ts-thread-first-all` and
15+
`clojure-ts-thread-last-all`.
1416

1517
## 0.3.0 (2025-04-15)
1618

README.md

+46-4
Original file line numberDiff line numberDiff line change
@@ -376,24 +376,66 @@ following customization:
376376

377377
### Threading macros related features
378378

379+
`clojure-thread`: Thread another form into the surrounding thread. Both
380+
`->>`/`some->>` and `->`/`some->` variants are supported.
381+
379382
`clojure-unwind`: Unwind a threaded expression. Supports both `->>`/`some->>`
380383
and `->`/`some->`.
381384

385+
`clojure-thread-first-all`: Introduce the thread first macro (`->`) and rewrite
386+
the entire form. With a prefix argument do not thread the last form.
387+
388+
`clojure-thread-last-all`: Introduce the thread last macro and rewrite the
389+
entire form. With a prefix argument do not thread the last form.
390+
382391
`clojure-unwind-all`: Fully unwind a threaded expression removing the threading
383392
macro.
384393

385394
### Default keybindings
386395

387-
| Keybinding | Command |
388-
|:------------|:--------------------|
389-
| `C-c SPC` | `clojure-ts-align` |
390-
| `C-c C-r u` | `clojure-ts-unwind` |
396+
| Keybinding | Command |
397+
|:----------------------------|:------------------------------|
398+
| `C-c SPC` | `clojure-ts-align` |
399+
| `C-c C-r t` / `C-c C-r C-t` | `clojure-ts-thread` |
400+
| `C-c C-r u` / `C-c C-r C-u` | `clojure-ts-unwind` |
401+
| `C-c C-r f` / `C-c C-r C-f` | `clojure-ts-thread-first-all` |
402+
| `C-c C-r l` / `C-c C-r C-l` | `clojure-ts-thread-last-all` |
391403

392404
### Customize refactoring commands prefix
393405

394406
By default prefix for all refactoring commands is `C-c C-r`. It can be changed
395407
by customizing `clojure-ts-refactor-map-prefix` variable.
396408

409+
### Customize threading refactoring behavior
410+
411+
By default `clojure-ts-thread-first-all` and `clojure-ts-thread-last-all` will
412+
thread all nested expressions. For example this expression:
413+
414+
```clojure
415+
(->map (assoc {} :key "value") :lock)
416+
```
417+
418+
After executing `clojure-ts-thread-last-all` will be converted to:
419+
420+
```clojure
421+
(-> {}
422+
(assoc :key "value")
423+
(->map :lock))
424+
```
425+
426+
This behavior can be changed by setting:
427+
428+
```emacs-lisp
429+
(setopt clojure-ts-thread-all-but-last t)
430+
```
431+
432+
Then the last expression will not be threaded and the result will be:
433+
434+
```clojure
435+
(-> (assoc {} :key "value")
436+
(->map :lock))
437+
```
438+
397439
## Migrating to clojure-ts-mode
398440

399441
If you are migrating to `clojure-ts-mode` note that `clojure-mode` is still

clojure-ts-mode.el

+153-22
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ three or more semicolons will be treated as outline headings. If set to
150150
:type 'string
151151
:package-version '(clojure-ts-mode . "0.4"))
152152

153+
(defcustom clojure-ts-thread-all-but-last nil
154+
"Non-nil means do not thread the last expression.
155+
156+
This means that `clojure-ts-thread-first-all' and
157+
`clojure-ts-thread-last-all' not thread the deepest sexp inside the
158+
current sexp."
159+
:package-version '(clojure-ts-mode . "0.4.0")
160+
:safe #'booleanp
161+
:type 'boolean)
162+
153163
(defcustom clojure-ts-align-reader-conditionals nil
154164
"Whether to align reader conditionals, as if they were maps."
155165
:package-version '(clojure-ts-mode . "0.4")
@@ -1291,9 +1301,9 @@ according to the rule. If NODE is nil, use next node after BOL."
12911301
(clojure-ts--anon-fn-node-p parent))
12921302
;; Can the following two clauses be replaced by checking indexes?
12931303
;; Does the second child exist, and is it not equal to the current node?
1294-
(treesit-node-child parent 1 t)
1295-
(not (treesit-node-eq (treesit-node-child parent 1 t) node))
1296-
(let ((first-child (treesit-node-child parent 0 t)))
1304+
(clojure-ts--node-child-skip-metadata parent 1)
1305+
(not (treesit-node-eq (clojure-ts--node-child-skip-metadata parent 1) node))
1306+
(let ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
12971307
(or (clojure-ts--symbol-node-p first-child)
12981308
(clojure-ts--keyword-node-p first-child)
12991309
(clojure-ts--var-node-p first-child)))))
@@ -1381,17 +1391,11 @@ if NODE has metadata and its parent has type NODE-TYPE."
13811391
(treesit-node-type
13821392
(clojure-ts--node-with-metadata-parent node)))))
13831393

1384-
(defun clojure-ts--anchor-nth-sibling (n &optional named)
1385-
"Return the start of the Nth child of PARENT.
1386-
1387-
NAMED non-nil means count only named nodes.
1388-
1389-
NOTE: This is a replacement for built-in `nth-sibling' anchor preset,
1390-
which doesn't work properly for named nodes (see the bug
1391-
https://debbugs.gnu.org/cgi/bugreport.cgi?bug=78065)"
1394+
(defun clojure-ts--anchor-nth-sibling (n)
1395+
"Return the start of the Nth child of PARENT skipping metadata."
13921396
(lambda (_n parent &rest _)
13931397
(treesit-node-start
1394-
(treesit-node-child parent n named))))
1398+
(clojure-ts--node-child-skip-metadata parent n))))
13951399

13961400
(defun clojure-ts--semantic-indent-rules ()
13971401
"Return a list of indentation rules for `treesit-simple-indent-rules'."
@@ -1423,7 +1427,7 @@ https://debbugs.gnu.org/cgi/bugreport.cgi?bug=78065)"
14231427
;; https://guide.clojure.style/#threading-macros-alignment
14241428
(clojure-ts--match-threading-macro-arg prev-sibling 0)
14251429
;; https://guide.clojure.style/#vertically-align-fn-args
1426-
(clojure-ts--match-function-call-arg ,(clojure-ts--anchor-nth-sibling 1 t) 0)
1430+
(clojure-ts--match-function-call-arg ,(clojure-ts--anchor-nth-sibling 1) 0)
14271431
;; https://guide.clojure.style/#one-space-indent
14281432
((parent-is "list_lit") parent 1))))
14291433

@@ -1539,8 +1543,8 @@ BOUND bounds the whitespace search."
15391543
(and (not (treesit-node-child-by-field-name cur-sexp "value"))
15401544
(string-empty-p (clojure-ts--named-node-text cur-sexp))))
15411545
(treesit-end-of-thing 'sexp 2 'restricted)
1542-
(treesit-end-of-thing 'sexp 1 'restrict))
1543-
(when (looking-at ",")
1546+
(treesit-end-of-thing 'sexp 1 'restricted))
1547+
(when (looking-at-p ",")
15441548
(forward-char))
15451549
;; Move past any whitespace or comment.
15461550
(search-forward-regexp regex bound)
@@ -1744,7 +1748,7 @@ Forms between BEG and END are aligned according to
17441748
(goto-char first-child-start)
17451749
(treesit-beginning-of-thing 'sexp -1)
17461750
(let ((contents (clojure-ts--delete-and-extract-sexp)))
1747-
(when (looking-at " *\n")
1751+
(when (looking-at-p " *\n")
17481752
(join-line 'following))
17491753
(just-one-space)
17501754
(goto-char first-child-start)
@@ -1753,9 +1757,11 @@ Forms between BEG and END are aligned according to
17531757
(clojure-ts--ensure-parens-around-function-name)
17541758
(down-list)
17551759
(forward-sexp)
1756-
(insert " " contents)
1757-
(when multiline-p
1758-
(insert "\n")))))))
1760+
(cond
1761+
((and multiline-p (looking-at-p " *\n"))
1762+
(insert "\n" contents))
1763+
(multiline-p (insert " " contents "\n"))
1764+
(t (insert " " contents))))))))
17591765

17601766
(defun clojure-ts--unwind-thread-last ()
17611767
"Unwind a thread last macro once."
@@ -1768,7 +1774,7 @@ Forms between BEG and END are aligned according to
17681774
(goto-char first-child-start)
17691775
(treesit-beginning-of-thing 'sexp -1)
17701776
(let ((contents (clojure-ts--delete-and-extract-sexp)))
1771-
(when (looking-at " *\n")
1777+
(when (looking-at-p " *\n")
17721778
(join-line 'following))
17731779
(just-one-space)
17741780
(goto-char first-child-start)
@@ -1804,10 +1810,16 @@ Forms between BEG and END are aligned according to
18041810

18051811
(defun clojure-ts--pop-out-of-threading ()
18061812
"Raise a sexp up a level to unwind a threading form."
1807-
(let ((threading-sexp (clojure-ts--threading-sexp-node)))
1813+
(let* ((threading-sexp (clojure-ts--threading-sexp-node))
1814+
(beg (thread-first threading-sexp
1815+
(treesit-node-child 0 t)
1816+
(treesit-node-start))))
18081817
(save-excursion
18091818
(clojure-ts--skip-first-child threading-sexp)
1810-
(raise-sexp))))
1819+
(delete-region beg (point))
1820+
;; `raise-sexp' doesn't work properly for function literals (it loses one
1821+
;; of the parenthesis). Seems like an Emacs' bug.
1822+
(delete-pair))))
18111823

18121824
(defun clojure-ts--fix-sexp-whitespace ()
18131825
"Fix whitespace after unwinding a threading form."
@@ -1870,10 +1882,125 @@ With universal argument \\[universal-argument], fully unwinds thread."
18701882
(interactive)
18711883
(clojure-ts-unwind '(4)))
18721884

1885+
(defun clojure-ts--remove-superfluous-parens ()
1886+
"Remove extra parens from a form."
1887+
(when-let* ((node (treesit-thing-at-point 'sexp 'nested))
1888+
((clojure-ts--list-node-p node))
1889+
((= 1 (treesit-node-child-count node t))))
1890+
(let ((delete-pair-blink-delay 0))
1891+
(delete-pair))))
1892+
1893+
(defun clojure-ts--thread-first ()
1894+
"Thread a sexp using ->."
1895+
(save-excursion
1896+
(clojure-ts--skip-first-child (clojure-ts--threading-sexp-node))
1897+
(down-list)
1898+
(treesit-beginning-of-thing 'sexp -1)
1899+
(let ((contents (clojure-ts--delete-and-extract-sexp)))
1900+
(delete-char -1)
1901+
(when (looking-at-p " *\n")
1902+
(join-line 'following))
1903+
(backward-up-list)
1904+
(insert contents "\n")
1905+
(clojure-ts--remove-superfluous-parens))))
1906+
1907+
(defun clojure-ts--thread-last ()
1908+
"Thread a sexp using ->>."
1909+
(save-excursion
1910+
(clojure-ts--skip-first-child (clojure-ts--threading-sexp-node))
1911+
(treesit-end-of-thing 'sexp)
1912+
(down-list -1)
1913+
(treesit-beginning-of-thing 'sexp)
1914+
(let ((contents (clojure-ts--delete-and-extract-sexp)))
1915+
(delete-char -1)
1916+
(treesit-end-of-thing 'sexp -1 'restricted)
1917+
(when (looking-at-p " *\n")
1918+
(join-line 'following))
1919+
(backward-up-list)
1920+
(insert contents "\n")
1921+
(clojure-ts--remove-superfluous-parens))))
1922+
1923+
(defun clojure-ts--threadable-p (node)
1924+
"Return non-nil if expression NODE can be threaded.
1925+
1926+
First argument after threading symbol itself should be a list and it
1927+
should have more than one named child."
1928+
(let ((second-child (treesit-node-child node 1 t)))
1929+
(and (clojure-ts--list-node-p second-child)
1930+
(> (treesit-node-child-count second-child t) 1))))
1931+
1932+
(defun clojure-ts-thread (&optional called-by-user-p)
1933+
"Thread by one more level an existing threading macro.
1934+
1935+
If CALLED-BY-USER-P is non-nil (which is always TRUE when called
1936+
interactively), the function signals a `user-error' if threading form
1937+
cannot be found."
1938+
(interactive "p")
1939+
(if-let* ((threading-sexp (clojure-ts--threading-sexp-node))
1940+
((clojure-ts--threadable-p threading-sexp))
1941+
(sym (thread-first threading-sexp
1942+
(treesit-node-child 0 t)
1943+
(clojure-ts--named-node-text))))
1944+
(let ((beg (thread-first threading-sexp
1945+
(treesit-node-start)
1946+
(copy-marker)))
1947+
(end (thread-first threading-sexp
1948+
(treesit-node-end)
1949+
(copy-marker))))
1950+
(cond
1951+
((string-match-p (rx bol (* "some") "->" eol) sym)
1952+
(clojure-ts--thread-first))
1953+
((string-match-p (rx bol (* "some") "->>" eol) sym)
1954+
(clojure-ts--thread-last)))
1955+
(indent-region beg end)
1956+
(delete-trailing-whitespace beg end)
1957+
t)
1958+
(when called-by-user-p
1959+
(user-error "No threading form at point"))))
1960+
1961+
(defun clojure-ts--thread-all (first-or-last-thread but-last)
1962+
"Fully thread the form at point.
1963+
1964+
FIRST-OR-LAST-THREAD is either \"->\" or \"->>\".
1965+
1966+
When BUT-LAST is non-nil, the last expression is not threaded. Default
1967+
value is `clojure-ts-thread-all-but-last.'"
1968+
(if-let* ((list-at-point (treesit-thing-at-point 'list 'nested)))
1969+
(save-excursion
1970+
(goto-char (treesit-node-start list-at-point))
1971+
(insert-parentheses 1)
1972+
(insert first-or-last-thread)
1973+
(while (clojure-ts-thread))
1974+
(when (or but-last clojure-ts-thread-all-but-last)
1975+
(clojure-ts-unwind)))
1976+
(user-error "No list to thread at point")))
1977+
1978+
(defun clojure-ts-thread-first-all (but-last)
1979+
"Fully thread the form at point using ->.
1980+
1981+
When BUT-LAST is non-nil, the last expression is not threaded. Default
1982+
value is `clojure-ts-thread-all-but-last'."
1983+
(interactive "P")
1984+
(clojure-ts--thread-all "-> " but-last))
1985+
1986+
(defun clojure-ts-thread-last-all (but-last)
1987+
"Fully thread the form at point using ->>.
1988+
1989+
When BUT-LAST is non-nil, the last expression is not threaded. Default
1990+
value is `clojure-ts-thread-all-but-last'."
1991+
(interactive "P")
1992+
(clojure-ts--thread-all "->> " but-last))
1993+
18731994
(defvar clojure-ts-refactor-map
18741995
(let ((map (make-sparse-keymap)))
1996+
(keymap-set map "C-t" #'clojure-ts-thread)
1997+
(keymap-set map "t" #'clojure-ts-thread)
18751998
(keymap-set map "C-u" #'clojure-ts-unwind)
18761999
(keymap-set map "u" #'clojure-ts-unwind)
2000+
(keymap-set map "C-f" #'clojure-ts-thread-first-all)
2001+
(keymap-set map "f" #'clojure-ts-thread-first-all)
2002+
(keymap-set map "C-l" #'clojure-ts-thread-last-all)
2003+
(keymap-set map "l" #'clojure-ts-thread-last-all)
18772004
map)
18782005
"Keymap for `clojure-ts-mode' refactoring commands.")
18792006

@@ -1886,6 +2013,10 @@ With universal argument \\[universal-argument], fully unwinds thread."
18862013
'("Clojure"
18872014
["Align expression" clojure-ts-align]
18882015
("Refactor -> and ->>"
2016+
["Thread once more" clojure-ts-thread]
2017+
["Fully thread a form with ->" clojure-ts-thread-first-all]
2018+
["Fully thread a form with ->>" clojure-ts-thread-last-all]
2019+
"--"
18892020
["Unwind once" clojure-ts-unwind]
18902021
["Fully unwind a threading macro" clojure-ts-unwind-all])))
18912022
map)

0 commit comments

Comments
 (0)