branch: elpa/clojure-ts-mode
commit a0b01b212723dea772dd8c06932be0f9f66ea000
Author: Roman Rudakov <rruda...@fastmail.com>
Commit: Bozhidar Batsov <bozhi...@batsov.dev>

    [#74] Add imenu support for keyword definitions
---
 CHANGELOG.md                       |  3 ++-
 README.md                          | 13 ++++++++++
 clojure-ts-mode.el                 | 50 ++++++++++++++++++++++++++++++++------
 test/clojure-ts-mode-imenu-test.el | 16 +++++++++---
 test/samples/spec.clj              |  7 ++++++
 5 files changed, 76 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f4517e232..0060edeb20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,7 +18,8 @@
 - [#70](https://github.com/clojure-emacs/clojure-ts-mode/pull/70): Add support 
for nested indentation rules.
 - [#71](https://github.com/clojure-emacs/clojure-ts-mode/pull/71): Properly 
highlight function name in `letfn` form.
 - [#72](https://github.com/clojure-emacs/clojure-ts-mode/pull/72): Pass fully 
qualified symbol to `clojure-ts-get-indent-function`.
-- Improve performance of semantic indentation by caching rules.
+- [#76](https://github.com/clojure-emacs/clojure-ts-mode/pull/76): Improve 
performance of semantic indentation by caching rules.
+- [#74](https://github.com/clojure-emacs/clojure-ts-mode/issues/74): Add imenu 
support for keywords definitions.
 
 ## 0.2.3 (2025-03-04)
 
diff --git a/README.md b/README.md
index c9374aca84..af70b42999 100644
--- a/README.md
+++ b/README.md
@@ -275,6 +275,19 @@ Every new line in the docstrings is indented by
 `clojure-ts-docstring-fill-prefix-width` number of spaces (set to 2 by default
 which matches the `clojure-mode` settings).
 
+#### imenu
+
+`clojure-ts-mode` supports various types of definition that can be navigated
+using `imenu`, such as:
+
+- namespace
+- function
+- macro
+- var
+- interface (forms such as `defprotocol`, `definterface` and `defmulti`)
+- class (forms such as `deftype`, `defrecord` and `defstruct`)
+- keyword (for example, spec definitions)
+
 ## Migrating to clojure-ts-mode
 
 If you are migrating to `clojure-ts-mode` note that `clojure-mode` is still 
required for cider and clj-refactor packages to work properly.
diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el
index cb71cb9259..96582346b9 100644
--- a/clojure-ts-mode.el
+++ b/clojure-ts-mode.el
@@ -637,17 +637,33 @@ See `clojure-ts--definition-node-p' when an exact match 
is possible."
   (and
    (clojure-ts--list-node-p node)
    (let* ((child (clojure-ts--node-child-skip-metadata node 0))
-          (child-txt (clojure-ts--named-node-text child)))
+          (child-txt (clojure-ts--named-node-text child))
+          (name-sym (clojure-ts--node-child-skip-metadata node 1)))
      (and (clojure-ts--symbol-node-p child)
+          (clojure-ts--symbol-node-p name-sym)
           (string-match-p definition-type-regexp child-txt)))))
 
+(defun clojure-ts--kwd-definition-node-match-p (node)
+  "Return non-nil if the NODE is a keyword definition."
+  (and (clojure-ts--list-node-p node)
+       (let* ((child (clojure-ts--node-child-skip-metadata node 0))
+              (child-txt (clojure-ts--named-node-text child))
+              (child-ns (clojure-ts--node-namespace-text child))
+              (name-kwd (clojure-ts--node-child-skip-metadata node 1)))
+         (and child-ns
+              (clojure-ts--symbol-node-p child)
+              (clojure-ts--keyword-node-p name-kwd)
+              (string-equal child-txt "def")))))
+
 (defun clojure-ts--standard-definition-node-name (node)
   "Return the definition name for the given NODE.
-Returns nil if NODE is not a list with symbols as the first two children.
-For example the node representing the expression (def foo 1) would return foo.
-The node representing (ns user) would return user.
-Does not does any matching on the first symbol (def, defn, etc), so identifying
-that a node is a definition is intended to be done elsewhere.
+
+Returns nil if NODE is not a list with symbols as the first two
+children.  For example the node representing the expression (def foo 1)
+would return foo.  The node representing (ns user) would return user.
+Does not do any matching on the first symbol (def, defn, etc), so
+identifying 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)
@@ -663,6 +679,21 @@ Can be called directly, but intended for use as 
`treesit-defun-name-function'."
               (concat (treesit-node-text ns) "/" (treesit-node-text name))
             (treesit-node-text name)))))))
 
