[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[nongnu] elpa/clojure-ts-mode e960a905ab: [#16] Add support for automati
From: |
ELPA Syncer |
Subject: |
[nongnu] elpa/clojure-ts-mode e960a905ab: [#16] Add support for automatic aligning forms |
Date: |
Fri, 25 Apr 2025 13:01:34 -0400 (EDT) |
branch: elpa/clojure-ts-mode
commit e960a905ab9ae6c77101ca1e65dd76e59c7f4009
Author: Roman Rudakov <rrudakov@fastmail.com>
Commit: Bozhidar Batsov <bozhidar@batsov.dev>
[#16] Add support for automatic aligning forms
---
CHANGELOG.md | 1 +
README.md | 4 +
clojure-ts-mode.el | 145 ++++++++++++++++--------
test/clojure-ts-mode-indentation-test.el | 188 ++++++++++++++++++++++++++++++-
test/samples/align.clj | 27 ++++-
5 files changed, 318 insertions(+), 47 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c11acd343..281c42581a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
- [#16](https://github.com/clojure-emacs/clojure-ts-mode/issues/16): Introduce
`clojure-ts-align`.
- [#11](https://github.com/clojure-emacs/clojure-ts-mode/issues/11): Enable
regex syntax highlighting.
+- [#16](https://github.com/clojure-emacs/clojure-ts-mode/issues/16): Add
support for automatic aligning forms.
## 0.3.0 (2025-04-15)
diff --git a/README.md b/README.md
index 02e598a878..251effc071 100644
--- a/README.md
+++ b/README.md
@@ -259,6 +259,10 @@ Leads to the following:
:other-key 2})
```
+This can also be done automatically (as part of indentation) by turning on
+`clojure-ts-align-forms-automatically`. This way it will happen whenever you
+select some code and hit `TAB`.
+
Forms that can be aligned vertically are configured via the following
variables:
- `clojure-ts-align-reader-conditionals` - align reader conditionals as if they
diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el
index f88f342158..f1de91d015 100644
--- a/clojure-ts-mode.el
+++ b/clojure-ts-mode.el
@@ -197,6 +197,22 @@ double quotes on the third column."
:safe #'listp
:type '(repeat string))
+(defcustom clojure-ts-align-forms-automatically nil
+ "If non-nil, vertically align some forms automatically.
+
+Automatically means it is done as part of indenting code. This applies
+to binding forms (`clojure-ts-align-binding-forms'), to cond
+forms (`clojure-ts-align-cond-forms') and to map literals. For
+instance, selecting a map a hitting
+\\<clojure-ts-mode-map>`\\[indent-for-tab-command]' will align the
+values like this:
+
+{:some-key 10
+ :key2 20}"
+ :package-version '(clojure-ts-mode . "0.4")
+ :safe #'booleanp
+ :type 'boolean)
+
(defvar clojure-ts-mode-remappings
'((clojure-mode . clojure-ts-mode)
(clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -1340,6 +1356,9 @@ if NODE has metadata and its parent has type NODE-TYPE."
((parent-is "vec_lit") parent 1) ;;
https://guide.clojure.style/#bindings-alignment
((parent-is "map_lit") parent 1) ;;
https://guide.clojure.style/#map-keys-alignment
((parent-is "set_lit") parent 2)
+ ((parent-is "splicing_read_cond_lit") parent 4)
+ ((parent-is "read_cond_lit") parent 3)
+ ((parent-is "tagged_or_ctor_lit") parent 0)
;; https://guide.clojure.style/#body-indentation
(clojure-ts--match-form-body clojure-ts--anchor-parent-skip-metadata 2)
;; https://guide.clojure.style/#threading-macros-alignment
@@ -1447,32 +1466,56 @@ Regular expression and syntax analysis code is borrowed
from
BOUND bounds the whitespace search."
(unwind-protect
- (when-let* ((cur-sexp (treesit-node-first-child-for-pos root-node
(point) t)))
- (goto-char (treesit-node-start cur-sexp))
- (if (and (string= "sym_lit" (treesit-node-type cur-sexp))
- (clojure-ts--metadata-node-p (treesit-node-child cur-sexp 0
t))
- (and (not (treesit-node-child-by-field-name cur-sexp "value"))
- (string-empty-p (clojure-ts--named-node-text cur-sexp))))
- (treesit-end-of-thing 'sexp 2 'restricted)
- (treesit-end-of-thing 'sexp 1 'restrict))
- (when (looking-at ",")
- (forward-char))
- ;; Move past any whitespace or comment.
- (search-forward-regexp "\\([,\s\t]*\\)\\(;+.*\\)?" bound)
- (pcase (syntax-after (point))
- ;; End-of-line, try again on next line.
- (`(12) (clojure-ts--search-whitespace-after-next-sexp root-node
bound))
- ;; Closing paren, stop here.
- (`(5 . ,_) nil)
- ;; Anything else is something to align.
- (_ (point))))
+ (let ((regex "\\([,\s\t]*\\)\\(;+.*\\)?"))
+ ;; If we're on an empty line, we should return match, otherwise
+ ;; `clojure-ts-align-separator' setting won't work.
+ (if (and (bolp) (looking-at-p "[[:blank:]]*$"))
+ (progn
+ (search-forward-regexp regex bound)
+ (point))
+ (when-let* ((cur-sexp (treesit-node-first-child-for-pos root-node
(point) t)))
+ (goto-char (treesit-node-start cur-sexp))
+ (if (and (string= "sym_lit" (treesit-node-type cur-sexp))
+ (clojure-ts--metadata-node-p (treesit-node-child cur-sexp
0 t))
+ (and (not (treesit-node-child-by-field-name cur-sexp
"value"))
+ (string-empty-p (clojure-ts--named-node-text
cur-sexp))))
+ (treesit-end-of-thing 'sexp 2 'restricted)
+ (treesit-end-of-thing 'sexp 1 'restrict))
+ (when (looking-at ",")
+ (forward-char))
+ ;; Move past any whitespace or comment.
+ (search-forward-regexp regex bound)
+ (pcase (syntax-after (point))
+ ;; End-of-line, try again on next line.
+ (`(12) (progn
+ (forward-char 1)
+ (clojure-ts--search-whitespace-after-next-sexp
root-node bound)))
+ ;; Closing paren, stop here.
+ (`(5 . ,_) nil)
+ ;; Anything else is something to align.
+ (_ (point))))))
(when (and bound (> (point) bound))
(goto-char bound))))
-(defun clojure-ts--get-nodes-to-align (region-node beg end)
+(defun clojure-ts--region-node (beg end)
+ "Return the smallest node that covers buffer positions BEG to END."
+ (let* ((root-node (treesit-buffer-root-node 'clojure)))
+ (treesit-node-descendant-for-range root-node beg end t)))
+
+(defun clojure-ts--node-from-sexp-data (beg end sexp)
+ "Return updated node using SEXP data in the region between BEG and END."
+ (let* ((new-region-node (clojure-ts--region-node beg end))
+ (sexp-beg (marker-position (plist-get sexp :beg-marker)))
+ (sexp-end (marker-position (plist-get sexp :end-marker))))
+ (treesit-node-descendant-for-range new-region-node
+ sexp-beg
+ sexp-end
+ t)))
+
+(defun clojure-ts--get-nodes-to-align (beg end)
"Return a plist of nodes data for alignment.
-The search is limited by BEG, END and REGION-NODE.
+The search is limited by BEG, END.
Possible node types are: map, bindings-vec, cond or read-cond.
@@ -1480,7 +1523,10 @@ The returned value is a list of property lists. Each
property list
includes `:sexp-type', `:node', `:beg-marker', and `:end-marker'.
Markers are necessary to fetch the same nodes after their boundaries
have changed."
- (let* ((query (treesit-query-compile 'clojure
+ ;; By default `treesit-query-capture' captures all nodes that cross the
range.
+ ;; We need to restrict it to only nodes inside of the range.
+ (let* ((region-node (clojure-ts--region-node beg end))
+ (query (treesit-query-compile 'clojure
(append
`(((map_lit) @map)
((list_lit
@@ -1492,7 +1538,8 @@ have changed."
(:match
,(clojure-ts-symbol-regexp clojure-ts-align-cond-forms) @sym)))
@cond))
(when
clojure-ts-align-reader-conditionals
- '(((read_cond_lit) @read-cond)))))))
+ '(((read_cond_lit) @read-cond)
+ ((splicing_read_cond_lit)
@read-cond)))))))
(thread-last (treesit-query-capture region-node query beg end)
(seq-remove (lambda (elt) (eq (car elt) 'sym)))
;; When first node is reindented, all other nodes become
@@ -1538,38 +1585,29 @@ between BEG and END."
(interactive (if (use-region-p)
(list (region-beginning) (region-end))
(save-excursion
- (let ((start (clojure-ts--beginning-of-defun-pos))
- (end (clojure-ts--end-of-defun-pos)))
- (list start end)))))
+ (if (not (treesit-defun-at-point))
+ (user-error "No defun at point")
+ (let ((start (clojure-ts--beginning-of-defun-pos))
+ (end (clojure-ts--end-of-defun-pos)))
+ (list start end))))))
(setq end (copy-marker end))
- (let* ((root-node (treesit-buffer-root-node 'clojure))
- ;; By default `treesit-query-capture' captures all nodes that cross
the
- ;; range. We need to restrict it to only nodes inside of the range.
- (region-node (treesit-node-descendant-for-range root-node beg
(marker-position end) t))
- (sexps-to-align (clojure-ts--get-nodes-to-align region-node beg
(marker-position end))))
+ (let* ((sexps-to-align (clojure-ts--get-nodes-to-align beg (marker-position
end)))
+ ;; We have to disable it here to avoid endless recursion.
+ (clojure-ts-align-forms-automatically nil))
(save-excursion
- (indent-region beg (marker-position end))
+ (indent-region beg end)
(dolist (sexp sexps-to-align)
;; After reindenting a node, all other nodes in the `sexps-to-align'
;; list become outdated, so we need to fetch updated nodes for every
;; iteration.
- (let* ((new-root-node (treesit-buffer-root-node 'clojure))
- (new-region-node (treesit-node-descendant-for-range
new-root-node
- beg
-
(marker-position end)
- t))
- (sexp-beg (marker-position (plist-get sexp :beg-marker)))
- (sexp-end (marker-position (plist-get sexp :end-marker)))
- (node (treesit-node-descendant-for-range new-region-node
- sexp-beg
- sexp-end
- t))
+ (let* ((node (clojure-ts--node-from-sexp-data beg (marker-position
end) sexp))
(sexp-type (plist-get sexp :sexp-type))
(node-end (treesit-node-end node)))
(clojure-ts--point-to-align-position sexp-type node)
(align-region (point) node-end nil
`((clojure-align (regexp . ,(lambda (&optional bound
_noerror)
-
(clojure-ts--search-whitespace-after-next-sexp node bound)))
+ (let ((updated-node
(clojure-ts--node-from-sexp-data beg (marker-position end) sexp)))
+
(clojure-ts--search-whitespace-after-next-sexp updated-node bound))))
(group . 1)
(separate .
,clojure-ts-align-separator)
(repeat . t)))
@@ -1577,8 +1615,20 @@ between BEG and END."
;; After every iteration we have to re-indent the s-expression,
;; otherwise some can be indented inconsistently.
(indent-region (marker-position (plist-get sexp :beg-marker))
- (marker-position (plist-get sexp :end-marker))))))))
+ (plist-get sexp :end-marker))))
+ ;; If `clojure-ts-align-separator' is used, `align-region' leaves
trailing
+ ;; whitespaces on empty lines.
+ (delete-trailing-whitespace beg (marker-position end)))))
+
+(defun clojure-ts-indent-region (beg end)
+ "Like `indent-region', but also maybe align forms.
+Forms between BEG and END are aligned according to
+`clojure-ts-align-forms-automatically'."
+ (prog1 (let ((indent-region-function #'treesit-indent-region))
+ (indent-region beg end))
+ (when clojure-ts-align-forms-automatically
+ (clojure-ts-align beg end))))
(defvar clojure-ts-mode-map
(let ((map (make-sparse-keymap)))
@@ -1717,6 +1767,11 @@ REGEX-AVAILABLE."
(treesit-major-mode-setup)
+ ;; We should assign this after calling `treesit-major-mode-setup',
+ ;; otherwise it will be owerwritten.
+ (when clojure-ts-align-forms-automatically
+ (setq-local indent-region-function #'clojure-ts-indent-region))
+
;; Initial indentation rules cache calculation.
(setq clojure-ts--semantic-indent-rules-cache
(clojure-ts--compute-semantic-indentation-rules-cache
clojure-ts-semantic-indent-rules))
diff --git a/test/clojure-ts-mode-indentation-test.el
b/test/clojure-ts-mode-indentation-test.el
index 75ceb6d6df..fe181f9c63 100644
--- a/test/clojure-ts-mode-indentation-test.el
+++ b/test/clojure-ts-mode-indentation-test.el
@@ -75,6 +75,38 @@ DESCRIPTION is a string with the description of the spec."
forms))))
+(defmacro when-aligning-it (description &rest forms)
+ "Return a buttercup spec.
+
+Check that all FORMS correspond to properly indented sexps.
+
+DESCRIPTION is a string with the description of the spec."
+ (declare (indent defun))
+ `(it ,description
+ (let ((clojure-ts-align-forms-automatically t)
+ (clojure-ts-align-reader-conditionals t))
+ ,@(mapcar (lambda (form)
+ `(with-temp-buffer
+ (clojure-ts-mode)
+ (insert "\n" ,(replace-regexp-in-string " +" " " form))
+ (indent-region (point-min) (point-max))
+ (should (equal (buffer-substring-no-properties
(point-min) (point-max))
+ ,(concat "\n" form)))))
+ forms))
+ (let ((clojure-ts-align-forms-automatically nil))
+ ,@(mapcar (lambda (form)
+ `(with-temp-buffer
+ (clojure-ts-mode)
+ (insert "\n" ,(replace-regexp-in-string " +" " " form))
+ ;; This is to check that we did NOT align anything. Run
+ ;; `indent-region' and then check that no extra spaces
+ ;; where inserted besides the start of the line.
+ (indent-region (point-min) (point-max))
+ (goto-char (point-min))
+ (should-not (search-forward-regexp "\\([^\s\n]\\) +"
nil 'noerror))))
+ forms))))
+
+
;; Provide font locking for easier test editing.
(font-lock-add-keywords
@@ -393,4 +425,158 @@ b |20])"
(it "should remove extra commas"
(with-clojure-ts-buffer-point "{|:a 2, ,:c 4}"
(call-interactively #'clojure-ts-align)
- (expect (buffer-string) :to-equal "{:a 2, :c 4}"))))
+ (expect (buffer-string) :to-equal "{:a 2, :c 4}"))))
+
+(describe "clojure-ts-align-forms-automatically"
+ ;; Copied from `clojure-mode'
+ (when-aligning-it "should basic forms"
+ "
+{:this-is-a-form b
+ c d}"
+
+ "
+{:this-is b
+ c d}"
+
+ "
+{:this b
+ c d}"
+
+ "
+{:a b
+ c d}"
+
+ "
+(let [this-is-a-form b
+ c d])"
+
+ "
+(let [this-is b
+ c d])"
+
+ "
+(let [this b
+ c d])"
+
+ "
+(let [a b
+ c d])")
+
+ (when-aligning-it "should handle a blank line"
+ "
+(let [this-is-a-form b
+ c d
+
+ another form
+ k g])"
+
+ "
+{:this-is-a-form b
+ c d
+
+ :another form
+ k g}")
+
+ (when-aligning-it "should handle basic forms (reversed)"
+ "
+{c d
+ :this-is-a-form b}"
+ "
+{c d
+ :this-is b}"
+ "
+{c d
+ :this b}"
+ "
+{c d
+ :a b}"
+
+ "
+(let [c d
+ this-is-a-form b])"
+
+ "
+(let [c d
+ this-is b])"
+
+ "
+(let [c d
+ this b])"
+
+ "
+(let [c d
+ a b])")
+
+ (when-aligning-it "should handle multiple words"
+ "
+(cond this is just
+ a test of
+ how well
+ multiple words will work)")
+
+ (when-aligning-it "should handle nested maps"
+ "
+{:a {:a :a
+ :bbbb :b}
+ :bbbb :b}")
+
+ (when-aligning-it "should regard end as a marker"
+ "
+{:a {:a :a
+ :aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa :a}
+ :b {:a :a
+ :aa :a}}")
+
+ (when-aligning-it "should handle trailing commas"
+ "
+{:a {:a :a,
+ :aa :a},
+ :b {:a :a,
+ :aa :a}}")
+
+ (when-aligning-it "should handle standard reader conditionals"
+ "
+#?(:clj 2
+ :cljs 2)")
+
+ (when-aligning-it "should handle splicing reader conditional"
+ "
+#?@(:clj [2]
+ :cljs [2])")
+
+ (when-aligning-it "should handle sexps broken up by line comments"
+ "
+(let [x 1
+ ;; comment
+ xx 1]
+ xx)"
+
+ "
+{:x 1
+ ;; comment
+ :xxx 2}"
+
+ "
+(case x
+ :aa 1
+ ;; comment
+ :a 2)")
+
+ (when-aligning-it "should work correctly when margin comments appear after
nested, multi-line, non-terminal sexps"
+ "
+(let [x {:a 1
+ :b 2} ; comment
+ xx 3]
+ x)"
+
+ "
+{:aa {:b 1
+ :cc 2} ;; comment
+ :a 1}}"
+
+ "
+(case x
+ :a (let [a 1
+ aa (+ a 1)]
+ aa); comment
+ :aa 2)"))
diff --git a/test/samples/align.clj b/test/samples/align.clj
index cf361cb23a..f70e767103 100644
--- a/test/samples/align.clj
+++ b/test/samples/align.clj
@@ -27,6 +27,31 @@
(let [a-long-name 10
b 20])
-
#?(:clj 2
:cljs 2)
+
+#?@(:clj [2]
+ :cljs [4])
+
+(let [this-is-a-form b
+ c d
+
+ another form
+ k g])
+
+{:this-is-a-form b
+ c d
+
+ :another form
+ k g}
+
+(let [x {:a 1
+ :b 2} ; comment
+ xx 3]
+ x)
+
+(case x
+ :a (let [a 1
+ aa (+ a 1)]
+ aa); comment
+ :aa 2)
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [nongnu] elpa/clojure-ts-mode e960a905ab: [#16] Add support for automatic aligning forms,
ELPA Syncer <=