;;; helm-imenu.el --- Helm interface for Imenu -*- lexical-binding: t -*- ;; Copyright (C) 2012 ~ 2021 Thierry Volpiatto ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Code: (require 'cl-lib) (require 'helm) (require 'helm-lib) (require 'imenu) (require 'helm-utils) (require 'helm-help) (declare-function which-function "which-func") (defgroup helm-imenu nil "Imenu related libraries and applications for Helm." :group 'helm) (defcustom helm-imenu-delimiter " / " "Delimit types of candidates and their value in `helm-buffer'." :group 'helm-imenu :type 'string) (defcustom helm-imenu-execute-action-at-once-if-one #'helm-imenu--execute-action-at-once-p "Goto the candidate when only one is remaining." :group 'helm-imenu :type 'function) (defcustom helm-imenu-all-buffer-assoc nil "Major mode association alist for `helm-imenu-in-all-buffers'. Allow `helm-imenu-in-all-buffers' searching in these associated buffers even if they are not derived from each other. The alist is bidirectional, i.e. no need to add '((foo . bar) (bar . foo)), only '((foo . bar)) is needed." :type '(alist :key-type symbol :value-type symbol) :group 'helm-imenu) (defcustom helm-imenu-in-all-buffers-separate-sources t "Display imenu index of each buffer in its own source when non-nil. When nil all candidates are displayed in a single source. NOTE: Each source will have as name \"Imenu \". `helm-source-imenu-all' will not be set, however it will continue to be used as a flag for using default as input. If you do not want this behavior, remove it from `helm-sources-using-default-as-input' even if not using a single source to display imenu in all buffers." :type 'boolean :group 'helm-imenu) (defcustom helm-imenu-type-faces '(("^Variables$" . font-lock-variable-name-face) ("^\\(Function\\|Functions\\|Defuns\\)$" . font-lock-function-name-face) ("^\\(Types\\|Provides\\|Requires\\|Classes\\|Class\\|Includes\\|Imports\\|Misc\\|Code\\)$" . font-lock-type-face)) "Faces for showing type in helm-imenu. This is a list of cons cells. The cdr of each cell is a face to be used, and it can also just be like \\='(:foreground \"yellow\"). Each car is a regexp match pattern of the imenu type string." :group 'helm-faces :type '(repeat (cons (regexp :tag "Imenu type regexp pattern") (sexp :tag "Face")))) (defcustom helm-imenu-extra-modes nil "Extra modes where `helm-imenu-in-all-buffers' should look into." :group 'helm-imenu :type '(repeat symbol)) ;;; keymap (defvar helm-imenu-map (let ((map (make-sparse-keymap))) (set-keymap-parent map helm-map) (define-key map (kbd "M-") 'helm-imenu-next-section) (define-key map (kbd "M-") 'helm-imenu-previous-section) map)) (defcustom helm-imenu-lynx-style-map nil "Use Arrow keys to jump to occurences." :group 'helm-imenu :type 'boolean :set (lambda (var val) (set var val) (if val (progn (define-key helm-imenu-map (kbd "") 'helm-execute-persistent-action) (define-key helm-imenu-map (kbd "") 'helm-maybe-exit-minibuffer)) (define-key helm-imenu-map (kbd "") nil) (define-key helm-imenu-map (kbd "") nil)))) (defun helm-imenu-next-or-previous-section (n) (with-helm-window (let* ((fn (lambda () (car (split-string (buffer-substring (point-at-bol) (point-at-eol)) helm-imenu-delimiter)))) (curtype (funcall fn)) (stop-fn (if (> n 0) #'helm-end-of-source-p #'helm-beginning-of-source-p))) (while (and (not (funcall stop-fn)) (string= curtype (funcall fn))) (forward-line n)) (helm-mark-current-line) (helm-follow-execute-persistent-action-maybe)))) (defun helm-imenu-next-section () (interactive) (helm-imenu-next-or-previous-section 1)) (defun helm-imenu-previous-section () (interactive) (helm-imenu-next-or-previous-section -1)) ;;; Internals (defvar helm-cached-imenu-alist nil) (make-variable-buffer-local 'helm-cached-imenu-alist) (defvar helm-cached-imenu-candidates nil) (make-variable-buffer-local 'helm-cached-imenu-candidates) (defvar helm-cached-imenu-tick nil) (make-variable-buffer-local 'helm-cached-imenu-tick) (defvar helm-imenu--in-all-buffers-cache nil) (defvar helm-source-imenu nil "See (info \"(emacs)Imenu\")") (defvar helm-source-imenu-all nil) (defclass helm-imenu-source (helm-source-sync) ((candidates :initform 'helm-imenu-candidates) (candidate-transformer :initform 'helm-imenu-transformer) (persistent-action :initform 'helm-imenu-persistent-action) (persistent-help :initform "Show this entry") (nomark :initform t) (keymap :initform 'helm-imenu-map) (help-message :initform 'helm-imenu-help-message) (action :initform 'helm-imenu-action) (find-file-target :initform #'helm-imenu-quit-and-find-file-fn) (group :initform 'helm-imenu))) (defcustom helm-imenu-fuzzy-match nil "Enable fuzzy matching in `helm-source-imenu'." :group 'helm-imenu :type 'boolean :set (lambda (var val) (set var val) (setq helm-source-imenu (helm-make-source "Imenu" 'helm-imenu-source :fuzzy-match helm-imenu-fuzzy-match)))) (defun helm-imenu--maybe-switch-to-buffer (candidate) (let ((cand (cdr candidate))) (helm-aif (and (markerp cand) (marker-buffer cand)) (switch-to-buffer it)))) (defun helm-imenu--execute-action-at-once-p () (let ((cur (helm-get-selection)) (mb (with-helm-current-buffer (save-excursion (goto-char (point-at-bol)) (point-marker))))) ;; Happen when cursor is on the line where a definition is. This ;; prevent jumping to the definition where we are already, instead ;; display helm with all definitions and preselection to the place ;; we already are. (if (equal (cdr cur) mb) (prog1 nil (helm-set-pattern "") (helm-force-update)) t))) (defun helm-imenu-quit-and-find-file-fn (source) (let ((sel (helm-get-selection nil nil source))) (when (and (consp sel) (markerp (cdr sel))) (buffer-file-name (marker-buffer (cdr sel)))))) (defun helm-imenu-action (candidate) "Default action for `helm-source-imenu'." (helm-log-run-hook 'helm-goto-line-before-hook) (helm-imenu--maybe-switch-to-buffer candidate) (imenu candidate) ;; If semantic is supported in this buffer ;; imenu used `semantic-imenu-goto-function' ;; and position have been highlighted, ;; no need to highlight again. (unless (eq imenu-default-goto-function 'semantic-imenu-goto-function) (helm-highlight-current-line))) (defun helm-imenu-persistent-action (candidate) "Default persistent action for `helm-source-imenu'." (helm-imenu--maybe-switch-to-buffer candidate) (imenu candidate) (helm-highlight-current-line)) (defun helm-imenu-candidates (&optional buffer) (with-current-buffer (or buffer helm-current-buffer) (let ((tick (buffer-modified-tick))) (if (eq helm-cached-imenu-tick tick) helm-cached-imenu-candidates (setq imenu--index-alist nil) (prog1 (setq helm-cached-imenu-candidates (let ((index (imenu--make-index-alist t))) (helm-imenu--candidates-1 (delete (assoc "*Rescan*" index) index)))) (setq helm-cached-imenu-tick tick)))))) (defun helm-imenu-candidates-in-all-buffers (&optional build-sources) (let* ((lst (buffer-list)) (progress-reporter (make-progress-reporter "Imenu indexing buffers..." 1 (length lst)))) (prog1 (cl-loop with cur-buf = (if build-sources (current-buffer) helm-current-buffer) for b in lst for count from 1 when (with-current-buffer b (and (or (member major-mode helm-imenu-extra-modes) (derived-mode-p 'prog-mode)) (helm-same-major-mode-p cur-buf helm-imenu-all-buffer-assoc))) if build-sources collect (helm-make-source (format "Imenu in %s" (buffer-name b)) 'helm-imenu-source :candidates (with-current-buffer b (helm-imenu-candidates b)) :fuzzy-match helm-imenu-fuzzy-match) else append (with-current-buffer b (helm-imenu-candidates b)) do (progress-reporter-update progress-reporter count)) (progress-reporter-done progress-reporter)))) (defun helm-imenu--candidates-1 (alist) (cl-loop for elm in alist nconc (cond ((imenu--subalist-p elm) (helm-imenu--candidates-1 (cl-loop for (e . v) in (cdr elm) collect (cons (propertize e 'helm-imenu-type (car elm)) ;; If value is an integer, convert it ;; to a marker, otherwise it is a cons cell ;; and it will be converted on next recursions. ;; (Bug#1060) [1]. (if (integerp v) (copy-marker v) v))))) ((listp (cdr elm)) (and elm (list elm))) (t ;; bug in imenu, should not be needed. (and (cdr elm) ;; Semantic uses overlays whereas imenu uses ;; markers (Bug#1706). (setcdr elm (pcase (cdr elm) ; Same as [1]. ((and ov (pred overlayp)) (copy-overlay ov)) ((and mk (or (pred markerp) (pred integerp))) (copy-marker mk)))) (list elm)))))) (defun helm-imenu--get-prop (item) ;; property value of ITEM can have itself ;; a property value which have itself a property value ;; ...and so on; Return a list of all these ;; properties values starting at ITEM. (let* ((prop (get-text-property 0 'helm-imenu-type item)) (lst (list prop item))) (when prop (while prop (setq prop (get-text-property 0 'helm-imenu-type prop)) (and prop (push prop lst))) lst))) (defun helm-imenu-transformer (candidates) (cl-loop for (k . v) in candidates ;; (k . v) == (symbol-name . marker) for bufname = (buffer-name (pcase v ((pred overlayp) (overlay-buffer v)) ((or (pred markerp) (pred integerp)) (marker-buffer v)))) for types = (or (helm-imenu--get-prop k) (list (if (with-current-buffer bufname (derived-mode-p 'prog-mode)) "Function" "Top level") k)) for disp1 = (mapconcat (lambda (x) (propertize x 'face (cl-loop for (p . f) in helm-imenu-type-faces when (string-match p x) return f finally return 'default))) types helm-imenu-delimiter) for disp = (propertize disp1 'help-echo bufname 'types types) collect (cons disp (cons k v)))) ;;;###autoload (defun helm-imenu () "Preconfigured `helm' for `imenu'." (interactive) (require 'which-func) (unless helm-source-imenu (setq helm-source-imenu (helm-make-source "Imenu" 'helm-imenu-source :fuzzy-match helm-imenu-fuzzy-match))) (let* ((imenu-auto-rescan t) (helm-highlight-matches-around-point-max-lines 'never) (str (thing-at-point 'symbol)) (init-reg (and str (concat "\\_<" (regexp-quote str) "\\_>"))) (helm-execute-action-at-once-if-one helm-imenu-execute-action-at-once-if-one)) (helm :sources 'helm-source-imenu :default (and str (list init-reg str)) :preselect (helm-aif (which-function) (concat "\\_<" (regexp-quote it) "\\_>") init-reg) :buffer "*helm imenu*"))) ;;;###autoload (defun helm-imenu-in-all-buffers () "Preconfigured `helm' for fetching imenu entries in all buffers with similar mode as current. A mode is similar as current if it is the same, it is derived i.e. `derived-mode-p' or it have an association in `helm-imenu-all-buffer-assoc'." (interactive) (require 'which-func) (unless helm-imenu-in-all-buffers-separate-sources (unless helm-source-imenu-all (setq helm-source-imenu-all (helm-make-source "Imenu in all buffers" 'helm-imenu-source :init (lambda () ;; Use a cache to avoid repeatedly sending ;; progress-reporter message when updating ;; (Bug#1704). (setq helm-imenu--in-all-buffers-cache (helm-imenu-candidates-in-all-buffers))) :candidates 'helm-imenu--in-all-buffers-cache :fuzzy-match helm-imenu-fuzzy-match)))) (let* ((imenu-auto-rescan t) (helm-highlight-matches-around-point-max-lines 'never) (str (thing-at-point 'symbol)) (init-reg (and str (concat "\\_<" (regexp-quote str) "\\_>"))) (helm-execute-action-at-once-if-one helm-imenu-execute-action-at-once-if-one) (helm-maybe-use-default-as-input (not (null (memq 'helm-source-imenu-all helm-sources-using-default-as-input)))) (sources (if helm-imenu-in-all-buffers-separate-sources (helm-imenu-candidates-in-all-buffers 'build-sources) '(helm-source-imenu-all)))) (helm :sources sources :default (and str (list init-reg str)) :preselect (helm-aif (which-function) (concat "\\_<" (regexp-quote it) "\\_>") init-reg) :buffer "*helm imenu all*"))) (provide 'helm-imenu) ;;; helm-imenu.el ends here