Skip to content

Fix some issues with indentation for items with metadata #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
line.
- Improve semantic indentation rules to be more consistent with cljfmt.
- Introduce `clojure-ts-semantic-indent-rules` customization option.
- [#61](https://github.com/clojure-emacs/clojure-ts-mode/issues/61): Fix issue with indentation of collection items with metadata.
- Proper syntax highlighting for expressions with metadata.

## 0.2.3 (2025-03-04)

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,11 @@ and `clojure-mode` (this is very helpful when dealing with `derived-mode-p` chec
- Navigation by sexp/lists might work differently on Emacs versions lower
than 31. Starting with version 31, Emacs uses TreeSitter 'things' settings, if
available, to rebind some commands.
- The indentation of list elements with metadata is inconsistent with other
collections. This inconsistency stems from the grammar's interpretation of
nearly every definition or function call as a list. Therefore, modifying the
indentation for list elements would adversely affect the indentation of
numerous other forms.

## Frequently Asked Questions

Expand Down
48 changes: 45 additions & 3 deletions clojure-ts-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,9 @@ with the markdown_inline grammar."
;; `clojure.core'.
:feature 'builtin
:language 'clojure
`(((list_lit meta: _ :? :anchor (sym_lit !namespace name: (sym_name) @font-lock-keyword-face))
`(((list_lit meta: _ :* :anchor (sym_lit !namespace name: (sym_name) @font-lock-keyword-face))
(:match ,clojure-ts--builtin-symbol-regexp @font-lock-keyword-face))
((list_lit meta: _ :? :anchor
((list_lit meta: _ :* :anchor
(sym_lit namespace: ((sym_ns) @ns
(:equal "clojure.core" @ns))
name: (sym_name) @font-lock-keyword-face))
Expand Down Expand Up @@ -608,6 +608,12 @@ This does not include the NODE's namespace."
(let ((first-child (treesit-node-child node 0 t)))
(treesit-node-child node (if (clojure-ts--metadata-node-p first-child) (1+ n) n) t)))

(defun clojure-ts--node-with-metadata-parent (node)
"Return parent for NODE only if NODE has metadata, otherwise returns nil."
(when-let* ((prev-sibling (treesit-node-prev-sibling node))
((clojure-ts--metadata-node-p prev-sibling)))
(treesit-node-parent (treesit-node-parent node))))

(defun clojure-ts--symbol-matches-p (symbol-regexp node)
"Return non-nil if NODE is a symbol that matches SYMBOL-REGEXP."
(and (clojure-ts--symbol-node-p node)
Expand Down Expand Up @@ -977,18 +983,54 @@ forms like deftype, defrecord, reify, proxy, etc."
(and prev-sibling
(clojure-ts--metadata-node-p prev-sibling))))

(defun clojure-ts--anchor-parent-skip-metadata (_node parent _bol)
"Anchor function that returns position of PARENT start for NODE.

If PARENT has optional metadata we skip it and return starting position
of the first child's opening paren.

NOTE: This anchor is used to fix indentation issue for forms with type
hints."
(let ((first-child (treesit-node-child parent 0 t)))
(if (clojure-ts--metadata-node-p first-child)
;; We don't need named node here
(treesit-node-start (treesit-node-child parent 1))
(treesit-node-start parent))))

(defun clojure-ts--match-collection-item-with-metadata (node-type)
"Returns a matcher for a collection item with metadata by NODE-TYPE.

The returned matcher accepts NODE, PARENT and BOL and returns true only
if NODE has metadata and its parent has type NODE-TYPE."
(lambda (node _parent _bol)
(string-equal node-type
(treesit-node-type
(clojure-ts--node-with-metadata-parent node)))))

(defun clojure-ts--semantic-indent-rules ()
"Return a list of indentation rules for `treesit-simple-indent-rules'."
`((clojure
((parent-is "source") parent-bol 0)
(clojure-ts--match-docstring parent 0)
;; https://guide.clojure.style/#body-indentation
(clojure-ts--match-method-body parent 2)
(clojure-ts--match-form-body parent 2)
(clojure-ts--match-form-body clojure-ts--anchor-parent-skip-metadata 2)
;; https://guide.clojure.style/#threading-macros-alignment
(clojure-ts--match-threading-macro-arg prev-sibling 0)
;; https://guide.clojure.style/#vertically-align-fn-args
(clojure-ts--match-function-call-arg (nth-sibling 2 nil) 0)
;; Collections items with metadata.
;;
;; This should be before `clojure-ts--match-with-metadata', otherwise they
;; will never be matched.
(,(clojure-ts--match-collection-item-with-metadata "vec_lit") grand-parent 1)
(,(clojure-ts--match-collection-item-with-metadata "map_lit") grand-parent 1)
(,(clojure-ts--match-collection-item-with-metadata "set_lit") grand-parent 2)
;;
;; If we enable this rule for lists, it will break many things.
;; (,(clojure-ts--match-collection-item-with-metadata "list_lit") grand-parent 1)
;;
;; All other forms with metadata.
(clojure-ts--match-with-metadata parent 0)
;; Literal Sequences
((parent-is "list_lit") parent 1) ;; https://guide.clojure.style/#one-space-indent
Expand Down
7 changes: 6 additions & 1 deletion test/clojure-ts-mode-font-lock-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,9 @@ DESCRIPTION is the description of the spec."
(when-fontifying-it "non-built-ins-with-same-name"
("(h/for query {})"
(2 2 font-lock-type-face)
(4 6 nil))))
(4 6 nil)))

(when-fontifying-it "special-forms-with-metadata"
("^long (if true 1 2)"
(2 5 font-lock-type-face)
(8 9 font-lock-keyword-face))))
27 changes: 26 additions & 1 deletion test/clojure-ts-mode-indentation-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,29 @@ DESCRIPTION is a string with the description of the spec."
(= x y)
2 3
4 5
6 6)"))))
6 6)")))

