Add treesit-aggregated-simple-imenu-settings

Now we support setting up Imenu for multiple languages

* doc/lispref/modes.texi: Update manual.
* lisp/treesit.el:
(treesit-aggregated-simple-imenu-settings): New variable.
(treesit--imenu-merge-entries): New function.
(treesit--generate-simple-imenu): This was previously
treesit-simple-imenu.
(treesit-simple-imenu): Support
treesit-aggregated-simple-imenu-settings.
(treesit-major-mode-setup): Recognize
treesit-aggregated-simple-imenu-settings.
* test/src/treesit-tests.el (treesit-imenu): New test.
This commit is contained in:
Yuan Fu 2024-12-24 13:17:51 -08:00
parent 833494d4b0
commit e2a9af4311
No known key found for this signature in database
GPG key ID: 56E19BC57664A442
3 changed files with 113 additions and 14 deletions

View file

@ -3109,6 +3109,15 @@ instead.
automatically sets up Imenu if this variable is non-@code{nil}.
@end defvar
@defvar treesit-aggregated-simple-imenu-settings
This variable allows major modes to configure Imenu for multiple
languages. Its value is an alist mapping language symbols to Imenu
settings described in @var{treesit-simple-imenu-settings}.
If both this variable and @var{treesit-simple-imenu-settings} is
non-@code{nil}, Emacs uses this variable for setting up Imenu.
@end defvar
@node Outline Minor Mode
@section Outline Minor Mode

View file

@ -3123,6 +3123,31 @@ node and returns the name of that defun node. If NAME-FN is nil,
`treesit-major-mode-setup' automatically sets up Imenu if this
variable is non-nil.")
;; `treesit-simple-imenu-settings' doesn't support multiple languages,
;; and we need to add multi-lang support for Imenu. One option is to
;; extend treesit-simple-imenu-settings to specify language, either by
;; making it optionally an alist (just like
;; `treesit-aggregated-simple-imenu-settings'), or add a fifth element
;; to each setting. But either way makes borrowing Imenu settings from
;; other modes difficult: with the alist approach, you'd need to check
;; whether other mode uses a plain list or an alist; with the fifth
;; element approach, again, you need to check if each setting has the
;; fifth element, and add it if not.
;;
;; OTOH, with `treesit-aggregated-simple-imenu-settings', borrowing
;; Imenu settings is easy: if `treesit-aggregated-simple-imenu-settings'
;; is non-nil, copy everything over; if `treesit-simple-imenu-settings'
;; is non-nil, copy the settings and put them under a language symbol.
(defvar treesit-aggregated-simple-imenu-settings nil
"Settings that configure `treesit-simple-imenu' for multi-language modes.
The value should be an alist of (LANG . SETTINGS), where LANG is a
language symbol, and SETTINGS has the same form as
`treesit-simple-imenu-settings'.
When both this variable and `treesit-simple-imenu-settings' are non-nil,
this variable takes priority.")
(defun treesit--simple-imenu-1 (node pred name-fn)
"Given a sparse tree, create an Imenu index.
@ -3170,20 +3195,69 @@ ENTRY. MARKER marks the start of each tree-sitter node."
;; Leaf node, return a (list of) plain index entry.
(t (list (cons name marker))))))
(defun treesit--imenu-merge-entries (entries)
"Merge ENTRIES by category.
ENTRIES is a list of (CATEGORY . SUB-ENTRIES...). Merge them so there's
no duplicate CATEGORY. CATEGORY's are strings. The merge is stable,
meaning the order of elements are kept."
(let ((return-entries nil))
(dolist (entry entries)
(let* ((category (car entry))
(sub-entries (cdr entry))
(existing-entries
(alist-get category return-entries nil nil #'equal)))
(if (not existing-entries)
(push entry return-entries)
(setf (alist-get category return-entries nil nil #'equal)
(append existing-entries sub-entries)))))
(nreverse return-entries)))
(defun treesit--generate-simple-imenu (node settings)
"Return an Imenu index for NODE with SETTINGS.
NODE usually should be a root node of a parser. SETTINGS is described
by `treesit-simple-imenu-settings'."
(mapcan (lambda (setting)
(pcase-let ((`(,category ,regexp ,pred ,name-fn)
setting))
(when-let* ((tree (treesit-induce-sparse-tree
node regexp))
(index (treesit--simple-imenu-1
tree pred name-fn)))
(if category
(list (cons category index))
index))))
settings))
(defun treesit-simple-imenu ()
"Return an Imenu index for the current buffer."
(let ((root (treesit-buffer-root-node)))
(mapcan (lambda (setting)
(pcase-let ((`(,category ,regexp ,pred ,name-fn)
setting))
(when-let* ((tree (treesit-induce-sparse-tree
root regexp))
(index (treesit--simple-imenu-1
tree pred name-fn)))
(if category
(list (cons category index))
index))))
treesit-simple-imenu-settings)))
(if (not treesit-aggregated-simple-imenu-settings)
(treesit--generate-simple-imenu
(treesit-parser-root-node treesit-primary-parser)
treesit-simple-imenu-settings)
;; Use `treesit-aggregated-simple-imenu-settings'. Remove languages
;; that doesn't have any Imenu entries.
(seq-filter
#'cdr
(mapcar
(lambda (entry)
(let* ((lang (car entry))
(settings (cdr entry))
(global-parser (car (treesit-parser-list nil lang)))
(local-parsers
(treesit-parser-list nil lang 'embedded)))
(cons (treesit-language-display-name lang)
;; No one says you can't have both global and local
;; parsers for the same language. E.g., Rust uses
;; local parsers for the same language to handle
;; macros.
(treesit--imenu-merge-entries
(mapcan (lambda (parser)
(treesit--generate-simple-imenu
(treesit-parser-root-node parser) settings))
(cons global-parser local-parsers))))))
treesit-aggregated-simple-imenu-settings))))
;;; Outline minor mode
@ -3321,7 +3395,8 @@ and `end-of-defun-function'.
If `treesit-defun-name-function' is non-nil, set up
`add-log-current-defun'.
If `treesit-simple-imenu-settings' is non-nil, set up Imenu.
If `treesit-simple-imenu-settings' or
`treesit-aggregated-simple-imenu-settings' is non-nil, set up Imenu.
If either `treesit-outline-predicate' or `treesit-simple-imenu-settings'
are non-nil, and Outline minor mode settings don't already exist, setup
@ -3395,7 +3470,8 @@ before calling this function."
(setq-local forward-sentence-function #'treesit-forward-sentence))
;; Imenu.
(when treesit-simple-imenu-settings
(when (or treesit-aggregated-simple-imenu-settings
treesit-simple-imenu-settings)
(setq-local imenu-create-index-function
#'treesit-simple-imenu))

View file

@ -1270,6 +1270,20 @@ This tests bug#60355."
(should node)
(should (equal (treesit-node-text node) "2"))))
;;; Imenu
(ert-deftest treesit-imenu ()
"Test imenu functions."
(should (equal (treesit--imenu-merge-entries
'(("Function" . (f1 f2))
("Function" . (f3 f4 f5))
("Class" . (c1 c2 c3))
("Variables" . (v1 v2))
("Class" . (c4))))
'(("Function" . (f1 f2 f3 f4 f5))
("Class" . (c1 c2 c3 c4))
("Variables" . (v1 v2))))))
;; TODO
;; - Functions in treesit.el