From 805f31e9d3de1683fcbf5cbb95b4beb1dbc51f07 Mon Sep 17 00:00:00 2001 From: Roman Rudakov Date: Fri, 25 Apr 2025 21:36:10 +0200 Subject: [PATCH] Fix some issues with short anonymous functions --- CHANGELOG.md | 4 ++ clojure-ts-mode.el | 58 +++++++++++++++++++----- test/clojure-ts-mode-font-lock-test.el | 4 ++ test/clojure-ts-mode-indentation-test.el | 17 +++++++ test/samples/test.clj | 13 ++++++ 5 files changed, 85 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cecf8a2..d40be97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - [#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. - [#82](https://github.com/clojure-emacs/clojure-ts-mode/issues/82): Introduce `clojure-ts-outline-variant`. +- [#86](https://github.com/clojure-emacs/clojure-ts-mode/pull/86): Better handling of function literals: + - Syntax highlighting of built-in keywords. + - Consistent indentation with regular forms. + - Support for automatic aligning forms. ## 0.3.0 (2025-04-15) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 51c7996..340e016 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -514,6 +514,13 @@ literals with regex grammar." (:equal "clojure.core" @ns)) name: (sym_name) @font-lock-keyword-face)) (:match ,clojure-ts--builtin-symbol-regexp @font-lock-keyword-face)) + ((anon_fn_lit meta: _ :* :anchor (sym_lit !namespace name: (sym_name) @font-lock-keyword-face)) + (:match ,clojure-ts--builtin-symbol-regexp @font-lock-keyword-face)) + ((anon_fn_lit meta: _ :* :anchor + (sym_lit namespace: ((sym_ns) @ns + (:equal "clojure.core" @ns)) + name: (sym_name) @font-lock-keyword-face)) + (:match ,clojure-ts--builtin-symbol-regexp @font-lock-keyword-face)) ((sym_name) @font-lock-builtin-face (:match ,clojure-ts--builtin-dynamic-var-regexp @font-lock-builtin-face))) @@ -726,6 +733,14 @@ literals with regex grammar." "Return non-nil if NODE is a Clojure list." (string-equal "list_lit" (treesit-node-type node))) +(defun clojure-ts--anon-fn-node-p (node) + "Return non-nil if NODE is a Clojure function literal." + (string-equal "anon_fn_lit" (treesit-node-type node))) + +(defun clojure-ts--opening-paren-node-p (node) + "Return non-nil if NODE is an opening paren." + (string-equal "(" (treesit-node-text node))) + (defun clojure-ts--symbol-node-p (node) "Return non-nil if NODE is a Clojure symbol." (string-equal "sym_lit" (treesit-node-type node))) @@ -1249,7 +1264,8 @@ PARENT not should be a list. If first symbol in the expression has an indentation rule in `clojure-ts--semantic-indent-rules-defaults' or `clojure-ts-semantic-indent-rules' check if NODE should be indented according to the rule. If NODE is nil, use next node after BOL." - (and (clojure-ts--list-node-p parent) + (and (or (clojure-ts--list-node-p parent) + (clojure-ts--anon-fn-node-p parent)) (let* ((first-child (clojure-ts--node-child-skip-metadata parent 0))) (when-let* ((rule (clojure-ts--find-semantic-rule node parent 0))) (and (not (clojure-ts--match-with-metadata node)) @@ -1265,7 +1281,8 @@ according to the rule. If NODE is nil, use next node after BOL." (defun clojure-ts--match-function-call-arg (node parent _bol) "Match NODE if PARENT is a list expressing a function or macro call." - (and (clojure-ts--list-node-p parent) + (and (or (clojure-ts--list-node-p parent) + (clojure-ts--anon-fn-node-p parent)) ;; Can the following two clauses be replaced by checking indexes? ;; Does the second child exist, and is it not equal to the current node? (treesit-node-child parent 1 t) @@ -1284,7 +1301,8 @@ according to the rule. If NODE is nil, use next node after BOL." "Match NODE if it is an argument to a PARENT threading macro." ;; We want threading macros to indent 2 only if the ->> is on it's own line. ;; If not, then align function arg. - (and (clojure-ts--list-node-p parent) + (and (or (clojure-ts--list-node-p parent) + (clojure-ts--anon-fn-node-p parent)) (let ((first-child (treesit-node-child parent 0 t))) (clojure-ts--symbol-matches-p clojure-ts--threading-macro @@ -1335,7 +1353,7 @@ according to the rule. If NODE is nil, use next node after BOL." (and prev-sibling (clojure-ts--metadata-node-p prev-sibling)))) -(defun clojure-ts--anchor-parent-skip-metadata (_node parent _bol) +(defun clojure-ts--anchor-parent-opening-paren (_node parent _bol) "Return position of PARENT start for NODE. If PARENT has optional metadata we skip it and return starting position @@ -1343,11 +1361,9 @@ of the first child's opening paren. NOTE: This serves as an anchor function to resolve an 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)))) + (thread-first parent + (treesit-search-subtree #'clojure-ts--opening-paren-node-p nil t 1) + (treesit-node-start))) (defun clojure-ts--match-collection-item-with-metadata (node-type) "Return a matcher for a collection item with metadata by NODE-TYPE. @@ -1359,6 +1375,18 @@ if NODE has metadata and its parent has type NODE-TYPE." (treesit-node-type (clojure-ts--node-with-metadata-parent node))))) +(defun clojure-ts--anchor-nth-sibling (n &optional named) + "Return the start of the Nth child of PARENT. + +NAMED non-nil means count only named nodes. + +NOTE: This is a replacement for built-in `nth-sibling' anchor preset, +which doesn't work properly for named nodes (see the bug +https://debbugs.gnu.org/cgi/bugreport.cgi?bug=78065)" + (lambda (_n parent &rest _) + (treesit-node-start + (treesit-node-child parent n named)))) + (defun clojure-ts--semantic-indent-rules () "Return a list of indentation rules for `treesit-simple-indent-rules'." `((clojure @@ -1385,11 +1413,11 @@ if NODE has metadata and its parent has type NODE-TYPE." ((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) + (clojure-ts--match-form-body clojure-ts--anchor-parent-opening-paren 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) + (clojure-ts--match-function-call-arg ,(clojure-ts--anchor-nth-sibling 1 t) 0) ;; https://guide.clojure.style/#one-space-indent ((parent-is "list_lit") parent 1)))) @@ -1561,6 +1589,14 @@ have changed." ((list_lit ((sym_lit) @sym (:match ,(clojure-ts-symbol-regexp clojure-ts-align-cond-forms) @sym))) + @cond) + ((anon_fn_lit + ((sym_lit) @sym + (:match ,(clojure-ts-symbol-regexp clojure-ts-align-binding-forms) @sym)) + (vec_lit) @bindings-vec)) + ((anon_fn_lit + ((sym_lit) @sym + (:match ,(clojure-ts-symbol-regexp clojure-ts-align-cond-forms) @sym))) @cond)) (when clojure-ts-align-reader-conditionals '(((read_cond_lit) @read-cond) diff --git a/test/clojure-ts-mode-font-lock-test.el b/test/clojure-ts-mode-font-lock-test.el index 02e0fa4..05eba9e 100644 --- a/test/clojure-ts-mode-font-lock-test.el +++ b/test/clojure-ts-mode-font-lock-test.el @@ -169,6 +169,10 @@ DESCRIPTION is the description of the spec." (2 5 font-lock-type-face) (8 9 font-lock-keyword-face))) + (when-fontifying-it "function literals" + ("#(or one two)" + (3 4 font-lock-keyword-face))) + (when-fontifying-it "should highlight function name in all known forms" ("(letfn [(add [x y] (+ x y)) diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el index fe181f9..942175a 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -184,6 +184,12 @@ DESCRIPTION is a string with the description of the spec." (#'foo 5 6)") +(when-indenting-it "should support function literals" + " +#(or true + false + %)") + (when-indenting-it "should support block-0 expressions" " (do (aligned) @@ -462,6 +468,17 @@ b |20])" (let [a b c d])") + (when-aligning-it "should handle function literals" + " +#(let [hello 1 + foo \"hone\"] + (pringln hello))" + + " +^{:some :metadata} #(let [foo % + bar-zzz %] + foo)") + (when-aligning-it "should handle a blank line" " (let [this-is-a-form b diff --git a/test/samples/test.clj b/test/samples/test.clj index 842ff5a..18ead86 100644 --- a/test/samples/test.clj +++ b/test/samples/test.clj @@ -41,6 +41,19 @@ 0 0i) +;; Function literals + +^{:some "metadata"} #(let [foo % + bar-zzz %] + foo) + +#(or one + two) + +#(let [hello 1 + foo "hone"] + (pringln hello)) + ;; examples of valid namespace definitions (comment (ns .validns)