From 11a51b93fc9b164d2e565992431ac15b2d753371 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 1 Nov 2026 11:08:27 +0200 Subject: [PATCH 1/2] Run the function call font lock rule after the definition rules At treesit-font-lock-level 4, the rule that highlights the head of every list as a function call came before the definition rules, and the first rule to set a face wins. So the names of methods in defrecord, reify and the like, and of letfn functions, were colored as calls instead of definitions. Move it after them. --- CHANGELOG.md | 1 + clojure-ts-mode.el | 15 ++++++++------- test/clojure-ts-mode-font-lock-test.el | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3de2a9..c6f63ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - [#170](https://github.com/clojure-emacs/clojure-ts-mode/pull/170): Indent forms like `if`, `when` or `let` as function calls when their body starts on the first line, like cljfmt and `clojure-mode` do, instead of indenting the rest of the body by 2 spaces. - [#170](https://github.com/clojure-emacs/clojure-ts-mode/pull/170): Count forms discarded with `#_` as arguments of forms like `when` or `let` when indenting them, like `clojure-mode` does. - [#170](https://github.com/clojure-emacs/clojure-ts-mode/pull/170): Add the `let*` semantic indentation rule from cljfmt. +- [#171](https://github.com/clojure-emacs/clojure-ts-mode/pull/171): Highlight the names of methods (in `defrecord`, `reify` and the like) and of `letfn` functions as definitions at `treesit-font-lock-level` 4, where they were colored as function calls. ## 0.6.0 (2025-12-02) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index bd75cb5..98b7c06 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -666,13 +666,6 @@ OVERRIDE, START and END are passed to `treesit-fontify-with-override'." ((sym_name) @font-lock-builtin-face (:match ,clojure-ts--builtin-dynamic-var-regexp @font-lock-builtin-face))) - ;; Any function calls, not built-ins. - ;; This can give false positives (macros, quoted lists, namespace imports) - ;; but is a level 4 feature and never enabled by default. - :feature 'function - :language 'clojure - '((list_lit :anchor (sym_lit (sym_name) @font-lock-function-call-face))) - :feature 'symbol :language 'clojure '((sym_ns) @font-lock-type-face) @@ -748,6 +741,14 @@ OVERRIDE, START and END are passed to `treesit-fontify-with-override'." :anchor (sym_lit (sym_name) @font-lock-variable-name-face)) (:match ,clojure-ts--variable-definition-symbol-regexp @font-lock-keyword-face))) + ;; Any function calls, not built-ins. + ;; This can give false positives (macros, quoted lists, namespace imports) + ;; but is a level 4 feature and never enabled by default. It comes after + ;; the definition rules, so that they win. + :feature 'function + :language 'clojure + '((list_lit :anchor (sym_lit (sym_name) @font-lock-function-call-face))) + ;; Can we support declarations in the namespace form? :feature 'type :language 'clojure diff --git a/test/clojure-ts-mode-font-lock-test.el b/test/clojure-ts-mode-font-lock-test.el index 6dd70b1..a619034 100644 --- a/test/clojure-ts-mode-font-lock-test.el +++ b/test/clojure-ts-mode-font-lock-test.el @@ -523,6 +523,25 @@ https://github.com/tree-sitter/tree-sitter/issues/5759." (expect (get-text-property 10 'face) :to-equal 'font-lock-function-name-face)))) +;;;; Function calls + +(describe "function call highlighting" + (it "should not take the names in definitions for calls at font-lock level 4" + (with-clojure-ts-buffer "(defrecord Foo [] P (bar [this] 1)) +(letfn [(baz [x] x)] (baz 1))" + (let ((treesit-font-lock-level 4)) + (clojure-ts-mode)) + (font-lock-ensure) + (dolist (name '("bar" "baz")) + (goto-char (point-min)) + (search-forward name) + (expect (get-text-property (match-beginning 0) 'face) + :to-equal 'font-lock-function-name-face)) + (goto-char (point-max)) + (search-backward "baz") + (expect (get-text-property (point) 'face) + :to-equal 'font-lock-function-call-face)))) + ;;;; Docstrings in various def forms (describe "docstring-highlighting" From d33358eef822d9fa1131a7e756e4be4a30fce7be Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Sun, 1 Nov 2026 11:10:15 +0200 Subject: [PATCH 2/2] Check for extra def forms in the built-in font lock patterns The forms in clojure-ts-extra-def-forms got font lock rules of their own, added to a buffer only when it had some, with code to keep the user's disabled features disabled when adding them. Their docstring rule wasn't part of the range settings, so the Markdown in those docstrings wasn't highlighted. Check for them with a predicate in the defn and docstring patterns instead. That costs nothing measurable over the regexps it replaces, and the Markdown range settings come with it. --- CHANGELOG.md | 1 + README.md | 7 +- clojure-ts-mode.el | 113 +++++++------------------ test/clojure-ts-mode-config-test.el | 17 +--- test/clojure-ts-mode-font-lock-test.el | 19 ++++- 5 files changed, 54 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f63ef..59ed35b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - [#170](https://github.com/clojure-emacs/clojure-ts-mode/pull/170): Count forms discarded with `#_` as arguments of forms like `when` or `let` when indenting them, like `clojure-mode` does. - [#170](https://github.com/clojure-emacs/clojure-ts-mode/pull/170): Add the `let*` semantic indentation rule from cljfmt. - [#171](https://github.com/clojure-emacs/clojure-ts-mode/pull/171): Highlight the names of methods (in `defrecord`, `reify` and the like) and of `letfn` functions as definitions at `treesit-font-lock-level` 4, where they were colored as function calls. +- [#171](https://github.com/clojure-emacs/clojure-ts-mode/pull/171): Highlight Markdown in the docstrings of the forms in `clojure-ts-extra-def-forms`. ## 0.6.0 (2025-12-02) diff --git a/README.md b/README.md index 3d90b7b..f2de937 100644 --- a/README.md +++ b/README.md @@ -456,7 +456,8 @@ or set this variable using `setopt`: (setopt clojure-ts-extra-def-forms '("defelem")) ``` -This setting will highlight `defelem` symbol, function name and the docstring. +This setting will highlight `defelem` symbol, function name and the docstring, +including the Markdown in it. > [!IMPORTANT] > @@ -705,10 +706,6 @@ 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 Tree-sitter 'things' settings, if available, to rebind some commands. -- If you set `clojure-ts-extra-def-forms`, `clojure-ts-mode` will highlight the - specified forms, including their docstrings, in a manner similar to Clojure's - `defn`. However, Markdown syntax will not be highlighted within these custom - docstrings. ## Frequently Asked Questions diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 98b7c06..ca8f416 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -92,6 +92,14 @@ itself." :initialize #'custom-initialize-default :set #'clojure-ts--set-font-lock-option) +(defcustom clojure-ts-extra-def-forms nil + "List of forms that should be fontified the same way as defn." + :package-version '(clojure-ts-mode . "0.5") + :safe #'listp + :type '(repeat string) + :initialize #'custom-initialize-default + :set #'clojure-ts--set-font-lock-option) + (defcustom clojure-ts-ensure-grammars t "When non-nil, ensure required Tree-sitter grammars are installed." :safe #'booleanp @@ -486,6 +494,28 @@ if a third argument (the value) is provided. (rx line-start (or "defprotocol" "definterface") line-end) "A regular expression matching a symbol used to define an interface.") +(defconst clojure-ts--defn-like-symbol-regexp + (rx line-start + (or "fn" "defn" "defn-" "defmulti" "defmethod" "deftest" "deftest-" + "defmacro" "definline" "defonce") + line-end) + "A regular expression matching a symbol used like defn.") + +(defun clojure-ts--def-symbol-p (regexp node) + "Return non-nil if NODE matches REGEXP or `clojure-ts-extra-def-forms'." + (let ((name (treesit-node-text node t)) + (case-fold-search nil)) + (or (string-match-p regexp name) + (member name clojure-ts-extra-def-forms)))) + +(defun clojure-ts--defn-like-p (node) + "Return non-nil if NODE names a form that's fontified like defn." + (clojure-ts--def-symbol-p clojure-ts--defn-like-symbol-regexp node)) + +(defun clojure-ts--function-docstring-symbol-p (node) + "Return non-nil if NODE names a form that takes a docstring like defn." + (clojure-ts--def-symbol-p clojure-ts-function-docstring-symbols node)) + (defun clojure-ts--docstring-query (capture-symbol &optional capture-quotes) "Return a query that captures docstrings with CAPTURE-SYMBOL. @@ -533,8 +563,7 @@ captured including quotes." :anchor (sym_lit) :anchor [(comment) (meta_lit) (old_meta_lit)] :* :anchor (str_lit (str_content) ,capture-symbol) ,quotes-symbol) - (:match ,clojure-ts-function-docstring-symbols - @_def_symbol)) + (:pred clojure-ts--function-docstring-symbol-p @_def_symbol)) ;; Captures docstrings in defprotocol, definterface ((list_lit :anchor [(comment) (meta_lit) (old_meta_lit)] :* :anchor (sym_lit) @_def_symbol @@ -680,21 +709,7 @@ OVERRIDE, START and END are passed to `treesit-fontify-with-override'." :anchor (sym_lit (sym_name) @font-lock-keyword-face) :anchor [(comment) (meta_lit) (old_meta_lit)] :* :anchor (sym_lit (sym_name) @font-lock-function-name-face)) - (:match ,(rx-to-string - `(seq bol - (or - "fn" - "defn" - "defn-" - "defmulti" - "defmethod" - "deftest" - "deftest-" - "defmacro" - "definline" - "defonce") - eol)) - @font-lock-keyword-face)) + (:pred clojure-ts--defn-like-p @font-lock-keyword-face)) ((anon_fn_lit marker: "#" @font-lock-punctuation-face)) ;; Methods implementation @@ -824,60 +839,6 @@ OVERRIDE, START and END are passed to `treesit-fontify-with-override'." '((derefing_lit marker: "@" @font-lock-punctuation-face)))) -(defvar clojure-ts-extra-def-forms) - -(defun clojure-ts--extra-def-form-p (node) - "Return non-nil if NODE is named like a form in `clojure-ts-extra-def-forms'." - (and clojure-ts-extra-def-forms - (member (treesit-node-text node t) clojure-ts-extra-def-forms))) - -(defconst clojure-ts--extra-def-font-lock-queries - (treesit-font-lock-rules - :feature 'definition - :language 'clojure - :override t - '(((list_lit :anchor [(comment) (meta_lit) (old_meta_lit)] :* - :anchor (sym_lit (sym_name) @font-lock-keyword-face) - :anchor [(comment) (meta_lit) (old_meta_lit)] :* - :anchor (sym_lit (sym_name) @font-lock-function-name-face)) - (:pred clojure-ts--extra-def-form-p @font-lock-keyword-face))) - - ;; NOTE: Here we also define queries to fontify docstrings in custom extra - ;; defn forms, but Markdown syntax won't work here, because it's not a part - ;; of range settings. - :feature 'doc - :language 'clojure - :override t - '(((list_lit :anchor [(comment) (meta_lit) (old_meta_lit)] :* - :anchor (sym_lit) @_def_symbol - :anchor [(comment) (meta_lit) (old_meta_lit)] :* - ;; Function_name - :anchor (sym_lit) - :anchor [(comment) (meta_lit) (old_meta_lit)] :* - :anchor (str_lit) @font-lock-doc-face) - (:pred clojure-ts--extra-def-form-p @_def_symbol)))) - "Font lock rules for the forms in `clojure-ts-extra-def-forms'.") - -(defvar-local clojure-ts--extra-def-font-lock-p nil - "Non-nil if the font lock rules for extra def forms are in use.") - -(defun clojure-ts--add-extra-def-font-lock () - "Use the font lock rules for `clojure-ts-extra-def-forms', if there are any. -They're left out until then, as they slow down font lock." - (when (and clojure-ts-extra-def-forms - (not clojure-ts--extra-def-font-lock-p)) - (setq clojure-ts--extra-def-font-lock-p t) - (let ((disabled (seq-remove (lambda (feature) - (seq-some (lambda (setting) - (and (eq (nth 2 setting) feature) - (nth 1 setting))) - treesit-font-lock-settings)) - '(definition doc)))) - (treesit-add-font-lock-rules clojure-ts--extra-def-font-lock-queries) - ;; The rules are added enabled. - (when disabled - (treesit-font-lock-recompute-features nil disabled))))) - (defun clojure-ts--set-font-lock-option (symbol value) "Set the default value of SYMBOL to VALUE and refontify Clojure buffers. @@ -885,17 +846,8 @@ NOTE: This function is not meant to be called directly." (set-default-toplevel-value symbol value) (dolist (buf (match-buffers '(derived-mode . clojure-ts-mode))) (with-current-buffer buf - (clojure-ts--add-extra-def-font-lock) (font-lock-flush)))) -(defcustom clojure-ts-extra-def-forms nil - "List of forms that should be fontified the same way as defn." - :package-version '(clojure-ts-mode . "0.5") - :safe #'listp - :type '(repeat string) - :initialize #'custom-initialize-default - :set #'clojure-ts--set-font-lock-option) - (defconst clojure-ts--markdown-font-lock-queries (treesit-font-lock-rules :feature 'doc @@ -3544,7 +3496,6 @@ REGEX-AVAILABLE." (defun clojure-ts--apply-local-variables () "Redo the parts of the setup that depend on file or directory-local variables. Those are set after the major mode." - (clojure-ts--add-extra-def-font-lock) (when (local-variable-p 'clojure-ts-indent-style) (setq-local treesit-simple-indent-rules (clojure-ts--configured-indent-rules)))) diff --git a/test/clojure-ts-mode-config-test.el b/test/clojure-ts-mode-config-test.el index 96221ff..43057e1 100644 --- a/test/clojure-ts-mode-config-test.el +++ b/test/clojure-ts-mode-config-test.el @@ -228,22 +228,7 @@ y) (expect (buffer-string) :to-match "(foo\n bar)"))) (setopt clojure-ts-extra-def-forms nil clojure-ts-comment-macro-font-lock-body nil - clojure-ts-indent-style 'semantic))))) - - (it "should keep the font lock features turned off in a buffer" - (with-clojure-ts-buffer "(defelem file-upload - \"Creates a file upload input.\" - [name])" - (treesit-font-lock-recompute-features nil '(doc)) - (unwind-protect - (progn - (setopt clojure-ts-extra-def-forms '("defelem")) - (font-lock-ensure) - (expect (clojure-ts-test--face-at "defelem") - :to-equal 'font-lock-keyword-face) - (expect (clojure-ts-test--face-at "Creates") - :not :to-equal 'font-lock-doc-face)) - (setopt clojure-ts-extra-def-forms nil))))) + clojure-ts-indent-style 'semantic)))))) (describe "clojure-ts-semantic-indent-rules" (it "should apply however it's set" diff --git a/test/clojure-ts-mode-font-lock-test.el b/test/clojure-ts-mode-font-lock-test.el index a619034..f16f9f6 100644 --- a/test/clojure-ts-mode-font-lock-test.el +++ b/test/clojure-ts-mode-font-lock-test.el @@ -521,7 +521,24 @@ https://github.com/tree-sitter/tree-sitter/issues/5759." (expect (get-text-property 2 'face) :to-equal 'font-lock-keyword-face) (expect (get-text-property 10 'face) - :to-equal 'font-lock-function-name-face)))) + :to-equal 'font-lock-function-name-face))) + + (it "should highlight Markdown in the docstrings of extra def forms" + (unless (treesit-ready-p 'markdown-inline t) + (signal 'buttercup-pending "tree-sitter Markdown grammar not available")) + (with-clojure-ts-buffer "(defelem file-upload + \"Creates a `file` input.\" + [name])" + (font-lock-ensure) + (unwind-protect + (progn + (setopt clojure-ts-extra-def-forms '("defelem")) + (font-lock-ensure) + (goto-char (point-min)) + (search-forward "`file`") + (expect (get-text-property (match-beginning 0) 'face) + :to-equal '(font-lock-constant-face font-lock-doc-face))) + (setopt clojure-ts-extra-def-forms nil))))) ;;;; Function calls