Skip to content
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

Support toplevel defs with metadata #43

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
41 changes: 31 additions & 10 deletions clojure-ts-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ if a third argument (the value) is provided.
(defun clojure-ts--docstring-query (capture-symbol)
"Return a query that captures docstrings with CAPTURE-SYMBOL."
`(;; Captures docstrings in def
((list_lit :anchor (sym_lit) @_def_symbol
((list_lit :anchor (meta_lit) :?
:anchor (sym_lit) @_def_symbol
:anchor (comment) :?
:anchor (sym_lit) ; variable name
:anchor (comment) :?
Expand Down Expand Up @@ -288,7 +289,8 @@ if a third argument (the value) is provided.
@_def_symbol)
(:equal @_doc-keyword ":doc"))
;; Captures docstrings defn, defmacro, ns, and things like that
((list_lit :anchor (sym_lit) @_def_symbol
((list_lit :anchor (meta_lit) :?
:anchor (sym_lit) @_def_symbol
:anchor (comment) :?
:anchor (sym_lit) ; function_name
:anchor (comment) :?
Expand Down Expand Up @@ -347,7 +349,7 @@ with the markdown_inline grammar."

:feature 'builtin
:language 'clojure
`(((list_lit :anchor (sym_lit (sym_name) @font-lock-keyword-face))
`(((list_lit meta: _ :? :anchor (sym_lit (sym_name) @font-lock-keyword-face))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets you some of the way to syntax highlighting with these types of forms. A similar fix I believe is also needed for just about anything that follows the pattern of matching

(list_lit :anchor (sym_lit (sym_name @capture)  ...) ...)

Because any list literal can have metadata attached to the beginning. A lot of these queries want to capture the first symbol in the list to match special function calls, so they had to be :anchored to the front of the list. They all break down when metadata is present because the syntax tree lists the metadata as the first literal sub element of the list node.

This also applies to the docstring queries in clojure-ts--docstring-query.

(defn a
 "hello" ;; <- highlighted with font-lock-doc-face
  [] "world")

and

^{:cat :dog}
(defn b
 "hello" ;; <- not highlighted
  [] "world")

The "hello" string in both should be highlighted with

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't get this to work for now. Shall I revert my change related to this and get the PR merged?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unsure. I need to set aside some time to get reacquainted with this problem. I'll try to do that within the next week and have a better answer for you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dannyfreeman thank you! meanwhile, I had a chance to dive deeper and I think I actually figured it out (mostly?). there are a bunch of similar queries where the the optional metadata node could be added, but I didn't have time to come up with actual clojure code examples testing them so a didn't want to touch them – maybe unnecessarily.

(: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)))
Expand All @@ -369,7 +371,8 @@ with the markdown_inline grammar."
;; No wonder the tree-sitter-clojure grammar only touches syntax, and not semantics
:feature 'definition ;; defn and defn like macros
:language 'clojure
`(((list_lit :anchor (sym_lit (sym_name) @def)
`(((list_lit :anchor meta: _ :?
:anchor (sym_lit (sym_name) @def)
:anchor (sym_lit (sym_name) @font-lock-function-name-face))
(:match ,(rx-to-string
`(seq bol
Expand Down Expand Up @@ -410,7 +413,8 @@ with the markdown_inline grammar."

:feature 'variable ;; def, defonce
:language 'clojure
`(((list_lit :anchor (sym_lit (sym_name) @def)
`(((list_lit :anchor meta: _ :?
:anchor (sym_lit (sym_name) @def)
:anchor (sym_lit (sym_name) @font-lock-variable-name-face))
(:match ,clojure-ts--variable-definition-symbol-regexp @def)))

Expand Down Expand Up @@ -520,6 +524,10 @@ with the markdown_inline grammar."
"Return non-nil if NODE is a Clojure keyword."
(string-equal "kwd_lit" (treesit-node-type node)))

(defun clojure-ts--metadata-node-p (node)
"Return non-nil if NODE is a Clojure metadata node."
(string-equal "meta_lit" (treesit-node-type node) ))

(defun clojure-ts--named-node-text (node)
"Gets the name of a symbol or keyword NODE.
This does not include the NODE's namespace."
Expand All @@ -530,6 +538,11 @@ This does not include the NODE's namespace."
(and (clojure-ts--symbol-node-p node)
(string-equal expected-symbol-name (clojure-ts--named-node-text node))))

(defun clojure-ts--node-child-skip-metadata (node n)
"Return the Nth child of NODE like `treesit-node-child`, skipping the optional metadata node at pos 0 if present."
(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--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 All @@ -550,7 +563,7 @@ like \"defn\".
See `clojure-ts--definition-node-p' when an exact match is possible."
(and
(clojure-ts--list-node-p node)
(let* ((child (treesit-node-child node 0 t))
(let* ((child (clojure-ts--node-child-skip-metadata node 0))
(child-txt (clojure-ts--named-node-text child)))
(and (clojure-ts--symbol-node-p child)
(string-match-p definition-type-regexp child-txt)))))
Expand All @@ -565,8 +578,8 @@ that a node is a definition is intended to be done elsewhere.

Can be called directly, but intended for use as `treesit-defun-name-function'."
(when (and (clojure-ts--list-node-p node)
(clojure-ts--symbol-node-p (treesit-node-child node 0 t)))
(let ((sym (treesit-node-child node 1 t)))
(clojure-ts--symbol-node-p (clojure-ts--node-child-skip-metadata node 0)))
(let ((sym (clojure-ts--node-child-skip-metadata node 1)))
(when (clojure-ts--symbol-node-p sym)
;; Extracts ns and name, and recreates the full var name.
;; We can't just get the node-text of the full symbol because
Expand Down Expand Up @@ -724,19 +737,20 @@ https://github.com/weavejester/cljfmt/blob/fb26b22f569724b05c93eb2502592dfc2de89
(or (clojure-ts--symbol-node-p first-child)
(clojure-ts--keyword-node-p first-child)))))

(defun clojure-ts--match-expression-in-body (_node parent _bol)
(defun clojure-ts--match-expression-in-body (node parent _bol)
"Match NODE if it is an expression used in a body argument.
PARENT is expected to be a list literal.
See `treesit-simple-indent-rules'."
(and
(clojure-ts--list-node-p parent)
(let ((first-child (treesit-node-child parent 0 t)))
(let ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
(and
(not
(clojure-ts--symbol-matches-p
;; Symbols starting with this are false positives
(rx line-start (or "default" "deflate" "defer"))
first-child))
(not (clojure-ts--match-with-metadata node))
(clojure-ts--symbol-matches-p
clojure-ts--symbols-with-body-expressions-regexp
first-child)))))
Expand Down Expand Up @@ -808,6 +822,12 @@ forms like deftype, defrecord, reify, proxy, etc."
(clojure-ts--match-fn-docstring parent)
(clojure-ts--match-method-docstring parent))))

(defun clojure-ts--match-with-metadata (node &optional _parent _bol)
"Match NODE when it has metadata."
(let ((prev-sibling (treesit-node-prev-sibling node)))
(and prev-sibling
(clojure-ts--metadata-node-p prev-sibling))))

(defun clojure-ts--semantic-indent-rules ()
"Return a list of indentation rules for `treesit-simple-indent-rules'."
`((clojure
Expand All @@ -820,6 +840,7 @@ forms like deftype, defrecord, reify, proxy, etc."
(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-with-metadata parent 0)
;; Literal Sequences
((parent-is "list_lit") parent 1) ;; https://guide.clojure.style/#one-space-indent
((parent-is "vec_lit") parent 1) ;; https://guide.clojure.style/#bindings-alignment
Expand Down
38 changes: 38 additions & 0 deletions test/clojure-ts-mode-imenu-test.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
;;; clojure-ts-mode-util-test.el --- Clojure TS Mode: util test suite -*- lexical-binding: t; -*-

;; Copyright © 2022-2024 Danny Freeman

;; This file is not part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; The unit test suite of Clojure TS Mode

(require 'clojure-ts-mode)
(require 'buttercup)
(require 'imenu)


(describe "clojure-ts-mode imenu integration"
(it "should index def with meta data"
(with-clojure-ts-buffer "^{:foo 1}(def a 1)"
(expect (imenu--in-alist "a" (imenu--make-index-alist))
:not :to-be nil)))

(it "should index defn with meta data"
(with-clojure-ts-buffer "^{:foo 1}(defn a [])"
(expect (imenu--in-alist "a" (imenu--make-index-alist))
:not :to-be nil))))
26 changes: 25 additions & 1 deletion test/samples/indentation.clj
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
IProto
(foo [this x] x))

(defn foo2 [x]b)
(defn foo2 [x] b)

(reify
IProto
Expand All @@ -118,3 +118,27 @@
([a] a)
([a b]
b))})

^:foo
(def a 1)

^{:foo true}
(def b 2)

^{:foo true}
[1 2]

(comment
^{:a 1}
(def a 2))

(defn hinted
(^String [])
(^java.util.List
[a & args]))

^{:foo true}
(defn c
"hello"
[_foo]
(+ 1 1))
3 changes: 3 additions & 0 deletions test/samples/test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ clojure.core/map

(def ^Integer x 1)

^{:foo true}
(defn b "hello" [] "world")

(comment
(defrecord TestRecord [field]
AutoCloseable
Expand Down