Skip to content

Add imenu support for keyword definitions #75

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

Merged
merged 1 commit into from
Apr 10, 2025
Merged
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)

13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 42 additions & 8 deletions clojure-ts-mode.el
Original file line number Diff line number Diff line change
@@ -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.")
16 changes: 12 additions & 4 deletions test/clojure-ts-mode-imenu-test.el
Original file line number Diff line number Diff line change
@@ -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")))))
7 changes: 7 additions & 0 deletions test/samples/spec.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(ns spec
(:require
[clojure.spec.alpha :as s]))

(s/def ::username string?)
(s/def ::age number?)
(s/def ::email string?)