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?)