;; Copyright (C) 2013-2014 Free Software Foundation, Inc.
;; Author: Leo Liu <sdl.web@gmail.com>
-;; Version: 0.8.0
+;; Version: 0.8.1
;; Keywords: tools, convenience
;; Created: 2013-01-29
;; URL: https://github.com/leoliu/ggtags
;;
;; Usage:
;;
-;; Type `M-x ggtags-mode' to enable the minor mode, or as usual enable
-;; it in your desired major mode hooks. When the mode is on the symbol
-;; at point is underlined if it is a valid (definition) tag.
+;; `ggtags' is similar to the standard `etags' package. These keys
+;; `M-.', `M-,', `M-*' and `C-M-.' should work as expected in
+;; `ggtags-mode'. See the README in https://github.com/leoliu/ggtags
+;; for more details.
;;
-;; `M-.' finds definition or references according to the context at
-;; point, i.e. if point is at a definition tag find references and
-;; vice versa. `M-]' finds references.
-;;
-;; If multiple matches are found, navigation mode is entered, the
-;; mode-line lighter changed, and a navigation menu-bar entry
-;; presented. In this mode, `M-n' and `M-p' moves to next and previous
-;; match, `M-}' and `M-{' to next and previous file respectively.
-;; `M-o' toggles between full and abbreviated displays of file names
-;; in the auxiliary popup window. When you locate the right match,
-;; press RET to finish which hides the auxiliary window and exits
-;; navigation mode. You can continue the search using `M-,'. To abort
-;; the search press `M-*'.
-;;
-;; Normally after a few searches a dozen buffers are created visiting
-;; files tracked by GNU Global. `C-c M-k' helps clean them up.
-;;
-;; Check the menu-bar entry `Ggtags' for other useful commands.
+;; All commands are available from the `Ggtags' menu in `ggtags-mode'.
;;; Code:
(require 'url-parse))
(require 'cl-lib)
+(require 'ewoc)
(require 'compile)
(require 'etags)
(require 'tabulated-list) ;preloaded since 24.3
(defmacro defvar-local (var val &optional docstring)
(declare (debug defvar) (doc-string 3))
(list 'progn (list 'defvar var val docstring)
- (list 'make-variable-buffer-local (list 'quote var))))))
+ (list 'make-variable-buffer-local (list 'quote var)))))
+
+ (defmacro ignore-errors-unless-debug (&rest body)
+ "Ignore all errors while executing BODY unless debug is on."
+ (declare (debug t) (indent 0))
+ `(condition-case-unless-debug nil (progn ,@body) (error nil))))
(eval-and-compile
(or (fboundp 'user-error) ;24.3
number)
:group 'ggtags)
+(defcustom ggtags-global-always-update nil
+ "If non-nil always update tags for current file on save."
+ :safe 'booleanp
+ :type 'boolean
+ :group 'ggtags)
+
(defcustom ggtags-include-pattern
'("^\\s-*#\\(?:include\\|import\\)\\s-*[\"<]\\(?:[./]*\\)?\\(.*?\\)[\">]" . 1)
"Pattern used to detect #include files.
:safe 'stringp
:group 'ggtags)
-(defcustom ggtags-global-always-update nil
- "If non-nil always update tags for current file on save."
- :safe 'booleanp
- :type 'boolean
- :group 'ggtags)
-
(defcustom ggtags-use-project-gtagsconf t
- "Non-nil to automatically use GTAGSCONF file at project root.
+ "Non-nil to use GTAGSCONF file found at project root.
File .globalrc and gtags.conf are checked in order."
:safe 'booleanp
:type 'boolean
:type '(repeat string)
:group 'ggtags)
-(defcustom ggtags-auto-jump-to-first-match t
- "Non-nil to automatically jump to the first match."
- :type 'boolean
+(defcustom ggtags-auto-jump-to-match 'first
+ "Strategy on how to jump to match: nil, first or history.
+
+ nil: never automatically jump to any match;
+ first: jump to the first match;
+history: jump to the match stored in search history."
+ :type '(choice (const :tag "First match" first)
+ (const :tag "Search History" history)
+ (const :tag "Never" nil))
:group 'ggtags)
(defcustom ggtags-global-window-height 8 ; ggtags-global-mode
- "Number of lines for the 'global' popup window.
+ "Number of lines for the *ggtags-global* popup window.
If nil, use Emacs default."
:type '(choice (const :tag "Default" nil) integer)
:group 'ggtags)
-(defcustom ggtags-global-abbreviate-filename 35
+(defcustom ggtags-global-abbreviate-filename 40
"Non-nil to display file names abbreviated e.g. \"/u/b/env\".
If an integer abbreviate only names longer than that number."
:type '(choice (const :tag "No" nil)
:group 'ggtags)
(defcustom ggtags-global-output-format 'grep
- "The output format for the 'global' command."
+ "Global output format: path, ctags, ctags-x, grep or cscope."
:type '(choice (const path)
(const ctags)
(const ctags-x)
(const cscope))
:group 'ggtags)
+(defcustom ggtags-global-use-color t
+ "Non-nil to use color in output if supported by Global."
+ :type 'boolean
+ :safe 'booleanp
+ :group 'ggtags)
+
(defcustom ggtags-global-ignore-case nil
- "Non-nil if Global should ignore case."
+ "Non-nil if Global should ignore case in the search pattern."
:safe 'booleanp
:type 'boolean
:group 'ggtags)
(defcustom ggtags-global-treat-text nil
- "Non-nil if Global should include matches from text files."
+ "Non-nil if Global should include matches from text files.
+This affects `ggtags-find-file' and `ggtags-grep'."
:safe 'booleanp
:type 'boolean
:group 'ggtags)
:type 'number
:group 'ggtags)
+(defcustom ggtags-global-history-length history-length
+ "Maximum number of items to keep in `ggtags-global-search-history'."
+ :type 'integer
+ :group 'ggtags)
+
+(defcustom ggtags-enable-navigation-keys t
+ "If non-nil key bindings in `ggtags-navigation-map' are enabled."
+ :safe 'booleanp
+ :type 'boolean
+ :group 'ggtags)
+
(defcustom ggtags-find-tag-hook nil
"Hook run immediately after finding a tag."
:options '(recenter reposition-window)
(defcustom ggtags-show-definition-function #'ggtags-show-definition-default
"Function called by `ggtags-show-definition' to show definition.
-It is passed a list of definnition candidates of the form:
+It is passed a list of definition candidates of the form:
(TEXT NAME FILE LINE)
:type 'key-sequence
:group 'ggtags)
-(defcustom ggtags-completing-read-function completing-read-function
- "Ggtags specific `completing-read-function' (which see)."
- :type 'function
- :group 'ggtags)
-
(defcustom ggtags-highlight-tag-delay 0.25
"Time in seconds before highlighting tag at point."
:set (lambda (sym value)
(defcustom ggtags-bounds-of-tag-function (lambda ()
(bounds-of-thing-at-point 'symbol))
- "Function to get the start and end locations of the tag at point."
+ "Function to get the start and end positions of the tag at point."
:type 'function
:group 'ggtags)
-(defvar ggtags-bug-url "https://github.com/leoliu/ggtags/issues")
+;; Used by ggtags-global-mode
+(defvar ggtags-global-error "match"
+ "Stem of message to print when no matches are found.")
+
+(defconst ggtags-bug-url "https://github.com/leoliu/ggtags/issues")
(defvar ggtags-global-last-buffer nil)
(defvar ggtags-highlight-tag-timer nil)
-;; Used by ggtags-global-mode
-(defvar ggtags-global-error "match"
- "Stem of message to print when no matches are found.")
-
(defmacro ggtags-ensure-global-buffer (&rest body)
(declare (indent 0))
`(progn
(defun ggtags-list-of-string-p (xs)
"Return non-nil if XS is a list of strings."
- (if (null xs)
- t
- (and (stringp (car xs))
- (ggtags-list-of-string-p (cdr xs)))))
+ (cl-every #'stringp xs))
+
+(defun ggtags-forward-to-line (line)
+ "Move to line number LINE in current buffer."
+ (cl-check-type line (integer 1))
+ (save-restriction
+ (widen)
+ (goto-char (point-min))
+ (forward-line (1- line))))
(defun ggtags-program-path (name)
(if ggtags-executable-directory
(defvar-local ggtags-project-root 'unset
"Internal variable for project root directory.")
+(defun ggtags-clear-project-root ()
+ (kill-local-variable 'ggtags-project-root))
+
;;;###autoload
(defun ggtags-find-project ()
+ ;; See https://github.com/leoliu/ggtags/issues/42
+ ;;
+ ;; It is unsafe to cache `ggtags-project-root' in non-file buffers.
+ ;; But we keep the cache for a command's duration so that multiple
+ ;; calls of `ggtags-find-project' has no performance impact.
+ (unless buffer-file-name
+ (add-hook 'pre-command-hook #'ggtags-clear-project-root nil t))
(let ((project (gethash ggtags-project-root ggtags-projects)))
(if (ggtags-project-p project)
(if (ggtags-project-expired-p project)
(ggtags-find-project))
project)
(setq ggtags-project-root
- (or (ignore-errors (file-name-as-directory
- (concat (file-remote-p default-directory)
- ;; Resolves symbolic links
- (ggtags-process-string "global" "-pr"))))
+ (or (ignore-errors-unless-debug
+ (file-name-as-directory
+ (concat (file-remote-p default-directory)
+ ;; Resolves symbolic links
+ (ggtags-process-string "global" "-pr"))))
;; 'global -pr' resolves symlinks before checking the
;; GTAGS file which could cause issues such as
;; https://github.com/leoliu/ggtags/issues/22, so
(defun ggtags-create-tags (root)
"Create tag files (e.g. GTAGS) in directory ROOT.
+If file .globalrc or gtags.conf exists in ROOT, it will be used
+as configuration file per `ggtags-use-project-gtagsconf'.
+
If file gtags.files exists in ROOT, it should be a list of source
files to index, which can be used to speed gtags up in large
source trees. See Info node `(global)gtags' for details."
(defun ggtags-update-tags (&optional force)
"Update GNU Global tag database.
-Do nothing if GTAGS exceeds the oversize limit unless FORCE is
-non-nil."
+Do nothing if GTAGS exceeds the oversize limit unless FORCE."
(interactive (progn
(ggtags-check-project)
;; Mark project info expired.
(unless (equal cache-key (car ggtags-completion-cache))
(setq ggtags-completion-cache
(cons cache-key
- (condition-case-unless-debug nil
- ;; May throw global: only name char is
- ;; allowed with -c option.
- (ggtags-with-current-project
- (split-string
- (apply #'ggtags-process-string
- "global"
- (append (and completion-ignore-case '("--ignore-case"))
- ;; Note -c alone returns only definitions
- (list (concat "-c" ggtags-completion-flag) prefix)))
- "\n" t))
- (error nil))))))
+ (ignore-errors-unless-debug
+ ;; May throw global: only name char is allowed
+ ;; with -c option.
+ (ggtags-with-current-project
+ (split-string
+ (apply #'ggtags-process-string
+ "global"
+ (append (and completion-ignore-case '("--ignore-case"))
+ ;; Note -c alone returns only definitions
+ (list (concat "-c" ggtags-completion-flag) prefix)))
+ "\n" t)))))))
(cdr ggtags-completion-cache))))
(defun ggtags-completion-at-point ()
(defun ggtags-read-tag (&optional type confirm prompt require-match default)
(ggtags-ensure-project)
(let ((default (or default (ggtags-tag-at-point)))
- (completing-read-function ggtags-completing-read-function)
(prompt (or prompt (capitalize (symbol-name (or type 'tag)))))
(ggtags-completion-flag (pcase type
(`(or nil definition) "T")
(completing-read
(format (if default "%s (default %s): " "%s: ") prompt default)
ggtags-completion-table nil require-match nil nil default))
- ((not default)
- (user-error "No tag at point"))
- (t (substring-no-properties default))))))
+ (default (substring-no-properties default))
+ (t (ggtags-read-tag type t prompt require-match default))))))
(defun ggtags-global-build-command (cmd &rest args)
;; CMD can be definition, reference, symbol, grep, idutils
"-v"
(format "--result=%s" ggtags-global-output-format)
(and ggtags-global-ignore-case "--ignore-case")
- (and (ggtags-find-project)
+ (and ggtags-global-use-color
+ (ggtags-find-project)
(ggtags-project-has-color (ggtags-find-project))
"--color=always")
(and (ggtags-find-project)
;; takes three values: nil, t and a marker
(defvar ggtags-global-start-marker nil)
-
(defvar ggtags-global-exit-status 0)
(defvar ggtags-global-match-count 0)
-
(defvar ggtags-tag-ring-index nil)
+(defvar ggtags-global-search-history nil)
+
+(defvar ggtags-auto-jump-to-match-target nil)
(defun ggtags-global-save-start-marker ()
(when (markerp ggtags-global-start-marker)
(split-window-preferred-function ggtags-split-window-function)
;; See http://debbugs.gnu.org/13594
(display-buffer-overriding-action
- (if (and ggtags-auto-jump-to-first-match
+ (if (and ggtags-auto-jump-to-match
;; Appeared in emacs 24.4.
(fboundp 'display-buffer-no-window))
(list #'display-buffer-no-window)
- display-buffer-overriding-action)))
+ display-buffer-overriding-action))
+ (env ggtags-process-environment))
(setq ggtags-global-start-marker (point-marker))
+ (setq ggtags-auto-jump-to-match-target
+ (nth 4 (assoc (ggtags-global-search-id command default-directory)
+ ggtags-global-search-history)))
(ggtags-navigation-mode +1)
(setq ggtags-global-exit-status 0
ggtags-global-match-count 0)
(ggtags-update-tags)
(ggtags-with-current-project
- (setq ggtags-global-last-buffer
- (compilation-start command 'ggtags-global-mode)))))
+ (with-current-buffer (compilation-start command 'ggtags-global-mode)
+ (setq-local ggtags-process-environment env)
+ (setq ggtags-global-last-buffer (current-buffer))))))
(defun ggtags-find-tag-continue ()
(interactive)
;;;###autoload
(defun ggtags-find-tag-dwim (name &optional what)
- "Find definitions or references of tag NAME by context.
+ "Find NAME by context.
If point is at a definition tag, find references, and vice versa.
-With a prefix arg always find definitions. If point is at a line
-that matches `ggtags-include-pattern', find the include file
-instead."
+If point is at a line that matches `ggtags-include-pattern', find
+the include file instead.
+
+When called interactively with a prefix arg, always find
+definition tags."
(interactive
(let ((include (and (not current-prefix-arg)
ggtags-include-pattern
(not buffer-file-name)
(and (ggtags-find-project)
(not (ggtags-project-has-refs (ggtags-find-project)))))
- (ggtags-find-tag 'definition name))
+ (ggtags-find-tag 'definition (shell-quote-argument name)))
(t (ggtags-find-tag
(format "--from-here=%d:%s"
(line-number-at-pos)
buffer-file-name)
(ggtags-current-project-root)
(locate-dominating-file buffer-file-name "GTAGS")))))
- name))))
+ (shell-quote-argument name)))))
(defun ggtags-find-reference (name)
(interactive (list (ggtags-read-tag 'reference current-prefix-arg)))
- (ggtags-find-tag 'reference name))
+ (ggtags-find-tag 'reference (shell-quote-argument name)))
(defun ggtags-find-other-symbol (name)
"Find tag NAME that is a reference without a definition."
(interactive (list (ggtags-read-tag 'symbol current-prefix-arg)))
- (ggtags-find-tag 'symbol name))
+ (ggtags-find-tag 'symbol (shell-quote-argument name)))
(defun ggtags-quote-pattern (pattern)
(prin1-to-string (substring-no-properties pattern)))
(ggtags-find-tag 'idutils "--" (ggtags-quote-pattern pattern)))
(defun ggtags-grep (pattern &optional invert-match)
- "Use `global --grep' to search for lines matching PATTERN.
+ "Grep for lines matching PATTERN.
Invert the match when called with a prefix arg \\[universal-argument]."
(interactive (list (ggtags-read-tag 'definition 'confirm
(if current-prefix-arg
;; NOTE: Coloured output in grep requested: http://goo.gl/Y9IcX
(defun ggtags-find-tag-regexp (regexp directory)
- "List tags matching REGEXP in DIRECTORY (default to project root)."
+ "List tags matching REGEXP in DIRECTORY (default to project root).
+When called interactively with a prefix, ask for the directory."
(interactive
(progn
(ggtags-check-project)
(ggtags-global-build-command nil nil "-l" "--" (ggtags-quote-pattern regexp))
(file-name-as-directory directory)))
+(defvar ggtags-navigation-mode)
+
(defun ggtags-query-replace (from to &optional delimited)
"Query replace FROM with TO on files in the Global buffer.
If not in navigation mode, do a grep on FROM first.
;; Note: in 24.4 query-replace-read-args returns a list of 4 elements.
(let ((args (query-replace-read-args "Query replace (regexp)" t t)))
(list (nth 0 args) (nth 1 args) (nth 2 args))))
- (unless (bound-and-true-p ggtags-navigation-mode)
- (let ((ggtags-auto-jump-to-first-match nil))
+ (unless ggtags-navigation-mode
+ (let ((ggtags-auto-jump-to-match nil))
(ggtags-grep from)))
(let ((file-form
'(let ((files))
(nreverse files))))
(tags-query-replace from to delimited file-form)))
+(defun ggtags-global-search-id (cmd directory)
+ (sha1 (concat directory (make-string 1 0) cmd)))
+
+(defun ggtags-global-current-search ()
+ ;; CMD DIR ENV LINE TEXT
+ (ggtags-ensure-global-buffer
+ (list (car compilation-arguments)
+ default-directory
+ ggtags-process-environment
+ (line-number-at-pos)
+ (buffer-substring-no-properties
+ (line-beginning-position) (line-end-position)))))
+
+(defun ggtags-global-rerun-search-1 (data)
+ (pcase data
+ (`(,cmd ,dir ,env ,line ,_text)
+ (with-current-buffer (let ((ggtags-auto-jump-to-match nil)
+ ;; Switch current project to DIR.
+ (default-directory dir)
+ (ggtags-project-root dir)
+ (ggtags-process-environment env))
+ (ggtags-global-start cmd dir))
+ (add-hook 'compilation-finish-functions
+ (lambda (buf _msg)
+ (with-current-buffer buf
+ (ggtags-forward-to-line line)
+ (compile-goto-error)))
+ nil t)))))
+
+(defvar-local ggtags-global-search-ewoc nil)
+(defvar ggtags-global-rerun-search-last nil)
+
+(defvar ggtags-global-rerun-search-map
+ (cl-labels
+ ((save ()
+ (setq ggtags-global-rerun-search-last
+ (ewoc-data (ewoc-locate ggtags-global-search-ewoc))))
+ (next (arg)
+ (interactive "p")
+ (ewoc-goto-next ggtags-global-search-ewoc arg)
+ (save))
+ (prev (arg)
+ (interactive "p")
+ (ewoc-goto-prev ggtags-global-search-ewoc arg)
+ (save))
+ (quit ()
+ (interactive)
+ (quit-windows-on (ewoc-buffer ggtags-global-search-ewoc) t))
+ (done ()
+ (interactive)
+ (let ((node (ewoc-locate ggtags-global-search-ewoc)))
+ (when node
+ (save)
+ (quit)
+ (ggtags-global-rerun-search-1 (cdr (ewoc-data node)))))))
+ (let ((m (make-sparse-keymap)))
+ (set-keymap-parent m special-mode-map)
+ (define-key m "p" #'prev)
+ (define-key m "\M-p" #'prev)
+ (define-key m "n" #'next)
+ (define-key m "\M-n" #'next)
+ (define-key m "r" #'ggtags-save-to-register)
+ (define-key m "q" #'quit)
+ (define-key m "\r" #'done)
+ m)))
+
+(defvar bookmark-make-record-function)
+
+(defun ggtags-global-rerun-search ()
+ "Pop up a buffer to choose a past search to re-run.
+
+\\{ggtags-global-rerun-search-map}"
+ (interactive)
+ (or ggtags-global-search-history (user-error "No search history"))
+ (let ((split-window-preferred-function ggtags-split-window-function)
+ (inhibit-read-only t))
+ (pop-to-buffer "*Ggtags Search History*")
+ (erase-buffer)
+ (special-mode)
+ (use-local-map ggtags-global-rerun-search-map)
+ (setq-local ggtags-enable-navigation-keys nil)
+ (setq-local bookmark-make-record-function #'ggtags-make-bookmark-record)
+ (setq truncate-lines t)
+ (cl-labels ((prop (s) (propertize s 'face 'minibuffer-prompt))
+ (pp (data)
+ (pcase data
+ (`(,_id ,cmd ,dir ,_env ,line ,text)
+ (insert (prop " cmd: ") cmd "\n"
+ (prop " dir: ") dir "\n"
+ (prop "line: ") (number-to-string line) "\n"
+ (prop "text: ") text "\n"
+ (propertize (make-string 32 ?-) 'face 'shadow))))))
+ (setq ggtags-global-search-ewoc
+ (ewoc-create #'pp "Global search history keys: n:next p:prev r:register RET:choose\n")))
+ (dolist (data ggtags-global-search-history)
+ (ewoc-enter-last ggtags-global-search-ewoc data))
+ (and ggtags-global-rerun-search-last
+ (re-search-forward (cadr ggtags-global-rerun-search-last) nil t)
+ (ewoc-goto-node ggtags-global-search-ewoc
+ (ewoc-locate ggtags-global-search-ewoc)))
+ (set-buffer-modified-p nil)
+ (fit-window-to-buffer nil (floor (frame-height) 2))))
+
(defun ggtags-save-to-register (r)
"Save current search session to register R.
Use \\[jump-to-register] to restore the search session."
- (interactive (list (ggtags-ensure-global-buffer
- (register-read-with-preview "Save search to register: "))))
- (ggtags-ensure-global-buffer
- (cl-labels ((jump (data)
- (pcase data
- (`(,command ,root ,line)
- (with-current-buffer
- (let ((ggtags-auto-jump-to-first-match nil)
- ;; Switch current project to ROOT.
- (default-directory root)
- (ggtags-project-root root))
- (ggtags-global-start command root))
- (add-hook 'compilation-finish-functions
- (lambda (buf _msg)
- (with-current-buffer buf
- (goto-char (point-min))
- (forward-line (1- line))
- (compile-goto-error)))
- nil t)))))
- (prn (data)
- (pcase data
- (`(,command ,root ,line)
- (princ (format "a ggtags search session `%s' in directory `%s' at line %d."
- command root line))))))
- (set-register r (registerv-make
- (list (car compilation-arguments) default-directory
- (line-number-at-pos))
- :jump-func #'jump :print-func #'prn)))))
+ (interactive (list (register-read-with-preview "Save search to register: ")))
+ (cl-labels ((prn (data)
+ (pcase data
+ (`(,command ,root ,_env ,line ,_)
+ (princ (format "a ggtags search session `%s' in directory `%s' at line %d."
+ command root line))))))
+ (set-register r (registerv-make
+ (if ggtags-global-search-ewoc
+ (cdr (ewoc-data (ewoc-locate ggtags-global-search-ewoc)))
+ (ggtags-global-current-search))
+ :jump-func #'ggtags-global-rerun-search-1
+ :print-func #'prn))))
+
+(defun ggtags-make-bookmark-record ()
+ `(,(and ggtags-current-tag-name (format "*ggtags %s*" ggtags-current-tag-name))
+ (ggtags-search . ,(if ggtags-global-search-ewoc
+ (cdr (ewoc-data (ewoc-locate ggtags-global-search-ewoc)))
+ (ggtags-global-current-search)))
+ (handler . ggtags-bookmark-jump)))
+
+(declare-function bookmark-prop-get "bookmark")
+
+(defun ggtags-bookmark-jump (bmk)
+ (ggtags-global-rerun-search-1 (bookmark-prop-get bmk 'ggtags-search)))
(defun ggtags-delete-tag-files ()
- "Delete the tag files generated by gtags."
+ "Delete the GTAGS, GRTAGS, GPATH etc. files generated by gtags."
(interactive (ignore (ggtags-check-project)))
(when (ggtags-current-project-root)
(let* ((re (concat "\\`" (regexp-opt '("GPATH" "GRTAGS" "GTAGS" "ID")) "\\'"))
(list (read-file-name "Browse file: " nil nil t)
(read-number "Line: " 1))
(list buffer-file-name (line-number-at-pos))))
- (cl-check-type line integer)
+ (cl-check-type line (integer 1))
(or (and file (file-exists-p file)) (error "File `%s' doesn't exist" file))
(ggtags-check-project)
(or (file-exists-p (expand-file-name "HTML" (ggtags-current-project-root)))
(interactive)
(ggtags-next-mark 'previous))
+(defvar ggtags-view-tag-history-mode-map
+ (let ((m (make-sparse-keymap)))
+ (define-key m "\M-n" 'next-error-no-select)
+ (define-key m "\M-p" 'previous-error-no-select)
+ (define-key m "q" (lambda () (interactive) (quit-window t)))
+ m))
+
+(define-derived-mode ggtags-view-tag-history-mode tabulated-list-mode "TagHist"
+ :abbrev-table nil :group 'ggtags)
+
(defun ggtags-view-tag-history ()
+ "Pop to a buffer listing visited locations from newest to oldest.
+The buffer is a next error buffer and works with standard
+commands `next-error' and `previous-error'.
+
+\\{ggtags-view-tag-history-mode-map}"
(interactive)
(and (ring-empty-p find-tag-marker-ring)
(user-error "Tag ring empty"))
(inhibit-read-only t))
(pop-to-buffer "*Tag Ring*")
(erase-buffer)
- (tabulated-list-mode)
+ (ggtags-view-tag-history-mode)
+ (setq next-error-function #'ggtags-view-tag-history-next-error
+ next-error-last-buffer (current-buffer))
(setq tabulated-list-entries
;; Use a function so that revert can work properly.
(lambda ()
(let ((counter (ring-length find-tag-marker-ring))
(elements (or (ring-elements find-tag-marker-ring)
(user-error "Tag ring empty")))
- (action
- (lambda (button) (interactive)
- (let ((m (button-get button 'marker)))
- (or (markerp m) (user-error "Marker dead"))
- (setq ggtags-tag-ring-index
- (ring-member find-tag-marker-ring m))
- (pop-to-buffer (marker-buffer m))
- (goto-char (marker-position m)))))
- (get-line
- (lambda (m)
- (with-current-buffer (marker-buffer m)
- (save-excursion
- (goto-char m)
- (buffer-substring (line-beginning-position)
- (line-end-position)))))))
+ (action (lambda (_button) (next-error 0)))
+ (get-line (lambda (m)
+ (with-current-buffer (marker-buffer m)
+ (save-excursion
+ (goto-char m)
+ (buffer-substring (line-beginning-position)
+ (line-end-position)))))))
(setq tabulated-list-format
`[("ID" ,(max (1+ (floor (log counter 10))) 2)
- (lambda (x y) (< (car x) (car y))))
+ car-less-than-car)
("Buffer" ,(max (cl-loop for m in elements
for b = (marker-buffer m)
maximize
elements))))
(setq tabulated-list-sort-key '("ID" . t))
(tabulated-list-print)
- (fit-window-to-buffer)))
+ (fit-window-to-buffer nil (floor (frame-height) 2))))
+
+(defun ggtags-view-tag-history-next-error (&optional arg reset)
+ (if (not reset)
+ (forward-button arg)
+ (goto-char (point-min))
+ (forward-button (if (button-at (point)) 0 1)))
+ (when (get-buffer-window)
+ (set-window-point (get-buffer-window) (point)))
+ (pcase (button-get (button-at (point)) 'marker)
+ ((and (pred markerp) m)
+ (if (eq (get-buffer-window) (selected-window))
+ (pop-to-buffer (marker-buffer m))
+ (switch-to-buffer (marker-buffer m)))
+ (goto-char (marker-position m)))
+ (_ (error "Dead marker"))))
(defun ggtags-global-exit-message-function (_process-status exit-status msg)
(setq ggtags-global-exit-status exit-status)
(defvar ggtags-global-error-regexp-alist-alist
(append
`((path "^\\(?:[^\"'\n]*/\\)?[^ )\t\n]+$" 0)
- ;; ACTIVE_ESCAPE src/dialog.cc 172
+ ;; ACTIVE_ESCAPE src/dialog.cc 172
(ctags "^\\([^ \t\n]+\\)[ \t]+\\(.*?\\)[ \t]+\\([0-9]+\\)$"
2 3 nil nil 2 (1 font-lock-function-name-face))
;; ACTIVE_ESCAPE 172 src/dialog.cc #undef ACTIVE_ESCAPE
(w (display-buffer buffer '(nil (allow-no-window . t)))))
(and w (compilation-set-window-height w))))))
-(defvar ggtags-navigation-mode)
-
(defun ggtags-global-filter ()
"Called from `compilation-filter-hook' (which see)."
(let ((ansi-color-apply-face-function
(count-lines compilation-filter-start (point)))
(when (and (> ggtags-global-output-lines 5) ggtags-navigation-mode)
(ggtags-global--display-buffer))
+ (when (and (eq ggtags-auto-jump-to-match 'history)
+ (numberp ggtags-auto-jump-to-match-target)
+ ;; `ggtags-global-output-lines' is imprecise.
+ (> (line-number-at-pos (point-max))
+ ggtags-auto-jump-to-match-target))
+ (ggtags-forward-to-line ggtags-auto-jump-to-match-target)
+ (setq-local ggtags-auto-jump-to-match-target nil)
+ ;;
+ ;; Can't call `compile-goto-error' here becuase
+ ;; `compilation-filter' restores point and as a result commands
+ ;; dependent on point such as `ggtags-navigation-next-file' and
+ ;; `ggtags-navigation-previous-file' fail to work.
+ (setq-local compilation-auto-jump-to-first-error t)
+ (run-with-idle-timer 0 nil #'compilation-auto-jump (current-buffer) (point)))
(make-local-variable 'ggtags-global-large-output)
(when (> ggtags-global-output-lines ggtags-global-large-output)
(cl-incf ggtags-global-large-output 500)
(message "Output %d lines (Type `C-c C-k' to cancel)"
ggtags-global-output-lines))))
-(defun ggtags-handle-single-match (buf how)
- (if (string-prefix-p "exited abnormally" how)
- ;; If exit abnormally display the buffer for inspection.
- (ggtags-global--display-buffer)
- (when (and ggtags-auto-jump-to-first-match
- (save-excursion
- (goto-char (point-min))
- (not (ignore-errors
- (goto-char (compilation-next-single-property-change
- (point) 'compilation-message))
- (end-of-line)
- (compilation-next-single-property-change
- (point) 'compilation-message)))))
- ;; For the `compilation-auto-jump' in idle timer to run. See also:
- ;; http://debbugs.gnu.org/13829
- (sit-for 0)
- (ggtags-navigation-mode -1)
- (ggtags-navigation-mode-cleanup buf 0))))
+(defun ggtags-global-handle-exit (buf how)
+ "A function for `compilation-finish-functions' (which see)."
+ (cond
+ ((string-prefix-p "exited abnormally" how)
+ ;; If exit abnormally display the buffer for inspection.
+ (ggtags-global--display-buffer))
+ ((and ggtags-auto-jump-to-match
+ (not (pcase (compilation-next-single-property-change
+ (point-min) 'compilation-message)
+ ((and pt (guard pt))
+ (compilation-next-single-property-change
+ (save-excursion (goto-char pt) (end-of-line) (point))
+ 'compilation-message)))))
+ ;; For the `compilation-auto-jump' in idle timer to run.
+ ;; See also: http://debbugs.gnu.org/13829
+ (sit-for 0)
+ (ggtags-navigation-mode -1)
+ (ggtags-navigation-mode-cleanup buf 0))))
(defvar ggtags-global-mode-font-lock-keywords
'(("^Global \\(exited abnormally\\|interrupt\\|killed\\|terminated\\)\\(?:.*with code \\([0-9]+\\)\\)?.*"
(make-local-variable 'ggtags-global-output-format)
(setq-local compilation-error-regexp-alist
(list ggtags-global-output-format))
- (setq-local compilation-auto-jump-to-first-error
- ggtags-auto-jump-to-first-match)
+ (pcase ggtags-auto-jump-to-match
+ (`history (make-local-variable 'ggtags-auto-jump-to-match-target)
+ (setq-local compilation-auto-jump-to-first-error
+ (not ggtags-auto-jump-to-match-target)))
+ (`nil (setq-local compilation-auto-jump-to-first-error nil))
+ (_ (setq-local compilation-auto-jump-to-first-error t)))
(setq-local compilation-scroll-output nil)
;; See `compilation-move-to-column' for details.
(setq-local compilation-first-column 0)
(setq-local truncate-lines t)
(jit-lock-register #'ggtags-abbreviate-files)
(add-hook 'compilation-filter-hook 'ggtags-global-filter nil 'local)
- (add-hook 'compilation-finish-functions 'ggtags-handle-single-match nil t)
+ (add-hook 'compilation-finish-functions 'ggtags-global-handle-exit nil t)
+ (setq-local bookmark-make-record-function #'ggtags-make-bookmark-record)
+ (setq-local ggtags-enable-navigation-keys nil)
(add-hook 'kill-buffer-hook (lambda () (ggtags-navigation-mode -1)) nil t))
;; NOTE: Need this to avoid putting menu items in
map))
(defvar ggtags-mode-map-alist
- `((ggtags-navigation-mode . ,ggtags-navigation-map)))
-
-;; Higher priority for `ggtags-navigation-mode' to avoid being
-;; hijacked by modes such as `view-mode'.
-(add-to-list 'emulation-mode-map-alists 'ggtags-mode-map-alist)
+ `((ggtags-enable-navigation-keys . ,ggtags-navigation-map)))
(defvar ggtags-navigation-mode-map
(let ((map (make-sparse-keymap))
'(menu-item "Finish navigation" ggtags-navigation-mode-done))
(define-key menu [abort]
'(menu-item "Abort" ggtags-navigation-mode-abort))
- (define-key menu [last-error]
- '(menu-item "Last error" ggtags-navigation-last-error))
- (define-key menu [fist-error]
- '(menu-item "Fist error" ggtags-navigation-first-error))
+ (define-key menu [last-match]
+ '(menu-item "Last match" ggtags-navigation-last-error))
+ (define-key menu [first-match]
+ '(menu-item "First match" ggtags-navigation-first-error))
(define-key menu [previous-file]
'(menu-item "Previous file" ggtags-navigation-previous-file))
(define-key menu [next-file]
'(menu-item "Next file" ggtags-navigation-next-file))
+ (define-key menu [isearch-forward]
+ '(menu-item "Find match with isearch" ggtags-navigation-isearch-forward))
(define-key menu [previous]
'(menu-item "Previous match" previous-error))
(define-key menu [next]
(kill-compilation))
(when (and (derived-mode-p 'ggtags-global-mode)
(get-buffer-window))
- (quit-window nil (get-buffer-window)))
+ (quit-windows-on (current-buffer)))
(and time (run-with-idle-timer time nil #'kill-buffer buf))))))
(defun ggtags-navigation-mode-done ()
(defun ggtags-navigation-mode-abort ()
(interactive)
(ggtags-navigation-mode -1)
+ (ggtags-navigation-mode-cleanup nil 0)
;; Run after (ggtags-navigation-mode -1) or
;; ggtags-global-start-marker might not have been saved.
(when (and ggtags-global-start-marker
(not (markerp ggtags-global-start-marker)))
(setq ggtags-global-start-marker nil)
- (pop-tag-mark))
- (ggtags-navigation-mode-cleanup nil 0))
+ (pop-tag-mark)))
(defun ggtags-navigation-next-file (n)
(interactive "p")
(defun ggtags-navigation-isearch-forward (&optional regexp-p)
(interactive "P")
(ggtags-ensure-global-buffer
- (let ((saved (if visible-mode 1 -1)))
- (visible-mode 1)
- (with-selected-window (get-buffer-window (current-buffer))
- (isearch-forward regexp-p)
- (beginning-of-line)
- (visible-mode saved)
- (compile-goto-error)))))
+ (let ((saved (if visible-mode 1 -1)))
+ (visible-mode 1)
+ (with-selected-window (get-buffer-window (current-buffer))
+ (isearch-forward regexp-p)
+ (beginning-of-line)
+ (visible-mode saved)
+ (compile-goto-error)))))
(defun ggtags-navigation-visible-mode (&optional arg)
(interactive (list (or current-prefix-arg 'toggle)))
(defvar ggtags-global-line-overlay nil)
(defun ggtags-global-next-error-function ()
- (ggtags-move-to-tag)
- (ggtags-global-save-start-marker)
- (and (ggtags-project-update-mtime-maybe)
- (message "File `%s' is newer than GTAGS"
- (file-name-nondirectory buffer-file-name)))
- (and ggtags-mode-sticky (ggtags-mode 1))
- (ignore-errors
- (ggtags-ensure-global-buffer
- (unless (overlayp ggtags-global-line-overlay)
- (setq ggtags-global-line-overlay (make-overlay (point) (point)))
- (overlay-put ggtags-global-line-overlay 'face 'ggtags-global-line))
- (move-overlay ggtags-global-line-overlay
- (line-beginning-position) (line-end-position)
- (current-buffer))))
- (run-hooks 'ggtags-find-tag-hook))
+ (when (eq next-error-last-buffer ggtags-global-last-buffer)
+ (ggtags-move-to-tag)
+ (ggtags-global-save-start-marker)
+ (and (ggtags-project-update-mtime-maybe)
+ (message "File `%s' is newer than GTAGS"
+ (file-name-nondirectory buffer-file-name)))
+ (and ggtags-mode-sticky (ggtags-mode 1))
+ (ignore-errors
+ (ggtags-ensure-global-buffer
+ (unless (overlayp ggtags-global-line-overlay)
+ (setq ggtags-global-line-overlay (make-overlay (point) (point)))
+ (overlay-put ggtags-global-line-overlay 'face 'ggtags-global-line))
+ (move-overlay ggtags-global-line-overlay
+ (line-beginning-position) (line-end-position)
+ (current-buffer))
+ ;; Update search history
+ (let ((id (ggtags-global-search-id (car compilation-arguments)
+ default-directory)))
+ (setq ggtags-global-search-history
+ (cl-remove id ggtags-global-search-history :test #'equal :key #'car))
+ (add-to-history 'ggtags-global-search-history
+ (cons id (ggtags-global-current-search))
+ ggtags-global-history-length))))
+ (run-hooks 'ggtags-find-tag-hook)))
(define-minor-mode ggtags-navigation-mode nil
:lighter
(" GG[" (:eval
(ignore-errors
(ggtags-ensure-global-buffer
- (let ((index (when (get-text-property (line-beginning-position)
- 'compilation-message)
- ;; Assume the first match appears at line 5
- (- (line-number-at-pos) 4))))
- `((:propertize ,(if index
- (number-to-string (max index 0))
- "?") face success) "/")))))
+ (let ((index (when (get-text-property (line-beginning-position)
+ 'compilation-message)
+ ;; Assume the first match appears at line 5
+ (- (line-number-at-pos) 4))))
+ `((:propertize ,(if index
+ (number-to-string (max index 0))
+ "?") face success) "/")))))
(:propertize (:eval (number-to-string ggtags-global-match-count))
face success)
(:eval
:global t
(if ggtags-navigation-mode
(progn
+ ;; Higher priority for `ggtags-navigation-mode' to avoid being
+ ;; hijacked by modes such as `view-mode'.
+ (add-to-list 'emulation-mode-map-alists 'ggtags-mode-map-alist)
(add-hook 'next-error-hook 'ggtags-global-next-error-function)
(add-hook 'minibuffer-setup-hook 'ggtags-minibuffer-setup-function))
+ (setq emulation-mode-map-alists
+ (delq 'ggtags-mode-map-alist emulation-mode-map-alists))
(remove-hook 'next-error-hook 'ggtags-global-next-error-function)
(remove-hook 'minibuffer-setup-hook 'ggtags-minibuffer-setup-function)))
(defun ggtags-minibuffer-setup-function ()
;; Disable ggtags-navigation-mode in minibuffer.
- (setq-local ggtags-navigation-mode nil))
+ (setq-local ggtags-enable-navigation-keys nil))
(defun ggtags-kill-file-buffers (&optional interactive)
"Kill all buffers visiting files in current project."
(file-relative-name buffer-file-name))))))
(defun ggtags-global-output (buffer cmds callback &optional cutoff)
- "Asynchrously pipe the output of running CMDS to BUFFER.
+ "Asynchronously pipe the output of running CMDS to BUFFER.
When finished invoke CALLBACK in BUFFER with process exit status."
(or buffer (error "Output buffer required"))
(let* ((program (car cmds))
(defvar ggtags-mode-prefix-map
(let ((m (make-sparse-keymap)))
- (define-key m "\M-'" 'previous-error)
+ ;; Globally bound to `M-g p'.
+ ;; (define-key m "\M-'" 'previous-error)
(define-key m (kbd "M-DEL") 'ggtags-delete-tag-files)
(define-key m "\M-p" 'ggtags-prev-mark)
(define-key m "\M-n" 'ggtags-next-mark)
(define-key m "\M-k" 'ggtags-kill-file-buffers)
(define-key m "\M-h" 'ggtags-view-tag-history)
(define-key m "\M-j" 'ggtags-visit-project-root)
+ (define-key m "\M-/" 'ggtags-global-rerun-search)
(define-key m (kbd "M-SPC") 'ggtags-save-to-register)
(define-key m (kbd "M-%") 'ggtags-query-replace)
(define-key m "\M-?" 'ggtags-show-definition)
(define-key menu [prev-mark]
'(menu-item "Previous mark" ggtags-prev-mark))
(define-key menu [sep1] menu-bar-separator)
+ (define-key menu [rerun-search]
+ '(menu-item "Rerun past search" ggtags-global-rerun-search))
(define-key menu [save-to-register]
'(menu-item "Save search session" ggtags-save-to-register))
(define-key menu [previous-error]
"mouse-1 to set project")
'mouse-face 'mode-line-highlight
'keymap ggtags-mode-line-project-keymap)))
- "]"))
+ "]")
+ "Mode line construct for displaying current project name.
+The value is the name of the project root directory. Setting it
+to nil disables displaying this information.")
;;;###autoload
(define-minor-mode ggtags-mode nil
;;; imenu
(defun ggtags-goto-imenu-index (name line &rest _args)
- (save-restriction
- (widen)
- (goto-char (point-min))
- (forward-line (1- line))
- (ggtags-move-to-tag name)))
+ (ggtags-forward-to-line line)
+ (ggtags-move-to-tag name))
;;;###autoload
(defun ggtags-build-imenu-index ()
;;; hippie-expand
;;;###autoload
-(defun try-complete-ggtags-tag (old)
+(defun ggtags-try-complete-tag (old)
"A function suitable for `hippie-expand-try-functions-list'."
- (with-no-warnings ; to avoid loading hippie-exp
- (unless old
- (he-init-string (if (looking-back "\\_<.*" (line-beginning-position))
- (match-beginning 0)
- (point))
- (point))
- (setq he-expand-list
- (and (not (equal he-search-string ""))
- (ggtags-find-project)
- (sort (all-completions he-search-string
- ggtags-completion-table)
- #'string-lessp))))
- (if (null he-expand-list)
- (progn
- (if old (he-reset-string))
- nil)
- (he-substitute-string (car he-expand-list))
- (setq he-expand-list (cdr he-expand-list))
- t)))
+ (eval-and-compile (require 'hippie-exp))
+ (unless old
+ (he-init-string (or (car (funcall ggtags-bounds-of-tag-function)) (point))
+ (point))
+ (setq he-expand-list
+ (and (not (equal he-search-string ""))
+ (ggtags-find-project)
+ (sort (all-completions he-search-string
+ ggtags-completion-table)
+ #'string-lessp))))
+ (if (null he-expand-list)
+ (progn
+ (if old (he-reset-string))
+ nil)
+ (he-substitute-string (car he-expand-list))
+ (setq he-expand-list (cdr he-expand-list))
+ t))
(defun ggtags-reload (&optional force)
(interactive "P")
(unload-feature 'ggtags force)
(require 'ggtags))
-(defun ggtags-unload-function ()
- (setq emulation-mode-map-alists
- (delq 'ggtags-mode-map-alist emulation-mode-map-alists))
- nil)
-
(provide 'ggtags)
;;; ggtags.el ends here