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 )
@@ -1061,7 +1130,9 @@ only if the CURRENT-DEPTH matches the rule's required depth."
1061
1130
(or (null rule-idx)
1062
1131
(equal rule-idx idx))))))
1063
1132
(seq-first)))
1064
- (when-let* ((new-parent (treesit-node-parent parent)))
1133
+ ; ; Let's go no more than 3 levels up to avoid performance degradation.
1134
+ (when-let* (((< current-depth 3 ))
1135
+ (new-parent (treesit-node-parent parent)))
1065
1136
(clojure-ts--find-semantic-rule parent
1066
1137
new-parent
1067
1138
(1+ current-depth))))))
@@ -1188,12 +1259,6 @@ if NODE has metadata and its parent has type NODE-TYPE."
1188
1259
`((clojure
1189
1260
((parent-is " source" ) parent-bol 0 )
1190
1261
(clojure-ts--match-docstring parent 0 )
1191
- ; ; https://guide.clojure.style/#body-indentation
1192
- (clojure-ts--match-form-body clojure-ts--anchor-parent-skip-metadata 2 )
1193
- ; ; https://guide.clojure.style/#threading-macros-alignment
1194
- (clojure-ts--match-threading-macro-arg prev-sibling 0 )
1195
- ; ; https://guide.clojure.style/#vertically-align-fn-args
1196
- (clojure-ts--match-function-call-arg (nth-sibling 2 nil ) 0 )
1197
1262
; ; Collections items with metadata.
1198
1263
; ;
1199
1264
; ; This should be before `clojure-ts--match-with-metadata' , otherwise they
@@ -1208,10 +1273,17 @@ if NODE has metadata and its parent has type NODE-TYPE."
1208
1273
; ; All other forms with metadata.
1209
1274
(clojure-ts--match-with-metadata parent 0 )
1210
1275
; ; Literal Sequences
1211
- ((parent-is " list_lit" ) parent 1 ) ; ; https://guide.clojure.style/#one-space-indent
1212
1276
((parent-is " vec_lit" ) parent 1 ) ; ; https://guide.clojure.style/#bindings-alignment
1213
1277
((parent-is " map_lit" ) parent 1 ) ; ; https://guide.clojure.style/#map-keys-alignment
1214
- ((parent-is " set_lit" ) parent 2 ))))
1278
+ ((parent-is " set_lit" ) parent 2 )
1279
+ ; ; https://guide.clojure.style/#body-indentation
1280
+ (clojure-ts--match-form-body clojure-ts--anchor-parent-skip-metadata 2 )
1281
+ ; ; https://guide.clojure.style/#threading-macros-alignment
1282
+ (clojure-ts--match-threading-macro-arg prev-sibling 0 )
1283
+ ; ; https://guide.clojure.style/#vertically-align-fn-args
1284
+ (clojure-ts--match-function-call-arg (nth-sibling 2 nil ) 0 )
1285
+ ; ; https://guide.clojure.style/#one-space-indent
1286
+ ((parent-is " list_lit" ) parent 1 ))))
1215
1287
1216
1288
(defun clojure-ts--configured-indent-rules ()
1217
1289
" Gets the configured choice of indent rules."
@@ -1277,9 +1349,177 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
1277
1349
(markdown-inline
1278
1350
(sexp ,(regexp-opt clojure-ts--markdown-inline-sexp-nodes))))))
1279
1351
1352
+ ; ;; Vertical alignment
1353
+
1354
+ (defun clojure-ts--beginning-of-defun-pos ()
1355
+ " Return the point that represents the beginning of the current defun."
1356
+ (treesit-node-start (treesit-defun-at-point)))
1357
+
1358
+ (defun clojure-ts--end-of-defun-pos ()
1359
+ " Return the point that represends the end of the current defun."
1360
+ (treesit-node-end (treesit-defun-at-point)))
1361
+
1362
+ (defun clojure-ts--search-whitespace-after-next-sexp (root-node bound )
1363
+ " Move the point after all whitespace following the next s-expression.
1364
+
1365
+ Set match data group 1 to this region of whitespace and return the
1366
+ point.
1367
+
1368
+ To move over the next s-expression, fetch the next node after the
1369
+ current cursor position that is a direct child of ROOT-NODE and navigate
1370
+ to its end. The most complex aspect here is handling nodes with
1371
+ metadata. Some forms are represented in the syntax tree as a single
1372
+ s-expression (for example, ^long my-var or ^String (str \" Hello\"
1373
+ \" world\" )), while other forms are two separate s-expressions (for
1374
+ example, ^long 123 or ^String \" Hello\" ). Expressions with two nodes
1375
+ share some common features:
1376
+
1377
+ - The top-level node type is usually sym_lit
1378
+
1379
+ - They do not have value children, or they have an empty name.
1380
+
1381
+ Regular expression and syntax analysis code is borrowed from
1382
+ `clojure-mode.'
1383
+
1384
+ BOUND bounds the whitespace search."
1385
+ (unwind-protect
1386
+ (when-let* ((cur-sexp (treesit-node-first-child-for-pos root-node (point ) t )))
1387
+ (goto-char (treesit-node-start cur-sexp))
1388
+ (if (and (string= " sym_lit" (treesit-node-type cur-sexp))
1389
+ (clojure-ts--metadata-node-p (treesit-node-child cur-sexp 0 t ))
1390
+ (and (not (treesit-node-child-by-field-name cur-sexp " value" ))
1391
+ (string-empty-p (clojure-ts--named-node-text cur-sexp))))
1392
+ (treesit-end-of-thing 'sexp 2 'restricted )
1393
+ (treesit-end-of-thing 'sexp 1 'restrict ))
1394
+ (when (looking-at " ," )
1395
+ (forward-char ))
1396
+ ; ; Move past any whitespace or comment.
1397
+ (search-forward-regexp " \\ ([,\s\t ]*\\ )\\ (;+.*\\ )?" bound)
1398
+ (pcase (syntax-after (point ))
1399
+ ; ; End-of-line, try again on next line.
1400
+ (`(12 ) (clojure-ts--search-whitespace-after-next-sexp root-node bound))
1401
+ ; ; Closing paren, stop here.
1402
+ (`(5 . , _ ) nil )
1403
+ ; ; Anything else is something to align.
1404
+ (_ (point ))))
1405
+ (when (and bound (> (point ) bound))
1406
+ (goto-char bound))))
1407
+
1408
+ (defun clojure-ts--get-nodes-to-align (region-node beg end )
1409
+ " Return a plist of nodes data for alignment.
1410
+
1411
+ The search is limited by BEG, END and REGION-NODE.
1412
+
1413
+ Possible node types are: map, bindings-vec, cond or read-cond.
1414
+
1415
+ The returned value is a list of property lists. Each property list
1416
+ includes `:sexp-type' , `:node' , `:beg-marker' , and `:end-marker' .
1417
+ Markers are necessary to fetch the same nodes after their boundaries
1418
+ have changed."
1419
+ (let* ((query (treesit-query-compile 'clojure
1420
+ (append
1421
+ `(((map_lit) @map)
1422
+ ((list_lit
1423
+ ((sym_lit) @sym
1424
+ (:match ,(clojure-ts-symbol-regexp clojure-ts-align-binding-forms) @sym))
1425
+ (vec_lit) @bindings-vec))
1426
+ ((list_lit
1427
+ ((sym_lit) @sym
1428
+ (:match ,(clojure-ts-symbol-regexp clojure-ts-align-cond-forms) @sym)))
1429
+ @cond))
1430
+ (when clojure-ts-align-reader-conditionals
1431
+ '(((read_cond_lit) @read-cond)))))))
1432
+ (thread-last (treesit-query-capture region-node query beg end)
1433
+ (seq-remove (lambda (elt ) (eq (car elt) 'sym )))
1434
+ ; ; When first node is reindented, all other nodes become
1435
+ ; ; outdated. Executing the entire query everytime is very
1436
+ ; ; expensive, instead we use markers for every captured node to
1437
+ ; ; retrieve only a single node later.
1438
+ (seq-map (lambda (elt )
1439
+ (let* ((sexp-type (car elt))
1440
+ (node (cdr elt))
1441
+ (beg-marker (copy-marker (treesit-node-start node) t ))
1442
+ (end-marker (copy-marker (treesit-node-end node))))
1443
+ (list :sexp-type sexp-type
1444
+ :node node
1445
+ :beg-marker beg-marker
1446
+ :end-marker end-marker)))))))
1447
+
1448
+ (defun clojure-ts--point-to-align-position (sexp-type node )
1449
+ " Move point to the appropriate position to align NODE.
1450
+
1451
+ For NODE with SEXP-TYPE map or bindings-vec, the appropriate
1452
+ position is after the first opening brace.
1453
+
1454
+ For NODE with SEXP-TYPE cond, we need to skip the first symbol and the
1455
+ subsequent special arguments based on block indentation rules."
1456
+ (goto-char (treesit-node-start node))
1457
+ (when-let* ((cur-sexp (treesit-node-first-child-for-pos node (point ) t )))
1458
+ (goto-char (treesit-node-start cur-sexp))
1459
+ ; ; For cond forms we need to skip first n + 1 nodes according to block
1460
+ ; ; indentation rules. First node to skip is the symbol itself.
1461
+ (when (equal sexp-type 'cond )
1462
+ (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node node))
1463
+ (rule (car rule-set))
1464
+ ((equal (car rule) :block )))
1465
+ (treesit-beginning-of-thing 'sexp (1- (- (cadr rule))) 'restrict )
1466
+ (treesit-beginning-of-thing 'sexp -1 )))))
1467
+
1468
+ (defun clojure-ts-align (beg end )
1469
+ " Vertically align the contents of the sexp around point.
1470
+
1471
+ If region is active, align it. Otherwise, align everything in the
1472
+ current \" top-level\" sexp. When called from lisp code align everything
1473
+ between BEG and END."
1474
+ (interactive (if (use-region-p )
1475
+ (list (region-beginning ) (region-end ))
1476
+ (save-excursion
1477
+ (let ((start (clojure-ts--beginning-of-defun-pos))
1478
+ (end (clojure-ts--end-of-defun-pos)))
1479
+ (list start end)))))
1480
+ (setq end (copy-marker end))
1481
+ (let* ((root-node (treesit-buffer-root-node 'clojure ))
1482
+ ; ; By default `treesit-query-capture' captures all nodes that cross the
1483
+ ; ; range. We need to restrict it to only nodes inside of the range.
1484
+ (region-node (treesit-node-descendant-for-range root-node beg (marker-position end) t ))
1485
+ (sexps-to-align (clojure-ts--get-nodes-to-align region-node beg (marker-position end))))
1486
+ (save-excursion
1487
+ (indent-region beg (marker-position end))
1488
+ (dolist (sexp sexps-to-align)
1489
+ ; ; After reindenting a node, all other nodes in the `sexps-to-align'
1490
+ ; ; list become outdated, so we need to fetch updated nodes for every
1491
+ ; ; iteration.
1492
+ (let* ((new-root-node (treesit-buffer-root-node 'clojure ))
1493
+ (new-region-node (treesit-node-descendant-for-range new-root-node
1494
+ beg
1495
+ (marker-position end)
1496
+ t ))
1497
+ (sexp-beg (marker-position (plist-get sexp :beg-marker )))
1498
+ (sexp-end (marker-position (plist-get sexp :end-marker )))
1499
+ (node (treesit-node-descendant-for-range new-region-node
1500
+ sexp-beg
1501
+ sexp-end
1502
+ t ))
1503
+ (sexp-type (plist-get sexp :sexp-type ))
1504
+ (node-end (treesit-node-end node)))
1505
+ (clojure-ts--point-to-align-position sexp-type node)
1506
+ (align-region (point ) node-end nil
1507
+ `((clojure-align (regexp . ,(lambda (&optional bound _noerror )
1508
+ (clojure-ts--search-whitespace-after-next-sexp node bound)))
1509
+ (group . 1 )
1510
+ (separate . , clojure-ts-align-separator )
1511
+ (repeat . t )))
1512
+ nil )
1513
+ ; ; After every iteration we have to re-indent the s-expression,
1514
+ ; ; otherwise some can be indented inconsistently.
1515
+ (indent-region (marker-position (plist-get sexp :beg-marker ))
1516
+ (marker-position (plist-get sexp :end-marker ))))))))
1517
+
1518
+
1280
1519
(defvar clojure-ts-mode-map
1281
1520
(let ((map (make-sparse-keymap )))
1282
1521
; ;(set-keymap-parent map clojure-mode-map)
1522
+ (keymap-set map " C-c SPC" #'clojure-ts-align )
1283
1523
map))
1284
1524
1285
1525
(defvar clojure-ts-clojurescript-mode-map
@@ -1347,6 +1587,7 @@ function can also be used to upgrade the grammars if they are outdated."
1347
1587
(defun clojure-ts-mode-variables (&optional markdown-available )
1348
1588
" Initialize buffer-local variables for `clojure-ts-mode' .
1349
1589
See `clojure-ts--font-lock-settings' for usage of MARKDOWN-AVAILABLE."
1590
+ (setq-local indent-tabs-mode nil )
1350
1591
(setq-local comment-add 1 )
1351
1592
(setq-local comment-start " ;" )
1352
1593
0 commit comments