emacs/org/elpa/helm-20220423.1712/helm-tags.el

349 lines
13 KiB
EmacsLisp
Raw Normal View History

2022-04-25 19:53:05 +00:00
;;; helm-tags.el --- Helm for Etags. -*- lexical-binding: t -*-
;; Copyright (C) 2012 ~ 2021 Thierry Volpiatto <thierry.volpiatto@gmail.com>
;; 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 <http://www.gnu.org/licenses/>.
;;; Code:
(require 'cl-lib)
(require 'helm)
(require 'helm-help)
(require 'helm-utils)
(require 'helm-grep)
(defvar helm-etags-fuzzy-match)
(declare-function ring-insert "ring")
(defgroup helm-tags nil
"Tags related Applications and libraries for Helm."
:group 'helm)
(defcustom helm-etags-tag-file-name "TAGS"
"Etags tag file name."
:type 'string
:group 'helm-tags)
(defcustom helm-etags-tag-file-search-limit 10
"The limit level of directory to search tag file.
Don't search tag file deeply if outside this value."
:type 'number
:group 'helm-tags)
(defcustom helm-etags-match-part-only 'tag
"Allow choosing the tag part of CANDIDATE in `helm-source-etags-select'.
A tag looks like this:
filename: \(defun foo
You can choose matching against the tag part (i.e \"(defun foo\"),
or against the whole candidate (i.e \"(filename:5:(defun foo\")."
:type '(choice
(const :tag "Match only tag" tag)
(const :tag "Match all file+tag" all))
:group 'helm-tags)
(defcustom helm-etags-execute-action-at-once-if-one t
"Whether to jump straight to the selected tag if there's only
one match."
:type 'boolean
:group 'helm-tags)
(defgroup helm-tags-faces nil
"Customize the appearance of helm-tags faces."
:prefix "helm-"
:group 'helm-tags
:group 'helm-faces)
(defface helm-etags-file
`((t ,@(and (>= emacs-major-version 27) '(:extend t))
:foreground "Lightgoldenrod4"
:underline t))
"Face used to highlight etags filenames."
:group 'helm-tags-faces)
;;; Etags
;;
;;
(defun helm-etags-run-switch-other-window ()
"Run switch to other window action from `helm-source-etags-select'."
(interactive)
(with-helm-alive-p
(helm-exit-and-execute-action
(lambda (c)
(helm-etags-action-goto 'find-file-other-window c)))))
(put 'helm-etags-run-switch-other-window 'helm-only t)
(defun helm-etags-run-switch-other-frame ()
"Run switch to other frame action from `helm-source-etags-select'."
(interactive)
(with-helm-alive-p
(helm-exit-and-execute-action
(lambda (c)
(helm-etags-action-goto 'find-file-other-frame c)))))
(put 'helm-etags-run-switch-other-frame 'helm-only t)
(defvar helm-etags-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map helm-map)
(define-key map (kbd "M-<down>") 'helm-goto-next-file)
(define-key map (kbd "M-<up>") 'helm-goto-precedent-file)
(define-key map (kbd "C-c o") 'helm-etags-run-switch-other-window)
(define-key map (kbd "C-c C-o") 'helm-etags-run-switch-other-frame)
map)
"Keymap used in Etags.")
(defvar helm-etags-mtime-alist nil
"Store the last modification time of etags files here.")
(defvar helm-etags-cache (make-hash-table :test 'equal)
"Cache content of etags files used here for faster access.")
(defun helm-etags-get-tag-file (&optional directory)
"Return the path of etags file if found in DIRECTORY.
Look recursively in parents directorys for a
`helm-etags-tag-file-name' file."
;; Get tag file from `default-directory' or upper directory.
(let ((current-dir (helm-etags-find-tag-file-directory
(or directory default-directory))))
;; Return nil if not find tag file.
(when current-dir
(expand-file-name helm-etags-tag-file-name current-dir))))
(defun helm-etags-all-tag-files ()
"Find Etags files.
Return files from the following sources:
1) An automatically located file in the parent directories,
by `helm-etags-get-tag-file'.
2) `tags-file-name', which is commonly set by `find-tag' command.
3) `tags-table-list' which is commonly set by `visit-tags-table' command."
(helm-fast-remove-dups
(delq nil
(append (list (helm-etags-get-tag-file)
tags-file-name)
tags-table-list))
:test 'equal))
(defun helm-etags-find-tag-file-directory (current-dir)
"Try to find the directory containing tag file.
If not found in CURRENT-DIR search in upper directory."
(let ((file-exists? (lambda (dir)
(let ((tag-path (expand-file-name
helm-etags-tag-file-name dir)))
(and (stringp tag-path)
(file-regular-p tag-path)
(file-readable-p tag-path))))))
(cl-loop with count = 0
until (funcall file-exists? current-dir)
;; Return nil if outside the value of
;; `helm-etags-tag-file-search-limit'.
if (= count helm-etags-tag-file-search-limit)
do (cl-return nil)
;; Or search upper directories.
else
do (cl-incf count)
(setq current-dir (expand-file-name (concat current-dir "../")))
finally return current-dir)))
(defun helm-etags-get-header-name (_x)
"Create header name for this helm etags session."
(concat "Etags in "
(with-helm-current-buffer
(helm-etags-get-tag-file))))
(defun helm-etags-create-buffer (file)
"Create the `helm-buffer' based on contents of etags tag FILE."
(let* (max
(split (with-temp-buffer
(insert-file-contents file)
(prog1
(split-string (buffer-string) "\n" 'omit-nulls)
(setq max (line-number-at-pos (point-max))))))
(progress-reporter (make-progress-reporter "Loading tag file..." 0 max)))
(cl-loop
with fname
with cand
for i in split for count from 0
for elm = (unless (string-match "^\x0c" i) ;; "^L"
(helm-aif (string-match "\177" i) ;; "^?"
(substring i 0 it)
i))
for linum = (when (string-match "[0-9]+,?[0-9]*$" i)
(car (split-string (match-string 0 i) ",")))
do (cond ((and elm (string-match "^\\([^,]+\\),[0-9]+$" elm))
(setq fname (propertize (match-string 1 elm)
'face 'helm-etags-file)))
(elm (setq cand (format "%s:%s:%s" fname linum elm)))
(t (setq cand nil)))
when cand do (progn
(insert (propertize (concat cand "\n") 'linum linum))
(progress-reporter-update progress-reporter count)))))
(defun helm-etags-init ()
"Feed `helm-buffer' using `helm-etags-cache' or tag file.
If there is no entry in cache, create one."
(let ((tagfiles (helm-etags-all-tag-files)))
(when tagfiles
(with-current-buffer (helm-candidate-buffer 'global)
(dolist (f tagfiles)
(helm-aif (gethash f helm-etags-cache)
;; An entry is present in cache, insert it.
(insert it)
;; No entry, create a new buffer using content of tag file (slower).
(helm-etags-create-buffer f)
;; Store content of buffer in cache.
(puthash f (buffer-string) helm-etags-cache)
;; Store or set the last modification of tag file.
(helm-aif (assoc f helm-etags-mtime-alist)
;; If an entry exists modify it.
(setcdr it (helm-etags-mtime f))
;; No entry create a new one.
(cl-pushnew (cons f (helm-etags-mtime f))
helm-etags-mtime-alist
:test 'equal))))))))
(defvar helm-source-etags-select nil
"Helm source for Etags.")
(defun helm-etags-build-source ()
(helm-build-in-buffer-source "Etags"
:header-name 'helm-etags-get-header-name
:init 'helm-etags-init
:get-line 'buffer-substring
:match-part (lambda (candidate)
;; Match only the tag part of CANDIDATE
;; and not the filename.
(cl-case helm-etags-match-part-only
(tag (cl-caddr (helm-grep-split-line candidate)))
(t candidate)))
:fuzzy-match helm-etags-fuzzy-match
:help-message 'helm-etags-help-message
:keymap helm-etags-map
:action '(("Go to tag" . (lambda (c)
(helm-etags-action-goto 'find-file c)))
("Go to tag in other window" . (lambda (c)
(helm-etags-action-goto
'find-file-other-window
c)))
("Go to tag in other frame" . (lambda (c)
(helm-etags-action-goto
'find-file-other-frame
c))))
:group 'helm-tags
:persistent-help "Go to line"
:persistent-action (lambda (candidate)
(helm-etags-action-goto 'find-file candidate)
(helm-highlight-current-line))))
(defcustom helm-etags-fuzzy-match nil
"Use fuzzy matching in `helm-etags-select'."
:group 'helm-tags
:type 'boolean
:set (lambda (var val)
(set var val)
(setq helm-source-etags-select
(helm-etags-build-source))))
(defvar find-tag-marker-ring)
(defsubst helm-etags--file-from-tag (fname)
(cl-loop for ext in
(cons "" (remove "" tags-compression-info-list))
for file = (concat fname ext)
when (file-exists-p file)
return file))
(defun helm-etags-action-goto (switcher candidate)
"Helm default action to jump to an etags entry in other window."
(require 'etags)
(deactivate-mark t)
(helm-log-run-hook 'helm-goto-line-before-hook)
(let* ((split (helm-grep-split-line candidate))
(fname (cl-loop for tagf being the hash-keys of helm-etags-cache
for f = (expand-file-name
(car split) (file-name-directory tagf))
;; Try to find an existing file, possibly compressed.
when (helm-etags--file-from-tag f)
return it))
(elm (cl-caddr split))
(linum (string-to-number (cadr split))))
(if (null fname)
(error "file %s not found" fname)
(ring-insert find-tag-marker-ring (point-marker))
(funcall switcher fname)
(helm-goto-line linum t)
(when (search-forward elm nil t)
(goto-char (match-beginning 0))))))
(defun helm-etags-mtime (file)
"Last modification time of etags tag FILE."
(cadr (nth 5 (file-attributes file))))
(defun helm-etags-file-modified-p (file)
"Check if tag FILE have been modified in this session.
If FILE is nil return nil."
(let ((last-modif (and file
(assoc-default file helm-etags-mtime-alist))))
(and last-modif
(/= last-modif (helm-etags-mtime file)))))
;;;###autoload
(defun helm-etags-select (reinit)
"Preconfigured helm for etags.
If called with a prefix argument REINIT
or if any of the tag files have been modified, reinitialize cache.
This function aggregates three sources of tag files:
1) An automatically located file in the parent directories,
by `helm-etags-get-tag-file'.
2) `tags-file-name', which is commonly set by `find-tag' command.
3) `tags-table-list' which is commonly set by `visit-tags-table' command."
(interactive "P")
(let ((tag-files (helm-etags-all-tag-files))
(helm-execute-action-at-once-if-one
helm-etags-execute-action-at-once-if-one)
(str (if (region-active-p)
(buffer-substring-no-properties
(region-beginning) (region-end))
(thing-at-point 'symbol))))
(if (cl-notany 'file-exists-p tag-files)
(message "Error: No tag file found.\
Create with etags shell command, or visit with `find-tag' or `visit-tags-table'.")
(cl-loop for k being the hash-keys of helm-etags-cache
unless (member k tag-files)
do (remhash k helm-etags-cache))
(mapc (lambda (f)
(when (or (equal reinit '(4))
(and helm-etags-mtime-alist
(helm-etags-file-modified-p f)))
(remhash f helm-etags-cache)))
tag-files)
(unless helm-source-etags-select
(setq helm-source-etags-select
(helm-etags-build-source)))
(helm :sources 'helm-source-etags-select
:keymap helm-etags-map
:default (and (stringp str)
(if (or helm-etags-fuzzy-match
(and (eq major-mode 'haskell-mode)
(string-match "[']\\'" str)))
str
(list (concat "\\_<" str "\\_>") str)))
:buffer "*helm etags*"))))
(provide 'helm-tags)
;;; helm-tags.el ends here