+(defun clojure-ts--kwd-definition-node-name (node)
+  "Return the keyword name for the given NODE.
+
+Returns nil if NODE is not a list where the first element is a symbol
+and the second is a keyword.  For example, a node representing the
+expression (s/def ::foo int?) would return foo.
+
+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 (clojure-ts--node-child-skip-metadata 
node 0)))
+    (let ((kwd (clojure-ts--node-child-skip-metadata node 1)))
+      (when (clojure-ts--keyword-node-p kwd)
+        (treesit-node-text (treesit-node-child-by-field-name kwd "name"))))))
+
 (defvar clojure-ts--function-type-regexp
   (rx string-start (or (seq "defn" (opt "-")) "defmethod" "deftest") 
string-end)
   "Regular expression for matching definition nodes that resemble functions.")
@@ -713,7 +744,6 @@ Includes a dispatch value when applicable (defmethods)."
   "Return non-nil if NODE represents a protocol or interface definition."
   (clojure-ts--definition-node-match-p clojure-ts--interface-type-regexp node))
 
-
 (defvar clojure-ts--imenu-settings
   `(("Namespace" "list_lit" clojure-ts--ns-node-p)
     ("Function" "list_lit" clojure-ts--function-node-p
@@ -722,7 +752,11 @@ Includes a dispatch value when applicable (defmethods)."
     ("Macro" "list_lit" clojure-ts--defmacro-node-p)
     ("Variable" "list_lit" clojure-ts--variable-definition-node-p)
     ("Interface" "list_lit" clojure-ts--interface-node-p)
-    ("Class" "list_lit" clojure-ts--class-node-p))
+    ("Class" "list_lit" clojure-ts--class-node-p)
+    ("Keyword"
+     "list_lit"
+     clojure-ts--kwd-definition-node-match-p
+     clojure-ts--kwd-definition-node-name))
   "The value for `treesit-simple-imenu-settings'.
 By default `treesit-defun-name-function' is used to extract definition names.
 See `clojure-ts--standard-definition-node-name' for the implementation used.")
diff --git a/test/clojure-ts-mode-imenu-test.el 
b/test/clojure-ts-mode-imenu-test.el
index 45675966f6..1822231496 100644
--- a/test/clojure-ts-mode-imenu-test.el
+++ b/test/clojure-ts-mode-imenu-test.el
@@ -29,10 +29,18 @@
 (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)))
+      (let ((flatten-index (imenu--flatten-index-alist 
(imenu--make-index-alist) t)))
+        (expect (imenu-find-default "a" flatten-index)
+                :to-equal "Variable:a"))))
 
   (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))))
+      (let ((flatten-index (imenu--flatten-index-alist 
(imenu--make-index-alist) t)))
+        (expect (imenu-find-default "a" flatten-index)
+                :to-equal "Function:a"))))
+
+  (it "should index def with keywords as a first item"
+    (with-clojure-ts-buffer "(s/def ::username string?)"
+      (let ((flatten-index (imenu--flatten-index-alist 
(imenu--make-index-alist) t)))
+        (expect (imenu-find-default "username" flatten-index)
+                :to-equal "Keyword:username")))))
diff --git a/test/samples/spec.clj b/test/samples/spec.clj
new file mode 100644
index 0000000000..b0770cf19b
--- /dev/null
+++ b/test/samples/spec.clj
@@ -0,0 +1,7 @@
+(ns spec
+  (:require
+   [clojure.spec.alpha :as s]))
+
+(s/def ::username string?)
+(s/def ::age number?)
+(s/def ::email string?)

Reply via email to