1 ;;; ggtags.el --- emacs frontend to GNU Global source code tagging system -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2013-2014 Free Software Foundation, Inc.
5 ;; Author: Leo Liu <sdl.web@gmail.com>
7 ;; Keywords: tools, convenience
9 ;; URL: https://github.com/leoliu/ggtags
10 ;; Package-Requires: ((emacs "24") (cl-lib "0.5"))
12 ;; This program is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation, either version 3 of the License, or
15 ;; (at your option) any later version.
17 ;; This program is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
27 ;; A package to integrate GNU Global source code tagging system
28 ;; (http://www.gnu.org/software/global) with Emacs.
32 ;; Type `M-x ggtags-mode' to enable the minor mode, or as usual enable
33 ;; it in your desired major mode hooks. When the mode is on the symbol
34 ;; at point is underlined if it is a valid (definition) tag.
36 ;; `M-.' finds definition or references according to the context at
37 ;; point, i.e. if point is at a definition tag find references and
38 ;; vice versa. `M-]' finds references.
40 ;; If multiple matches are found, navigation mode is entered, the
41 ;; mode-line lighter changed, and a navigation menu-bar entry
42 ;; presented. In this mode, `M-n' and `M-p' moves to next and previous
43 ;; match, `M-}' and `M-{' to next and previous file respectively.
44 ;; `M-o' toggles between full and abbreviated displays of file names
45 ;; in the auxiliary popup window. When you locate the right match,
46 ;; press RET to finish which hides the auxiliary window and exits
47 ;; navigation mode. You can continue the search using `M-,'. To abort
48 ;; the search press `M-*'.
50 ;; Normally after a few searches a dozen buffers are created visiting
51 ;; files tracked by GNU Global. `C-c M-k' helps clean them up.
53 ;; Check the menu-bar entry `Ggtags' for other useful commands.
63 (require 'tabulated-list) ;preloaded since 24.3
66 (unless (fboundp 'setq-local)
67 (defmacro setq-local (var val)
68 (list 'set (list 'make-local-variable (list 'quote var)) val)))
70 (unless (fboundp 'defvar-local)
71 (defmacro defvar-local (var val &optional docstring)
72 (declare (debug defvar) (doc-string 3))
73 (list 'progn (list 'defvar var val docstring)
74 (list 'make-variable-buffer-local (list 'quote var)))))
76 (defmacro ignore-errors-unless-debug (&rest body)
77 "Ignore all errors while executing BODY unless debug is on."
78 (declare (debug t) (indent 0))
79 `(condition-case-unless-debug nil (progn ,@body) (error nil))))
82 (or (fboundp 'user-error) ;24.3
83 (defalias 'user-error 'error))
84 (or (fboundp 'read-only-mode) ;24.3
85 (defalias 'read-only-mode 'toggle-read-only))
86 (or (fboundp 'register-read-with-preview) ;24.4
87 (defalias 'register-read-with-preview 'read-char)))
90 "GNU Global source code tagging system."
93 (defface ggtags-highlight '((t (:underline t)))
94 "Face used to highlight a valid tag at point."
97 (defface ggtags-global-line '((t (:inherit secondary-selection)))
98 "Face used to highlight matched line in Global buffer."
101 (defcustom ggtags-executable-directory nil
102 "If non-nil the directory to search global executables."
103 :type '(choice (const :tag "Unset" nil) directory)
107 (defcustom ggtags-oversize-limit (* 10 1024 1024)
108 "The over size limit for the GTAGS file.
109 For large source trees, running 'global -u' can be expensive.
110 Thus when GTAGS file is larger than this limit, ggtags
111 automatically switches to 'global --single-update'."
113 :type '(choice (const :tag "None" nil)
114 (const :tag "Always" t)
118 (defcustom ggtags-include-pattern
119 '("^\\s-*#\\(?:include\\|import\\)\\s-*[\"<]\\(?:[./]*\\)?\\(.*?\\)[\">]" . 1)
120 "Pattern used to detect #include files.
121 Value can be (REGEXP . SUB) or a function with no arguments."
122 :type '(choice (const :tag "Disable" nil)
123 (cons regexp integer)
128 (defcustom ggtags-global-always-update nil
129 "If non-nil always update tags for current file on save."
134 (defcustom ggtags-use-project-gtagsconf t
135 "Non-nil to automatically use GTAGSCONF file at project root.
136 File .globalrc and gtags.conf are checked in order."
141 (defcustom ggtags-project-duration 600
142 "Seconds to keep information of a project in memory."
146 (defcustom ggtags-process-environment nil
147 "Similar to `process-environment' with higher precedence.
148 Elements are run through `substitute-env-vars' before use.
149 GTAGSROOT will always be expanded to current project root
150 directory. This is intended for project-wise ggtags-specific
151 process environment settings. Note on remote hosts (e.g. tramp)
152 directory local variables is not enabled by default per
153 `enable-remote-dir-locals' (which see)."
154 :safe 'ggtags-list-of-string-p
155 :type '(repeat string)
158 (defcustom ggtags-auto-jump-to-first-match t
159 "Non-nil to automatically jump to the first match."
163 (defcustom ggtags-global-window-height 8 ; ggtags-global-mode
164 "Number of lines for the 'global' popup window.
165 If nil, use Emacs default."
166 :type '(choice (const :tag "Default" nil) integer)
169 (defcustom ggtags-global-abbreviate-filename 35
170 "Non-nil to display file names abbreviated e.g. \"/u/b/env\".
171 If an integer abbreviate only names longer than that number."
172 :type '(choice (const :tag "No" nil)
173 (const :tag "Always" t)
177 (defcustom ggtags-split-window-function split-window-preferred-function
178 "A function to control how ggtags pops up the auxiliary window."
182 (defcustom ggtags-use-idutils (and (executable-find "mkid") t)
183 "Non-nil to also generate the idutils DB."
187 (defcustom ggtags-global-output-format 'grep
188 "The output format for the 'global' command."
189 :type '(choice (const path)
196 (defcustom ggtags-global-ignore-case nil
197 "Non-nil if Global should ignore case."
202 (defcustom ggtags-global-treat-text nil
203 "Non-nil if Global should include matches from text files."
208 (defcustom ggtags-global-large-output 1000
209 "Number of lines in the Global buffer to indicate large output."
213 (defcustom ggtags-find-tag-hook nil
214 "Hook run immediately after finding a tag."
215 :options '(recenter reposition-window)
219 (defcustom ggtags-show-definition-function #'ggtags-show-definition-default
220 "Function called by `ggtags-show-definition' to show definition.
221 It is passed a list of definnition candidates of the form:
223 (TEXT NAME FILE LINE)
225 where TEXT is usually the source line of the definition."
229 (defcustom ggtags-mode-sticky t
230 "If non-nil enable Ggtags Mode in files visited."
235 (defcustom ggtags-mode-prefix-key "\C-c"
236 "Key binding used for `ggtags-mode-prefix-map'.
237 Users should change the value using `customize-variable' to
238 properly update `ggtags-mode-map'."
239 :set (lambda (sym value)
240 (when (bound-and-true-p ggtags-mode-map)
241 (let ((old (and (boundp sym) (symbol-value sym))))
242 (and old (define-key ggtags-mode-map old nil)))
244 (bound-and-true-p ggtags-mode-prefix-map)
245 (define-key ggtags-mode-map value ggtags-mode-prefix-map)))
246 (set-default sym value))
250 (defcustom ggtags-completing-read-function completing-read-function
251 "Ggtags specific `completing-read-function' (which see)."
255 (defcustom ggtags-highlight-tag-delay 0.25
256 "Time in seconds before highlighting tag at point."
257 :set (lambda (sym value)
258 (when (bound-and-true-p ggtags-highlight-tag-timer)
259 (timer-set-idle-time ggtags-highlight-tag-timer value t))
260 (set-default sym value))
264 (defcustom ggtags-bounds-of-tag-function (lambda ()
265 (bounds-of-thing-at-point 'symbol))
266 "Function to get the start and end locations of the tag at point."
270 (defvar ggtags-bug-url "https://github.com/leoliu/ggtags/issues")
272 (defvar ggtags-global-last-buffer nil)
274 (defvar ggtags-current-tag-name nil)
276 (defvar ggtags-highlight-tag-overlay nil)
278 (defvar ggtags-highlight-tag-timer nil)
280 ;; Used by ggtags-global-mode
281 (defvar ggtags-global-error "match"
282 "Stem of message to print when no matches are found.")
284 (defmacro ggtags-ensure-global-buffer (&rest body)
287 (or (and (buffer-live-p ggtags-global-last-buffer)
288 (with-current-buffer ggtags-global-last-buffer
289 (derived-mode-p 'ggtags-global-mode)))
290 (error "No global buffer found"))
291 (with-current-buffer ggtags-global-last-buffer ,@body)))
293 (defun ggtags-list-of-string-p (xs)
294 "Return non-nil if XS is a list of strings."
295 (cl-every #'stringp xs))
297 (defun ggtags-forward-to-line (line)
298 "Move to line number LINE in current buffer."
299 (cl-check-type line (integer 1))
302 (goto-char (point-min))
303 (forward-line (1- line))))
305 (defun ggtags-program-path (name)
306 (if ggtags-executable-directory
307 (expand-file-name name ggtags-executable-directory)
310 (defun ggtags-process-string (program &rest args)
312 (let ((exit (apply #'process-file
313 (ggtags-program-path program) nil t nil args))
315 (goto-char (point-max))
316 (skip-chars-backward " \t\n")
317 (buffer-substring (point-min) (point)))))
319 (error "`%s' non-zero exit: %s" program output))
322 (defun ggtags-tag-at-point ()
323 (pcase (funcall ggtags-bounds-of-tag-function)
324 (`(,beg . ,end) (buffer-substring beg end))))
326 ;;; Store for project info and settings
328 (defvar ggtags-projects (make-hash-table :size 7 :test #'equal))
330 (cl-defstruct (ggtags-project (:constructor ggtags-project--make)
334 root config tag-size has-refs has-path-style has-color dirty-p mtime timestamp)
336 (defun ggtags-make-project (root)
337 (cl-check-type root string)
338 (pcase (nthcdr 5 (file-attributes (expand-file-name "GTAGS" root)))
339 (`(,mtime ,_ ,tag-size . ,_)
340 (let* ((default-directory (file-name-as-directory root))
341 (config (cl-some (lambda (c) (and (file-exists-p c) c))
342 '(".globalrc" "gtags.conf")))
343 (rtags-size (nth 7 (file-attributes "GRTAGS")))
346 (and (or (> rtags-size (* 32 1024))
348 (not (equal "" (ggtags-process-string "global" "-crs")))))
350 ;; http://thread.gmane.org/gmane.comp.gnu.global.bugs/1518
352 (with-demoted-errors ; in case `global' not found
353 (and (zerop (process-file (ggtags-program-path "global")
355 "--path-style" "shorter" "--help"))
357 ;; http://thread.gmane.org/gmane.comp.gnu.global.bugs/1542
360 (and (zerop (process-file (ggtags-program-path "global")
364 (puthash default-directory
365 (ggtags-project--make :root default-directory
369 :has-path-style has-path-style
371 :mtime (float-time mtime)
372 :timestamp (float-time))
375 (defun ggtags-project-expired-p (project)
376 (or (< (ggtags-project-timestamp project) 0)
378 (ggtags-project-timestamp project))
379 ggtags-project-duration)))
381 (defun ggtags-project-update-mtime-maybe (&optional project)
382 "Update PROJECT's modtime and if current file is newer.
383 Value is new modtime if updated."
384 (let ((project (or project (ggtags-find-project))))
385 (when (and (ggtags-project-p project)
386 (consp (visited-file-modtime))
387 (> (float-time (visited-file-modtime))
388 (ggtags-project-mtime project)))
389 (setf (ggtags-project-dirty-p project) t)
390 (setf (ggtags-project-mtime project)
391 (float-time (visited-file-modtime))))))
393 (defun ggtags-project-oversize-p (&optional project)
394 (pcase ggtags-oversize-limit
397 (size (let ((project (or project (ggtags-find-project))))
398 (and project (> (ggtags-project-tag-size project) size))))))
400 (defvar-local ggtags-project-root 'unset
401 "Internal variable for project root directory.")
404 (defun ggtags-find-project ()
405 (let ((project (gethash ggtags-project-root ggtags-projects)))
406 (if (ggtags-project-p project)
407 (if (ggtags-project-expired-p project)
409 (remhash ggtags-project-root ggtags-projects)
410 (ggtags-find-project))
412 (setq ggtags-project-root
413 (or (ignore-errors-unless-debug
414 (file-name-as-directory
415 (concat (file-remote-p default-directory)
416 ;; Resolves symbolic links
417 (ggtags-process-string "global" "-pr"))))
418 ;; 'global -pr' resolves symlinks before checking the
419 ;; GTAGS file which could cause issues such as
420 ;; https://github.com/leoliu/ggtags/issues/22, so
421 ;; let's help it out.
423 ;; Note: `locate-dominating-file' doesn't accept
424 ;; function for NAME before 24.3.
425 (let ((dir (locate-dominating-file default-directory "GTAGS")))
426 ;; `file-truename' may strip the trailing '/' on
427 ;; remote hosts, see http://debbugs.gnu.org/16851
428 (and dir (file-regular-p (expand-file-name "GTAGS" dir))
429 (file-name-as-directory (file-truename dir))))))
430 (when ggtags-project-root
431 (if (gethash ggtags-project-root ggtags-projects)
432 (ggtags-find-project)
433 (ggtags-make-project ggtags-project-root))))))
435 (defun ggtags-current-project-root ()
436 (and (ggtags-find-project)
437 (ggtags-project-root (ggtags-find-project))))
439 (defun ggtags-check-project ()
440 (or (ggtags-find-project) (error "File GTAGS not found")))
442 (defun ggtags-ensure-project ()
443 (or (ggtags-find-project)
444 (when (or (yes-or-no-p "File GTAGS not found; run gtags? ")
445 (user-error "Aborted"))
446 (call-interactively #'ggtags-create-tags)
447 ;; Need checking because `ggtags-create-tags' can create tags
449 (ggtags-check-project))))
451 (defvar delete-trailing-lines) ;new in 24.3
453 (defun ggtags-save-project-settings (&optional noconfirm)
454 "Save Gnu Global's specific environment variables."
456 (ggtags-check-project)
457 (let* ((inhibit-read-only t) ; for `add-dir-local-variable'
458 (default-directory (ggtags-current-project-root))
459 ;; Not using `ggtags-with-current-project' to preserve
460 ;; environment variables that may be present in
461 ;; `ggtags-process-environment'.
463 (append ggtags-process-environment
465 (and (not (ggtags-project-has-refs (ggtags-find-project)))
466 (list "GTAGSLABEL=ctags"))))
467 (envlist (delete-dups
468 (cl-loop for x in process-environment
470 "^\\(GTAGS[^=\n]*\\|MAKEOBJDIRPREFIX\\)=" x)
471 ;; May have duplicates thus `delete-dups'.
472 collect (concat (match-string 1 x)
474 (getenv (match-string 1 x))))))
475 (help-form (format "y: save\nn: don't save\n=: diff\n?: help\n")))
476 (add-dir-local-variable nil 'ggtags-process-environment envlist)
477 ;; Remove trailing newlines by `add-dir-local-variable'.
478 (let ((delete-trailing-lines t)) (delete-trailing-whitespace))
480 (while (pcase (read-char-choice
481 (format "Save `%s'? (y/n/=/?) " buffer-file-name)
483 ;; ` required for 24.1 and 24.2
484 (`?n (user-error "Aborted"))
486 (`?= (diff-buffer-with-file) 'loop)
487 (`?? (help-form-show) 'loop))))
491 (defun ggtags-toggle-project-read-only ()
493 (ggtags-check-project)
494 (let ((inhibit-read-only t) ; for `add-dir-local-variable'
495 (val (not buffer-read-only))
496 (default-directory (ggtags-current-project-root)))
497 (add-dir-local-variable nil 'buffer-read-only val)
500 (when buffer-file-name
501 (read-only-mode (if val +1 -1)))
502 (when (called-interactively-p 'interactive)
503 (message "Project read-only-mode is %s" (if val "on" "off")))
506 (defun ggtags-visit-project-root ()
508 (ggtags-ensure-project)
509 (dired (ggtags-current-project-root)))
511 (defmacro ggtags-with-current-project (&rest body)
512 "Eval BODY in current project's `process-environment'."
514 (let ((gtagsroot (make-symbol "-gtagsroot-"))
515 (root (make-symbol "-ggtags-project-root-")))
516 `(let* ((,root ggtags-project-root)
517 (,gtagsroot (when (ggtags-find-project)
518 (directory-file-name (ggtags-current-project-root))))
520 (append (let ((process-environment process-environment))
521 (and ,gtagsroot (setenv "GTAGSROOT" ,gtagsroot))
522 (mapcar #'substitute-env-vars ggtags-process-environment))
524 (and ,gtagsroot (list (concat "GTAGSROOT=" ,gtagsroot)))
525 (and (ggtags-find-project)
526 (not (ggtags-project-has-refs (ggtags-find-project)))
527 (list "GTAGSLABEL=ctags"))
528 (and ggtags-use-project-gtagsconf ,gtagsroot
529 (ggtags-project-config (ggtags-find-project))
530 (list (concat "GTAGSCONF="
531 (expand-file-name (ggtags-project-config
532 (ggtags-find-project))
534 (unwind-protect (save-current-buffer ,@body)
535 (setq ggtags-project-root ,root)))))
537 (defun ggtags-get-libpath ()
538 (let ((path (ggtags-with-current-project (getenv "GTAGSLIBPATH"))))
539 (and path (mapcar (apply-partially #'concat (file-remote-p default-directory))
540 (split-string path (regexp-quote path-separator) t)))))
542 (defun ggtags-create-tags (root)
543 "Create tag files (e.g. GTAGS) in directory ROOT.
544 If file gtags.files exists in ROOT, it should be a list of source
545 files to index, which can be used to speed gtags up in large
546 source trees. See Info node `(global)gtags' for details."
547 (interactive "DRoot directory: ")
548 (let ((process-environment process-environment))
549 (when (zerop (length root)) (error "No root directory provided"))
550 (setenv "GTAGSROOT" (expand-file-name
551 (directory-file-name (file-name-as-directory root))))
552 (ggtags-with-current-project
553 (let ((conf (and ggtags-use-project-gtagsconf
554 (cl-loop for name in '(".globalrc" "gtags.conf")
555 for full = (expand-file-name name root)
556 thereis (and (file-exists-p full) full)))))
557 (cond (conf (setenv "GTAGSCONF" conf))
558 ((and (not (getenv "GTAGSLABEL"))
559 (yes-or-no-p "Use `ctags' backend? "))
560 (setenv "GTAGSLABEL" "ctags"))))
561 (with-temp-message "`gtags' in progress..."
562 (let ((default-directory (file-name-as-directory root)))
564 (apply #'ggtags-process-string
565 "gtags" (and ggtags-use-idutils '("--idutils")))
566 (error (if (and ggtags-use-idutils
568 (string-match-p "mkid not found" (cadr err)))
569 ;; Retry without mkid
570 (ggtags-process-string "gtags")
571 (signal (car err) (cdr err))))))))
572 (message "GTAGS generated in `%s'" root)
575 (defun ggtags-update-tags (&optional force)
576 "Update GNU Global tag database.
577 Do nothing if GTAGS exceeds the oversize limit unless FORCE is
580 (ggtags-check-project)
581 ;; Mark project info expired.
582 (setf (ggtags-project-timestamp (ggtags-find-project)) -1)
584 (when (or force (and (ggtags-find-project)
585 (not (ggtags-project-oversize-p))
586 (ggtags-project-dirty-p (ggtags-find-project))))
587 (ggtags-with-current-project
588 (with-temp-message "`global -u' in progress..."
589 (ggtags-process-string "global" "-u")
590 (setf (ggtags-project-dirty-p (ggtags-find-project)) nil)
591 (setf (ggtags-project-mtime (ggtags-find-project)) (float-time))))))
593 (defvar-local ggtags-completion-cache nil)
595 ;; See global/libutil/char.c
596 ;; (defconst ggtags-regexp-metachars "[][$()*+.?\\{}|^]")
597 (defvar ggtags-completion-flag "") ;internal use
599 (defvar ggtags-completion-table
600 (completion-table-dynamic
602 (let ((cache-key (concat prefix "$" ggtags-completion-flag)))
603 (unless (equal cache-key (car ggtags-completion-cache))
604 (setq ggtags-completion-cache
606 (ignore-errors-unless-debug
607 ;; May throw global: only name char is allowed
609 (ggtags-with-current-project
611 (apply #'ggtags-process-string
613 (append (and completion-ignore-case '("--ignore-case"))
614 ;; Note -c alone returns only definitions
615 (list (concat "-c" ggtags-completion-flag) prefix)))
617 (cdr ggtags-completion-cache))))
619 (defun ggtags-completion-at-point ()
620 "A function for `completion-at-point-functions'."
621 (pcase (funcall ggtags-bounds-of-tag-function)
623 (and (< beg end) (list beg end ggtags-completion-table)))))
625 (defun ggtags-read-tag (&optional type confirm prompt require-match default)
626 (ggtags-ensure-project)
627 (let ((default (or default (ggtags-tag-at-point)))
628 (completing-read-function ggtags-completing-read-function)
629 (prompt (or prompt (capitalize (symbol-name (or type 'tag)))))
630 (ggtags-completion-flag (pcase type
631 (`(or nil definition) "T")
636 ((pred stringp) type)
637 (_ ggtags-completion-flag))))
638 (setq ggtags-current-tag-name
642 (format (if default "%s (default %s): " "%s: ") prompt default)
643 ggtags-completion-table nil require-match nil nil default))
645 (user-error "No tag at point"))
646 (t (substring-no-properties default))))))
648 (defun ggtags-global-build-command (cmd &rest args)
649 ;; CMD can be definition, reference, symbol, grep, idutils
650 (let ((xs (append (list (shell-quote-argument (ggtags-program-path "global"))
652 (format "--result=%s" ggtags-global-output-format)
653 (and ggtags-global-ignore-case "--ignore-case")
654 (and (ggtags-find-project)
655 (ggtags-project-has-color (ggtags-find-project))
657 (and (ggtags-find-project)
658 (ggtags-project-has-path-style (ggtags-find-project))
659 "--path-style=shorter")
660 (and ggtags-global-treat-text "--other")
663 (`definition "") ;-d not supported by Global 5.7.1
668 (`idutils "--idutils")))
670 (mapconcat #'identity (delq nil xs) " ")))
672 ;; takes three values: nil, t and a marker
673 (defvar ggtags-global-start-marker nil)
675 (defvar ggtags-global-exit-status 0)
676 (defvar ggtags-global-match-count 0)
678 (defvar ggtags-tag-ring-index nil)
680 (defun ggtags-global-save-start-marker ()
681 (when (markerp ggtags-global-start-marker)
682 (setq ggtags-tag-ring-index nil)
683 (ring-insert find-tag-marker-ring ggtags-global-start-marker)
684 (setq ggtags-global-start-marker t)))
686 (defun ggtags-global-start (command &optional directory)
687 (let* ((default-directory (or directory (ggtags-current-project-root)))
688 (split-window-preferred-function ggtags-split-window-function)
689 ;; See http://debbugs.gnu.org/13594
690 (display-buffer-overriding-action
691 (if (and ggtags-auto-jump-to-first-match
692 ;; Appeared in emacs 24.4.
693 (fboundp 'display-buffer-no-window))
694 (list #'display-buffer-no-window)
695 display-buffer-overriding-action)))
696 (setq ggtags-global-start-marker (point-marker))
697 (ggtags-navigation-mode +1)
698 (setq ggtags-global-exit-status 0
699 ggtags-global-match-count 0)
701 (ggtags-with-current-project
702 (setq ggtags-global-last-buffer
703 (compilation-start command 'ggtags-global-mode)))))
705 (defun ggtags-find-tag-continue ()
707 (ggtags-ensure-global-buffer
708 (ggtags-navigation-mode +1)
709 (let ((split-window-preferred-function ggtags-split-window-function))
710 (ignore-errors (compilation-next-error 1))
711 (compile-goto-error))))
713 (defun ggtags-find-tag (cmd &rest args)
714 (ggtags-check-project)
715 (ggtags-global-start (apply #'ggtags-global-build-command cmd args)))
718 (defun ggtags-find-tag-dwim (name &optional what)
719 "Find definitions or references of tag NAME by context.
720 If point is at a definition tag, find references, and vice versa.
721 With a prefix arg always find definitions. If point is at a line
722 that matches `ggtags-include-pattern', find the include file
725 (let ((include (and (not current-prefix-arg)
726 ggtags-include-pattern
729 (if (functionp ggtags-include-pattern)
730 (funcall ggtags-include-pattern)
731 (and (looking-at (car ggtags-include-pattern))
732 (match-string (cdr ggtags-include-pattern))))))))
733 (if include (list include 'include)
734 (list (ggtags-read-tag 'definition current-prefix-arg)
735 (and current-prefix-arg 'definition)))))
736 (ggtags-check-project) ; for `ggtags-current-project-root' below
739 (ggtags-find-file name))
740 ((or (eq what 'definition)
741 (not buffer-file-name)
742 (and (ggtags-find-project)
743 (not (ggtags-project-has-refs (ggtags-find-project)))))
744 (ggtags-find-tag 'definition name))
746 (format "--from-here=%d:%s"
748 (shell-quote-argument
749 ;; Note `ggtags-global-start' binds default-directory to
753 (if (string-prefix-p (ggtags-current-project-root)
755 (ggtags-current-project-root)
756 (locate-dominating-file buffer-file-name "GTAGS")))))
759 (defun ggtags-find-reference (name)
760 (interactive (list (ggtags-read-tag 'reference current-prefix-arg)))
761 (ggtags-find-tag 'reference name))
763 (defun ggtags-find-other-symbol (name)
764 "Find tag NAME that is a reference without a definition."
765 (interactive (list (ggtags-read-tag 'symbol current-prefix-arg)))
766 (ggtags-find-tag 'symbol name))
768 (defun ggtags-quote-pattern (pattern)
769 (prin1-to-string (substring-no-properties pattern)))
771 (defun ggtags-idutils-query (pattern)
772 (interactive (list (ggtags-read-tag 'id t)))
773 (ggtags-find-tag 'idutils "--" (ggtags-quote-pattern pattern)))
775 (defun ggtags-grep (pattern &optional invert-match)
776 "Use `global --grep' to search for lines matching PATTERN.
777 Invert the match when called with a prefix arg \\[universal-argument]."
778 (interactive (list (ggtags-read-tag 'definition 'confirm
779 (if current-prefix-arg
780 "Inverted grep pattern" "Grep pattern"))
782 (ggtags-find-tag 'grep (and invert-match "--invert-match")
783 "--" (ggtags-quote-pattern pattern)))
785 (defun ggtags-find-file (pattern &optional invert-match)
786 (interactive (list (ggtags-read-tag 'path 'confirm (if current-prefix-arg
787 "Inverted path pattern"
789 nil (thing-at-point 'filename))
791 (let ((ggtags-global-output-format 'path))
792 (ggtags-find-tag 'path (and invert-match "--invert-match")
793 "--" (ggtags-quote-pattern pattern))))
795 ;; NOTE: Coloured output in grep requested: http://goo.gl/Y9IcX
796 (defun ggtags-find-tag-regexp (regexp directory)
797 "List tags matching REGEXP in DIRECTORY (default to project root)."
800 (ggtags-check-project)
801 (list (ggtags-read-tag "" t "POSIX regexp")
802 (if current-prefix-arg
803 (read-directory-name "Directory: " nil nil t)
804 (ggtags-current-project-root)))))
805 (ggtags-check-project)
807 (ggtags-global-build-command nil nil "-l" "--" (ggtags-quote-pattern regexp))
808 (file-name-as-directory directory)))
810 (defun ggtags-query-replace (from to &optional delimited)
811 "Query replace FROM with TO on files in the Global buffer.
812 If not in navigation mode, do a grep on FROM first.
814 Note: the regular expression FROM must be supported by both
817 ;; Note: in 24.4 query-replace-read-args returns a list of 4 elements.
818 (let ((args (query-replace-read-args "Query replace (regexp)" t t)))
819 (list (nth 0 args) (nth 1 args) (nth 2 args))))
820 (unless (bound-and-true-p ggtags-navigation-mode)
821 (let ((ggtags-auto-jump-to-first-match nil))
825 (ggtags-ensure-global-buffer
826 (with-temp-message "Waiting for Grep to finish..."
827 (while (get-buffer-process (current-buffer))
829 (goto-char (point-min))
830 (while (ignore-errors (compilation-next-file 1) t)
831 (let ((m (get-text-property (point) 'compilation-message)))
832 (push (expand-file-name
833 (caar (compilation--loc->file-struct
834 (compilation--message->loc m))))
836 (ggtags-navigation-mode -1)
838 (tags-query-replace from to delimited file-form)))
840 (defun ggtags-save-to-register (r)
841 "Save current search session to register R.
842 Use \\[jump-to-register] to restore the search session."
843 (interactive (list (ggtags-ensure-global-buffer
844 (register-read-with-preview "Save search to register: "))))
845 (ggtags-ensure-global-buffer
846 (cl-labels ((jump (data)
848 (`(,command ,root ,line)
850 (let ((ggtags-auto-jump-to-first-match nil)
851 ;; Switch current project to ROOT.
852 (default-directory root)
853 (ggtags-project-root root))
854 (ggtags-global-start command root))
855 (add-hook 'compilation-finish-functions
857 (with-current-buffer buf
858 (ggtags-forward-to-line line)
859 (compile-goto-error)))
863 (`(,command ,root ,line)
864 (princ (format "a ggtags search session `%s' in directory `%s' at line %d."
865 command root line))))))
866 (set-register r (registerv-make
867 (list (car compilation-arguments) default-directory
868 (line-number-at-pos))
869 :jump-func #'jump :print-func #'prn)))))
871 (defun ggtags-delete-tag-files ()
872 "Delete the tag files generated by gtags."
873 (interactive (ignore (ggtags-check-project)))
874 (when (ggtags-current-project-root)
875 (let* ((re (concat "\\`" (regexp-opt '("GPATH" "GRTAGS" "GTAGS" "ID")) "\\'"))
876 (files (cl-remove-if-not
878 ;; Don't trust `directory-files'.
879 (let ((case-fold-search nil))
880 (string-match-p re (file-name-nondirectory file))))
881 (directory-files (ggtags-current-project-root) t re)))
882 (buffer "*GTags File List*"))
883 (or files (user-error "No tag files found"))
884 (with-output-to-temp-buffer buffer
885 (princ (mapconcat #'identity files "\n")))
886 (let ((win (get-buffer-window buffer)))
889 (fit-window-to-buffer win)
890 (when (yes-or-no-p "Remove GNU Global tag files? ")
891 (with-demoted-errors (mapc #'delete-file files))
892 (remhash (ggtags-current-project-root) ggtags-projects)
893 (and (overlayp ggtags-highlight-tag-overlay)
894 (delete-overlay ggtags-highlight-tag-overlay))))
895 (when (window-live-p win)
896 (quit-window t win)))))))
898 (defun ggtags-browse-file-as-hypertext (file line)
899 "Browse FILE in hypertext (HTML) form."
900 (interactive (if (or current-prefix-arg (not buffer-file-name))
901 (list (read-file-name "Browse file: " nil nil t)
902 (read-number "Line: " 1))
903 (list buffer-file-name (line-number-at-pos))))
904 (cl-check-type line integer)
905 (or (and file (file-exists-p file)) (error "File `%s' doesn't exist" file))
906 (ggtags-check-project)
907 (or (file-exists-p (expand-file-name "HTML" (ggtags-current-project-root)))
908 (if (yes-or-no-p "No hypertext form exists; run htags? ")
909 (let ((default-directory (ggtags-current-project-root)))
910 (ggtags-with-current-project (ggtags-process-string "htags")))
911 (user-error "Aborted")))
912 (let ((url (ggtags-process-string "gozilla" "-p" (format "+%d" line)
913 (file-relative-name file))))
914 (or (equal (file-name-extension
915 (url-filename (url-generic-parse-url url))) "html")
916 (user-error "No hypertext form for `%s'" file))
917 (when (called-interactively-p 'interactive)
918 (message "Browsing %s" url))
921 (defun ggtags-next-mark (&optional arg)
922 "Move to the next (newer) mark in the tag marker ring."
924 (and (ring-empty-p find-tag-marker-ring) (user-error "Tag ring empty"))
925 (setq ggtags-tag-ring-index
926 ;; Note `ring-minus1' gets newer item.
927 (funcall (if arg #'ring-plus1 #'ring-minus1)
928 (or ggtags-tag-ring-index
930 (ring-insert find-tag-marker-ring (point-marker))
932 (ring-length find-tag-marker-ring)))
933 (let ((m (ring-ref find-tag-marker-ring ggtags-tag-ring-index))
934 (i (- (ring-length find-tag-marker-ring) ggtags-tag-ring-index))
935 (message-log-max nil))
936 (message "%d%s marker%s" i (pcase (mod i 10)
937 ;; ` required for 24.1 and 24.2
942 (if (marker-buffer m) "" " (dead)"))
943 (if (not (marker-buffer m))
945 (switch-to-buffer (marker-buffer m))
948 (defun ggtags-prev-mark ()
949 "Move to the previous (older) mark in the tag marker ring."
951 (ggtags-next-mark 'previous))
953 (defun ggtags-view-tag-history ()
955 (and (ring-empty-p find-tag-marker-ring)
956 (user-error "Tag ring empty"))
957 (let ((split-window-preferred-function ggtags-split-window-function)
958 (inhibit-read-only t))
959 (pop-to-buffer "*Tag Ring*")
961 (tabulated-list-mode)
962 (setq tabulated-list-entries
963 ;; Use a function so that revert can work properly.
965 (let ((counter (ring-length find-tag-marker-ring))
966 (elements (or (ring-elements find-tag-marker-ring)
967 (user-error "Tag ring empty")))
969 (lambda (button) (interactive)
970 (let ((m (button-get button 'marker)))
971 (or (markerp m) (user-error "Marker dead"))
972 (setq ggtags-tag-ring-index
973 (ring-member find-tag-marker-ring m))
974 (pop-to-buffer (marker-buffer m))
975 (goto-char (marker-position m)))))
978 (with-current-buffer (marker-buffer m)
981 (buffer-substring (line-beginning-position)
982 (line-end-position)))))))
983 (setq tabulated-list-format
984 `[("ID" ,(max (1+ (floor (log counter 10))) 2)
985 (lambda (x y) (< (car x) (car y))))
986 ("Buffer" ,(max (cl-loop for m in elements
987 for b = (marker-buffer m)
989 (length (and b (buffer-name b))))
992 ("Position" ,(max (cl-loop for m in elements
993 for p = (or (marker-position m) 1)
994 maximize (1+ (floor (log p 10))))
997 (< (string-to-number (aref (cadr x) 2))
998 (string-to-number (aref (cadr y) 2))))
1000 ("Contents" 100 t)])
1001 (tabulated-list-init-header)
1005 (if (marker-buffer x)
1006 (vector (number-to-string counter)
1007 `(,(buffer-name (marker-buffer x))
1012 (number-to-string (marker-position x))
1013 (funcall get-line x))
1014 (vector (number-to-string counter)
1018 (setq tabulated-list-sort-key '("ID" . t))
1019 (tabulated-list-print)
1020 (fit-window-to-buffer)))
1022 (defun ggtags-global-exit-message-function (_process-status exit-status msg)
1023 (setq ggtags-global-exit-status exit-status)
1024 (pcase-let ((`(,count . ,db)
1026 (goto-char (point-max))
1027 (if (re-search-backward
1028 "^\\w+ \\(not found\\)\\|^\\([0-9]+\\) \\w+ located" nil t)
1029 (cons (or (and (match-string 1) 0)
1030 (string-to-number (match-string 2)))
1031 (when (re-search-forward
1032 "using \\(?:\\(idutils\\)\\|'[^']*/\\(\\w+\\)'\\)"
1035 (or (and (match-string 1) "ID")
1038 (setq ggtags-global-match-count count)
1039 ;; Clear the start marker in case of zero matches.
1041 (markerp ggtags-global-start-marker)
1042 (setq ggtags-global-start-marker nil))
1043 (cons (if (> exit-status 0)
1045 (format "found %d %s"
1047 (funcall (if (= count 1) #'car #'cadr)
1049 ;; ` required for 24.1 and 24.2
1050 (`"GTAGS" '("definition" "definitions"))
1051 (`"GSYMS" '("symbol" "symbols"))
1052 (`"GRTAGS" '("reference" "references"))
1053 (`"GPATH" '("file" "files"))
1054 (`"ID" '("identifier" "identifiers"))
1055 (_ '("match" "matches"))))))
1058 (defun ggtags-global-column (start)
1059 ;; START is the beginning position of source text.
1060 (let ((mbeg (text-property-any start (line-end-position) 'global-color t)))
1061 (and mbeg (- mbeg start))))
1063 ;;; NOTE: Must not match the 'Global started at Mon Jun 3 10:24:13'
1064 ;;; line or `compilation-auto-jump' will jump there and fail. See
1065 ;;; comments before the 'gnu' entry in
1066 ;;; `compilation-error-regexp-alist-alist'.
1067 (defvar ggtags-global-error-regexp-alist-alist
1069 `((path "^\\(?:[^\"'\n]*/\\)?[^ )\t\n]+$" 0)
1070 ;; ACTIVE_ESCAPE src/dialog.cc 172
1071 (ctags "^\\([^ \t\n]+\\)[ \t]+\\(.*?\\)[ \t]+\\([0-9]+\\)$"
1072 2 3 nil nil 2 (1 font-lock-function-name-face))
1073 ;; ACTIVE_ESCAPE 172 src/dialog.cc #undef ACTIVE_ESCAPE
1074 (ctags-x "^\\([^ \t\n]+\\)[ \t]+\\([0-9]+\\)[ \t]+\\(\\(?:[^/\n]*/\\)?[^ \t\n]+\\)"
1075 3 2 (,(lambda () (ggtags-global-column (1+ (match-end 0)))))
1076 nil 3 (1 font-lock-function-name-face))
1077 ;; src/dialog.cc:172:#undef ACTIVE_ESCAPE
1078 (grep "^\\(.+?\\):\\([0-9]+\\):\\(?:$\\|[^0-9\n]\\|[0-9][^0-9\n]\\|[0-9][0-9].\\)"
1079 1 2 (,(lambda () (ggtags-global-column (1+ (match-end 2))))) nil 1)
1080 ;; src/dialog.cc ACTIVE_ESCAPE 172 #undef ACTIVE_ESCAPE
1081 (cscope "^\\(.+?\\)[ \t]+\\([^ \t\n]+\\)[ \t]+\\([0-9]+\\).*\\(?:[^0-9\n]\\|[^0-9\n][0-9]\\|[^:\n][0-9][0-9]\\)$"
1082 1 3 nil nil 1 (2 font-lock-function-name-face)))
1083 compilation-error-regexp-alist-alist))
1085 (defun ggtags-abbreviate-file (start end)
1086 (let ((inhibit-read-only t)
1087 (amount (if (numberp ggtags-global-abbreviate-filename)
1088 (- (- end start) ggtags-global-abbreviate-filename)
1090 (advance-word (lambda ()
1091 "Return the length of the text made invisible."
1092 (let ((wend (min end (progn (forward-word 1) (point))))
1093 (wbeg (max start (progn (backward-word 1) (point)))))
1095 (if (<= (- wend wbeg) 1)
1097 (put-text-property (1+ wbeg) wend 'invisible t)
1098 (1- (- wend wbeg)))))))
1100 (while (and (> amount 0) (> end (point)))
1101 (cl-decf amount (funcall advance-word)))))
1103 (defun ggtags-abbreviate-files (start end)
1105 (let* ((error-re (cdr (assq ggtags-global-output-format
1106 ggtags-global-error-regexp-alist-alist)))
1107 (sub (cadr error-re)))
1108 (when (and ggtags-global-abbreviate-filename error-re)
1109 (while (re-search-forward (car error-re) end t)
1110 (when (and (or (not (numberp ggtags-global-abbreviate-filename))
1111 (> (length (match-string sub))
1112 ggtags-global-abbreviate-filename))
1113 ;; Ignore bogus file lines such as:
1114 ;; Global found 2 matches at Thu Jan 31 13:45:19
1115 (get-text-property (match-beginning sub) 'compilation-message))
1116 (ggtags-abbreviate-file (match-beginning sub) (match-end sub)))))))
1118 (defvar-local ggtags-global-output-lines 0)
1120 (defun ggtags-global--display-buffer (&optional buffer)
1121 (let ((buffer (or buffer (current-buffer))))
1122 (unless (get-buffer-window buffer)
1123 (let* ((split-window-preferred-function ggtags-split-window-function)
1124 (w (display-buffer buffer '(nil (allow-no-window . t)))))
1125 (and w (compilation-set-window-height w))))))
1127 (defvar ggtags-navigation-mode)
1129 (defun ggtags-global-filter ()
1130 "Called from `compilation-filter-hook' (which see)."
1131 (let ((ansi-color-apply-face-function
1132 (lambda (beg end face)
1134 (ansi-color-apply-overlay-face beg end face)
1135 (put-text-property beg end 'global-color t)))))
1136 (ansi-color-apply-on-region compilation-filter-start (point)))
1137 ;; Get rid of line "Using config file '/PATH/TO/.globalrc'." or
1138 ;; "Using default configuration."
1139 (when (re-search-backward
1140 "^ *Using \\(?:config file '.*\\|default configuration.\\)\n"
1141 compilation-filter-start t)
1143 (cl-incf ggtags-global-output-lines
1144 (count-lines compilation-filter-start (point)))
1145 (when (and (> ggtags-global-output-lines 5) ggtags-navigation-mode)
1146 (ggtags-global--display-buffer))
1147 (make-local-variable 'ggtags-global-large-output)
1148 (when (> ggtags-global-output-lines ggtags-global-large-output)
1149 (cl-incf ggtags-global-large-output 500)
1150 (let ((message-log-max nil))
1151 (message "Output %d lines (Type `C-c C-k' to cancel)"
1152 ggtags-global-output-lines))))
1154 (defun ggtags-handle-single-match (buf how)
1155 (if (string-prefix-p "exited abnormally" how)
1156 ;; If exit abnormally display the buffer for inspection.
1157 (ggtags-global--display-buffer)
1158 (when (and ggtags-auto-jump-to-first-match
1160 (goto-char (point-min))
1162 (goto-char (compilation-next-single-property-change
1163 (point) 'compilation-message))
1165 (compilation-next-single-property-change
1166 (point) 'compilation-message)))))
1167 ;; For the `compilation-auto-jump' in idle timer to run. See also:
1168 ;; http://debbugs.gnu.org/13829
1170 (ggtags-navigation-mode -1)
1171 (ggtags-navigation-mode-cleanup buf 0))))
1173 (defvar ggtags-global-mode-font-lock-keywords
1174 '(("^Global \\(exited abnormally\\|interrupt\\|killed\\|terminated\\)\\(?:.*with code \\([0-9]+\\)\\)?.*"
1175 (1 'compilation-error)
1176 (2 'compilation-error nil t))
1177 ("^Global found \\([0-9]+\\)" (1 compilation-info-face))))
1179 (defvar compilation-always-kill) ;new in 24.3
1181 (define-compilation-mode ggtags-global-mode "Global"
1182 "A mode for showing outputs from gnu global."
1183 ;; Make it buffer local for `ggtags-abbreviate-files'.
1184 (make-local-variable 'ggtags-global-output-format)
1185 (setq-local compilation-error-regexp-alist
1186 (list ggtags-global-output-format))
1187 (setq-local compilation-auto-jump-to-first-error
1188 ggtags-auto-jump-to-first-match)
1189 (setq-local compilation-scroll-output nil)
1190 ;; See `compilation-move-to-column' for details.
1191 (setq-local compilation-first-column 0)
1192 (setq-local compilation-error-screen-columns nil)
1193 (setq-local compilation-disable-input t)
1194 (setq-local compilation-always-kill t)
1195 (setq-local compilation-error-face 'compilation-info)
1196 (setq-local compilation-exit-message-function
1197 'ggtags-global-exit-message-function)
1198 ;; See: https://github.com/leoliu/ggtags/issues/26
1199 (setq-local find-file-suppress-same-file-warnings t)
1200 (setq-local truncate-lines t)
1201 (jit-lock-register #'ggtags-abbreviate-files)
1202 (add-hook 'compilation-filter-hook 'ggtags-global-filter nil 'local)
1203 (add-hook 'compilation-finish-functions 'ggtags-handle-single-match nil t)
1204 (add-hook 'kill-buffer-hook (lambda () (ggtags-navigation-mode -1)) nil t))
1206 ;; NOTE: Need this to avoid putting menu items in
1207 ;; `emulation-mode-map-alists', which creates double entries. See
1208 ;; http://i.imgur.com/VJJTzVc.png
1209 (defvar ggtags-navigation-map
1210 (let ((map (make-sparse-keymap)))
1211 (define-key map "\M-n" 'next-error)
1212 (define-key map "\M-p" 'previous-error)
1213 (define-key map "\M-}" 'ggtags-navigation-next-file)
1214 (define-key map "\M-{" 'ggtags-navigation-previous-file)
1215 (define-key map "\M->" 'ggtags-navigation-last-error)
1216 (define-key map "\M-<" 'ggtags-navigation-first-error)
1217 ;; Note: shadows `isearch-forward-regexp' but it can be invoked
1218 ;; with C-u C-s instead.
1219 (define-key map "\C-\M-s" 'ggtags-navigation-isearch-forward)
1220 (define-key map "\C-c\C-k"
1221 (lambda () (interactive)
1222 (ggtags-ensure-global-buffer (kill-compilation))))
1223 (define-key map "\M-o" 'ggtags-navigation-visible-mode)
1224 (define-key map [return] 'ggtags-navigation-mode-done)
1225 (define-key map "\r" 'ggtags-navigation-mode-done)
1226 (define-key map [remap pop-tag-mark] 'ggtags-navigation-mode-abort)
1229 (defvar ggtags-mode-map-alist
1230 `((ggtags-navigation-mode . ,ggtags-navigation-map)))
1232 ;; Higher priority for `ggtags-navigation-mode' to avoid being
1233 ;; hijacked by modes such as `view-mode'.
1234 (add-to-list 'emulation-mode-map-alists 'ggtags-mode-map-alist)
1236 (defvar ggtags-navigation-mode-map
1237 (let ((map (make-sparse-keymap))
1238 (menu (make-sparse-keymap "GG-Navigation")))
1239 ;; Menu items: (info "(elisp)Extended Menu Items")
1240 (define-key map [menu-bar ggtags-navigation] (cons "GG-Navigation" menu))
1241 ;; Ordered backwards
1242 (define-key menu [visible-mode]
1243 '(menu-item "Visible mode" ggtags-navigation-visible-mode
1244 :button (:toggle . (ignore-errors
1245 (ggtags-ensure-global-buffer
1247 (define-key menu [done]
1248 '(menu-item "Finish navigation" ggtags-navigation-mode-done))
1249 (define-key menu [abort]
1250 '(menu-item "Abort" ggtags-navigation-mode-abort))
1251 (define-key menu [last-error]
1252 '(menu-item "Last error" ggtags-navigation-last-error))
1253 (define-key menu [fist-error]
1254 '(menu-item "Fist error" ggtags-navigation-first-error))
1255 (define-key menu [previous-file]
1256 '(menu-item "Previous file" ggtags-navigation-previous-file))
1257 (define-key menu [next-file]
1258 '(menu-item "Next file" ggtags-navigation-next-file))
1259 (define-key menu [previous]
1260 '(menu-item "Previous match" previous-error))
1261 (define-key menu [next]
1262 '(menu-item "Next match" next-error))
1265 (defun ggtags-move-to-tag (&optional name)
1266 "Move to NAME tag in current line."
1267 (let ((tag (or name ggtags-current-tag-name)))
1268 ;; Do nothing if on the tag already i.e. by `ggtags-global-column'.
1269 (unless (or (not tag) (looking-at (concat (regexp-quote tag) "\\_>")))
1270 (let ((orig (point))
1271 (regexps (mapcar (lambda (fmtstr)
1272 (format fmtstr (regexp-quote tag)))
1273 '("\\_<%s\\_>" "%s\\_>" "%s"))))
1275 (if (cl-loop for re in regexps
1276 ;; Note: tag might not agree with current
1277 ;; major-mode's symbol, so try harder. For
1278 ;; example, in `php-mode' $cacheBackend is a
1279 ;; symbol, but cacheBackend is a tag.
1280 thereis (re-search-forward re (line-end-position) t))
1281 (goto-char (match-beginning 0))
1282 (goto-char orig))))))
1284 (defun ggtags-navigation-mode-cleanup (&optional buf time)
1285 (let ((buf (or buf ggtags-global-last-buffer)))
1286 (and (buffer-live-p buf)
1287 (with-current-buffer buf
1288 (when (get-buffer-process (current-buffer))
1290 (when (and (derived-mode-p 'ggtags-global-mode)
1291 (get-buffer-window))
1292 (quit-window nil (get-buffer-window)))
1293 (and time (run-with-idle-timer time nil #'kill-buffer buf))))))
1295 (defun ggtags-navigation-mode-done ()
1297 (ggtags-navigation-mode -1)
1298 (setq tags-loop-scan t
1299 tags-loop-operate '(ggtags-find-tag-continue))
1300 (ggtags-navigation-mode-cleanup))
1302 (defun ggtags-navigation-mode-abort ()
1304 (ggtags-navigation-mode -1)
1305 ;; Run after (ggtags-navigation-mode -1) or
1306 ;; ggtags-global-start-marker might not have been saved.
1307 (when (and ggtags-global-start-marker
1308 (not (markerp ggtags-global-start-marker)))
1309 (setq ggtags-global-start-marker nil)
1311 (ggtags-navigation-mode-cleanup nil 0))
1313 (defun ggtags-navigation-next-file (n)
1315 (ggtags-ensure-global-buffer
1316 (compilation-next-file n)
1317 (compile-goto-error)))
1319 (defun ggtags-navigation-previous-file (n)
1321 (ggtags-navigation-next-file (- n)))
1323 (defun ggtags-navigation-first-error ()
1325 (ggtags-ensure-global-buffer
1326 (goto-char (point-min))
1327 (compilation-next-error 1)
1328 (compile-goto-error)))
1330 (defun ggtags-navigation-last-error ()
1332 (ggtags-ensure-global-buffer
1333 (goto-char (point-max))
1334 (compilation-previous-error 1)
1335 (compile-goto-error)))
1337 (defun ggtags-navigation-isearch-forward (&optional regexp-p)
1339 (ggtags-ensure-global-buffer
1340 (let ((saved (if visible-mode 1 -1)))
1342 (with-selected-window (get-buffer-window (current-buffer))
1343 (isearch-forward regexp-p)
1345 (visible-mode saved)
1346 (compile-goto-error)))))
1348 (defun ggtags-navigation-visible-mode (&optional arg)
1349 (interactive (list (or current-prefix-arg 'toggle)))
1350 (ggtags-ensure-global-buffer
1351 (visible-mode arg)))
1353 (defvar ggtags-global-line-overlay nil)
1355 (defun ggtags-global-next-error-function ()
1356 (ggtags-move-to-tag)
1357 (ggtags-global-save-start-marker)
1358 (and (ggtags-project-update-mtime-maybe)
1359 (message "File `%s' is newer than GTAGS"
1360 (file-name-nondirectory buffer-file-name)))
1361 (and ggtags-mode-sticky (ggtags-mode 1))
1363 (ggtags-ensure-global-buffer
1364 (unless (overlayp ggtags-global-line-overlay)
1365 (setq ggtags-global-line-overlay (make-overlay (point) (point)))
1366 (overlay-put ggtags-global-line-overlay 'face 'ggtags-global-line))
1367 (move-overlay ggtags-global-line-overlay
1368 (line-beginning-position) (line-end-position)
1370 (run-hooks 'ggtags-find-tag-hook))
1372 (define-minor-mode ggtags-navigation-mode nil
1376 (ggtags-ensure-global-buffer
1377 (let ((index (when (get-text-property (line-beginning-position)
1378 'compilation-message)
1379 ;; Assume the first match appears at line 5
1380 (- (line-number-at-pos) 4))))
1381 `((:propertize ,(if index
1382 (number-to-string (max index 0))
1383 "?") face success) "/")))))
1384 (:propertize (:eval (number-to-string ggtags-global-match-count))
1387 (unless (zerop ggtags-global-exit-status)
1388 `(":" (:propertize ,(number-to-string ggtags-global-exit-status)
1392 (if ggtags-navigation-mode
1394 (add-hook 'next-error-hook 'ggtags-global-next-error-function)
1395 (add-hook 'minibuffer-setup-hook 'ggtags-minibuffer-setup-function))
1396 (remove-hook 'next-error-hook 'ggtags-global-next-error-function)
1397 (remove-hook 'minibuffer-setup-hook 'ggtags-minibuffer-setup-function)))
1399 (defun ggtags-minibuffer-setup-function ()
1400 ;; Disable ggtags-navigation-mode in minibuffer.
1401 (setq-local ggtags-navigation-mode nil))
1403 (defun ggtags-kill-file-buffers (&optional interactive)
1404 "Kill all buffers visiting files in current project."
1406 (ggtags-check-project)
1407 (let ((directories (cons (ggtags-current-project-root) (ggtags-get-libpath)))
1409 (dolist (buf (buffer-list))
1410 (let ((file (and (buffer-live-p buf)
1411 (not (eq buf (current-buffer)))
1412 (buffer-file-name buf))))
1413 (when (and file (cl-some (lambda (dir)
1414 ;; Don't use `file-in-directory-p'
1415 ;; to allow symbolic links.
1416 (string-prefix-p dir file))
1418 (and (kill-buffer buf) (cl-incf count)))))
1420 (message "%d %s killed" count (if (= count 1) "buffer" "buffers")))))
1422 (defun ggtags-after-save-function ()
1423 (when (ggtags-find-project)
1424 (ggtags-project-update-mtime-maybe)
1425 ;; When oversize update on a per-save basis.
1426 (when (and buffer-file-name
1427 (or ggtags-global-always-update (ggtags-project-oversize-p)))
1428 (ggtags-with-current-project
1429 (process-file (ggtags-program-path "global") nil 0 nil "--single-update"
1430 (file-relative-name buffer-file-name))))))
1432 (defun ggtags-global-output (buffer cmds callback &optional cutoff)
1433 "Asynchrously pipe the output of running CMDS to BUFFER.
1434 When finished invoke CALLBACK in BUFFER with process exit status."
1435 (or buffer (error "Output buffer required"))
1436 (let* ((program (car cmds))
1438 (cutoff (and cutoff (+ cutoff (if (get-buffer buffer)
1439 (with-current-buffer buffer
1440 (line-number-at-pos (point-max)))
1442 (proc (apply #'start-file-process program buffer program args))
1443 (filter (lambda (proc string)
1444 (and (buffer-live-p (process-buffer proc))
1445 (with-current-buffer (process-buffer proc)
1446 (goto-char (process-mark proc))
1448 (when (and (> (line-number-at-pos (point-max)) cutoff)
1449 (process-live-p proc))
1450 (interrupt-process (current-buffer)))))))
1451 (sentinel (lambda (proc _msg)
1452 (when (memq (process-status proc) '(exit signal))
1453 (with-current-buffer (process-buffer proc)
1454 (set-process-buffer proc nil)
1455 (funcall callback (process-exit-status proc)))))))
1456 (set-process-query-on-exit-flag proc nil)
1457 (and cutoff (set-process-filter proc filter))
1458 (set-process-sentinel proc sentinel)
1461 (defun ggtags-show-definition-default (defs)
1462 (let (message-log-max)
1463 (message "%s%s" (or (caar defs) "[definition not found]")
1464 (if (cdr defs) " [guess]" ""))))
1466 (defun ggtags-show-definition (name)
1467 (interactive (list (ggtags-read-tag 'definition current-prefix-arg)))
1468 (ggtags-check-project)
1469 (let* ((re (cadr (assq 'grep ggtags-global-error-regexp-alist-alist)))
1470 (current (current-buffer))
1471 (buffer (get-buffer-create " *ggtags-definition*"))
1472 (fn ggtags-show-definition-function)
1473 (show (lambda (_status)
1474 (goto-char (point-min))
1475 (let ((defs (cl-loop while (re-search-forward re nil t)
1476 collect (list (buffer-substring (1+ (match-end 2))
1477 (line-end-position))
1480 (string-to-number (match-string 2))))))
1481 (kill-buffer buffer)
1482 (with-current-buffer current
1483 (funcall fn defs))))))
1484 (ggtags-with-current-project
1485 (ggtags-global-output
1487 (list (ggtags-program-path "global")
1488 "--result=grep" "--path-style=absolute" name)
1491 (defvar ggtags-mode-prefix-map
1492 (let ((m (make-sparse-keymap)))
1493 (define-key m "\M-'" 'previous-error)
1494 (define-key m (kbd "M-DEL") 'ggtags-delete-tag-files)
1495 (define-key m "\M-p" 'ggtags-prev-mark)
1496 (define-key m "\M-n" 'ggtags-next-mark)
1497 (define-key m "\M-f" 'ggtags-find-file)
1498 (define-key m "\M-o" 'ggtags-find-other-symbol)
1499 (define-key m "\M-g" 'ggtags-grep)
1500 (define-key m "\M-i" 'ggtags-idutils-query)
1501 (define-key m "\M-b" 'ggtags-browse-file-as-hypertext)
1502 (define-key m "\M-k" 'ggtags-kill-file-buffers)
1503 (define-key m "\M-h" 'ggtags-view-tag-history)
1504 (define-key m "\M-j" 'ggtags-visit-project-root)
1505 (define-key m (kbd "M-SPC") 'ggtags-save-to-register)
1506 (define-key m (kbd "M-%") 'ggtags-query-replace)
1507 (define-key m "\M-?" 'ggtags-show-definition)
1510 (defvar ggtags-mode-map
1511 (let ((map (make-sparse-keymap))
1512 (menu (make-sparse-keymap "Ggtags")))
1513 (define-key map "\M-." 'ggtags-find-tag-dwim)
1514 (define-key map (kbd "M-]") 'ggtags-find-reference)
1515 (define-key map (kbd "C-M-.") 'ggtags-find-tag-regexp)
1516 (define-key map ggtags-mode-prefix-key ggtags-mode-prefix-map)
1518 (define-key map [menu-bar ggtags] (cons "Ggtags" menu))
1519 ;; Ordered backwards
1520 (define-key menu [report-bugs]
1521 `(menu-item "Report bugs"
1522 (lambda () (interactive)
1523 (browse-url ggtags-bug-url)
1524 (message "Please visit %s" ggtags-bug-url))
1525 :help ,(format "Visit %s" ggtags-bug-url)))
1526 (define-key menu [custom-ggtags]
1527 '(menu-item "Customize Ggtags"
1528 (lambda () (interactive) (customize-group 'ggtags))))
1529 (define-key menu [save-project]
1530 '(menu-item "Save project settings" ggtags-save-project-settings))
1531 (define-key menu [toggle-read-only]
1532 '(menu-item "Toggle project read-only" ggtags-toggle-project-read-only
1533 :button (:toggle . buffer-read-only)))
1534 (define-key menu [visit-project-root]
1535 '(menu-item "Visit project root" ggtags-visit-project-root))
1536 (define-key menu [sep2] menu-bar-separator)
1537 (define-key menu [browse-hypertext]
1538 '(menu-item "Browse as hypertext" ggtags-browse-file-as-hypertext
1539 :enable (ggtags-find-project)))
1540 (define-key menu [delete-tags]
1541 '(menu-item "Delete tag files" ggtags-delete-tag-files
1542 :enable (ggtags-find-project)))
1543 (define-key menu [kill-buffers]
1544 '(menu-item "Kill project file buffers" ggtags-kill-file-buffers
1545 :enable (ggtags-find-project)))
1546 (define-key menu [view-tag]
1547 '(menu-item "View tag history" ggtags-view-tag-history))
1548 (define-key menu [pop-mark]
1549 '(menu-item "Pop mark" pop-tag-mark
1550 :help "Pop to previous mark and destroy it"))
1551 (define-key menu [next-mark]
1552 '(menu-item "Next mark" ggtags-next-mark))
1553 (define-key menu [prev-mark]
1554 '(menu-item "Previous mark" ggtags-prev-mark))
1555 (define-key menu [sep1] menu-bar-separator)
1556 (define-key menu [save-to-register]
1557 '(menu-item "Save search session" ggtags-save-to-register))
1558 (define-key menu [previous-error]
1559 '(menu-item "Previous match" previous-error))
1560 (define-key menu [next-error]
1561 '(menu-item "Next match" next-error))
1562 (define-key menu [find-file]
1563 '(menu-item "Find files" ggtags-find-file))
1564 (define-key menu [query-replace]
1565 '(menu-item "Query replace" ggtags-query-replace))
1566 (define-key menu [idutils]
1567 '(menu-item "Query idutils DB" ggtags-idutils-query))
1568 (define-key menu [grep]
1569 '(menu-item "Grep" ggtags-grep))
1570 (define-key menu [find-symbol]
1571 '(menu-item "Find other symbol" ggtags-find-other-symbol))
1572 (define-key menu [find-tag-regexp]
1573 '(menu-item "Find tag matching regexp" ggtags-find-tag-regexp))
1574 (define-key menu [show-definition]
1575 '(menu-item "Show definition" ggtags-show-definition))
1576 (define-key menu [find-reference]
1577 '(menu-item "Find reference" ggtags-find-reference))
1578 (define-key menu [find-tag-continue]
1579 '(menu-item "Continue find tag" tags-loop-continue))
1580 (define-key menu [find-tag]
1581 '(menu-item "Find tag" ggtags-find-tag-dwim))
1582 (define-key menu [update-tags]
1583 '(menu-item "Update tag files" ggtags-update-tags
1584 :visible (ggtags-find-project)))
1585 (define-key menu [run-gtags]
1586 '(menu-item "Run gtags" ggtags-create-tags
1587 :visible (not (ggtags-find-project))))
1590 (defvar ggtags-mode-line-project-keymap
1591 (let ((map (make-sparse-keymap)))
1592 (define-key map [mode-line mouse-1] 'ggtags-visit-project-root)
1595 (put 'ggtags-mode-line-project-name 'risky-local-variable t)
1596 (defvar ggtags-mode-line-project-name
1597 '("[" (:eval (let ((name (if (stringp ggtags-project-root)
1598 (file-name-nondirectory
1599 (directory-file-name ggtags-project-root))
1602 name 'face compilation-info-face
1603 'help-echo (if (stringp ggtags-project-root)
1604 (concat "mouse-1 to visit " ggtags-project-root)
1605 "mouse-1 to set project")
1606 'mouse-face 'mode-line-highlight
1607 'keymap ggtags-mode-line-project-keymap)))
1611 (define-minor-mode ggtags-mode nil
1612 :lighter (:eval (if ggtags-navigation-mode "" " GG"))
1613 (unless (timerp ggtags-highlight-tag-timer)
1614 (setq ggtags-highlight-tag-timer
1615 (run-with-idle-timer
1616 ggtags-highlight-tag-delay t #'ggtags-highlight-tag-at-point)))
1619 (add-hook 'after-save-hook 'ggtags-after-save-function nil t)
1620 ;; Append to serve as a fallback method.
1621 (add-hook 'completion-at-point-functions
1622 #'ggtags-completion-at-point t t)
1623 (unless (memq 'ggtags-mode-line-project-name
1624 mode-line-buffer-identification)
1625 (setq mode-line-buffer-identification
1626 (append mode-line-buffer-identification
1627 '(ggtags-mode-line-project-name)))))
1628 (remove-hook 'after-save-hook 'ggtags-after-save-function t)
1629 (remove-hook 'completion-at-point-functions #'ggtags-completion-at-point t)
1630 (setq mode-line-buffer-identification
1631 (delq 'ggtags-mode-line-project-name mode-line-buffer-identification))
1632 (and (overlayp ggtags-highlight-tag-overlay)
1633 (delete-overlay ggtags-highlight-tag-overlay))
1634 (setq ggtags-highlight-tag-overlay nil)))
1636 (defvar ggtags-highlight-tag-map
1637 (let ((map (make-sparse-keymap)))
1638 ;; Bind down- events so that the global keymap won't ``shine
1639 ;; through''. See `mode-line-buffer-identification-keymap' for
1640 ;; similar workaround.
1641 (define-key map [S-mouse-1] 'ggtags-find-tag-dwim)
1642 (define-key map [S-down-mouse-1] 'ignore)
1643 (define-key map [S-mouse-3] 'ggtags-find-reference)
1644 (define-key map [S-down-mouse-3] 'ignore)
1646 "Keymap used for valid tag at point.")
1648 (put 'ggtags-active-tag 'face 'ggtags-highlight)
1649 (put 'ggtags-active-tag 'keymap ggtags-highlight-tag-map)
1650 ;; (put 'ggtags-active-tag 'mouse-face 'match)
1651 (put 'ggtags-active-tag 'help-echo
1652 "S-mouse-1 for definitions\nS-mouse-3 for references")
1654 (defun ggtags-highlight-tag-at-point ()
1655 (when (and ggtags-mode ggtags-project-root (ggtags-find-project))
1656 (unless (overlayp ggtags-highlight-tag-overlay)
1657 (setq ggtags-highlight-tag-overlay (make-overlay (point) (point) nil t))
1658 (overlay-put ggtags-highlight-tag-overlay 'modification-hooks
1659 (list (lambda (o after &rest _args)
1660 (and (not after) (delete-overlay o))))))
1661 (let ((bounds (funcall ggtags-bounds-of-tag-function))
1662 (o ggtags-highlight-tag-overlay))
1665 (eq (overlay-buffer o) (current-buffer))
1666 (= (overlay-start o) (car bounds))
1667 (= (overlay-end o) (cdr bounds)))
1668 ;; Overlay matches current tag so do nothing.
1670 ((and bounds (let ((completion-ignore-case nil))
1672 (buffer-substring (car bounds) (cdr bounds))
1673 ggtags-completion-table)))
1674 (move-overlay o (car bounds) (cdr bounds) (current-buffer))
1675 (overlay-put o 'category 'ggtags-active-tag))
1677 (or (car bounds) (point))
1678 (or (cdr bounds) (point))
1680 (overlay-put o 'category nil))))))
1684 (defun ggtags-goto-imenu-index (name line &rest _args)
1685 (ggtags-forward-to-line line)
1686 (ggtags-move-to-tag name))
1689 (defun ggtags-build-imenu-index ()
1690 "A function suitable for `imenu-create-index-function'."
1691 (let ((file (and buffer-file-name (file-relative-name buffer-file-name))))
1692 (and file (with-temp-buffer
1693 (when (with-demoted-errors
1694 (zerop (ggtags-with-current-project
1695 (process-file (ggtags-program-path "global")
1696 nil t nil "-x" "-f" file))))
1697 (goto-char (point-min))
1698 (cl-loop while (re-search-forward
1699 "^\\([^ \t]+\\)[ \t]+\\([0-9]+\\)" nil t)
1700 collect (list (match-string 1)
1701 (string-to-number (match-string 2))
1702 'ggtags-goto-imenu-index)))))))
1707 (defun try-complete-ggtags-tag (old)
1708 "A function suitable for `hippie-expand-try-functions-list'."
1709 (with-no-warnings ; to avoid loading hippie-exp
1711 (he-init-string (if (looking-back "\\_<.*" (line-beginning-position))
1715 (setq he-expand-list
1716 (and (not (equal he-search-string ""))
1717 (ggtags-find-project)
1718 (sort (all-completions he-search-string
1719 ggtags-completion-table)
1721 (if (null he-expand-list)
1723 (if old (he-reset-string))
1725 (he-substitute-string (car he-expand-list))
1726 (setq he-expand-list (cdr he-expand-list))
1729 (defun ggtags-reload (&optional force)
1731 (unload-feature 'ggtags force)
1734 (defun ggtags-unload-function ()
1735 (setq emulation-mode-map-alists
1736 (delq 'ggtags-mode-map-alist emulation-mode-map-alists))
1740 ;;; ggtags.el ends here