]> code.delx.au - gnu-emacs-elpa/blobdiff - packages/ggtags/ggtags.el
* GNUmakefile: Obey a .elpaignore file in a package's root directory.
[gnu-emacs-elpa] / packages / ggtags / ggtags.el
index 20600bfb2a190cf3da05439d9d05bb3d00e22715..3f77656b943d1157a8ccfe8f359f926d47f7bbf0 100644 (file)
@@ -3,7 +3,7 @@
 ;; Copyright (C) 2013  Free Software Foundation, Inc.
 
 ;; Author: Leo Liu <sdl.web@gmail.com>
-;; Version: 0.6.1
+;; Version: 0.6.7
 ;; Keywords: tools, convenience
 ;; Created: 2013-01-29
 ;; URL: https://github.com/leoliu/ggtags
 
 ;;; Commentary:
 
-;; Use GNU Global source code tagging system in Emacs.
-;; http://www.gnu.org/software/global
+;; A package to integrate GNU Global source code tagging system
+;; (http://www.gnu.org/software/global) with Emacs.
+;;
+;; 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.
+;;
+;; `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. `C-u M-.' is verbose and will ask you the name - with
+;; completion - and the type of tag to search.
+;;
+;; If multiple matches are found, navigation mode is entered. 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
+;; resume 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.
 
 ;;; Code:
 
 (eval-when-compile (require 'cl))
 (require 'compile)
-(require 'etags)                        ; for find-tag-marker-ring
 
 (if (not (fboundp 'comment-string-strip))
     (autoload 'comment-string-strip "newcomment"))
 (eval-when-compile
   (unless (fboundp 'setq-local)
     (defmacro setq-local (var val)
-      (list 'set (list 'make-local-variable (list 'quote var)) val))))
+      (list 'set (list 'make-local-variable (list 'quote var)) val)))
+
+  (unless (fboundp 'defvar-local)
+    (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))))))
+
+(eval-and-compile
+  (unless (fboundp 'user-error)
+    (defalias 'user-error 'error)))
 
 (defgroup ggtags nil
   "GNU Global source code tagging system."
@@ -65,6 +96,32 @@ If nil, use Emacs default."
                  integer)
   :group 'ggtags)
 
+(defcustom ggtags-oversize-limit (* 50 1024 1024)
+  "The over size limit for the  GTAGS file."
+  :type '(choice (const :tag "None" nil)
+                 (const :tag "Always" t)
+                 number)
+  :group 'ggtags)
+
+(defcustom ggtags-split-window-function split-window-preferred-function
+  "A function to control how ggtags pops up the auxiliary window."
+  :type 'function
+  :group 'ggtags)
+
+(defcustom ggtags-global-output-format 'grep
+  "The output format for the 'global' command."
+  :type '(choice (const path)
+                 (const ctags)
+                 (const ctags-x)
+                 (const grep)
+                 (const cscope))
+  :group 'ggtags)
+
+(defcustom ggtags-completing-read-function completing-read-function
+  "Ggtags specific `completing-read-function' (which see)."
+  :type 'function
+  :group 'ggtags)
+
 (defvar ggtags-cache nil)               ; (ROOT TABLE DIRTY TIMESTAMP)
 
 (defvar ggtags-current-tag-name nil)
@@ -73,20 +130,18 @@ If nil, use Emacs default."
 (defvar ggtags-global-error "match"
   "Stem of message to print when no matches are found.")
 
-(defmacro ggtags-ignore-file-error (&rest body)
-  (declare (indent 0))
-  `(condition-case nil
-       (progn ,@body)
-     (file-error nil)))
-
 ;; http://thread.gmane.org/gmane.comp.gnu.global.bugs/1518
 (defvar ggtags-global-has-path-style    ; introduced in global 6.2.8
-  (ggtags-ignore-file-error
-    (and (string-match-p "^--path-style "
-                         (shell-command-to-string "global --help"))
-         t))
+  (with-demoted-errors                  ; in case `global' not found
+    (zerop (call-process "global" nil nil nil
+                         "--path-style" "shorter" "--help")))
   "Non-nil if `global' supports --path-style switch.")
 
+;; http://thread.gmane.org/gmane.comp.gnu.global.bugs/1542
+(defvar ggtags-global-has-color         ; introduced in global 6.2.9
+  (with-demoted-errors
+    (zerop (call-process "global" nil nil nil "--color" "--help"))))
+
 (defmacro ggtags-ensure-global-buffer (&rest body)
   (declare (indent 0))
   `(progn
@@ -96,6 +151,16 @@ If nil, use Emacs default."
          (error "No global buffer found"))
      (with-current-buffer compilation-last-buffer ,@body)))
 
+(defun ggtags-oversize-p ()
+  (pcase ggtags-oversize-limit
+    (`nil nil)
+    (`t t)
+    (t (when (ggtags-root-directory)
+         (> (or (nth 7 (file-attributes
+                        (expand-file-name "GTAGS" (ggtags-root-directory))))
+                0)
+            ggtags-oversize-limit)))))
+
 (defun ggtags-get-timestamp (root)
   "Get the timestamp (float) of file GTAGS in ROOT directory.
 Return -1 if it does not exist."
@@ -132,72 +197,79 @@ Return -1 if it does not exist."
   (> (ggtags-get-timestamp key)
      (or (fourth (ggtags-cache-get key)) 0)))
 
+(defvar-local ggtags-root-directory nil
+  "Internal; use function `ggtags-root-directory' instead.")
+
 ;;;###autoload
 (defun ggtags-root-directory ()
-  (ggtags-ignore-file-error
-    (with-temp-buffer
-      (when (zerop (call-process "global" nil (list t nil) nil "-pr"))
-        (file-name-as-directory
-         (comment-string-strip (buffer-string) t t))))))
+  (or ggtags-root-directory
+      (setq ggtags-root-directory
+            (with-temp-buffer
+              (when (zerop (call-process "global" nil (list t nil) nil "-pr"))
+                (file-name-as-directory
+                 (comment-string-strip (buffer-string) t t)))))))
 
 (defun ggtags-check-root-directory ()
   (or (ggtags-root-directory) (error "File GTAGS not found")))
 
 (defun ggtags-ensure-root-directory ()
   (or (ggtags-root-directory)
-      (if (yes-or-no-p "File GTAGS not found; run gtags? ")
-          (let ((root (read-directory-name "Directory: " nil nil t)))
-            (and (= (length root) 0) (error "No directory chosen"))
-            (ggtags-ignore-file-error
-              (with-temp-buffer
-                (if (zerop (let ((default-directory
-                                   (file-name-as-directory root)))
-                             (call-process "gtags" nil t)))
-                    (message "File GTAGS generated in `%s'"
-                             (ggtags-root-directory))
-                  (error "%s" (comment-string-strip (buffer-string) t t))))))
-        (error "Aborted"))))
-
-(defun ggtags-tag-names-1 (root &optional prefix)
+      (when (or (yes-or-no-p "File GTAGS not found; run gtags? ")
+                (error "Aborted"))
+        (let ((root (read-directory-name "Directory: " nil nil t)))
+          (and (= (length root) 0) (error "No directory chosen"))
+          (when (with-temp-buffer
+                  (let ((default-directory
+                          (file-name-as-directory root)))
+                    (or (zerop (call-process "gtags" nil t))
+                        (error "%s" (comment-string-strip
+                                     (buffer-string) t t)))))
+            (message "File GTAGS generated in `%s'"
+                     (ggtags-root-directory)))))))
+
+(defun ggtags-tag-names-1 (root &optional from-cache)
   (when root
-    (if (ggtags-cache-stale-p root)
+    (if (and (not from-cache) (ggtags-cache-stale-p root))
         (let* ((default-directory (file-name-as-directory root))
                (tags (with-demoted-errors
-                       (split-string
-                        (with-output-to-string
-                          (call-process "global" nil (list standard-output nil)
-                                        nil "-c" (or prefix "")))))))
+                       (process-lines "global" "-c" ""))))
           (and tags (ggtags-cache-set root tags))
           tags)
       (cadr (ggtags-cache-get root)))))
 
 ;;;###autoload
-(defun ggtags-tag-names (&optional prefix)
-  "Get a list of tag names starting with PREFIX."
+(defun ggtags-tag-names (&optional from-cache)
+  "Get a list of tag names."
   (let ((root (ggtags-root-directory)))
-    (when (and root (ggtags-cache-dirty-p root))
+    (when (and root
+               (not (ggtags-oversize-p))
+               (not from-cache)
+               (ggtags-cache-dirty-p root))
       (if (zerop (call-process "global" nil nil nil "-u"))
           (ggtags-cache-mark-dirty root nil)
         (message "ggtags: error running 'global -u'")))
     (apply 'append (mapcar (lambda (r)
-                             (ggtags-tag-names-1 r prefix))
+                             (ggtags-tag-names-1 r from-cache))
                            (cons root (ggtags-get-libpath))))))
 
 (defun ggtags-read-tag (quick)
   (ggtags-ensure-root-directory)
-  (let* ((tags (ggtags-tag-names))
-         (sym (thing-at-point 'symbol))
-         (default (and (member sym tags) sym)))
+  (let ((default (thing-at-point 'symbol))
+        (completing-read-function ggtags-completing-read-function))
     (setq ggtags-current-tag-name
-          (if quick (or default (error "No valid tag at point"))
+          (if quick (or default (user-error "No tag at point"))
             (completing-read
              (format (if default "Tag (default %s): " "Tag: ") default)
-             tags nil t nil nil default)))))
+             ;; XXX: build tag names more lazily such as using
+             ;; `completion-table-dynamic'.
+             (ggtags-tag-names)
+             nil t nil nil default)))))
 
-(defvar ggtags-global-options
-  (concat "-v --result=grep"
-          (and ggtags-global-has-path-style " --path-style=shorter"))
-  "Options (as a string) for running `global'.")
+(defun ggtags-global-options ()
+  (concat "-v --result="
+          (symbol-name ggtags-global-output-format)
+          (and ggtags-global-has-color " --color")
+          (and ggtags-global-has-path-style " --path-style=shorter")))
 
 ;;;###autoload
 (defun ggtags-find-tag (name &optional verbose)
@@ -207,37 +279,146 @@ When called with prefix, ask the name and kind of tag."
   (interactive (list (ggtags-read-tag (not current-prefix-arg))
                      current-prefix-arg))
   (ggtags-check-root-directory)
-  (ggtags-navigation-mode +1)
-  (ring-insert find-tag-marker-ring (point-marker))
-  (let ((split-window-preferred-function
-         (lambda (w) (split-window (frame-root-window w))))
-        (default-directory (ggtags-root-directory)))
+  (let ((split-window-preferred-function ggtags-split-window-function)
+        (default-directory (ggtags-root-directory))
+        (help-char ??)
+        (help-form "\
+d: definitions          (-d)
+r: references           (-r)
+s: symbols              (-s)
+?: show this help\n"))
     (compilation-start
-     (if verbose
-         (format "global %s %s \"%s\""
-                 ggtags-global-options
-                 (if (y-or-n-p "Kind (y for definition n for reference)? ")
-                     "" "-r")
+     (if (or verbose (not buffer-file-name))
+         (format "global %s -%s \"%s\""
+                 (ggtags-global-options)
+                 (char-to-string
+                  (read-char-choice "Tag type? (d/r/s/?) " '(?d ?r ?s)))
                  name)
        (format "global %s --from-here=%d:%s \"%s\""
-               ggtags-global-options
+               (ggtags-global-options)
                (line-number-at-pos)
-               (expand-file-name buffer-file-name)
+               (shell-quote-argument
+                (expand-file-name (file-truename buffer-file-name)))
                name))
-     'ggtags-global-mode)))
+     'ggtags-global-mode))
+  (eval-and-compile (require 'etags))
+  (ring-insert find-tag-marker-ring (point-marker))
+  (ggtags-navigation-mode +1))
 
 (defun ggtags-find-tag-resume ()
   (interactive)
   (ggtags-ensure-global-buffer
     (ggtags-navigation-mode +1)
-    (let ((split-window-preferred-function
-           (lambda (w) (split-window (frame-root-window w)))))
+    (let ((split-window-preferred-function ggtags-split-window-function))
       (compile-goto-error))))
 
+;; NOTE: Coloured output in grep requested: http://goo.gl/Y9IcX
+(defun ggtags-list-tags (regexp file-or-directory)
+  "List all tags matching REGEXP in FILE-OR-DIRECTORY."
+  (interactive (list (read-string "POSIX regexp: ")
+                     (read-file-name "Directory: "
+                                     (if current-prefix-arg
+                                         (ggtags-root-directory)
+                                       default-directory)
+                                     buffer-file-name t)))
+  (let ((split-window-preferred-function ggtags-split-window-function)
+        (default-directory (if (file-directory-p file-or-directory)
+                               (file-name-as-directory file-or-directory)
+                             (file-name-directory file-or-directory))))
+    (ggtags-check-root-directory)
+    (eval-and-compile (require 'etags))
+    (ggtags-navigation-mode +1)
+    (ring-insert find-tag-marker-ring (point-marker))
+    (with-current-buffer
+        (compilation-start (format "global %s -e %s %s"
+                                   (ggtags-global-options)
+                                   regexp
+                                   (if (file-directory-p file-or-directory)
+                                       "-l ."
+                                     (concat "-f " (shell-quote-argument
+                                                    (file-name-nondirectory
+                                                     file-or-directory)))))
+                           'ggtags-global-mode)
+      (setq-local compilation-auto-jump-to-first-error nil)
+      (remove-hook 'compilation-finish-functions 'ggtags-handle-single-match t))))
+
+(defun ggtags-query-replace (from to &optional delimited directory)
+  "Query replace FROM with TO on all files in DIRECTORY."
+  (interactive
+   (append (query-replace-read-args "Query replace (regexp)" t t)
+           (list (read-directory-name "In directory: " nil nil t))))
+  (let ((default-directory (file-name-as-directory directory)))
+    (ggtags-check-root-directory)
+    (dolist (file (process-lines "global" "-P" "-l" "."))
+      (let ((file (expand-file-name file directory)))
+        (when (file-exists-p file)
+          (let* ((message-log-max nil)
+                 (visited (get-file-buffer file))
+                 (buffer (or visited
+                             (with-demoted-errors
+                               (find-file-noselect file)))))
+            (when buffer
+              (set-buffer buffer)
+              (if (save-excursion
+                    (goto-char (point))
+                    (re-search-forward from nil t))
+                  (progn
+                    (switch-to-buffer (current-buffer))
+                    (perform-replace from to t t delimited
+                                     nil multi-query-replace-map))
+                (message "Nothing to do for `%s'" file)
+                (or visited (kill-buffer))))))))))
+
+(defun ggtags-delete-tag-files ()
+  "Delete the tag files generated by gtags."
+  (interactive)
+  (when (ggtags-root-directory)
+    (let ((files (directory-files (ggtags-root-directory) t
+                                  (regexp-opt '("GPATH" "GRTAGS" "GTAGS" "ID"))))
+          (buffer "*GTags File List*"))
+      (or files (user-error "No tag files found"))
+      (with-output-to-temp-buffer buffer
+        (dolist (file files)
+          (princ file)
+          (princ "\n")))
+      (let ((win (get-buffer-window buffer)))
+        (unwind-protect
+            (progn
+              (fit-window-to-buffer win)
+              (when (yes-or-no-p "Remove GNU Global tag files? ")
+                (mapc 'delete-file files)))
+          (when (window-live-p win)
+            (quit-window t win)))))))
+
+(defvar ggtags-current-mark nil)
+
+(defun ggtags-next-mark (&optional arg)
+  "Move to the next mark in the tag marker ring."
+  (interactive)
+  (or (> (ring-length find-tag-marker-ring) 1)
+      (user-error "No %s mark" (if arg "previous" "next")))
+  (let ((mark (or (and ggtags-current-mark
+                       (marker-buffer ggtags-current-mark)
+                       (funcall (if arg #'ring-previous #'ring-next)
+                                find-tag-marker-ring ggtags-current-mark))
+                  (progn
+                    (ring-insert find-tag-marker-ring (point-marker))
+                    (ring-ref find-tag-marker-ring 0)))))
+    (switch-to-buffer (marker-buffer mark))
+    (goto-char mark)
+    (setq ggtags-current-mark mark)))
+
+(defun ggtags-prev-mark ()
+  (interactive)
+  (ggtags-next-mark 'previous))
+
+(defvar-local ggtags-global-exit-status nil)
+
 (defun ggtags-global-exit-message-function (_process-status exit-status msg)
+  (setq ggtags-global-exit-status exit-status)
   (let ((count (save-excursion
                  (goto-char (point-max))
-                 (if (re-search-backward "^\\([0-9]+\\) objects? located" nil t)
+                 (if (re-search-backward "^\\([0-9]+\\) \\w+ located" nil t)
                      (string-to-number (match-string 1))
                    0))))
     (cons (if (> exit-status 0)
@@ -245,6 +426,27 @@ When called with prefix, ask the name and kind of tag."
             (format "found %d %s" count (if (= count 1) "match" "matches")))
           exit-status)))
 
+;;; NOTE: Must not match the 'Global started at Mon Jun 3 10:24:13'
+;;; line or `compilation-auto-jump' will jump there and fail. See
+;;; comments before the 'gnu' entry in
+;;; `compilation-error-regexp-alist-alist'.
+(defvar ggtags-global-error-regexp-alist-alist
+  (append
+   '((path "^\\(?:[^/\n]*/\\)?[^ )\t\n]+$" 0)
+     ;; 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
+     (ctags-x "^\\([^ \t\n]+\\)[ \t]+\\([0-9]+\\)[ \t]+\\(\\(?:[^/\n]*/\\)?[^ \t\n]+\\)"
+              3 2 nil nil 3 (1 font-lock-function-name-face))
+     ;; src/dialog.cc:172:#undef ACTIVE_ESCAPE
+     (grep "^\\(.+?\\):\\([0-9]+\\):\\(?:[^0-9\n]\\|[0-9][^0-9\n]\\|[0-9][0-9].\\)"
+           1 2 nil nil 1)
+     ;; src/dialog.cc ACTIVE_ESCAPE 172 #undef ACTIVE_ESCAPE
+     (cscope "^\\(.+?\\)[ \t]+\\([^ \t\n]+\\)[ \t]+\\([0-9]+\\).*\\(?:[^0-9\n]\\|[^0-9\n][0-9]\\|[^:\n][0-9][0-9]\\)$"
+             1 3 nil nil 1 (2 font-lock-function-name-face)))
+   compilation-error-regexp-alist-alist))
+
 (defun ggtags-abbreviate-file (start end)
   (let ((inhibit-read-only t)
         (amount (if (numberp ggtags-global-abbreviate-filename)
@@ -265,33 +467,50 @@ When called with prefix, ask the name and kind of tag."
 
 (defun ggtags-abbreviate-files (start end)
   (goto-char start)
-  (when ggtags-global-abbreviate-filename
-    (while (re-search-forward "^\\([^:\n]+\\):[0-9]+:" end t)
-      (when (and (or (not (numberp ggtags-global-abbreviate-filename))
-                     (> (length (match-string 1))
-                        ggtags-global-abbreviate-filename))
-                 ;; Ignore bogus file lines such as:
-                 ;;     Global found 2 matches at Thu Jan 31 13:45:19
-                 (get-text-property (match-beginning 0) 'compilation-message))
-        (ggtags-abbreviate-file (match-beginning 1) (match-end 1))))))
+  (let* ((error-re (cdr (assq ggtags-global-output-format
+                              ggtags-global-error-regexp-alist-alist)))
+         (sub (cadr error-re)))
+    (when (and ggtags-global-abbreviate-filename error-re)
+      (while (re-search-forward (car error-re) end t)
+        (when (and (or (not (numberp ggtags-global-abbreviate-filename))
+                       (> (length (match-string sub))
+                          ggtags-global-abbreviate-filename))
+                   ;; Ignore bogus file lines such as:
+                   ;;     Global found 2 matches at Thu Jan 31 13:45:19
+                   (get-text-property (match-beginning sub) 'compilation-message))
+          (ggtags-abbreviate-file (match-beginning sub) (match-end sub)))))))
+
+(defun ggtags-global-filter ()
+  "Called from `compilation-filter-hook' (which see)."
+  (ansi-color-apply-on-region compilation-filter-start (point)))
 
 (defun ggtags-handle-single-match (buf _how)
-  (unless (or (not ggtags-auto-jump-to-first-match)
-              (save-excursion
-                (goto-char (point-min))
-                (ignore-errors
-                  (goto-char (compilation-next-single-property-change
-                              (point) 'compilation-message))
-                  (end-of-line)
-                  (compilation-next-single-property-change
-                   (point) 'compilation-message))))
+  (when (and ggtags-auto-jump-to-first-match
+             ;; If exit abnormally keep the window for inspection.
+             (zerop ggtags-global-exit-status)
+             (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)))))
     (ggtags-navigation-mode -1)
     ;; 0.5s delay for `ggtags-auto-jump-to-first-match'
     (sit-for 0)                    ; See: http://debbugs.gnu.org/13829
     (ggtags-navigation-mode-cleanup buf 0.5)))
 
+(defvar ggtags-global-mode-font-lock-keywords
+  '(("^Global \\(exited abnormally\\|interrupt\\|killed\\|terminated\\)\\(?:.*with code \\([0-9]+\\)\\)?.*"
+     (1 'compilation-error)
+     (2 'compilation-error nil t))
+    ("^Global found \\([0-9]+\\)" (1 compilation-info-face))))
+
 (define-compilation-mode ggtags-global-mode "Global"
   "A mode for showing outputs from gnu global."
+  (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)
   (setq-local compilation-scroll-output 'first-error)
@@ -302,6 +521,7 @@ When called with prefix, ask the name and kind of tag."
               'ggtags-global-exit-message-function)
   (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)
   (define-key ggtags-global-mode-map "o" 'visible-mode))
 
@@ -312,6 +532,7 @@ When called with prefix, ask the name and kind of tag."
     (define-key map "\M-}" 'ggtags-navigation-next-file)
     (define-key map "\M-{" 'ggtags-navigation-previous-file)
     (define-key map "\M-o" 'ggtags-navigation-visible-mode)
+    (define-key map [return] 'ggtags-navigation-mode-done)
     (define-key map "\r" 'ggtags-navigation-mode-done)
     ;; Intercept M-. and M-* keys
     (define-key map [remap pop-tag-mark] 'ggtags-navigation-mode-abort)
@@ -338,7 +559,7 @@ When called with prefix, ask the name and kind of tag."
              (kill-compilation))
            (when (and (derived-mode-p 'ggtags-global-mode)
                       (get-buffer-window))
-             (delete-window (get-buffer-window)))
+             (quit-window nil (get-buffer-window)))
            (and time (run-with-idle-timer time nil 'kill-buffer buf))))))
 
 (defun ggtags-navigation-mode-done ()
@@ -402,22 +623,48 @@ When called with prefix, ask the name and kind of tag."
          (message "%d %s killed" count (if (= count 1) "buffer" "buffers")))))
 
 (defun ggtags-after-save-function ()
-  (let ((root (ggtags-root-directory)))
-    (and root (ggtags-cache-mark-dirty root t))))
+  (let ((root (with-demoted-errors (ggtags-root-directory))))
+    (when root
+      (ggtags-cache-mark-dirty root t)
+      ;; When oversize update on a per-save basis.
+      (when (and buffer-file-name (ggtags-oversize-p))
+        (with-demoted-errors
+          (call-process "global" nil 0 nil
+                        "--single-update"
+                        (file-truename buffer-file-name)))))))
 
 (defvar ggtags-tag-overlay nil)
 (defvar ggtags-highlight-tag-timer nil)
-(make-variable-buffer-local 'ggtags-tag-overlay)
 
-(defun ggtags-highlight-tag-at-point (buffer)
-  (when (eq buffer (current-buffer))
+(defvar ggtags-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "\M-." 'ggtags-find-tag)
+    (define-key map "\M-," 'ggtags-find-tag-resume)
+    (define-key map "\C-c\M-k" 'ggtags-kill-file-buffers)
+    map))
+
+;;;###autoload
+(define-minor-mode ggtags-mode nil
+  :lighter (:eval (if ggtags-navigation-mode "" " GG"))
+  (if ggtags-mode
+      (progn
+        (add-hook 'after-save-hook 'ggtags-after-save-function nil t)
+        (or (executable-find "global")
+            (message "Failed to find GNU Global")))
+    (remove-hook 'after-save-hook 'ggtags-after-save-function t)
+    (and (overlayp ggtags-tag-overlay)
+         (delete-overlay ggtags-tag-overlay))
+    (setq ggtags-tag-overlay nil)))
+
+(defun ggtags-highlight-tag-at-point ()
+  (when ggtags-mode
     (unless (overlayp ggtags-tag-overlay)
       (setq ggtags-tag-overlay (make-overlay (point) (point)))
       (overlay-put ggtags-tag-overlay 'ggtags t))
     (let* ((bounds (bounds-of-thing-at-point 'symbol))
            (valid-tag (when bounds
                         (member (buffer-substring (car bounds) (cdr bounds))
-                                (ggtags-tag-names))))
+                                (ggtags-tag-names (ggtags-oversize-p)))))
            (o ggtags-tag-overlay)
            (done-p (lambda ()
                      (and (memq o (overlays-at (car bounds)))
@@ -428,41 +675,13 @@ When called with prefix, ask the name and kind of tag."
       (cond
        ((not bounds)
         (overlay-put ggtags-tag-overlay 'face nil)
-        (move-overlay ggtags-tag-overlay (point) (point)))
+        (move-overlay ggtags-tag-overlay (point) (point) (current-buffer)))
        ((not (funcall done-p))
-        (move-overlay o (car bounds) (cdr bounds))
+        (move-overlay o (car bounds) (cdr bounds) (current-buffer))
         (overlay-put o 'face (and valid-tag 'ggtags-highlight)))))))
 
-(defun ggtags-post-command-function ()
-  (when (timerp ggtags-highlight-tag-timer)
-    (cancel-timer ggtags-highlight-tag-timer))
-  (setq ggtags-highlight-tag-timer
-        (run-with-idle-timer 0.2 nil 'ggtags-highlight-tag-at-point
-                             (current-buffer))))
-
-(defvar ggtags-mode-map
-  (let ((map (make-sparse-keymap)))
-    (define-key map "\M-." 'ggtags-find-tag)
-    (define-key map "\M-," 'ggtags-find-tag-resume)
-    (define-key map "\C-c\M-k" 'ggtags-kill-file-buffers)
-    map))
-
-;;;###autoload
-(define-minor-mode ggtags-mode nil
-  :lighter (:eval (if ggtags-navigation-mode "" " GG"))
-  (if ggtags-mode
-      (progn
-        (or (ggtags-root-directory)
-            (message "File GTAGS not found"))
-        (add-hook 'after-save-hook 'ggtags-after-save-function nil t)
-        (add-hook 'post-command-hook 'ggtags-post-command-function nil t))
-    (remove-hook 'after-save-hook 'ggtags-after-save-function t)
-    (remove-hook 'post-command-hook 'ggtags-post-command-function t)
-    (and (overlayp ggtags-tag-overlay)
-         (delete-overlay ggtags-tag-overlay))
-    (setq ggtags-tag-overlay nil)))
-
 ;;; imenu
+
 (defun ggtags-goto-imenu-index (name line &rest _args)
   (save-restriction
     (widen)
@@ -475,15 +694,55 @@ When called with prefix, ask the name and kind of tag."
   "A function suitable for `imenu-create-index-function'."
   (when buffer-file-name
     (let ((file (file-truename buffer-file-name)))
-      (ggtags-ignore-file-error
-        (with-temp-buffer
-          (when (zerop (call-process "global" nil t nil "-f" file))
-            (goto-char (point-min))
-            (loop while (re-search-forward
-                         "^\\([^ \t]+\\)[ \t]+\\([0-9]+\\)" nil t)
-                  collect (list (match-string 1)
-                                (string-to-number (match-string 2))
-                                'ggtags-goto-imenu-index))))))))
+      (with-temp-buffer
+        (when (with-demoted-errors
+                (zerop (call-process "global" nil t nil "-f" file)))
+          (goto-char (point-min))
+          (loop while (re-search-forward
+                       "^\\([^ \t]+\\)[ \t]+\\([0-9]+\\)" nil t)
+                collect (list (match-string 1)
+                              (string-to-number (match-string 2))
+                              'ggtags-goto-imenu-index)))))))
+
+;;; hippie-expand
+
+;;;###autoload
+(defun try-complete-ggtags-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 ""))
+                 (with-demoted-errors (ggtags-root-directory))
+                 (sort (all-completions he-search-string
+                                        (ggtags-tag-names))
+                       '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)))
+
+;;; Finish up
+
+(when ggtags-highlight-tag-timer
+  (cancel-timer ggtags-highlight-tag-timer))
+
+(setq ggtags-highlight-tag-timer
+      (run-with-idle-timer 0.2 t 'ggtags-highlight-tag-at-point))
+
+;; Higher priority for `ggtags-navigation-mode' to avoid being
+;; hijacked by modes such as `view-mode'.
+(defvar ggtags-mode-map-alist
+  `((ggtags-navigation-mode . ,ggtags-navigation-mode-map)))
+
+(add-to-list 'emulation-mode-map-alists 'ggtags-mode-map-alist)
 
 (provide 'ggtags)
 ;;; ggtags.el ends here