5656; ;; Code:
5757
5858(require 'treesit )
59+ (require 'align )
5960
6061(declare-function treesit-parser-create " treesit.c" )
6162(declare-function treesit-node-eq " treesit.c" )
@@ -126,6 +127,70 @@ double quotes on the third column."
126127 :type 'boolean
127128 :package-version '(clojure-ts-mode . " 0.3" ))
128129
130+ (defcustom clojure-ts-align-reader-conditionals nil
131+ " Whether to align reader conditionals, as if they were maps."
132+ :package-version '(clojure-ts-mode . " 0.4" )
133+ :safe #'booleanp
134+ :type 'boolean )
135+
136+ (defcustom clojure-ts-align-binding-forms
137+ '(" let"
138+ " when-let"
139+ " when-some"
140+ " if-let"
141+ " if-some"
142+ " binding"
143+ " loop"
144+ " doseq"
145+ " for"
146+ " with-open"
147+ " with-local-vars"
148+ " with-redefs"
149+ " clojure.core/let"
150+ " clojure.core/when-let"
151+ " clojure.core/when-some"
152+ " clojure.core/if-let"
153+ " clojure.core/if-some"
154+ " clojure.core/binding"
155+ " clojure.core/loop"
156+ " clojure.core/doseq"
157+ " clojure.core/for"
158+ " clojure.core/with-open"
159+ " clojure.core/with-local-vars"
160+ " clojure.core/with-redefs" )
161+ " List of strings matching forms that have binding forms."
162+ :package-version '(clojure-ts-mode . " 0.4" )
163+ :safe #'listp
164+ :type '(repeat string))
165+
166+ (defconst clojure-ts--align-separator-newline-regexp " ^ *$" )
167+
168+ (defcustom clojure-ts-align-separator clojure-ts--align-separator-newline-regexp
169+ " Separator passed to `align-region' when performing vertical alignment."
170+ :package-version '(clojure-ts-mode . " 0.4" )
171+ :type `(choice (const :tag " Make blank lines prevent vertical alignment from happening."
172+ , clojure-ts--align-separator-newline-regexp )
173+ (other :tag " Allow blank lines to happen within a vertically-aligned expression."
174+ entire)))
175+
176+ (defcustom clojure-ts-align-cond-forms
177+ '(" condp"
178+ " cond"
179+ " cond->"
180+ " cond->>"
181+ " case"
182+ " are"
183+ " clojure.core/condp"
184+ " clojure.core/cond"
185+ " clojure.core/cond->"
186+ " clojure.core/cond->>"
187+ " clojure.core/case"
188+ " clojure.core/are" )
189+ " List of strings identifying cond-like forms."
190+ :package-version '(clojure-ts-mode . " 0.4" )
191+ :safe #'listp
192+ :type '(repeat string))
193+
129194(defvar clojure-ts-mode-remappings
130195 '((clojure-mode . clojure-ts-mode)
131196 (clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -1025,6 +1090,18 @@ If NS is defined, then the fully qualified symbol is passed to
10251090 (seq-sort (lambda (spec1 _spec2 )
10261091 (equal (car spec1) :block )))))))))
10271092
1093+ (defun clojure-ts--find-semantic-rules-for-node (node )
1094+ " Return a list of semantic rules for NODE."
1095+ (let* ((first-child (clojure-ts--node-child-skip-metadata node 0 ))
1096+ (symbol-name (clojure-ts--named-node-text first-child))
1097+ (symbol-namespace (clojure-ts--node-namespace-text first-child)))
1098+ (or (clojure-ts--dynamic-indent-for-symbol symbol-name symbol-namespace)
1099+ (alist-get symbol-name
1100+ clojure-ts--semantic-indent-rules-cache
1101+ nil
1102+ nil
1103+ #'equal ))))
1104+
10281105(defun clojure-ts--find-semantic-rule (node parent current-depth )
10291106 " Return a suitable indentation rule for NODE, considering the CURRENT-DEPTH.
10301107
@@ -1034,16 +1111,8 @@ syntax tree and recursively attempts to find a rule, incrementally
10341111increasing the CURRENT-DEPTH. If a rule is not found upon reaching the
10351112root of the syntax tree, it returns nil. A rule is considered a match
10361113only if the CURRENT-DEPTH matches the rule's required depth."
1037- (let* ((first-child (clojure-ts--node-child-skip-metadata parent 0 ))
1038- (symbol-name (clojure-ts--named-node-text first-child))
1039- (symbol-namespace (clojure-ts--node-namespace-text first-child))
1040- (idx (- (treesit-node-index node) 2 )))
1041- (if-let* ((rule-set (or (clojure-ts--dynamic-indent-for-symbol symbol-name symbol-namespace)
1042- (alist-get symbol-name
1043- clojure-ts--semantic-indent-rules-cache
1044- nil
1045- nil
1046- #'equal ))))
1114+ (let* ((idx (- (treesit-node-index node) 2 )))
1115+ (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node parent)))
10471116 (if (zerop current-depth)
10481117 (let ((rule (car rule-set)))
10491118 (if (equal (car rule) :block )
@@ -1277,9 +1346,147 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
12771346 (markdown-inline
12781347 (sexp ,(regexp-opt clojure-ts--markdown-inline-sexp-nodes))))))
12791348
1349+ ; ;; Vertical alignment
1350+
1351+ (defun clojure-ts--beginning-of-defun-pos ()
1352+ " Return the point that represents the beginning of the current defun."
1353+ (treesit-beginning-of-defun)
1354+ (point ))
1355+
1356+ (defun clojure-ts--end-of-defun-pos ()
1357+ " Return the point that represends the end of the current defun."
1358+ (treesit-end-of-defun)
1359+ (point ))
1360+
1361+ (defun clojure-ts--search-whitespace-after-next-sexp (root-node bound )
1362+ " Move the point after all whitespace following the next s-expression.
1363+
1364+ Set match data group 1 to this region of whitespace and return the
1365+ point.
1366+
1367+ To move over the next s-expression, fetch the next node after the
1368+ current cursor position that is a direct child of ROOT-NODE and navigate
1369+ to its end. The most complex aspect here is handling nodes with
1370+ metadata. Some forms are represented in the syntax tree as a single
1371+ s-expression (for example, ^long my-var or ^String (str \" Hello\"
1372+ \" world\" )), while other forms are two separate s-expressions (for
1373+ example, ^long 123 or ^String \" Hello\" ). Expressions with two nodes
1374+ share some common features:
1375+
1376+ - The top-level node type is usually sym_lit
1377+
1378+ - They do not have value children, or they have an empty name.
1379+
1380+ Regular expression and syntax analysis code is borrowed from
1381+ `clojure-mode.'
1382+
1383+ BOUND bounds the whitespace search."
1384+ (unwind-protect
1385+ (when-let* ((cur-sexp (treesit-node-first-child-for-pos root-node (point ) t )))
1386+ (goto-char (treesit-node-start cur-sexp))
1387+ (if (and (string= " sym_lit" (treesit-node-type cur-sexp))
1388+ (clojure-ts--metadata-node-p (treesit-node-child cur-sexp 0 t ))
1389+ (and (not (treesit-node-child-by-field-name cur-sexp " value" ))
1390+ (string-empty-p (clojure-ts--named-node-text cur-sexp))))
1391+ (treesit-end-of-thing 'sexp 2 'restricted )
1392+ (treesit-end-of-thing 'sexp 1 'restrict ))
1393+ (when (looking-at " ," )
1394+ (forward-char ))
1395+ ; ; Move past any whitespace or comment.
1396+ (search-forward-regexp " \\ ([,\s\t ]*\\ )\\ (;+.*\\ )?" bound)
1397+ (pcase (syntax-after (point ))
1398+ ; ; End-of-line, try again on next line.
1399+ (`(12 ) (clojure-ts--search-whitespace-after-next-sexp root-node bound))
1400+ ; ; Closing paren, stop here.
1401+ (`(5 . , _ ) nil )
1402+ ; ; Anything else is something to align.
1403+ (_ (point ))))
1404+ (when (and bound (> (point ) bound))
1405+ (goto-char bound))))
1406+
1407+ (defun clojure-ts--get-nodes-to-align (beg end )
1408+ " Return an alist of node types and nodes for alignment.
1409+
1410+ The search is limited by BEG and END.
1411+
1412+ Possible node types are: map, bindings-vec, cond or read-cond."
1413+ (let* ((query (treesit-query-compile 'clojure
1414+ (append
1415+ `(((map_lit) @map)
1416+ ((list_lit
1417+ ((sym_lit) @sym
1418+ (:match ,(clojure-ts-symbol-regexp clojure-ts-align-binding-forms) @sym))
1419+ (vec_lit) @bindings-vec))
1420+ ((list_lit
1421+ ((sym_lit) @sym
1422+ (:match ,(clojure-ts-symbol-regexp clojure-ts-align-cond-forms) @sym)))
1423+ @cond))
1424+ (when clojure-ts-align-reader-conditionals
1425+ '(((read_cond_lit) @read-cond)))))))
1426+ (thread-last (treesit-query-capture 'clojure query beg end)
1427+ (seq-filter (lambda (elt )
1428+ (member (car elt)
1429+ '(map bindings-vec cond read-cond)))))))
1430+
1431+ (defun clojure-ts--point-to-align-position (sexp )
1432+ " Move point to the appropriate position to align SEXP.
1433+
1434+ For node with sexp type map or bindings-vec, the appropriate position is
1435+ after the first opening brace.
1436+
1437+ For node with sexp type cond, we need to skip the first symbol and the
1438+ subsequent special arguments based on block indentation rules."
1439+ (pcase-let ((`(, sexp-type . , root-node ) sexp))
1440+ (goto-char (treesit-node-start root-node))
1441+ (when-let* ((cur-sexp (treesit-node-first-child-for-pos root-node (point ) t )))
1442+ (goto-char (treesit-node-start cur-sexp))
1443+ ; ; For cond forms we need to skip first n + 1 nodes according to block
1444+ ; ; indentation rules. First node to skip is the symbol itself.
1445+ (when (equal sexp-type 'cond )
1446+ (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node root-node))
1447+ (rule (car rule-set))
1448+ ((equal (car rule) :block )))
1449+ (treesit-beginning-of-thing 'sexp (1- (- (cadr rule))) 'restrict )
1450+ (treesit-beginning-of-thing 'sexp -1 ))))))
1451+
1452+ (defun clojure-ts-align (beg end )
1453+ " Vertically align the contents of the sexp around point.
1454+
1455+ If region is active, align it. Otherwise, align everything in the
1456+ current \" top-level\" sexp. When called from lisp code align everything
1457+ between BEG and END."
1458+ (interactive (if (use-region-p )
1459+ (list (region-beginning ) (region-end ))
1460+ (save-excursion
1461+ (let ((start (clojure-ts--beginning-of-defun-pos))
1462+ (end (clojure-ts--end-of-defun-pos)))
1463+ (list start end)))))
1464+ (setq end (copy-marker end))
1465+ (let ((sexps-to-align (clojure-ts--get-nodes-to-align beg (marker-position end))))
1466+ (save-excursion
1467+ (dotimes (idx (seq-count #'identity sexps-to-align))
1468+ ; ; After every iteration we have to re-indent the entire defun,
1469+ ; ; otherwise some can be indented inconsistently.
1470+ (indent-region beg (clojure-ts--end-of-defun-pos))
1471+ ; ; After reindenting a node, all other nodes in the `sexps-to-align'
1472+ ; ; list become outdated, so we need to fetch updated nodes for every
1473+ ; ; iteration.
1474+ (let* ((sexp-to-align (seq-elt (clojure-ts--get-nodes-to-align beg (marker-position end)) idx))
1475+ (node (cdr sexp-to-align))
1476+ (node-end (treesit-node-end node)))
1477+ (clojure-ts--point-to-align-position sexp-to-align)
1478+ (align-region (point ) node-end nil
1479+ `((clojure-align (regexp . ,(lambda (&optional bound _noerror )
1480+ (clojure-ts--search-whitespace-after-next-sexp node bound)))
1481+ (group . 1 )
1482+ (separate . , clojure-ts-align-separator )
1483+ (repeat . t )))
1484+ nil ))))))
1485+
12801486(defvar clojure-ts-mode-map
12811487 (let ((map (make-sparse-keymap )))
12821488 ; ;(set-keymap-parent map clojure-mode-map)
1489+ (keymap-set map " C-c SPC" #'clojure-ts-align )
12831490 map))
12841491
12851492(defvar clojure-ts-clojurescript-mode-map
@@ -1347,6 +1554,7 @@ function can also be used to upgrade the grammars if they are outdated."
13471554(defun clojure-ts-mode-variables (&optional markdown-available )
13481555 " Initialize buffer-local variables for `clojure-ts-mode' .
13491556See `clojure-ts--font-lock-settings' for usage of MARKDOWN-AVAILABLE."
1557+ (setq-local indent-tabs-mode nil )
13501558 (setq-local comment-add 1 )
13511559 (setq-local comment-start " ;" )
13521560
0 commit comments