Skip to content

Commit 4bdd7f2

Browse files
rrudakovbbatsov
authored andcommitted
Add unwind refactoring commands
1 parent ff3969c commit 4bdd7f2

8 files changed

+461
-40
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Syntax highlighting of built-in keywords.
1111
- Consistent indentation with regular forms.
1212
- Support for automatic aligning forms.
13+
- [#88](https://github.com/clojure-emacs/clojure-ts-mode/pull/88): Introduce `clojure-ts-unwind` and `clojure-ts-unwind-all`.
1314

1415
## 0.3.0 (2025-04-15)
1516

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,28 @@ following customization:
372372
(setopt clojure-ts-outline-variant 'imenu)
373373
```
374374

375+
## Refactoring support
376+
377+
### Threading macros related features
378+
379+
`clojure-unwind`: Unwind a threaded expression. Supports both `->>`/`some->>`
380+
and `->`/`some->`.
381+
382+
`clojure-unwind-all`: Fully unwind a threaded expression removing the threading
383+
macro.
384+
385+
### Default keybindings
386+
387+
| Keybinding | Command |
388+
|:------------|:--------------------|
389+
| `C-c SPC` | `clojure-ts-align` |
390+
| `C-c C-r u` | `clojure-ts-unwind` |
391+
392+
### Customize refactoring commands prefix
393+
394+
By default prefix for all refactoring commands is `C-c C-r`. It can be changed
395+
by customizing `clojure-ts-refactor-map-prefix` variable.
396+
375397
## Migrating to clojure-ts-mode
376398

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

clojure-ts-mode.el

+195-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757

5858
(require 'treesit)
5959
(require 'align)
60+
(require 'subr-x)
6061

6162
(declare-function treesit-parser-create "treesit.c")
6263
(declare-function treesit-node-eq "treesit.c")
@@ -144,6 +145,11 @@ three or more semicolons will be treated as outline headings. If set to
144145
(const :tag "Use imenu" imenu))
145146
:package-version '(clojure-ts-mode . "0.4"))
146147

148+
(defcustom clojure-ts-refactor-map-prefix "C-c C-r"
149+
"Clojure refactor keymap prefix."
150+
:type 'string
151+
:package-version '(clojure-ts-mode . "0.4"))
152+
147153
(defcustom clojure-ts-align-reader-conditionals nil
148154
"Whether to align reader conditionals, as if they were maps."
149155
:package-version '(clojure-ts-mode . "0.4")
@@ -1691,11 +1697,199 @@ Forms between BEG and END are aligned according to
16911697
(when clojure-ts-align-forms-automatically
16921698
(clojure-ts-align beg end))))
16931699

1700+
;;; Refactoring
1701+
1702+
(defun clojure-ts--threading-sexp-node ()
1703+
"Return list node at point which is a threading expression."
1704+
(when-let* ((node-at-point (treesit-node-at (point) 'clojure t)))
1705+
;; We don't want to match `cond->' and `cond->>', so we should define a very
1706+
;; specific regexp.
1707+
(let ((sym-regex (rx bol (* "some") "->" (* ">") eol)))
1708+
(treesit-parent-until node-at-point
1709+
(lambda (node)
1710+
(and (or (clojure-ts--list-node-p node)
1711+
(clojure-ts--anon-fn-node-p node))
1712+
(let ((first-child (treesit-node-child node 0 t)))
1713+
(clojure-ts--symbol-matches-p sym-regex first-child))))
1714+
t))))
1715+
1716+
(defun clojure-ts--delete-and-extract-sexp ()
1717+
"Delete the surrounding sexp and return it."
1718+
(let* ((sexp-node (treesit-thing-at-point 'sexp 'nested))
1719+
(result (treesit-node-text sexp-node)))
1720+
(delete-region (treesit-node-start sexp-node)
1721+
(treesit-node-end sexp-node))
1722+
result))
1723+
1724+
(defun clojure-ts--ensure-parens-around-function-name ()
1725+
"Insert parens around function name if necessary."
1726+
(unless (string= (treesit-node-text (treesit-node-at (point))) "(")
1727+
(insert-parentheses 1)
1728+
(backward-up-list)))
1729+
1730+
(defun clojure-ts--multiline-sexp-p ()
1731+
"Return TRUE if s-expression at point is multiline."
1732+
(let ((sexp (treesit-thing-at-point 'sexp 'nested)))
1733+
(not (= (line-number-at-pos (treesit-node-start sexp))
1734+
(line-number-at-pos (treesit-node-end sexp))))))
1735+
1736+
(defun clojure-ts--unwind-thread-first ()
1737+
"Unwind a thread first macro once."
1738+
(let* ((threading-sexp (clojure-ts--threading-sexp-node))
1739+
(first-child-start (thread-first threading-sexp
1740+
(treesit-node-child 0 t)
1741+
(treesit-node-start)
1742+
(copy-marker))))
1743+
(save-excursion
1744+
(goto-char first-child-start)
1745+
(treesit-beginning-of-thing 'sexp -1)
1746+
(let ((contents (clojure-ts--delete-and-extract-sexp)))
1747+
(when (looking-at " *\n")
1748+
(join-line 'following))
1749+
(just-one-space)
1750+
(goto-char first-child-start)
1751+
(treesit-beginning-of-thing 'sexp -1)
1752+
(let ((multiline-p (clojure-ts--multiline-sexp-p)))
1753+
(clojure-ts--ensure-parens-around-function-name)
1754+
(down-list)
1755+
(forward-sexp)
1756+
(insert " " contents)
1757+
(when multiline-p
1758+
(insert "\n")))))))
1759+
1760+
(defun clojure-ts--unwind-thread-last ()
1761+
"Unwind a thread last macro once."
1762+
(let* ((threading-sexp (clojure-ts--threading-sexp-node))
1763+
(first-child-start (thread-first threading-sexp
1764+
(treesit-node-child 0 t)
1765+
(treesit-node-start)
1766+
(copy-marker))))
1767+
(save-excursion
1768+
(goto-char first-child-start)
1769+
(treesit-beginning-of-thing 'sexp -1)
1770+
(let ((contents (clojure-ts--delete-and-extract-sexp)))
1771+
(when (looking-at " *\n")
1772+
(join-line 'following))
1773+
(just-one-space)
1774+
(goto-char first-child-start)
1775+
(treesit-beginning-of-thing 'sexp -1)
1776+
(let ((multiline-p (clojure-ts--multiline-sexp-p)))
1777+
(clojure-ts--ensure-parens-around-function-name)
1778+
(forward-list)
1779+
(down-list -1)
1780+
(when multiline-p
1781+
(insert "\n"))
1782+
(insert " " contents))))))
1783+
1784+
(defun clojure-ts--node-threading-p (node)
1785+
"Return non-nil if NODE is a threading macro s-expression."
1786+
(and (or (clojure-ts--list-node-p node)
1787+
(clojure-ts--anon-fn-node-p node))
1788+
(let ((first-child (treesit-node-child node 0 t)))
1789+
(clojure-ts--symbol-matches-p clojure-ts--threading-macro first-child))))
1790+
1791+
(defun clojure-ts--skip-first-child (parent)
1792+
"Move point to the beginning of the first child of the PARENT node."
1793+
(thread-first parent
1794+
(treesit-node-child 1 t)
1795+
(treesit-node-start)
1796+
(goto-char)))
1797+
1798+
(defun clojure-ts--nothing-more-to-unwind ()
1799+
"Return TRUE if threading expression at point has only one argument."
1800+
(let ((threading-sexp (clojure-ts--threading-sexp-node)))
1801+
(save-excursion
1802+
(clojure-ts--skip-first-child threading-sexp)
1803+
(not (treesit-end-of-thing 'sexp 2 'restricted)))))
1804+
1805+
(defun clojure-ts--pop-out-of-threading ()
1806+
"Raise a sexp up a level to unwind a threading form."
1807+
(let ((threading-sexp (clojure-ts--threading-sexp-node)))
1808+
(save-excursion
1809+
(clojure-ts--skip-first-child threading-sexp)
1810+
(raise-sexp))))
1811+
1812+
(defun clojure-ts--fix-sexp-whitespace ()
1813+
"Fix whitespace after unwinding a threading form."
1814+
(save-excursion
1815+
(let ((beg (point)))
1816+
(treesit-end-of-thing 'sexp)
1817+
(indent-region beg (point))
1818+
(delete-trailing-whitespace beg (point)))))
1819+
1820+
(defun clojure-ts--unwind-sexps-counter ()
1821+
"Return total number of s-expressions of a threading form at point."
1822+
(if-let* ((threading-sexp (clojure-ts--threading-sexp-node)))
1823+
(save-excursion
1824+
(clojure-ts--skip-first-child threading-sexp)
1825+
(let ((n 0))
1826+
(while (treesit-end-of-thing 'sexp 1 'restricted)
1827+
(setq n (1+ n)))
1828+
n))
1829+
(user-error "No threading form to unwind at point")))
1830+
1831+
(defun clojure-ts-unwind (&optional n)
1832+
"Unwind thread at point or above point by N levels.
1833+
1834+
With universal argument \\[universal-argument], fully unwinds thread."
1835+
(interactive "P")
1836+
(setq n (cond
1837+
((equal n '(4)) (clojure-ts--unwind-sexps-counter))
1838+
(n)
1839+
(1)))
1840+
(if-let* ((threading-sexp (clojure-ts--threading-sexp-node))
1841+
(sym (thread-first threading-sexp
1842+
(treesit-node-child 0 t)
1843+
(clojure-ts--named-node-text))))
1844+
(save-excursion
1845+
(let ((beg (thread-first threading-sexp
1846+
(treesit-node-start)
1847+
(copy-marker)))
1848+
(end (thread-first threading-sexp
1849+
(treesit-node-end)
1850+
(copy-marker))))
1851+
(while (> n 0)
1852+
(cond
1853+
((string-match-p (rx bol (* "some") "->" eol) sym)
1854+
(clojure-ts--unwind-thread-first))
1855+
((string-match-p (rx bol (* "some") "->>" eol) sym)
1856+
(clojure-ts--unwind-thread-last)))
1857+
(setq n (1- n))
1858+
;; After unwinding we check if it is the last expression and maybe
1859+
;; splice it.
1860+
(when (clojure-ts--nothing-more-to-unwind)
1861+
(clojure-ts--pop-out-of-threading)
1862+
(clojure-ts--fix-sexp-whitespace)
1863+
(setq n 0)))
1864+
(indent-region beg end)
1865+
(delete-trailing-whitespace beg end)))
1866+
(user-error "No threading form to unwind at point")))
1867+
1868+
(defun clojure-ts-unwind-all ()
1869+
"Fully unwind thread at point or above point."
1870+
(interactive)
1871+
(clojure-ts-unwind '(4)))
1872+
1873+
(defvar clojure-ts-refactor-map
1874+
(let ((map (make-sparse-keymap)))
1875+
(keymap-set map "C-u" #'clojure-ts-unwind)
1876+
(keymap-set map "u" #'clojure-ts-unwind)
1877+
map)
1878+
"Keymap for `clojure-ts-mode' refactoring commands.")
1879+
16941880
(defvar clojure-ts-mode-map
16951881
(let ((map (make-sparse-keymap)))
16961882
;;(set-keymap-parent map clojure-mode-map)
16971883
(keymap-set map "C-c SPC" #'clojure-ts-align)
1698-
map))
1884+
(keymap-set map clojure-ts-refactor-map-prefix clojure-ts-refactor-map)
1885+
(easy-menu-define clojure-ts-mode-menu map "Clojure[TS] Mode Menu"
1886+
'("Clojure"
1887+
["Align expression" clojure-ts-align]
1888+
("Refactor -> and ->>"
1889+
["Unwind once" clojure-ts-unwind]
1890+
["Fully unwind a threading macro" clojure-ts-unwind-all])))
1891+
map)
1892+
"Keymap for `clojure-ts-mode'.")
16991893

17001894
(defvar clojure-ts-clojurescript-mode-map
17011895
(let ((map (make-sparse-keymap)))

test/clojure-ts-mode-font-lock-test.el

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@
3434
(declare (debug t)
3535
(indent 1))
3636
`(with-clojure-ts-buffer ,content
37-
(font-lock-ensure)
38-
(goto-char (point-min))
39-
,@body))
37+
(font-lock-ensure)
38+
(goto-char (point-min))
39+
,@body))
4040

4141
(defun clojure-ts-get-face-at (start end content)
4242
"Get the face between START and END in CONTENT."

0 commit comments

Comments
 (0)