Skip to content
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 @@ -41,6 +41,8 @@
- [#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.
- [#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)

Expand Down
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
>
Expand Down Expand Up @@ -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

Expand Down
128 changes: 40 additions & 88 deletions clojure-ts-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -666,13 +695,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)
Expand All @@ -687,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
Expand Down Expand Up @@ -748,6 +756,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
Expand Down Expand Up @@ -823,78 +839,15 @@ 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.

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
Expand Down Expand Up @@ -3543,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))))

Expand Down
17 changes: 1 addition & 16 deletions test/clojure-ts-mode-config-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
38 changes: 37 additions & 1 deletion test/clojure-ts-mode-font-lock-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,43 @@ 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

(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

Expand Down
Loading