Skip to content

Commit 7497ae9

Browse files
rrudakovbbatsov
authored andcommitted
[#61] Fix some issues with indentation for items with metadata
- Collection elements (except of lists) are properly indented. - Body of special forms when the entire form has metadata is properly indented. - Additionally fix syntax highlighting of special forms when the entire form has metadata.
1 parent 4cf4bd1 commit 7497ae9

6 files changed

+140
-5
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
line.
1313
- Improve semantic indentation rules to be more consistent with cljfmt.
1414
- Introduce `clojure-ts-semantic-indent-rules` customization option.
15+
- [#61](https://github.com/clojure-emacs/clojure-ts-mode/issues/61): Fix issue with indentation of collection items with metadata.
16+
- Proper syntax highlighting for expressions with metadata.
1517

1618
## 0.2.3 (2025-03-04)
1719

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ and `clojure-mode` (this is very helpful when dealing with `derived-mode-p` chec
291291
- Navigation by sexp/lists might work differently on Emacs versions lower
292292
than 31. Starting with version 31, Emacs uses TreeSitter 'things' settings, if
293293
available, to rebind some commands.
294+
- The indentation of list elements with metadata is inconsistent with other
295+
collections. This inconsistency stems from the grammar's interpretation of
296+
nearly every definition or function call as a list. Therefore, modifying the
297+
indentation for list elements would adversely affect the indentation of
298+
numerous other forms.
294299

295300
## Frequently Asked Questions
296301

clojure-ts-mode.el

+45-3
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,9 @@ with the markdown_inline grammar."
404404
;; `clojure.core'.
405405
:feature 'builtin
406406
:language 'clojure
407-
`(((list_lit meta: _ :? :anchor (sym_lit !namespace name: (sym_name) @font-lock-keyword-face))
407+
`(((list_lit meta: _ :* :anchor (sym_lit !namespace name: (sym_name) @font-lock-keyword-face))
408408
(:match ,clojure-ts--builtin-symbol-regexp @font-lock-keyword-face))
409-
((list_lit meta: _ :? :anchor
409+
((list_lit meta: _ :* :anchor
410410
(sym_lit namespace: ((sym_ns) @ns
411411
(:equal "clojure.core" @ns))
412412
name: (sym_name) @font-lock-keyword-face))
@@ -608,6 +608,12 @@ This does not include the NODE's namespace."
608608
(let ((first-child (treesit-node-child node 0 t)))
609609
(treesit-node-child node (if (clojure-ts--metadata-node-p first-child) (1+ n) n) t)))
610610

611+
(defun clojure-ts--node-with-metadata-parent (node)
612+
"Return parent for NODE only if NODE has metadata, otherwise returns nil."
613+
(when-let* ((prev-sibling (treesit-node-prev-sibling node))
614+
((clojure-ts--metadata-node-p prev-sibling)))
615+
(treesit-node-parent (treesit-node-parent node))))
616+
611617
(defun clojure-ts--symbol-matches-p (symbol-regexp node)
612618
"Return non-nil if NODE is a symbol that matches SYMBOL-REGEXP."
613619
(and (clojure-ts--symbol-node-p node)
@@ -977,18 +983,54 @@ forms like deftype, defrecord, reify, proxy, etc."
977983
(and prev-sibling
978984
(clojure-ts--metadata-node-p prev-sibling))))
979985

986+
(defun clojure-ts--anchor-parent-skip-metadata (_node parent _bol)
987+
"Anchor function that returns position of PARENT start for NODE.
988+
989+
If PARENT has optional metadata we skip it and return starting position
990+
of the first child's opening paren.
991+
992+
NOTE: This anchor is used to fix indentation issue for forms with type
993+
hints."
994+
(let ((first-child (treesit-node-child parent 0 t)))
995+
(if (clojure-ts--metadata-node-p first-child)
996+
;; We don't need named node here
997+
(treesit-node-start (treesit-node-child parent 1))
998+
(treesit-node-start parent))))
999+
1000+
(defun clojure-ts--match-collection-item-with-metadata (node-type)
1001+
"Returns a matcher for a collection item with metadata by NODE-TYPE.
1002+
1003+
The returned matcher accepts NODE, PARENT and BOL and returns true only
1004+
if NODE has metadata and its parent has type NODE-TYPE."
1005+
(lambda (node _parent _bol)
1006+
(string-equal node-type
1007+
(treesit-node-type
1008+
(clojure-ts--node-with-metadata-parent node)))))
1009+
9801010
(defun clojure-ts--semantic-indent-rules ()
9811011
"Return a list of indentation rules for `treesit-simple-indent-rules'."
9821012
`((clojure
9831013
((parent-is "source") parent-bol 0)
9841014
(clojure-ts--match-docstring parent 0)
9851015
;; https://guide.clojure.style/#body-indentation
9861016
(clojure-ts--match-method-body parent 2)
987-
(clojure-ts--match-form-body parent 2)
1017+
(clojure-ts--match-form-body clojure-ts--anchor-parent-skip-metadata 2)
9881018
;; https://guide.clojure.style/#threading-macros-alignment
9891019
(clojure-ts--match-threading-macro-arg prev-sibling 0)
9901020
;; https://guide.clojure.style/#vertically-align-fn-args
9911021
(clojure-ts--match-function-call-arg (nth-sibling 2 nil) 0)
1022+
;; Collections items with metadata.
1023+
;;
1024+
;; This should be before `clojure-ts--match-with-metadata', otherwise they
1025+
;; will never be matched.
1026+
(,(clojure-ts--match-collection-item-with-metadata "vec_lit") grand-parent 1)
1027+
(,(clojure-ts--match-collection-item-with-metadata "map_lit") grand-parent 1)
1028+
(,(clojure-ts--match-collection-item-with-metadata "set_lit") grand-parent 2)
1029+
;;
1030+
;; If we enable this rule for lists, it will break many things.
1031+
;; (,(clojure-ts--match-collection-item-with-metadata "list_lit") grand-parent 1)
1032+
;;
1033+
;; All other forms with metadata.
9921034
(clojure-ts--match-with-metadata parent 0)
9931035
;; Literal Sequences
9941036
((parent-is "list_lit") parent 1) ;; https://guide.clojure.style/#one-space-indent

test/clojure-ts-mode-font-lock-test.el

+6-1
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,9 @@ DESCRIPTION is the description of the spec."
162162
(when-fontifying-it "non-built-ins-with-same-name"
163163
("(h/for query {})"
164164
(2 2 font-lock-type-face)
165-
(4 6 nil))))
165+
(4 6 nil)))
166+
167+
(when-fontifying-it "special-forms-with-metadata"
168+
("^long (if true 1 2)"
169+
(2 5 font-lock-type-face)
170+
(8 9 font-lock-keyword-face))))

test/clojure-ts-mode-indentation-test.el

+26-1
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,29 @@ DESCRIPTION is a string with the description of the spec."
239239
(= x y)
240240
2 3
241241
4 5
242-
6 6)"))))
242+
6 6)")))
243+
244+
(it "should indent collections elements with metadata correctly"
245+
"
246+
(def x
247+
[a b [c ^:foo
248+
d
249+
e]])"
250+
251+
"
252+
#{x
253+
y ^:foo
254+
z}"
255+
256+
"
257+
{:hello ^:foo
258+
\"world\"
259+
:foo
260+
\"bar\"}")
261+
262+
(it "should indent body of special forms correctly considering metadata"
263+
"
264+
(let [result ^long
265+
(if true
266+
1
267+
2)])"))

test/samples/indentation.clj

+56
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,59 @@
210210
(close
211211
[this]
212212
(is properly indented)))
213+
214+
(def x
215+
[a b [c ^:foo
216+
d
217+
e]])
218+
219+
#{x
220+
y ^:foo
221+
z}
222+
223+
{:hello ^:foo
224+
"world"
225+
:foo
226+
"bar"}
227+
228+
;; NOTE: List elements with metadata are not indented correctly.
229+
'(one
230+
two ^:foo
231+
three)
232+
233+
^{:nextjournal.clerk/visibility {:code :hide}}
234+
(defn actual
235+
[args])
236+
237+
(def ^:private hello
238+
"World")
239+
240+
;; A few examples from clojure core.
241+
242+
;; NOTE: This one is not indented correctly, I'm keeping it here as a reminder
243+
;; to fix it later.
244+
(defonce ^:dynamic
245+
^{:private true
246+
:doc "A ref to a sorted set of symbols representing loaded libs"}
247+
*loaded-libs* (ref (sorted-set)))
248+
249+
(defn index-of
250+
"Return index of value (string or char) in s, optionally searching
251+
forward from from-index. Return nil if value not found."
252+
{:added "1.8"}
253+
([^CharSequence s value]
254+
(let [result ^long
255+
(if (instance? Character value)
256+
(.indexOf (.toString s) ^int (.charValue ^Character value))
257+
(.indexOf (.toString s) ^String value))]
258+
(if (= result -1)
259+
nil
260+
result)))
261+
([^CharSequence s value ^long from-index]
262+
(let [result ^long
263+
(if (instance? Character value)
264+
(.indexOf (.toString s) ^int (.charValue ^Character value) (unchecked-int from-index))
265+
(.indexOf (.toString s) ^String value (unchecked-int from-index)))]
266+
(if (= result -1)
267+
nil
268+
result))))

0 commit comments

Comments
 (0)