imenus.el 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. ;;; imenus.el --- Imenu for multiple buffers and without subgroups
  2. ;; Copyright © 2014–2018, 2020 Alex Kost
  3. ;; Author: Alex Kost <alezost@gmail.com>
  4. ;; Created: 19 Dec 2014
  5. ;; Version: 0.2
  6. ;; Package-Requires: ((cl-lib "0.5"))
  7. ;; URL: https://github.com/alezost/imenus.el
  8. ;; Keywords: tools convenience
  9. ;; This program is free software; you can redistribute it and/or modify
  10. ;; it under the terms of the GNU General Public License as published by
  11. ;; the Free Software Foundation, either version 3 of the License, or
  12. ;; (at your option) any later version.
  13. ;;
  14. ;; This program is distributed in the hope that it will be useful,
  15. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. ;; GNU General Public License for more details.
  18. ;;
  19. ;; You should have received a copy of the GNU General Public License
  20. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. ;;; Commentary:
  22. ;; This file provides an `imenus' command which may be used as a
  23. ;; substitution for "M-x imenu". It allows to jump to imenu items in
  24. ;; multiple buffers. Also it provides additional key bindings for
  25. ;; rescanning, "isearch"-ing and performing "occur" using a current
  26. ;; minibuffer input.
  27. ;; To install the package manually, add the following to your init file:
  28. ;;
  29. ;; (add-to-list 'load-path "/path/to/imenus-dir")
  30. ;; (autoload 'imenus "imenus" nil t)
  31. ;; (autoload 'imenus-mode-buffers "imenus" nil t)
  32. ;; The main purpose of this package is to provide a framework to use
  33. ;; `imenu' indexes of multiple buffers/files. For example, you may
  34. ;; search for imenu items in elisp files of your "~/.emacs.d/" directory
  35. ;; with a command like this:
  36. ;;
  37. ;; (defun imenus-my-elisp-files ()
  38. ;; "Perform `imenus' on elisp files from `user-emacs-directory'."
  39. ;; (interactive)
  40. ;; (imenus-files
  41. ;; (directory-files user-emacs-directory t "^[^.].*\\.el\\'")))
  42. ;;; Code:
  43. (require 'cl-lib)
  44. (require 'imenu)
  45. (require 'misearch)
  46. (defgroup imenus nil
  47. "Easy jumping to buffers places."
  48. :group 'convenience)
  49. (defgroup imenus-faces nil
  50. "Imenus faces."
  51. :group 'imenus
  52. :group 'faces)
  53. (defface imenus-section-face
  54. '((t :inherit font-lock-comment-face))
  55. "Face used for titles of sections."
  56. :group 'imenus-faces)
  57. (defcustom imenus-sort-function nil
  58. "Function used to sort imenus items.
  59. The function should take 2 arguments and return t if the first
  60. element should come before the second. The arguments are cons
  61. cells: (NAME . POSITION).
  62. If nil, do not use any sorting (faster)."
  63. :type '(choice (const :tag "No sorting" nil)
  64. (const :tag "Sort by name" imenu--sort-by-name)
  65. (function :tag "Another function"))
  66. :group 'imenus)
  67. (defcustom imenus-item-name-function #'imenus-item-name-default
  68. "Function used to name imenus items.
  69. The function should take 3 arguments: imenu item name, imenu
  70. subsection name and a buffer where the item come from."
  71. :type '(choice (function-item imenus-item-name-default)
  72. (function-item imenus-item-name-full)
  73. (function :tag "Another function"))
  74. :group 'imenus)
  75. (defcustom imenus-delimiter "|"
  76. "String used to separate parts of an index item name."
  77. :type 'string
  78. :group 'imenus)
  79. (defcustom imenus-inherit-input-method t
  80. "If non-nil, inherit the input method from the current buffer.
  81. This value is passed as the last argument to
  82. `imenus-completing-read-function'. See `completing-read' for
  83. details."
  84. :type 'boolean
  85. :group 'imenus)
  86. (defvar imenus-completing-read-function completing-read-function
  87. "Function used to read a string from minibuffer with completions.
  88. It should accept the same arguments as `completing-read'.")
  89. (defvar imenus-actions
  90. '((isearch . imenus-isearch)
  91. (occur . multi-occur))
  92. "Alist of exit statuses and functions.
  93. Whenever imenus prompt is finished with a non-nil
  94. `imenus-exit-status', an according function is called with 2
  95. arguments: list of buffers and a user input string.")
  96. (defvar imenus-exit-status nil
  97. "Exit status of the current (latest) imenus command.
  98. This variable is used to define what action should be done after
  99. quitting the minibuffer (rescan an index, switch to isearch, etc.)")
  100. (defvar imenus-minibuffer-map
  101. (let ((map (make-sparse-keymap)))
  102. (define-key map (kbd "M-r") 'imenus-rescan)
  103. (define-key map (kbd "M-s") 'imenus-exit-to-isearch)
  104. (define-key map (kbd "M-o") 'imenus-exit-to-occur)
  105. map)
  106. "Keymap with additional imenus commands for minibuffer.")
  107. (defvar-local imenus-index nil
  108. "Imenus index alist of a current buffer.
  109. Elements in the alist have the following form:
  110. (ITEM-NAME . POSITION)
  111. POSITION is the buffer position of the item. To go to the item
  112. is to switch to the buffer and to move point to that position.
  113. POSITION is passed to `imenus-goto'.")
  114. (cl-defstruct (imenus-position
  115. (:constructor nil)
  116. (:constructor imenus-make-position
  117. (buffer imenu-position))
  118. (:copier nil))
  119. buffer imenu-position)
  120. (defun imenus-minibuffer-setup ()
  121. "Prepare minibuffer for imenus needs."
  122. (use-local-map
  123. (make-composed-keymap imenus-minibuffer-map
  124. (current-local-map))))
  125. (declare-function ido-select-text "ido" nil)
  126. (declare-function ivy-immediate-done "ivy" nil)
  127. (defun imenus-exit-minibuffer ()
  128. "Quit the current minibuffer command.
  129. Make this command return the current user input."
  130. (cond
  131. ((boundp 'ido-cur-item) ; if inside ido
  132. (ido-select-text))
  133. ((memq 'ivy--queue-exhibit post-command-hook) ; if inside ivy
  134. (ivy-immediate-done))
  135. (t (exit-minibuffer))))
  136. (defun imenus-rescan ()
  137. "Rescan the current imenus index."
  138. (interactive)
  139. (setq imenus-exit-status 'rescan)
  140. (imenus-exit-minibuffer))
  141. (defun imenus-exit-to-isearch ()
  142. "Exit from imenu prompt; start isearch with the current input."
  143. (interactive)
  144. (setq imenus-exit-status 'isearch)
  145. (imenus-exit-minibuffer))
  146. (defun imenus-exit-to-occur ()
  147. "Exit from imenu prompt; start `occur' using the current input."
  148. (interactive)
  149. (setq imenus-exit-status 'occur)
  150. (imenus-exit-minibuffer))
  151. (defun imenus-item-name-default (item-name &optional section _buffer)
  152. "Concatenate SECTION and ITEM-NAME with `imenus-delimiter'."
  153. (if section
  154. (concat (propertize section 'face 'imenus-section-face)
  155. imenus-delimiter item-name)
  156. item-name))
  157. (defun imenus-item-name-full (item-name section buffer)
  158. "Concatenate BUFFER name, SECTION and ITEM-NAME with `imenus-delimiter'."
  159. (concat (buffer-name buffer) imenus-delimiter
  160. (imenus-item-name-default item-name section)))
  161. (defun imenus-item-name (item-name section buffer)
  162. "Make an item name by calling `imenus-item-name-function'."
  163. (funcall (if (functionp imenus-item-name-function)
  164. imenus-item-name-function
  165. #'imenus-item-name-default)
  166. item-name section buffer))
  167. (defun imenus-imenu-item-to-imenus-item (item section buffer)
  168. "Convert imenu index ITEM into imenus index item.
  169. Change its name and transform imenu position into imenus position."
  170. (cons (imenus-item-name (car item) section buffer)
  171. (imenus-make-position buffer (cdr item))))
  172. (defun imenus-imenu-index-to-imenus-index (index buffer &optional section)
  173. "Convert imenu INDEX into imenus index."
  174. (cl-mapcan (lambda (item)
  175. (when item
  176. (if (imenu--subalist-p item)
  177. (let ((name (car item))
  178. (subindex (cdr item)))
  179. (imenus-imenu-index-to-imenus-index
  180. subindex buffer
  181. (if section
  182. (concat section imenus-delimiter name)
  183. name)))
  184. (list (imenus-imenu-item-to-imenus-item
  185. item section buffer)))))
  186. index))
  187. (defun imenus-sort-index-maybe (index)
  188. "Sort INDEX depending on `imenus-sort-function'."
  189. (if (functionp imenus-sort-function)
  190. (sort index imenus-sort-function)
  191. index))
  192. (defun imenus-buffer-index (buffer &optional rescan)
  193. "Return imenus index for BUFFER.
  194. If RESCAN is non-nil, rescan imenu items.
  195. This is an auxiliary function; do not use it if you want to get
  196. an imenus index for a single buffer. Use `imenus-buffers-index'
  197. instead: it takes care about a rescan option."
  198. (with-current-buffer buffer
  199. (when (or rescan (null imenus-index))
  200. (setq imenu--index-alist nil)
  201. (imenu--make-index-alist t)
  202. (setq imenus-index
  203. (imenus-imenu-index-to-imenus-index
  204. imenu--index-alist buffer)))
  205. imenus-index))
  206. (defun imenus-buffers-index (buffers &optional rescan)
  207. "Return imenus index for list of BUFFERS.
  208. If RESCAN is non-nil, rescan imenu items."
  209. (let ((index (if (cdr buffers)
  210. ;; Multiple buffers.
  211. (cl-mapcan (lambda (buf)
  212. (copy-sequence
  213. (imenus-buffer-index buf rescan)))
  214. buffers)
  215. ;; Single buffer.
  216. (imenus-buffer-index (car buffers) rescan))))
  217. ;; Add a rescan option to the index.
  218. (cons imenu--rescan-item
  219. (imenus-sort-index-maybe index))))
  220. (defun imenus-goto (item)
  221. "Go to imenus ITEM."
  222. (let* ((name (car item))
  223. (imenus-pos (cdr item))
  224. (buffer (imenus-position-buffer imenus-pos))
  225. (imenu-pos (imenus-position-imenu-position imenus-pos)))
  226. (pop-to-buffer buffer
  227. '((display-buffer-reuse-window
  228. display-buffer-same-window)))
  229. (push-mark nil t)
  230. ;; Imenu item can have 2 forms. See `imenu' and the docstring of
  231. ;; `imenu--index-alist'.
  232. (pcase imenu-pos
  233. (`(,position ,function . ,args)
  234. (apply function name position args))
  235. (position
  236. (funcall imenu-default-goto-function nil position)))
  237. (run-hooks 'imenu-after-jump-hook)))
  238. (defun imenus-prepare-index (index)
  239. "Replace space with `imenu-space-replacement' in INDEX items."
  240. ;; The code is taken from `imenu--completion-buffer'.
  241. (if (not imenu-space-replacement)
  242. index
  243. (mapcar (lambda (item)
  244. (cons (subst-char-in-string
  245. ?\s (aref imenu-space-replacement 0) (car item))
  246. (cdr item)))
  247. index)))
  248. (defun imenus-completing-read (index &optional prompt initial-input)
  249. "Prompt for an INDEX item and return it.
  250. This function is almost the same as `imenu--completion-buffer'.
  251. The main difference is it returns a user input string (not nil)
  252. if this string does not match any item."
  253. (setq imenus-exit-status nil)
  254. (let* ((index (imenus-prepare-index index))
  255. (name (thing-at-point 'symbol))
  256. (name (and (stringp name)
  257. (imenu-find-default name index)))
  258. (prompt (or prompt
  259. (and name
  260. (imenu--in-alist name index)
  261. (format "Index item (default '%s'): " name))
  262. "Index item: "))
  263. (minibuffer-setup-hook minibuffer-setup-hook))
  264. (or imenu-eager-completion-buffer
  265. (add-hook 'minibuffer-setup-hook 'minibuffer-completion-help))
  266. (add-hook 'minibuffer-setup-hook 'imenus-minibuffer-setup)
  267. (let* ((input (funcall imenus-completing-read-function
  268. prompt index nil nil initial-input
  269. 'imenu--history-list name
  270. imenus-inherit-input-method))
  271. (item (assoc input index)))
  272. (if (or imenus-exit-status (null item))
  273. input
  274. item))))
  275. (defun imenus-buffers (buffers &optional rescan prompt initial-input)
  276. "Prompt for a place from a list of BUFFERS and jump to it.
  277. If a user input does not match any item, start Isearch-ing of the
  278. current input.
  279. Interactively, use the current buffer."
  280. (let* ((index (imenus-buffers-index buffers rescan))
  281. (input (imenus-completing-read index prompt initial-input)))
  282. (cond ((eq imenus-exit-status 'rescan)
  283. (imenu--cleanup index)
  284. (imenus-buffers buffers 'rescan prompt input))
  285. ((equal input imenu--rescan-item)
  286. (imenu--cleanup index)
  287. (imenus-buffers buffers 'rescan prompt initial-input))
  288. (imenus-exit-status
  289. (let ((fun (cdr (assq imenus-exit-status imenus-actions))))
  290. (and fun (funcall fun buffers input))))
  291. ((consp input)
  292. (imenus-goto input)))))
  293. (defun imenus-files (files &optional rescan prompt initial-input)
  294. "Perform `imenus' on FILES."
  295. (imenus-buffers (mapcar #'find-file-noselect files)
  296. rescan prompt initial-input))
  297. ;;;###autoload
  298. (defun imenus (buffers)
  299. "Prompt for a place from a list of BUFFERS and jump to it.
  300. Interactively, use the current buffer. With a prefix argument,
  301. prompt for multiple buffers.
  302. In a minibuffer prompt you may use the following commands:
  303. \\{imenus-minibuffer-map}"
  304. (interactive
  305. (list (if current-prefix-arg
  306. (multi-isearch-read-buffers)
  307. (list (current-buffer)))))
  308. (imenus-buffers buffers))
  309. ;;;###autoload
  310. (defun imenus-mode-buffers (mode)
  311. "Perform `imenus' on all buffers with MODE.
  312. Interactively, use the major mode of the current buffer."
  313. (interactive (list major-mode))
  314. (let ((buffers (cl-remove-if-not
  315. (lambda (buf)
  316. (eq (buffer-local-value 'major-mode buf)
  317. mode))
  318. (buffer-list))))
  319. (imenus-buffers buffers)))
  320. ;;; Isearch
  321. (defvar imenus-isearch-string nil
  322. "String to be used as a default isearch string.")
  323. (defun imenus-isearch (buffers &optional string)
  324. "Start Isearch on a list of BUFFERS.
  325. Use STRING as an initial string for searching."
  326. (let ((imenus-isearch-string string))
  327. (when (and string
  328. (not (string= string "")))
  329. (if (cdr buffers)
  330. (multi-isearch-buffers buffers)
  331. (isearch-mode t))
  332. (isearch-search)
  333. (isearch-push-state)
  334. (isearch-update))))
  335. (defun imenus-isearch-setup ()
  336. "Set up isearch for searching `imenus-isearch-string'.
  337. Intended to be added to `isearch-mode-hook'."
  338. (when imenus-isearch-string
  339. (setq isearch-string imenus-isearch-string
  340. isearch-message imenus-isearch-string)
  341. (add-hook 'isearch-mode-end-hook 'imenus-isearch-end)))
  342. (defun imenus-isearch-end ()
  343. "Clean up after terminating imenus isearch."
  344. (setq imenus-isearch-string nil)
  345. (remove-hook 'isearch-mode-end-hook 'imenus-isearch-end))
  346. (add-hook 'isearch-mode-hook 'imenus-isearch-setup)
  347. (provide 'imenus)
  348. ;;; imenus.el ends here