-
Notifications
You must be signed in to change notification settings - Fork 74
/
Copy pathtree-sitter-imenu.el
173 lines (143 loc) · 6.43 KB
/
tree-sitter-imenu.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
;;; tree-sitter-imenu.el --- Jump to buffer points of note, in the current buffer, based on tree-sitter -*- lexical-binding: t; coding: utf-8 -*-
;; Copyright (C) 2020 Tuấn-Anh Nguyễn
;;
;; Author: Tuấn-Anh Nguyễn <[email protected]>
;; Timo von Hartz <[email protected]>
;; SPDX-License-Identifier: MIT
;;; Commentary:
;; This file adds an `imenu' backend using `tree-sitter'.
;;
;; This lets users call `imenu' to interactively select relevent
;; points of interest in the current buffer such as class declarations
;; or variable assignments.
;;; Code:
(require 'tree-sitter)
(eval-when-compile
(require 'cl-lib))
(defgroup tree-sitter-imenu nil
"`imenu' using tree-sitter."
:group 'tree-sitter)
;;; ----------------------------------------------------------------------------
;;; Interfaces for modes and end users.
(defvar-local tree-sitter-imenu--query nil
"Tree query used for `imenu', compiled from patterns.")
(defvar-local tree-sitter-imenu-default-patterns nil
"Default imenu patterns.
This should be set by major modes that want to integrate with `tree-sitter-imenu'.
It plays a similar role to `imenu-generic-expression'.
It is either a string, or a vector of S-expressions. For more details on the
syntax, see https://emacs-tree-sitter.github.io/syntax-highlighting/queries/.")
(defvar tree-sitter-imenu--patterns-alist nil
"Additional language-specific `imenu' patterns.
It plays a similar role to `font-lock-keywords-alist', except that its keys are
language symbols, not major mode symbols.")
(put 'tree-sitter-imenu--patterns-alist 'risky-local-variable t)
(defvar-local tree-sitter-imenu--extra-patterns-list nil
"Additional buffer-local syntax highlighting patterns.")
(defcustom tree-sitter-imenu-type-matching-function
#'identity
"Function used to map capture names in query patterns to `imenu' types.
This can also be used to selectively disable certain capture names."
:group 'tree-sitter-imenu
:type 'function)
(defun tree-sitter-imenu--query ()
(or tree-sitter-imenu--query
(setq tree-sitter-imenu--query
(when tree-sitter-imenu-default-patterns
(tsc-make-query
tree-sitter-language
(mapconcat #'tsc--stringify-patterns
(append tree-sitter-imenu--extra-patterns-list
(alist-get (tsc--lang-symbol tree-sitter-language)
tree-sitter-imenu--patterns-alist)
(list tree-sitter-imenu-default-patterns))
"\n")
tree-sitter-imenu-type-matching-function)))))
;; TODO: tree-sitter-imenu-add-patterns
;;; ----------------------------------------------------------------------------
;;; Index function & helpers
(defun tree-sitter-imenu--captures-leaf-node (captures)
"Return a node in an entry, CAPTURES, from `tsc--query-cursor-matches'."
(cdr (aref (cdr captures) (1- (length (cdr captures))))))
(defun tree-sitter-imenu--captures ()
"Execute variable `tree-sitter-imenu--query' and fetch unique matches.
Returns a value of the form [(PATTERN-INDEX . [CAPTURE...])], that
is a vector of all captures grouped by the pattern index."
(thread-first
(tsc--query-cursor-matches
(tsc-make-query-cursor)
(tree-sitter-imenu--query)
(tsc-root-node tree-sitter-tree)
#'tsc--buffer-substring-no-properties)
(cl-stable-sort #'< :key #'car)
;; TODO: Write custom duplicate remover, `cl-delete-duplicates'
;; seems to work by overwriting earlier values matching later
;; values with those later values, which is why we double reverse
;; here to ensure the order of the original sequence is maintained.
(nreverse)
(cl-delete-duplicates
:key #'tree-sitter-imenu--captures-leaf-node
:test #'tsc-node-eq)
(nreverse)))
(defun tree-sitter-imenu--stream (captures)
"Convert each tree-sitter match in CAPTURES to a list of nodes.
Each node in the return-value is a string except for the final node
which is a cons of the form (\"NAME\" . POINT)."
(cl-loop for (_ . it) across captures
with len = nil do (setq len (length it))
with leaf = nil do (setq leaf (aref it (1- len)))
collect
(append (list (car leaf))
(mapcar #'tsc-node-text
(mapcar #'cdr (cl-subseq it 0 (1- len))))
(cons (tsc-node-text (cdr leaf))
(tsc-node-start-position (cdr leaf))))))
(defun tree-sitter-imenu--group-stream (stream)
"Group STREAM into a tree of nodes as expected by `imenu'."
(let ((mem (make-hash-table :test 'equal :size (length stream)))
leaves)
(dolist (it stream)
(if (numberp (cdr it))
(push it leaves)
(if-let ((val (gethash (car it) mem)))
(puthash (car it) (append val (list (cdr it))) mem)
(puthash (car it) (list (cdr it)) mem))))
(maphash
(lambda (key sub-stream)
(setq leaves
(append leaves
(list (cons key (tree-sitter-imenu--group-stream sub-stream))))))
mem)
leaves))
(defun tree-sitter-imenu-index-function ()
"Tree-sittters `imenu-create-index-function'."
(thread-first
(tree-sitter-imenu--captures)
(tree-sitter-imenu--stream)
(tree-sitter-imenu--group-stream)))
;;; ----------------------------------------------------------------------------
;;; Setup and teardown.
(defun tree-sitter-imenu--setup ()
(when (tree-sitter-imenu--query)
(setq imenu-create-index-function
#'tree-sitter-imenu-index-function))
)
(defun tree-sitter-imenu--teardown ()
;; TODO: Maybe restore previous index function?
(when (eq imenu-create-index-function
#'tree-sitter-imenu-index-function)
(kill-local-variable 'imenu-create-index-function)))
(define-minor-mode tree-sitter-imenu-mode
"Toggle `imenu' support based on Tree-sitter's syntax tree.
If `tree-sitter-imenu-default-patterns' is nil, turning on this mode does nothing,
and does not interfere with `font-lock-mode'.
Enabling this automatically enables `tree-sitter-mode' in the buffer.
To enable this automatically whenever `tree-sitter-mode' is enabled:
(add-hook 'tree-sitter-after-on-hook #'tree-sitter-imenu-mode)"
:init-value nil
:group 'tree-sitter
(tree-sitter--handle-dependent tree-sitter-imenu-mode
#'tree-sitter-imenu--setup
#'tree-sitter-imenu--teardown))
(provide 'tree-sitter-imenu)
;;; tree-sitter-imenu.el ends here