diff --git a/CHANGELOG.md b/CHANGELOG.md index a4cf968..8d7953f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 4fb47b2..b1aa3cf 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 19fa605..d0fa310 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -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)) @@ -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) @@ -977,6 +983,30 @@ 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 @@ -984,11 +1014,23 @@ forms like deftype, defrecord, reify, proxy, etc." (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 diff --git a/test/clojure-ts-mode-font-lock-test.el b/test/clojure-ts-mode-font-lock-test.el index c3cafd5..b6ea46c 100644 --- a/test/clojure-ts-mode-font-lock-test.el +++ b/test/clojure-ts-mode-font-lock-test.el @@ -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)))) diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el index 23a432b..8375f80 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -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)])")) diff --git a/test/samples/indentation.clj b/test/samples/indentation.clj index 78a7aa6..79d7809 100644 --- a/test/samples/indentation.clj +++ b/test/samples/indentation.clj @@ -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))))