(it "should indent collections elements with metadata correctly"
"
(def x
[a b [c ^:foo
d
e]])"

"
#{x
y ^:foo
z}"

"
{:hello ^:foo
\"world\"
:foo
\"bar\"}")

(it "should indent body of special forms correctly considering metadata"
"
(let [result ^long
(if true
1
2)])"))
56 changes: 56 additions & 0 deletions test/samples/indentation.clj
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,59 @@
(close
[this]
(is properly indented)))

(def x
[a b [c ^:foo
d
e]])

#{x
y ^:foo
z}

{:hello ^:foo
"world"
:foo
"bar"}

;; NOTE: List elements with metadata are not indented correctly.
'(one
two ^:foo
three)

^{:nextjournal.clerk/visibility {:code :hide}}
(defn actual
[args])

(def ^:private hello
"World")

;; A few examples from clojure core.

;; NOTE: This one is not indented correctly, I'm keeping it here as a reminder
;; to fix it later.
(defonce ^:dynamic
^{:private true
:doc "A ref to a sorted set of symbols representing loaded libs"}
*loaded-libs* (ref (sorted-set)))

(defn index-of
"Return index of value (string or char) in s, optionally searching
forward from from-index. Return nil if value not found."
{:added "1.8"}
([^CharSequence s value]
(let [result ^long
(if (instance? Character value)
(.indexOf (.toString s) ^int (.charValue ^Character value))
(.indexOf (.toString s) ^String value))]
(if (= result -1)
nil
result)))
([^CharSequence s value ^long from-index]
(let [result ^long
(if (instance? Character value)
(.indexOf (.toString s) ^int (.charValue ^Character value) (unchecked-int from-index))
(.indexOf (.toString s) ^String value (unchecked-int from-index)))]
(if (= result -1)
nil
result))))
Loading