|
57 | 57 |
|
58 | 58 | (require 'treesit)
|
59 | 59 | (require 'align)
|
| 60 | +(require 'subr-x) |
60 | 61 |
|
61 | 62 | (declare-function treesit-parser-create "treesit.c")
|
62 | 63 | (declare-function treesit-node-eq "treesit.c")
|
@@ -144,6 +145,11 @@ three or more semicolons will be treated as outline headings. If set to
|
144 | 145 | (const :tag "Use imenu" imenu))
|
145 | 146 | :package-version '(clojure-ts-mode . "0.4"))
|
146 | 147 |
|
| 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 | + |
147 | 153 | (defcustom clojure-ts-align-reader-conditionals nil
|
148 | 154 | "Whether to align reader conditionals, as if they were maps."
|
149 | 155 | :package-version '(clojure-ts-mode . "0.4")
|
@@ -1691,11 +1697,199 @@ Forms between BEG and END are aligned according to
|
1691 | 1697 | (when clojure-ts-align-forms-automatically
|
1692 | 1698 | (clojure-ts-align beg end))))
|
1693 | 1699 |
|
| 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 | + |
1694 | 1880 | (defvar clojure-ts-mode-map
|
1695 | 1881 | (let ((map (make-sparse-keymap)))
|
1696 | 1882 | ;;(set-keymap-parent map clojure-mode-map)
|
1697 | 1883 | (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'.") |
1699 | 1893 |
|
1700 | 1894 | (defvar clojure-ts-clojurescript-mode-map
|
1701 | 1895 | (let ((map (make-sparse-keymap)))
|
|
0 commit comments