Skip to content

Commit ad21b18

Browse files
committed
[#74] Add imenu support for keyword definitions
1 parent 897659a commit ad21b18

File tree

5 files changed

+75
-12
lines changed

5 files changed

+75
-12
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- [#71](https://github.com/clojure-emacs/clojure-ts-mode/pull/71): Properly highlight function name in `letfn` form.
2020
- [#72](https://github.com/clojure-emacs/clojure-ts-mode/pull/72): Pass fully qualified symbol to `clojure-ts-get-indent-function`.
2121
- Improve performance of semantic indentation by caching rules.
22+
- [#74](https://github.com/clojure-emacs/clojure-ts-mode/issues/74): Add imenu support for keywords definitions.
2223

2324
## 0.2.3 (2025-03-04)
2425

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,19 @@ Every new line in the docstrings is indented by
275275
`clojure-ts-docstring-fill-prefix-width` number of spaces (set to 2 by default
276276
which matches the `clojure-mode` settings).
277277

278+
#### imenu
279+
280+
`clojure-ts-mode` supports various types of definition that can be navigated
281+
using `imenu`, such as:
282+
283+
- namespace
284+
- function
285+
- macro
286+
- var
287+
- interface (forms such as `defprotocol`, `definterface` and `defmulti`)
288+
- class (forms such as `deftype`, `defrecord` and `defstruct`)
289+
- keyword (for example, spec definitions)
290+
278291
## Migrating to clojure-ts-mode
279292

280293
If you are migrating to `clojure-ts-mode` note that `clojure-mode` is still required for cider and clj-refactor packages to work properly.

clojure-ts-mode.el

+42-8
Original file line numberDiff line numberDiff line change
@@ -637,17 +637,33 @@ See `clojure-ts--definition-node-p' when an exact match is possible."
637637
(and
638638
(clojure-ts--list-node-p node)
639639
(let* ((child (clojure-ts--node-child-skip-metadata node 0))
640-
(child-txt (clojure-ts--named-node-text child)))
640+
(child-txt (clojure-ts--named-node-text child))
641+
(name-sym (clojure-ts--node-child-skip-metadata node 1)))
641642
(and (clojure-ts--symbol-node-p child)
643+
(clojure-ts--symbol-node-p name-sym)
642644
(string-match-p definition-type-regexp child-txt)))))
643645

646+
(defun clojure-ts--kwd-definition-node-match-p (node)
647+
"Return non-nil if the NODE is a keyword definition."
648+
(and (clojure-ts--list-node-p node)
649+
(let* ((child (clojure-ts--node-child-skip-metadata node 0))
650+
(child-txt (clojure-ts--named-node-text child))
651+
(child-ns (clojure-ts--node-namespace-text child))
652+
(name-kwd (clojure-ts--node-child-skip-metadata node 1)))
653+
(and child-ns
654+
(clojure-ts--symbol-node-p child)
655+
(clojure-ts--keyword-node-p name-kwd)
656+
(string-equal child-txt "def")))))
657+
644658
(defun clojure-ts--standard-definition-node-name (node)
645659
"Return the definition name for the given NODE.
646-
Returns nil if NODE is not a list with symbols as the first two children.
647-
For example the node representing the expression (def foo 1) would return foo.
648-
The node representing (ns user) would return user.
649-
Does not does any matching on the first symbol (def, defn, etc), so identifying
650-
that a node is a definition is intended to be done elsewhere.
660+
661+
Returns nil if NODE is not a list with symbols as the first two
662+
children. For example the node representing the expression (def foo 1)
663+
would return foo. The node representing (ns user) would return user.
664+
Does not do any matching on the first symbol (def, defn, etc), so
665+
identifying that a node is a definition is intended to be done
666+
elsewhere.
651667
652668
Can be called directly, but intended for use as `treesit-defun-name-function'."
653669
(when (and (clojure-ts--list-node-p node)
@@ -663,6 +679,21 @@ Can be called directly, but intended for use as `treesit-defun-name-function'."
663679
(concat (treesit-node-text ns) "/" (treesit-node-text name))
664680
(treesit-node-text name)))))))
665681

682+
(defun clojure-ts--kwd-definition-node-name (node)
683+
"Return the keyword name for the given NODE.
684+
685+
Returns nil if NODE is not a list where the first element is a symbol
686+
and the second is a keyword. For example, a node representing the
687+
expression (s/def ::foo int?) would return foo.
688+
689+
Can be called directly, but intended for use as
690+
`treesit-defun-name-function'."
691+
(when (and (clojure-ts--list-node-p node)
692+
(clojure-ts--symbol-node-p (clojure-ts--node-child-skip-metadata node 0)))
693+
(let ((kwd (clojure-ts--node-child-skip-metadata node 1)))
694+
(when (clojure-ts--keyword-node-p kwd)
695+
(treesit-node-text (treesit-node-child-by-field-name kwd "name"))))))
696+
666697
(defvar clojure-ts--function-type-regexp
667698
(rx string-start (or (seq "defn" (opt "-")) "defmethod" "deftest") string-end)
668699
"Regular expression for matching definition nodes that resemble functions.")
@@ -713,7 +744,6 @@ Includes a dispatch value when applicable (defmethods)."
713744
"Return non-nil if NODE represents a protocol or interface definition."
714745
(clojure-ts--definition-node-match-p clojure-ts--interface-type-regexp node))
715746

716-
717747
(defvar clojure-ts--imenu-settings
718748
`(("Namespace" "list_lit" clojure-ts--ns-node-p)
719749
("Function" "list_lit" clojure-ts--function-node-p
@@ -722,7 +752,11 @@ Includes a dispatch value when applicable (defmethods)."
722752
("Macro" "list_lit" clojure-ts--defmacro-node-p)
723753
("Variable" "list_lit" clojure-ts--variable-definition-node-p)
724754
("Interface" "list_lit" clojure-ts--interface-node-p)
725-
("Class" "list_lit" clojure-ts--class-node-p))
755+
("Class" "list_lit" clojure-ts--class-node-p)
756+
("Keyword"
757+
"list_lit"
758+
clojure-ts--kwd-definition-node-match-p
759+
clojure-ts--kwd-definition-node-name))
726760
"The value for `treesit-simple-imenu-settings'.
727761
By default `treesit-defun-name-function' is used to extract definition names.
728762
See `clojure-ts--standard-definition-node-name' for the implementation used.")

test/clojure-ts-mode-imenu-test.el

+12-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,18 @@
2929
(describe "clojure-ts-mode imenu integration"
3030
(it "should index def with meta data"
3131
(with-clojure-ts-buffer "^{:foo 1}(def a 1)"
32-
(expect (imenu--in-alist "a" (imenu--make-index-alist))
33-
:not :to-be nil)))
32+
(let ((flatten-index (imenu--flatten-index-alist (imenu--make-index-alist) t)))
33+
(expect (imenu-find-default "a" flatten-index)
34+
:to-equal "Variable:a"))))
3435

3536
(it "should index defn with meta data"
3637
(with-clojure-ts-buffer "^{:foo 1}(defn a [])"
37-
(expect (imenu--in-alist "a" (imenu--make-index-alist))
38-
:not :to-be nil))))
38+
(let ((flatten-index (imenu--flatten-index-alist (imenu--make-index-alist) t)))
39+
(expect (imenu-find-default "a" flatten-index)
40+
:to-equal "Function:a"))))
41+
42+
(it "should index def with keywords as a first item"
43+
(with-clojure-ts-buffer "(s/def ::username string?)"
44+
(let ((flatten-index (imenu--flatten-index-alist (imenu--make-index-alist) t)))
45+
(expect (imenu-find-default "username" flatten-index)
46+
:to-equal "Keyword:username")))))

test/samples/spec.clj

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
(ns spec
2+
(:require
3+
[clojure.spec.alpha :as s]))
4+
5+
(s/def ::username string?)
6+
(s/def ::age number?)
7+
(s/def ::email string?)

0 commit comments

Comments
 (0)