56
56
; ;; Code:
57
57
58
58
(require 'treesit )
59
+ (require 'align )
59
60
60
61
(declare-function treesit-parser-create " treesit.c" )
61
62
(declare-function treesit-node-eq " treesit.c" )
@@ -126,6 +127,70 @@ double quotes on the third column."
126
127
:type 'boolean
127
128
:package-version '(clojure-ts-mode . " 0.3" ))
128
129
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
+
129
194
(defvar clojure-ts-mode-remappings
130
195
'((clojure-mode . clojure-ts-mode)
131
196
(clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -1025,6 +1090,18 @@ If NS is defined, then the fully qualified symbol is passed to
1025
1090
(seq-sort (lambda (spec1 _spec2 )
1026
1091
(equal (car spec1) :block )))))))))
1027
1092
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
+
1028
1105
(defun clojure-ts--find-semantic-rule (node parent current-depth )
1029
1106
" Return a suitable indentation rule for NODE, considering the CURRENT-DEPTH.
1030
1107
@@ -1034,16 +1111,8 @@ syntax tree and recursively attempts to find a rule, incrementally
1034
1111
increasing the CURRENT-DEPTH. If a rule is not found upon reaching the
1035
1112
root of the syntax tree, it returns nil. A rule is considered a match
1036
1113
only 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)))
1047
1116
(if (zerop current-depth)
1048
1117
(let ((rule (car rule-set)))
1049
1118
(if (equal (car rule) :block )
@@ -1277,9 +1346,147 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
1277
1346
(markdown-inline
1278
1347
(sexp ,(regexp-opt clojure-ts--markdown-inline-sexp-nodes))))))
1279
1348
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
+
1280
1486
(defvar clojure-ts-mode-map
1281
1487
(let ((map (make-sparse-keymap )))
1282
1488
; ;(set-keymap-parent map clojure-mode-map)
1489
+ (keymap-set map " C-c SPC" #'clojure-ts-align )
1283
1490
map))
1284
1491
1285
1492
(defvar clojure-ts-clojurescript-mode-map
@@ -1347,6 +1554,7 @@ function can also be used to upgrade the grammars if they are outdated."
1347
1554
(defun clojure-ts-mode-variables (&optional markdown-available )
1348
1555
" Initialize buffer-local variables for `clojure-ts-mode' .
1349
1556
See `clojure-ts--font-lock-settings' for usage of MARKDOWN-AVAILABLE."
1557
+ (setq-local indent-tabs-mode nil )
1350
1558
(setq-local comment-add 1 )
1351
1559
(setq-local comment-start " ;" )
1352
1560
0 commit comments