]> code.delx.au - gnu-emacs-elpa/blob - ggtags.el
Fix #59: detect missing tag files and remove project cache
[gnu-emacs-elpa] / ggtags.el
1 ;;; ggtags.el --- emacs frontend to GNU Global source code tagging system -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2013-2014 Free Software Foundation, Inc.
4
5 ;; Author: Leo Liu <sdl.web@gmail.com>
6 ;; Version: 0.8.5
7 ;; Keywords: tools, convenience
8 ;; Created: 2013-01-29
9 ;; URL: https://github.com/leoliu/ggtags
10 ;; Package-Requires: ((emacs "24") (cl-lib "0.5"))
11
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.
16
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.
21
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/>.
24
25 ;;; Commentary:
26
27 ;; A package to integrate GNU Global source code tagging system
28 ;; (http://www.gnu.org/software/global) with Emacs.
29 ;;
30 ;; Usage:
31 ;;
32 ;; `ggtags' is similar to the standard `etags' package. These keys
33 ;; `M-.', `M-,', `M-*' and `C-M-.' should work as expected in
34 ;; `ggtags-mode'. See the README in https://github.com/leoliu/ggtags
35 ;; for more details.
36 ;;
37 ;; All commands are available from the `Ggtags' menu in `ggtags-mode'.
38
39 ;;; Code:
40
41 (eval-when-compile
42 (require 'url-parse))
43
44 (require 'cl-lib)
45 (require 'ewoc)
46 (require 'compile)
47 (require 'etags)
48 (require 'tabulated-list) ;preloaded since 24.3
49
50 (eval-when-compile
51 (unless (fboundp 'setq-local)
52 (defmacro setq-local (var val)
53 (list 'set (list 'make-local-variable (list 'quote var)) val)))
54
55 (unless (fboundp 'defvar-local)
56 (defmacro defvar-local (var val &optional docstring)
57 (declare (debug defvar) (doc-string 3))
58 (list 'progn (list 'defvar var val docstring)
59 (list 'make-variable-buffer-local (list 'quote var)))))
60
61 (defmacro ignore-errors-unless-debug (&rest body)
62 "Ignore all errors while executing BODY unless debug is on."
63 (declare (debug t) (indent 0))
64 `(condition-case-unless-debug nil (progn ,@body) (error nil)))
65
66 (defmacro with-display-buffer-no-window (&rest body)
67 (declare (debug t) (indent 0))
68 ;; See http://debbugs.gnu.org/13594
69 `(let ((display-buffer-overriding-action
70 (if (and ggtags-auto-jump-to-match
71 ;; Appeared in emacs 24.4.
72 (fboundp 'display-buffer-no-window))
73 (list #'display-buffer-no-window)
74 display-buffer-overriding-action)))
75 ,@body)))
76
77 (eval-and-compile
78 (or (fboundp 'user-error) ;24.3
79 (defalias 'user-error 'error))
80 (or (fboundp 'read-only-mode) ;24.3
81 (defalias 'read-only-mode 'toggle-read-only))
82 (or (fboundp 'register-read-with-preview) ;24.4
83 (defalias 'register-read-with-preview 'read-char)))
84
85 (defgroup ggtags nil
86 "GNU Global source code tagging system."
87 :group 'tools)
88
89 (defface ggtags-highlight '((t (:underline t)))
90 "Face used to highlight a valid tag at point."
91 :group 'ggtags)
92
93 (defface ggtags-global-line '((t (:inherit secondary-selection)))
94 "Face used to highlight matched line in Global buffer."
95 :group 'ggtags)
96
97 (defcustom ggtags-executable-directory nil
98 "If non-nil the directory to search global executables."
99 :type '(choice (const :tag "Unset" nil) directory)
100 :risky t
101 :group 'ggtags)
102
103 (defcustom ggtags-oversize-limit (* 10 1024 1024)
104 "The over size limit for the GTAGS file.
105 When the size of the GTAGS file is below this limit, ggtags
106 always maintains up-to-date tags for the whole source tree by
107 running `global -u'. For projects with GTAGS larger than this
108 limit, only files edited in Ggtags mode are updated (via `global
109 --single-update')."
110 :safe 'numberp
111 :type '(choice (const :tag "None" nil)
112 (const :tag "Always" t)
113 number)
114 :group 'ggtags)
115
116 (defcustom ggtags-include-pattern
117 '("^\\s-*#\\(?:include\\|import\\)\\s-*[\"<]\\(?:[./]*\\)?\\(.*?\\)[\">]" . 1)
118 "Pattern used to detect #include files.
119 Value can be (REGEXP . SUB) or a function with no arguments.
120 REGEXP should match from the beginning of line."
121 :type '(choice (const :tag "Disable" nil)
122 (cons regexp integer)
123 function)
124 :safe 'stringp
125 :group 'ggtags)
126
127 ;; See also: http://article.gmane.org/gmane.comp.gnu.global.bugs/1751
128 (defcustom ggtags-use-project-gtagsconf t
129 "Non-nil to use GTAGSCONF file found at project root.
130 File .globalrc and gtags.conf are checked in order.
131
132 Note: GNU Global v6.2.13 has the feature of using gtags.conf at
133 project root. Setting this variable to nil doesn't disable this
134 feature."
135 :safe 'booleanp
136 :type 'boolean
137 :group 'ggtags)
138
139 (defcustom ggtags-project-duration 600
140 "Seconds to keep information of a project in memory."
141 :type 'number
142 :group 'ggtags)
143
144 (defcustom ggtags-process-environment nil
145 "Similar to `process-environment' with higher precedence.
146 Elements are run through `substitute-env-vars' before use.
147 GTAGSROOT will always be expanded to current project root
148 directory. This is intended for project-wise ggtags-specific
149 process environment settings. Note on remote hosts (e.g. tramp)
150 directory local variables is not enabled by default per
151 `enable-remote-dir-locals' (which see)."
152 :safe 'ggtags-list-of-string-p
153 :type '(repeat string)
154 :group 'ggtags)
155
156 (defcustom ggtags-auto-jump-to-match 'history
157 "Strategy on how to jump to match: nil, first or history.
158
159 nil: never automatically jump to any match;
160 first: jump to the first match;
161 history: jump to the match stored in search history."
162 :type '(choice (const :tag "First match" first)
163 (const :tag "Search History" history)
164 (const :tag "Never" nil))
165 :group 'ggtags)
166
167 (defcustom ggtags-global-window-height 8 ; ggtags-global-mode
168 "Number of lines for the *ggtags-global* popup window.
169 If nil, use Emacs default."
170 :type '(choice (const :tag "Default" nil) integer)
171 :group 'ggtags)
172
173 (defcustom ggtags-global-abbreviate-filename 40
174 "Non-nil to display file names abbreviated e.g. \"/u/b/env\".
175 If an integer abbreviate only names longer than that number."
176 :type '(choice (const :tag "No" nil)
177 (const :tag "Always" t)
178 integer)
179 :group 'ggtags)
180
181 (defcustom ggtags-split-window-function split-window-preferred-function
182 "A function to control how ggtags pops up the auxiliary window."
183 :type 'function
184 :group 'ggtags)
185
186 (defcustom ggtags-use-idutils (and (executable-find "mkid") t)
187 "Non-nil to also generate the idutils DB."
188 :type 'boolean
189 :group 'ggtags)
190
191 (defcustom ggtags-global-output-format 'grep
192 "Global output format: path, ctags, ctags-x, grep or cscope."
193 :type '(choice (const path)
194 (const ctags)
195 (const ctags-x)
196 (const grep)
197 (const cscope))
198 :group 'ggtags)
199
200 (defcustom ggtags-global-use-color t
201 "Non-nil to use color in output if supported by Global.
202 Note: processing colored output takes noticeable time
203 particularly when the output is large."
204 :type 'boolean
205 :safe 'booleanp
206 :group 'ggtags)
207
208 (defcustom ggtags-global-ignore-case nil
209 "Non-nil if Global should ignore case in the search pattern."
210 :safe 'booleanp
211 :type 'boolean
212 :group 'ggtags)
213
214 (defcustom ggtags-global-treat-text nil
215 "Non-nil if Global should include matches from text files.
216 This affects `ggtags-find-file' and `ggtags-grep'."
217 :safe 'booleanp
218 :type 'boolean
219 :group 'ggtags)
220
221 ;; See also https://github.com/leoliu/ggtags/issues/52
222 (defcustom ggtags-global-search-libpath-for-reference t
223 "If non-nil global will search GTAGSLIBPATH for references.
224 Search is only continued in GTAGSLIBPATH if it finds no matches
225 in current project."
226 :safe 'booleanp
227 :type 'boolean
228 :group 'ggtags)
229
230 (defcustom ggtags-global-large-output 1000
231 "Number of lines in the Global buffer to indicate large output."
232 :type 'number
233 :group 'ggtags)
234
235 (defcustom ggtags-global-history-length history-length
236 "Maximum number of items to keep in `ggtags-global-search-history'."
237 :type 'integer
238 :group 'ggtags)
239
240 (defcustom ggtags-enable-navigation-keys t
241 "If non-nil key bindings in `ggtags-navigation-map' are enabled."
242 :safe 'booleanp
243 :type 'boolean
244 :group 'ggtags)
245
246 (defcustom ggtags-find-tag-hook nil
247 "Hook run immediately after finding a tag."
248 :options '(recenter reposition-window)
249 :type 'hook
250 :group 'ggtags)
251
252 (defcustom ggtags-get-definition-function #'ggtags-get-definition-default
253 "Function called by `ggtags-show-definition' to get definition.
254 It is passed a list of definition candidates of the form:
255
256 (TEXT NAME FILE LINE)
257
258 where TEXT is usually the source line of the definition.
259
260 The return value is passed to `ggtags-print-definition-function'."
261 :type 'function
262 :group 'ggtags)
263
264 (defcustom ggtags-print-definition-function
265 (lambda (s) (ggtags-echo "%s" (or s "[definition not found]")))
266 "Function used by `ggtags-show-definition' to print definition."
267 :type 'function
268 :group 'ggtags)
269
270 (defcustom ggtags-mode-sticky t
271 "If non-nil enable Ggtags Mode in files visited."
272 :safe 'booleanp
273 :type 'boolean
274 :group 'ggtags)
275
276 (defcustom ggtags-mode-prefix-key "\C-c"
277 "Key binding used for `ggtags-mode-prefix-map'.
278 Users should change the value using `customize-variable' to
279 properly update `ggtags-mode-map'."
280 :set (lambda (sym value)
281 (when (bound-and-true-p ggtags-mode-map)
282 (let ((old (and (boundp sym) (symbol-value sym))))
283 (and old (define-key ggtags-mode-map old nil)))
284 (and value
285 (bound-and-true-p ggtags-mode-prefix-map)
286 (define-key ggtags-mode-map value ggtags-mode-prefix-map)))
287 (set-default sym value))
288 :type 'key-sequence
289 :group 'ggtags)
290
291 (defcustom ggtags-completing-read-function nil
292 "Ggtags specific `completing-read-function' (which see).
293 Nil means using the value of `completing-read-function'."
294 :type '(choice (const :tag "Use completing-read-function" nil)
295 function)
296 :group 'ggtags)
297
298 (defcustom ggtags-highlight-tag-delay 0.25
299 "Time in seconds before highlighting tag at point."
300 :set (lambda (sym value)
301 (when (bound-and-true-p ggtags-highlight-tag-timer)
302 (timer-set-idle-time ggtags-highlight-tag-timer value t))
303 (set-default sym value))
304 :type 'number
305 :group 'ggtags)
306
307 (defcustom ggtags-bounds-of-tag-function (lambda ()
308 (bounds-of-thing-at-point 'symbol))
309 "Function to get the start and end positions of the tag at point."
310 :type 'function
311 :group 'ggtags)
312
313 ;; Used by ggtags-global-mode
314 (defvar ggtags-global-error "match"
315 "Stem of message to print when no matches are found.")
316
317 (defconst ggtags-bug-url "https://github.com/leoliu/ggtags/issues")
318
319 (defvar ggtags-global-last-buffer nil)
320
321 (defvar ggtags-global-continuation nil)
322
323 (defvar ggtags-current-tag-name nil)
324
325 (defvar ggtags-highlight-tag-overlay nil)
326
327 (defvar ggtags-highlight-tag-timer nil)
328
329 (defmacro ggtags-with-temp-message (message &rest body)
330 (declare (debug t) (indent 1))
331 (let ((init-time (make-symbol "-init-time-"))
332 (tmp-msg (make-symbol "-tmp-msg-")))
333 `(let ((,init-time (float-time))
334 (,tmp-msg ,message))
335 (with-temp-message ,tmp-msg
336 (prog1 (progn ,@body)
337 (message "%sdone (%.2fs)" ,(or tmp-msg "")
338 (- (float-time) ,init-time)))))))
339
340 (defmacro ggtags-delay-finish-functions (&rest body)
341 "Delay running `compilation-finish-functions' until after BODY."
342 (declare (indent 0) (debug t))
343 (let ((saved (make-symbol "-saved-"))
344 (exit-args (make-symbol "-exit-args-")))
345 `(let ((,saved compilation-finish-functions)
346 ,exit-args)
347 (setq-local compilation-finish-functions nil)
348 (add-hook 'compilation-finish-functions
349 (lambda (&rest args) (setq ,exit-args args))
350 nil t)
351 (unwind-protect (progn ,@body)
352 (setq-local compilation-finish-functions ,saved)
353 (and ,exit-args (apply #'run-hook-with-args
354 'compilation-finish-functions ,exit-args))))))
355
356 (defmacro ggtags-ensure-global-buffer (&rest body)
357 (declare (debug t) (indent 0))
358 `(progn
359 (or (and (buffer-live-p ggtags-global-last-buffer)
360 (with-current-buffer ggtags-global-last-buffer
361 (derived-mode-p 'ggtags-global-mode)))
362 (error "No global buffer found"))
363 (with-current-buffer ggtags-global-last-buffer ,@body)))
364
365 (defun ggtags-list-of-string-p (xs)
366 "Return non-nil if XS is a list of strings."
367 (cl-every #'stringp xs))
368
369 (defun ggtags-ensure-localname (file)
370 (and file (or (file-remote-p file 'localname) file)))
371
372 (defun ggtags-echo (format-string &rest args)
373 "Print formatted text to echo area."
374 (let (message-log-max) (apply #'message format-string args)))
375
376 (defun ggtags-forward-to-line (line)
377 "Move to line number LINE in current buffer."
378 (cl-check-type line (integer 1))
379 (save-restriction
380 (widen)
381 (goto-char (point-min))
382 (forward-line (1- line))))
383
384 (defun ggtags-program-path (name)
385 (if ggtags-executable-directory
386 (expand-file-name name ggtags-executable-directory)
387 name))
388
389 (defun ggtags-process-string (program &rest args)
390 (with-temp-buffer
391 (let ((exit (apply #'process-file
392 (ggtags-program-path program) nil t nil args))
393 (output (progn
394 (goto-char (point-max))
395 (skip-chars-backward " \t\n")
396 (buffer-substring (point-min) (point)))))
397 (or (zerop exit)
398 (error "`%s' non-zero exit: %s" program output))
399 output)))
400
401 (defun ggtags-tag-at-point ()
402 (pcase (funcall ggtags-bounds-of-tag-function)
403 (`(,beg . ,end) (buffer-substring beg end))))
404
405 ;;; Store for project info and settings
406
407 (defvar ggtags-projects (make-hash-table :size 7 :test #'equal))
408
409 (cl-defstruct (ggtags-project (:constructor ggtags-project--make)
410 (:copier nil)
411 (:type vector)
412 :named)
413 root tag-size has-refs has-path-style has-color dirty-p mtime timestamp)
414
415 (defun ggtags-make-project (root)
416 (cl-check-type root string)
417 (pcase (nthcdr 5 (file-attributes (expand-file-name "GTAGS" root)))
418 (`(,mtime ,_ ,tag-size . ,_)
419 (let* ((default-directory (file-name-as-directory root))
420 (rtags-size (nth 7 (file-attributes "GRTAGS")))
421 (has-refs
422 (when rtags-size
423 (and (or (> rtags-size (* 32 1024))
424 (with-demoted-errors "ggtags-make-project: %S"
425 (not (equal "" (ggtags-process-string "global" "-crs")))))
426 'has-refs)))
427 ;; http://thread.gmane.org/gmane.comp.gnu.global.bugs/1518
428 (has-path-style
429 (with-demoted-errors "ggtags-make-project: %S"
430 ;; in case `global' not found
431 (and (zerop (process-file (ggtags-program-path "global")
432 nil nil nil
433 "--path-style" "shorter" "--help"))
434 'has-path-style)))
435 ;; http://thread.gmane.org/gmane.comp.gnu.global.bugs/1542
436 (has-color
437 (with-demoted-errors "ggtags-make-project: %S"
438 (and (zerop (process-file (ggtags-program-path "global")
439 nil nil nil
440 "--color" "--help"))
441 'has-color))))
442 (puthash default-directory
443 (ggtags-project--make :root default-directory
444 :tag-size tag-size
445 :has-refs has-refs
446 :has-path-style has-path-style
447 :has-color has-color
448 :mtime (float-time mtime)
449 :timestamp (float-time))
450 ggtags-projects)))))
451
452 (defun ggtags-project-expired-p (project)
453 (or (< (ggtags-project-timestamp project) 0)
454 (> (- (float-time)
455 (ggtags-project-timestamp project))
456 ggtags-project-duration)))
457
458 (defun ggtags-project-update-mtime-maybe (&optional project)
459 "Update PROJECT's modtime and if current file is newer.
460 Value is new modtime if updated."
461 (let ((project (or project (ggtags-find-project))))
462 (when (and (ggtags-project-p project)
463 (consp (visited-file-modtime))
464 (> (float-time (visited-file-modtime))
465 (ggtags-project-mtime project)))
466 (setf (ggtags-project-dirty-p project) t)
467 (setf (ggtags-project-mtime project)
468 (float-time (visited-file-modtime))))))
469
470 (defun ggtags-project-oversize-p (&optional project)
471 (pcase ggtags-oversize-limit
472 (`nil nil)
473 (`t t)
474 (size (let ((project (or project (ggtags-find-project))))
475 (and project (> (ggtags-project-tag-size project) size))))))
476
477 (defvar-local ggtags-project-root 'unset
478 "Internal variable for project root directory.")
479
480 (defun ggtags-clear-project-root ()
481 (kill-local-variable 'ggtags-project-root))
482
483 ;;;###autoload
484 (defun ggtags-find-project ()
485 ;; See https://github.com/leoliu/ggtags/issues/42
486 ;;
487 ;; It is unsafe to cache `ggtags-project-root' in non-file buffers.
488 ;; But we keep the cache for a command's duration so that multiple
489 ;; calls of `ggtags-find-project' has no performance impact.
490 (unless buffer-file-name
491 (add-hook 'pre-command-hook #'ggtags-clear-project-root nil t))
492 (let ((project (gethash ggtags-project-root ggtags-projects)))
493 (if (ggtags-project-p project)
494 (if (ggtags-project-expired-p project)
495 (progn
496 (remhash ggtags-project-root ggtags-projects)
497 (ggtags-find-project))
498 project)
499 (setq ggtags-project-root
500 (or (ignore-errors-unless-debug
501 (file-name-as-directory
502 (concat (file-remote-p default-directory)
503 ;; Resolves symbolic links
504 (ggtags-process-string "global" "-pr"))))
505 ;; 'global -pr' resolves symlinks before checking the
506 ;; GTAGS file which could cause issues such as
507 ;; https://github.com/leoliu/ggtags/issues/22, so
508 ;; let's help it out.
509 ;;
510 ;; Note: `locate-dominating-file' doesn't accept
511 ;; function for NAME before 24.3.
512 (let ((dir (locate-dominating-file default-directory "GTAGS")))
513 ;; `file-truename' may strip the trailing '/' on
514 ;; remote hosts, see http://debbugs.gnu.org/16851
515 (and dir (file-regular-p (expand-file-name "GTAGS" dir))
516 (file-name-as-directory (file-truename dir))))))
517 (when ggtags-project-root
518 (if (gethash ggtags-project-root ggtags-projects)
519 (ggtags-find-project)
520 (ggtags-make-project ggtags-project-root))))))
521
522 (defun ggtags-current-project-root ()
523 (and (ggtags-find-project)
524 (ggtags-project-root (ggtags-find-project))))
525
526 (defun ggtags-check-project ()
527 (or (ggtags-find-project) (error "File GTAGS not found")))
528
529 (defun ggtags-ensure-project ()
530 (or (ggtags-find-project)
531 (when (or (yes-or-no-p "File GTAGS not found; run gtags? ")
532 (user-error "Aborted"))
533 (call-interactively #'ggtags-create-tags)
534 ;; Need checking because `ggtags-create-tags' can create tags
535 ;; in any directory.
536 (ggtags-check-project))))
537
538 (defvar delete-trailing-lines) ;new in 24.3
539
540 (defun ggtags-save-project-settings (&optional noconfirm)
541 "Save Gnu Global's specific environment variables."
542 (interactive "P")
543 (ggtags-check-project)
544 (let* ((inhibit-read-only t) ; for `add-dir-local-variable'
545 (default-directory (ggtags-current-project-root))
546 ;; Not using `ggtags-with-current-project' to preserve
547 ;; environment variables that may be present in
548 ;; `ggtags-process-environment'.
549 (process-environment
550 (append ggtags-process-environment
551 process-environment
552 (and (not (ggtags-project-has-refs (ggtags-find-project)))
553 (list "GTAGSLABEL=ctags"))))
554 (envlist (delete-dups
555 (cl-loop for x in process-environment
556 when (string-match
557 "^\\(GTAGS[^=\n]*\\|MAKEOBJDIRPREFIX\\)=" x)
558 ;; May have duplicates thus `delete-dups'.
559 collect (concat (match-string 1 x)
560 "="
561 (getenv (match-string 1 x))))))
562 (help-form (format "y: save\nn: don't save\n=: diff\n?: help\n")))
563 (add-dir-local-variable nil 'ggtags-process-environment envlist)
564 ;; Remove trailing newlines by `add-dir-local-variable'.
565 (let ((delete-trailing-lines t)) (delete-trailing-whitespace))
566 (or noconfirm
567 (while (pcase (read-char-choice
568 (format "Save `%s'? (y/n/=/?) " buffer-file-name)
569 '(?y ?n ?= ??))
570 ;; ` required for 24.1 and 24.2
571 (`?n (user-error "Aborted"))
572 (`?y nil)
573 (`?= (diff-buffer-with-file) 'loop)
574 (`?? (help-form-show) 'loop))))
575 (save-buffer)
576 (kill-buffer)))
577
578 (defun ggtags-toggle-project-read-only ()
579 (interactive)
580 (ggtags-check-project)
581 (let ((inhibit-read-only t) ; for `add-dir-local-variable'
582 (val (not buffer-read-only))
583 (default-directory (ggtags-current-project-root)))
584 (add-dir-local-variable nil 'buffer-read-only val)
585 (save-buffer)
586 (kill-buffer)
587 (when buffer-file-name
588 (read-only-mode (if val +1 -1)))
589 (when (called-interactively-p 'interactive)
590 (message "Project read-only-mode is %s" (if val "on" "off")))
591 val))
592
593 (defun ggtags-visit-project-root ()
594 (interactive)
595 (ggtags-ensure-project)
596 (dired (ggtags-current-project-root)))
597
598 (defmacro ggtags-with-current-project (&rest body)
599 "Eval BODY in current project's `process-environment'."
600 (declare (debug t) (indent 0))
601 (let ((gtagsroot (make-symbol "-gtagsroot-"))
602 (root (make-symbol "-ggtags-project-root-")))
603 `(let* ((,root ggtags-project-root)
604 (,gtagsroot (when (ggtags-find-project)
605 (ggtags-ensure-localname
606 (directory-file-name (ggtags-current-project-root)))))
607 (process-environment
608 (append (let ((process-environment process-environment))
609 (and ,gtagsroot (setenv "GTAGSROOT" ,gtagsroot))
610 (mapcar #'substitute-env-vars ggtags-process-environment))
611 process-environment
612 (and ,gtagsroot (list (concat "GTAGSROOT=" ,gtagsroot)))
613 (and (ggtags-find-project)
614 (not (ggtags-project-has-refs (ggtags-find-project)))
615 (list "GTAGSLABEL=ctags")))))
616 (unwind-protect (save-current-buffer ,@body)
617 (setq ggtags-project-root ,root)))))
618
619 (defun ggtags-get-libpath ()
620 (let ((path (ggtags-with-current-project (getenv "GTAGSLIBPATH"))))
621 (and path (mapcar (apply-partially #'concat (file-remote-p default-directory))
622 (split-string path (regexp-quote path-separator) t)))))
623
624 (defun ggtags-project-relative-file (file)
625 "Get file name relative to current project root."
626 (ggtags-check-project)
627 (if (file-name-absolute-p file)
628 (file-relative-name file (if (string-prefix-p (ggtags-current-project-root)
629 file)
630 (ggtags-current-project-root)
631 (locate-dominating-file file "GTAGS")))
632 file))
633
634 (defun ggtags-project-file-p (file)
635 "Return non-nil if FILE is part of current project."
636 (when (ggtags-find-project)
637 (with-temp-buffer
638 (ggtags-with-current-project
639 (process-file (ggtags-program-path "global") nil t nil
640 "-vP" (concat "^" (ggtags-project-relative-file file) "$")))
641 (goto-char (point-min))
642 (not (re-search-forward "^file not found" nil t)))))
643
644 (defun ggtags-create-tags (root)
645 "Create tag files (e.g. GTAGS) in directory ROOT.
646 If file .globalrc or gtags.conf exists in ROOT, it will be used
647 as configuration file per `ggtags-use-project-gtagsconf'.
648
649 If file gtags.files exists in ROOT, it should be a list of source
650 files to index, which can be used to speed gtags up in large
651 source trees. See Info node `(global)gtags' for details."
652 (interactive "DRoot directory: ")
653 (let ((process-environment process-environment))
654 (when (zerop (length root)) (error "No root directory provided"))
655 (setenv "GTAGSROOT" (ggtags-ensure-localname
656 (expand-file-name
657 (directory-file-name (file-name-as-directory root)))))
658 (ggtags-with-current-project
659 (let ((conf (and ggtags-use-project-gtagsconf
660 (cl-loop for name in '(".globalrc" "gtags.conf")
661 for full = (expand-file-name name root)
662 thereis (and (file-exists-p full) full)))))
663 (unless (or conf (getenv "GTAGSLABEL")
664 (not (yes-or-no-p "Use `ctags' backend? ")))
665 (setenv "GTAGSLABEL" "ctags"))
666 (ggtags-with-temp-message "`gtags' in progress..."
667 (let ((default-directory (file-name-as-directory root))
668 (args (cl-remove-if #'null
669 (list (and ggtags-use-idutils "--idutils")
670 (and conf "--gtagsconf")
671 (and conf (ggtags-ensure-localname conf))))))
672 (condition-case err
673 (apply #'ggtags-process-string "gtags" args)
674 (error (if (and ggtags-use-idutils
675 (stringp (cadr err))
676 (string-match-p "mkid not found" (cadr err)))
677 ;; Retry without mkid
678 (apply #'ggtags-process-string
679 "gtags" (cl-remove "--idutils" args))
680 (signal (car err) (cdr err)))))))))
681 (message "GTAGS generated in `%s'" root)
682 root))
683
684 (defun ggtags-update-tags (&optional force)
685 "Update GNU Global tag database.
686 Do nothing if GTAGS exceeds the oversize limit unless FORCE."
687 (interactive (progn
688 (ggtags-check-project)
689 ;; Mark project info expired.
690 (setf (ggtags-project-timestamp (ggtags-find-project)) -1)
691 (list t)))
692 (when (or force (and (ggtags-find-project)
693 (not (ggtags-project-oversize-p))
694 (ggtags-project-dirty-p (ggtags-find-project))))
695 (ggtags-with-current-project
696 (ggtags-with-temp-message "`global -u' in progress..."
697 (ggtags-process-string "global" "-u")
698 (setf (ggtags-project-dirty-p (ggtags-find-project)) nil)
699 (setf (ggtags-project-mtime (ggtags-find-project)) (float-time))))))
700
701 (defun ggtags-update-tags-single (file &optional nowait)
702 (cl-check-type file string)
703 (ggtags-with-current-project
704 (process-file (ggtags-program-path "global") nil (and nowait 0) nil
705 "--single-update" (ggtags-project-relative-file file))))
706
707 (defun ggtags-delete-tags ()
708 "Delete file GTAGS, GRTAGS, GPATH, ID etc. generated by gtags."
709 (interactive (ignore (ggtags-check-project)))
710 (when (ggtags-current-project-root)
711 (let* ((re (concat "\\`" (regexp-opt '("GPATH" "GRTAGS" "GTAGS" "ID")) "\\'"))
712 (files (cl-remove-if-not
713 (lambda (file)
714 ;; Don't trust `directory-files'.
715 (let ((case-fold-search nil))
716 (string-match-p re (file-name-nondirectory file))))
717 (directory-files (ggtags-current-project-root) t re)))
718 (buffer "*GTags File List*"))
719 (or files (user-error "No tag files found"))
720 (with-output-to-temp-buffer buffer
721 (princ (mapconcat #'identity files "\n")))
722 (let ((win (get-buffer-window buffer)))
723 (unwind-protect
724 (progn
725 (fit-window-to-buffer win)
726 (when (yes-or-no-p "Remove GNU Global tag files? ")
727 (with-demoted-errors (mapc #'delete-file files))
728 (remhash (ggtags-current-project-root) ggtags-projects)
729 (and (overlayp ggtags-highlight-tag-overlay)
730 (delete-overlay ggtags-highlight-tag-overlay))))
731 (when (window-live-p win)
732 (quit-window t win)))))))
733
734 (defvar-local ggtags-completion-cache nil)
735
736 ;; See global/libutil/char.c
737 ;; (defconst ggtags-regexp-metachars "[][$()*+.?\\{}|^]")
738 (defvar ggtags-completion-flag "") ;internal use
739
740 (defvar ggtags-completion-table
741 (completion-table-dynamic
742 (lambda (prefix)
743 (let ((cache-key (concat prefix "$" ggtags-completion-flag)))
744 (unless (equal cache-key (car ggtags-completion-cache))
745 (setq ggtags-completion-cache
746 (cons cache-key
747 (ignore-errors-unless-debug
748 ;; May throw global: only name char is allowed
749 ;; with -c option.
750 (ggtags-with-current-project
751 (split-string
752 (apply #'ggtags-process-string
753 "global"
754 (append (and completion-ignore-case '("--ignore-case"))
755 ;; Note -c alone returns only definitions
756 (list (concat "-c" ggtags-completion-flag) prefix)))
757 "\n" t)))))))
758 (cdr ggtags-completion-cache))))
759
760 (defun ggtags-completion-at-point ()
761 "A function for `completion-at-point-functions'."
762 (pcase (funcall ggtags-bounds-of-tag-function)
763 (`(,beg . ,end)
764 (and (< beg end) (list beg end ggtags-completion-table)))))
765
766 (defun ggtags-read-tag (&optional type confirm prompt require-match default)
767 (ggtags-ensure-project)
768 (let ((default (or default (ggtags-tag-at-point)))
769 (prompt (or prompt (capitalize (symbol-name (or type 'tag)))))
770 (ggtags-completion-flag (pcase type
771 (`(or nil definition) "T")
772 (`symbol "s")
773 (`reference "r")
774 (`id "I")
775 (`path "P")
776 ((pred stringp) type)
777 (_ ggtags-completion-flag))))
778 (setq ggtags-current-tag-name
779 (cond (confirm
780 (ggtags-update-tags)
781 (let ((completing-read-function
782 (or ggtags-completing-read-function
783 completing-read-function)))
784 (completing-read
785 (format (if default "%s (default %s): " "%s: ") prompt default)
786 ggtags-completion-table nil require-match nil nil default)))
787 (default (substring-no-properties default))
788 (t (ggtags-read-tag type t prompt require-match default))))))
789
790 (defun ggtags-global-build-command (cmd &rest args)
791 ;; CMD can be definition, reference, symbol, grep, idutils
792 (let ((xs (append (list (shell-quote-argument (ggtags-program-path "global"))
793 "-v"
794 (format "--result=%s" ggtags-global-output-format)
795 (and ggtags-global-ignore-case "--ignore-case")
796 (and ggtags-global-use-color
797 (ggtags-find-project)
798 (ggtags-project-has-color (ggtags-find-project))
799 "--color=always")
800 (and (ggtags-find-project)
801 (ggtags-project-has-path-style (ggtags-find-project))
802 "--path-style=shorter")
803 (and ggtags-global-treat-text "--other")
804 (pcase cmd
805 ((pred stringp) cmd)
806 (`definition "") ;-d not supported by Global 5.7.1
807 (`reference "-r")
808 (`symbol "-s")
809 (`path "--path")
810 (`grep "--grep")
811 (`idutils "--idutils")))
812 args)))
813 (mapconcat #'identity (delq nil xs) " ")))
814
815 ;; Can be three values: nil, t and a marker; t means start marker has
816 ;; been saved in the tag ring.
817 (defvar ggtags-global-start-marker nil)
818 (defvar ggtags-tag-ring-index nil)
819 (defvar ggtags-global-search-history nil)
820
821 (defvar ggtags-auto-jump-to-match-target nil)
822
823 (defvar-local ggtags-global-exit-info nil) ; (EXIT-STATUS COUNT DB)
824
825 (defun ggtags-global-save-start-marker ()
826 (when (markerp ggtags-global-start-marker)
827 (setq ggtags-tag-ring-index nil)
828 (ring-insert find-tag-marker-ring ggtags-global-start-marker)
829 (setq ggtags-global-start-marker t)))
830
831 (defun ggtags-global-start (command &optional directory)
832 (let* ((default-directory (or directory (ggtags-current-project-root)))
833 (split-window-preferred-function ggtags-split-window-function)
834 (env ggtags-process-environment))
835 (unless (markerp ggtags-global-start-marker)
836 (setq ggtags-global-start-marker (point-marker)))
837 (setq ggtags-auto-jump-to-match-target
838 (nth 4 (assoc (ggtags-global-search-id command default-directory)
839 ggtags-global-search-history)))
840 (ggtags-navigation-mode +1)
841 (ggtags-update-tags)
842 (ggtags-with-current-project
843 (with-current-buffer (with-display-buffer-no-window
844 (compilation-start command 'ggtags-global-mode))
845 (setq-local ggtags-process-environment env)
846 (setq ggtags-global-last-buffer (current-buffer))))))
847
848 (defun ggtags-find-tag-continue ()
849 (interactive)
850 (ggtags-ensure-global-buffer
851 (ggtags-navigation-mode +1)
852 (let ((split-window-preferred-function ggtags-split-window-function))
853 (ignore-errors (compilation-next-error 1))
854 (compile-goto-error))))
855
856 (defun ggtags-find-tag (cmd &rest args)
857 (ggtags-check-project)
858 (ggtags-global-start (apply #'ggtags-global-build-command cmd args)))
859
860 (defun ggtags-include-file ()
861 "Calculate the include file based on `ggtags-include-pattern'."
862 (pcase ggtags-include-pattern
863 (`nil nil)
864 ((pred functionp)
865 (funcall ggtags-include-pattern))
866 (`(,re . ,sub)
867 (save-excursion
868 (beginning-of-line)
869 (and (looking-at re) (match-string sub))))
870 (_ (warn "Invalid value for `ggtags-include-pattern': %s"
871 ggtags-include-pattern)
872 nil)))
873
874 ;;;###autoload
875 (defun ggtags-find-tag-dwim (name &optional what)
876 "Find NAME by context.
877 If point is at a definition tag, find references, and vice versa.
878 If point is at a line that matches `ggtags-include-pattern', find
879 the include file instead.
880
881 When called interactively with a prefix arg, always find
882 definition tags."
883 (interactive
884 (let ((include (and (not current-prefix-arg) (ggtags-include-file))))
885 (ggtags-ensure-project)
886 (if include (list include 'include)
887 (list (ggtags-read-tag 'definition current-prefix-arg)
888 (and current-prefix-arg 'definition)))))
889 (ggtags-check-project) ; For `ggtags-current-project-root' below.
890 (cond
891 ((eq what 'include)
892 (ggtags-find-file name))
893 ((or (eq what 'definition)
894 (not buffer-file-name)
895 (not (ggtags-project-has-refs (ggtags-find-project)))
896 (not (ggtags-project-file-p buffer-file-name)))
897 (ggtags-find-definition name))
898 (t (ggtags-find-tag (format "--from-here=%d:%s"
899 (line-number-at-pos)
900 (shell-quote-argument
901 ;; Note `ggtags-global-start' binds
902 ;; default-directory to project root.
903 (ggtags-project-relative-file buffer-file-name)))
904 (shell-quote-argument name)))))
905
906 (defun ggtags-find-tag-mouse (event)
907 (interactive "e")
908 (with-selected-window (posn-window (event-start event))
909 (save-excursion
910 (goto-char (posn-point (event-start event)))
911 (call-interactively #'ggtags-find-tag-dwim))))
912
913 ;; Another option for `M-.'.
914 (defun ggtags-find-definition (name)
915 (interactive (list (ggtags-read-tag 'definition current-prefix-arg)))
916 (ggtags-find-tag 'definition (shell-quote-argument name)))
917
918 (defun ggtags-setup-libpath-search (type name)
919 (pcase (and ggtags-global-search-libpath-for-reference
920 (ggtags-get-libpath))
921 ((and libs (guard libs))
922 (cl-labels ((cont (buf how)
923 (pcase ggtags-global-exit-info
924 (`(0 0 ,_)
925 (with-temp-buffer
926 (setq default-directory
927 (file-name-as-directory (pop libs)))
928 (and libs (setq ggtags-global-continuation #'cont))
929 (if (ggtags-find-project)
930 (ggtags-find-tag type (shell-quote-argument name))
931 (cont buf how))))
932 (_ (ggtags-global-handle-exit buf how)))))
933 (setq ggtags-global-continuation #'cont)))))
934
935 (defun ggtags-find-reference (name)
936 (interactive (list (ggtags-read-tag 'reference current-prefix-arg)))
937 (ggtags-setup-libpath-search 'reference name)
938 (ggtags-find-tag 'reference (shell-quote-argument name)))
939
940 (defun ggtags-find-other-symbol (name)
941 "Find tag NAME that is a reference without a definition."
942 (interactive (list (ggtags-read-tag 'symbol current-prefix-arg)))
943 (ggtags-setup-libpath-search 'symbol name)
944 (ggtags-find-tag 'symbol (shell-quote-argument name)))
945
946 (defun ggtags-quote-pattern (pattern)
947 (prin1-to-string (substring-no-properties pattern)))
948
949 (defun ggtags-idutils-query (pattern)
950 (interactive (list (ggtags-read-tag 'id t)))
951 (ggtags-find-tag 'idutils "--" (ggtags-quote-pattern pattern)))
952
953 (defun ggtags-grep (pattern &optional invert-match)
954 "Grep for lines matching PATTERN.
955 Invert the match when called with a prefix arg \\[universal-argument]."
956 (interactive (list (ggtags-read-tag 'definition 'confirm
957 (if current-prefix-arg
958 "Inverted grep pattern" "Grep pattern"))
959 current-prefix-arg))
960 (ggtags-find-tag 'grep (and invert-match "--invert-match")
961 "--" (ggtags-quote-pattern pattern)))
962
963 (defun ggtags-find-file (pattern &optional invert-match)
964 (interactive (list (ggtags-read-tag 'path 'confirm (if current-prefix-arg
965 "Inverted path pattern"
966 "Path pattern")
967 nil (thing-at-point 'filename))
968 current-prefix-arg))
969 (let ((ggtags-global-output-format 'path))
970 (ggtags-find-tag 'path (and invert-match "--invert-match")
971 "--" (ggtags-quote-pattern pattern))))
972
973 ;; Note: Coloured output requested in http://goo.gl/Y9IcX and appeared
974 ;; in global v6.2.12.
975 (defun ggtags-find-tag-regexp (regexp directory)
976 "List tags matching REGEXP in DIRECTORY (default to project root).
977 When called interactively with a prefix, ask for the directory."
978 (interactive
979 (progn
980 (ggtags-check-project)
981 (list (ggtags-read-tag "" t "POSIX regexp")
982 (if current-prefix-arg
983 (read-directory-name "Directory: " nil nil t)
984 (ggtags-current-project-root)))))
985 (ggtags-check-project)
986 (ggtags-global-start
987 (ggtags-global-build-command nil nil "-l" "--" (ggtags-quote-pattern regexp))
988 (file-name-as-directory directory)))
989
990 (defvar ggtags-navigation-mode)
991
992 (defun ggtags-query-replace (from to &optional delimited)
993 "Query replace FROM with TO on files in the Global buffer.
994 If not in navigation mode, do a grep on FROM first.
995
996 Note: the regular expression FROM must be supported by both
997 Global and Emacs."
998 (interactive
999 ;; Note: in 24.4 query-replace-read-args returns a list of 4 elements.
1000 (let ((args (query-replace-read-args "Query replace (regexp)" t t)))
1001 (list (nth 0 args) (nth 1 args) (nth 2 args))))
1002 (unless ggtags-navigation-mode
1003 (let ((ggtags-auto-jump-to-match nil))
1004 (ggtags-grep from)))
1005 (let ((file-form
1006 '(let ((files))
1007 (ggtags-ensure-global-buffer
1008 (ggtags-with-temp-message "Waiting for Grep to finish..."
1009 (while (get-buffer-process (current-buffer))
1010 (sit-for 0.2)))
1011 (goto-char (point-min))
1012 (while (ignore-errors (compilation-next-file 1) t)
1013 (let ((m (get-text-property (point) 'compilation-message)))
1014 (push (expand-file-name
1015 (caar (compilation--loc->file-struct
1016 (compilation--message->loc m))))
1017 files))))
1018 (ggtags-navigation-mode -1)
1019 (nreverse files))))
1020 (tags-query-replace from to delimited file-form)))
1021
1022 (defun ggtags-global-search-id (cmd directory)
1023 (sha1 (concat directory (make-string 1 0) cmd)))
1024
1025 (defun ggtags-global-current-search ()
1026 ;; CMD DIR ENV LINE TEXT
1027 (ggtags-ensure-global-buffer
1028 (list (car compilation-arguments)
1029 default-directory
1030 ggtags-process-environment
1031 (line-number-at-pos)
1032 (buffer-substring-no-properties
1033 (line-beginning-position) (line-end-position)))))
1034
1035 (defun ggtags-global-rerun-search-1 (data)
1036 (pcase data
1037 (`(,cmd ,dir ,env ,line ,_text)
1038 (with-current-buffer (let ((ggtags-auto-jump-to-match nil)
1039 ;; Switch current project to DIR.
1040 (default-directory dir)
1041 (ggtags-project-root dir)
1042 (ggtags-process-environment env))
1043 (ggtags-global-start cmd dir))
1044 (add-hook 'compilation-finish-functions
1045 (lambda (buf _msg)
1046 (with-current-buffer buf
1047 (ggtags-forward-to-line line)
1048 (compile-goto-error)))
1049 nil t)))))
1050
1051 (defvar-local ggtags-global-search-ewoc nil)
1052 (defvar ggtags-global-rerun-search-last nil)
1053
1054 (defvar ggtags-global-rerun-search-map
1055 (cl-labels
1056 ((save ()
1057 (setq ggtags-global-rerun-search-last
1058 (ewoc-data (ewoc-locate ggtags-global-search-ewoc))))
1059 (next (arg)
1060 (interactive "p")
1061 (ewoc-goto-next ggtags-global-search-ewoc arg)
1062 (save))
1063 (prev (arg)
1064 (interactive "p")
1065 (ewoc-goto-prev ggtags-global-search-ewoc arg)
1066 (save))
1067 (quit ()
1068 (interactive)
1069 (quit-windows-on (ewoc-buffer ggtags-global-search-ewoc) t))
1070 (done ()
1071 (interactive)
1072 (let ((node (ewoc-locate ggtags-global-search-ewoc)))
1073 (when node
1074 (save)
1075 (quit)
1076 (ggtags-global-rerun-search-1 (cdr (ewoc-data node)))))))
1077 (let ((m (make-sparse-keymap)))
1078 (set-keymap-parent m special-mode-map)
1079 (define-key m "p" #'prev)
1080 (define-key m "\M-p" #'prev)
1081 (define-key m "n" #'next)
1082 (define-key m "\M-n" #'next)
1083 (define-key m "r" #'ggtags-save-to-register)
1084 (define-key m "q" #'quit)
1085 (define-key m "\r" #'done)
1086 m)))
1087
1088 (defvar bookmark-make-record-function)
1089
1090 (defun ggtags-global-rerun-search ()
1091 "Pop up a buffer to choose a past search to re-run.
1092
1093 \\{ggtags-global-rerun-search-map}"
1094 (interactive)
1095 (or ggtags-global-search-history (user-error "No search history"))
1096 (let ((split-window-preferred-function ggtags-split-window-function)
1097 (inhibit-read-only t))
1098 (pop-to-buffer "*Ggtags Search History*")
1099 (erase-buffer)
1100 (special-mode)
1101 (use-local-map ggtags-global-rerun-search-map)
1102 (setq-local ggtags-enable-navigation-keys nil)
1103 (setq-local bookmark-make-record-function #'ggtags-make-bookmark-record)
1104 (setq truncate-lines t)
1105 (cl-labels ((prop (s)
1106 (propertize s 'face 'minibuffer-prompt))
1107 (pp (data)
1108 (pcase data
1109 (`(,_id ,cmd ,dir ,_env ,line ,text)
1110 (insert (prop " cmd: ") cmd "\n"
1111 (prop " dir: ") dir "\n"
1112 (prop "line: ") (number-to-string line) "\n"
1113 (prop "text: ") text "\n"
1114 (propertize (make-string 32 ?-) 'face 'shadow))))))
1115 (setq ggtags-global-search-ewoc
1116 (ewoc-create #'pp "Global search history keys: n:next p:prev r:register RET:choose\n")))
1117 (dolist (data ggtags-global-search-history)
1118 (ewoc-enter-last ggtags-global-search-ewoc data))
1119 (and ggtags-global-rerun-search-last
1120 (re-search-forward (cadr ggtags-global-rerun-search-last) nil t)
1121 (ewoc-goto-node ggtags-global-search-ewoc
1122 (ewoc-locate ggtags-global-search-ewoc)))
1123 (set-buffer-modified-p nil)
1124 (fit-window-to-buffer nil (floor (frame-height) 2))))
1125
1126 (defun ggtags-save-to-register (r)
1127 "Save current search session to register R.
1128 Use \\[jump-to-register] to restore the search session."
1129 (interactive (list (register-read-with-preview "Save search to register: ")))
1130 (cl-labels ((prn (data)
1131 (pcase data
1132 (`(,command ,root ,_env ,line ,_)
1133 (princ (format "a ggtags search session `%s' in directory `%s' at line %d."
1134 command root line))))))
1135 (set-register r (registerv-make
1136 (if ggtags-global-search-ewoc
1137 (cdr (ewoc-data (ewoc-locate ggtags-global-search-ewoc)))
1138 (ggtags-global-current-search))
1139 :jump-func #'ggtags-global-rerun-search-1
1140 :print-func #'prn))))
1141
1142 (defun ggtags-make-bookmark-record ()
1143 `(,(and ggtags-current-tag-name (format "*ggtags %s*" ggtags-current-tag-name))
1144 (ggtags-search . ,(if ggtags-global-search-ewoc
1145 (cdr (ewoc-data (ewoc-locate ggtags-global-search-ewoc)))
1146 (ggtags-global-current-search)))
1147 (handler . ggtags-bookmark-jump)))
1148
1149 (declare-function bookmark-prop-get "bookmark")
1150
1151 (defun ggtags-bookmark-jump (bmk)
1152 (ggtags-global-rerun-search-1 (bookmark-prop-get bmk 'ggtags-search)))
1153
1154 (defun ggtags-browse-file-as-hypertext (file line)
1155 "Browse FILE in hypertext (HTML) form."
1156 (interactive (if (or current-prefix-arg (not buffer-file-name))
1157 (list (read-file-name "Browse file: " nil nil t)
1158 (read-number "Line: " 1))
1159 (list buffer-file-name (line-number-at-pos))))
1160 (cl-check-type line (integer 1))
1161 (or (and file (file-exists-p file)) (error "File `%s' doesn't exist" file))
1162 (ggtags-check-project)
1163 (or (file-exists-p (expand-file-name "HTML" (ggtags-current-project-root)))
1164 (if (yes-or-no-p "No hypertext form exists; run htags? ")
1165 (let ((default-directory (ggtags-current-project-root)))
1166 (ggtags-with-current-project (ggtags-process-string "htags")))
1167 (user-error "Aborted")))
1168 (let ((url (ggtags-process-string "gozilla" "-p" (format "+%d" line)
1169 (file-relative-name file))))
1170 (or (equal (file-name-extension
1171 (url-filename (url-generic-parse-url url))) "html")
1172 (user-error "No hypertext form for `%s'" file))
1173 (when (called-interactively-p 'interactive)
1174 (message "Browsing %s" url))
1175 (browse-url url)))
1176
1177 (defun ggtags-next-mark (&optional arg)
1178 "Move to the next (newer) mark in the tag marker ring."
1179 (interactive)
1180 (and (ring-empty-p find-tag-marker-ring) (user-error "Tag ring empty"))
1181 (setq ggtags-tag-ring-index
1182 ;; Note `ring-minus1' gets newer item.
1183 (funcall (if arg #'ring-plus1 #'ring-minus1)
1184 (or ggtags-tag-ring-index
1185 (progn
1186 (ring-insert find-tag-marker-ring (point-marker))
1187 0))
1188 (ring-length find-tag-marker-ring)))
1189 (let ((m (ring-ref find-tag-marker-ring ggtags-tag-ring-index))
1190 (i (- (ring-length find-tag-marker-ring) ggtags-tag-ring-index)))
1191 (ggtags-echo "%d%s marker%s" i (pcase (mod i 10)
1192 ;; ` required for 24.1 and 24.2
1193 (`1 "st")
1194 (`2 "nd")
1195 (`3 "rd")
1196 (_ "th"))
1197 (if (marker-buffer m) "" " (dead)"))
1198 (if (not (marker-buffer m))
1199 (ding)
1200 (switch-to-buffer (marker-buffer m))
1201 (goto-char m))))
1202
1203 (defun ggtags-prev-mark ()
1204 "Move to the previous (older) mark in the tag marker ring."
1205 (interactive)
1206 (ggtags-next-mark 'previous))
1207
1208 (defvar ggtags-view-tag-history-mode-map
1209 (let ((m (make-sparse-keymap)))
1210 (define-key m "\M-n" 'next-error-no-select)
1211 (define-key m "\M-p" 'previous-error-no-select)
1212 (define-key m "q" (lambda () (interactive) (quit-window t)))
1213 m))
1214
1215 (define-derived-mode ggtags-view-tag-history-mode tabulated-list-mode "TagHist"
1216 :abbrev-table nil :group 'ggtags)
1217
1218 (defun ggtags-view-tag-history ()
1219 "Pop to a buffer listing visited locations from newest to oldest.
1220 The buffer is a next error buffer and works with standard
1221 commands `next-error' and `previous-error'.
1222
1223 \\{ggtags-view-tag-history-mode-map}"
1224 (interactive)
1225 (and (ring-empty-p find-tag-marker-ring)
1226 (user-error "Tag ring empty"))
1227 (let ((split-window-preferred-function ggtags-split-window-function)
1228 (inhibit-read-only t))
1229 (pop-to-buffer "*Tag Ring*")
1230 (erase-buffer)
1231 (ggtags-view-tag-history-mode)
1232 (setq next-error-function #'ggtags-view-tag-history-next-error
1233 next-error-last-buffer (current-buffer))
1234 (setq tabulated-list-entries
1235 ;; Use a function so that revert can work properly.
1236 (lambda ()
1237 (let ((counter (ring-length find-tag-marker-ring))
1238 (elements (or (ring-elements find-tag-marker-ring)
1239 (user-error "Tag ring empty")))
1240 (action (lambda (_button) (next-error 0)))
1241 (get-line (lambda (m)
1242 (with-current-buffer (marker-buffer m)
1243 (save-excursion
1244 (goto-char m)
1245 (buffer-substring (line-beginning-position)
1246 (line-end-position)))))))
1247 (setq tabulated-list-format
1248 `[("ID" ,(max (1+ (floor (log counter 10))) 2)
1249 car-less-than-car)
1250 ("Buffer" ,(max (cl-loop for m in elements
1251 for b = (marker-buffer m)
1252 maximize
1253 (length (and b (buffer-name b))))
1254 6)
1255 t :right-align t)
1256 ("Position" ,(max (cl-loop for m in elements
1257 for p = (or (marker-position m) 1)
1258 maximize (1+ (floor (log p 10))))
1259 8)
1260 (lambda (x y)
1261 (< (string-to-number (aref (cadr x) 2))
1262 (string-to-number (aref (cadr y) 2))))
1263 :right-align t)
1264 ("Contents" 100 t)])
1265 (tabulated-list-init-header)
1266 (mapcar (lambda (x)
1267 (prog1
1268 (list counter
1269 (if (marker-buffer x)
1270 (vector (number-to-string counter)
1271 `(,(buffer-name (marker-buffer x))
1272 face link
1273 follow-link t
1274 marker ,x
1275 action ,action)
1276 (number-to-string (marker-position x))
1277 (funcall get-line x))
1278 (vector (number-to-string counter)
1279 "(dead)" "?" "?")))
1280 (cl-decf counter)))
1281 elements))))
1282 (setq tabulated-list-sort-key '("ID" . t))
1283 (tabulated-list-print)
1284 (fit-window-to-buffer nil (floor (frame-height) 2))))
1285
1286 (defun ggtags-view-tag-history-next-error (&optional arg reset)
1287 (if (not reset)
1288 (forward-button arg)
1289 (goto-char (point-min))
1290 (forward-button (if (button-at (point)) 0 1)))
1291 (when (get-buffer-window)
1292 (set-window-point (get-buffer-window) (point)))
1293 (pcase (button-get (button-at (point)) 'marker)
1294 ((and (pred markerp) m)
1295 (if (eq (get-buffer-window) (selected-window))
1296 (pop-to-buffer (marker-buffer m))
1297 (switch-to-buffer (marker-buffer m)))
1298 (goto-char (marker-position m)))
1299 (_ (error "Dead marker"))))
1300
1301 (defun ggtags-global-exit-message-1 ()
1302 "Get the total of matches and db file used."
1303 (save-excursion
1304 (goto-char (point-max))
1305 (if (re-search-backward
1306 "^\\w+ \\(not found\\)\\|^\\([0-9]+\\) \\w+ located" nil t)
1307 (cons (or (and (match-string 1) 0)
1308 (string-to-number (match-string 2)))
1309 (when (re-search-forward
1310 "using \\(?:\\(idutils\\)\\|'[^']*/\\(\\w+\\)'\\)"
1311 (line-end-position)
1312 t)
1313 (or (and (match-string 1) "ID")
1314 (match-string 2))))
1315 (cons 0 nil))))
1316
1317 (defun ggtags-global-exit-message-function (_process-status exit-status msg)
1318 "A function for `compilation-exit-message-function'."
1319 (pcase (ggtags-global-exit-message-1)
1320 (`(,count . ,db)
1321 (setq ggtags-global-exit-info (list exit-status count db))
1322 ;; Clear the start marker in case of zero matches.
1323 (and (zerop count)
1324 (markerp ggtags-global-start-marker)
1325 (not ggtags-global-continuation)
1326 (setq ggtags-global-start-marker nil))
1327 (cons (if (> exit-status 0)
1328 msg
1329 (format "found %d %s" count
1330 (funcall (if (= count 1) #'car #'cadr)
1331 (pcase db
1332 ;; ` required for 24.1 and 24.2
1333 (`"GTAGS" '("definition" "definitions"))
1334 (`"GSYMS" '("symbol" "symbols"))
1335 (`"GRTAGS" '("reference" "references"))
1336 (`"GPATH" '("file" "files"))
1337 (`"ID" '("identifier" "identifiers"))
1338 (_ '("match" "matches"))))))
1339 exit-status))))
1340
1341 (defun ggtags-global-column (start)
1342 ;; START is the beginning position of source text.
1343 (let ((mbeg (text-property-any start (line-end-position) 'global-color t)))
1344 (and mbeg (- mbeg start))))
1345
1346 ;;; NOTE: Must not match the 'Global started at Mon Jun 3 10:24:13'
1347 ;;; line or `compilation-auto-jump' will jump there and fail. See
1348 ;;; comments before the 'gnu' entry in
1349 ;;; `compilation-error-regexp-alist-alist'.
1350 (defvar ggtags-global-error-regexp-alist-alist
1351 (append
1352 `((path "^\\(?:[^\"'\n]*/\\)?[^ )\t\n]+$" 0)
1353 ;; ACTIVE_ESCAPE src/dialog.cc 172
1354 (ctags "^\\([^ \t\n]+\\)[ \t]+\\(.*?\\)[ \t]+\\([0-9]+\\)$"
1355 2 3 nil nil 2 (1 font-lock-function-name-face))
1356 ;; ACTIVE_ESCAPE 172 src/dialog.cc #undef ACTIVE_ESCAPE
1357 (ctags-x "^\\([^ \t\n]+\\)[ \t]+\\([0-9]+\\)[ \t]+\\(\\(?:[^/\n]*/\\)?[^ \t\n]+\\)"
1358 3 2 (,(lambda () (ggtags-global-column (1+ (match-end 0)))))
1359 nil 3 (1 font-lock-function-name-face))
1360 ;; src/dialog.cc:172:#undef ACTIVE_ESCAPE
1361 (grep "^\\(.+?\\):\\([0-9]+\\):\\(?:$\\|[^0-9\n]\\|[0-9][^0-9\n]\\|[0-9][0-9].\\)"
1362 1 2 (,(lambda () (ggtags-global-column (1+ (match-end 2))))) nil 1)
1363 ;; src/dialog.cc ACTIVE_ESCAPE 172 #undef ACTIVE_ESCAPE
1364 (cscope "^\\(.+?\\)[ \t]+\\([^ \t\n]+\\)[ \t]+\\([0-9]+\\).*\\(?:[^0-9\n]\\|[^0-9\n][0-9]\\|[^:\n][0-9][0-9]\\)$"
1365 1 3 nil nil 1 (2 font-lock-function-name-face)))
1366 compilation-error-regexp-alist-alist))
1367
1368 (defun ggtags-abbreviate-file (start end)
1369 (let ((inhibit-read-only t)
1370 (amount (if (numberp ggtags-global-abbreviate-filename)
1371 (- (- end start) ggtags-global-abbreviate-filename)
1372 999))
1373 (advance-word (lambda ()
1374 "Return the length of the text made invisible."
1375 (let ((wend (min end (progn (forward-word 1) (point))))
1376 (wbeg (max start (progn (backward-word 1) (point)))))
1377 (goto-char wend)
1378 (if (<= (- wend wbeg) 1)
1379 0
1380 (put-text-property (1+ wbeg) wend 'invisible t)
1381 (1- (- wend wbeg)))))))
1382 (goto-char start)
1383 (while (and (> amount 0) (> end (point)))
1384 (cl-decf amount (funcall advance-word)))))
1385
1386 (defun ggtags-abbreviate-files (start end)
1387 (goto-char start)
1388 (let* ((error-re (cdr (assq (car compilation-error-regexp-alist)
1389 ggtags-global-error-regexp-alist-alist)))
1390 (sub (cadr error-re)))
1391 (when (and ggtags-global-abbreviate-filename error-re)
1392 (while (re-search-forward (car error-re) end t)
1393 (when (and (or (not (numberp ggtags-global-abbreviate-filename))
1394 (> (length (match-string sub))
1395 ggtags-global-abbreviate-filename))
1396 ;; Ignore bogus file lines such as:
1397 ;; Global found 2 matches at Thu Jan 31 13:45:19
1398 (get-text-property (match-beginning sub) 'compilation-message))
1399 (ggtags-abbreviate-file (match-beginning sub) (match-end sub)))))))
1400
1401 (defvar-local ggtags-global-output-lines 0)
1402
1403 (defun ggtags-global--display-buffer (&optional buffer desired-point)
1404 (pcase (let ((buffer (or buffer (current-buffer)))
1405 (split-window-preferred-function ggtags-split-window-function))
1406 (and (not (get-buffer-window buffer))
1407 (display-buffer buffer '(nil (allow-no-window . t)))))
1408 ((and (pred windowp) w)
1409 (with-selected-window w
1410 (compilation-set-window-height w)
1411 (and desired-point (goto-char desired-point))))))
1412
1413 (defun ggtags-global-filter ()
1414 "Called from `compilation-filter-hook' (which see)."
1415 (let ((ansi-color-apply-face-function
1416 (lambda (beg end face)
1417 (when face
1418 (ansi-color-apply-overlay-face beg end face)
1419 (put-text-property beg end 'global-color t)))))
1420 (ansi-color-apply-on-region compilation-filter-start (point)))
1421 ;; Get rid of line "Using config file '/PATH/TO/.globalrc'." or
1422 ;; "Using default configuration."
1423 (when (re-search-backward
1424 "^ *Using \\(?:config file '.*\\|default configuration.\\)\n"
1425 compilation-filter-start t)
1426 (replace-match ""))
1427 (cl-incf ggtags-global-output-lines
1428 (count-lines compilation-filter-start (point)))
1429 ;; If the number of output lines is small
1430 ;; `ggtags-global-handle-exit' takes care of displaying the buffer.
1431 (when (and (> ggtags-global-output-lines 30) ggtags-navigation-mode)
1432 (ggtags-global--display-buffer nil (or compilation-current-error (point-min))))
1433 (when (and (eq ggtags-auto-jump-to-match 'history)
1434 (numberp ggtags-auto-jump-to-match-target)
1435 (not compilation-current-error)
1436 ;; `ggtags-global-output-lines' is imprecise but use it
1437 ;; as first approximation.
1438 (> (+ 10 ggtags-global-output-lines) ggtags-auto-jump-to-match-target)
1439 (> (line-number-at-pos (point-max))
1440 ggtags-auto-jump-to-match-target))
1441 (ggtags-forward-to-line ggtags-auto-jump-to-match-target)
1442 (setq-local ggtags-auto-jump-to-match-target nil)
1443 ;;
1444 ;; Can't call `compile-goto-error' here becuase
1445 ;; `compilation-filter' restores point and as a result commands
1446 ;; dependent on point such as `ggtags-navigation-next-file' and
1447 ;; `ggtags-navigation-previous-file' fail to work.
1448 (run-with-idle-timer 0 nil (lambda (buf pt)
1449 (and (buffer-live-p buf)
1450 (with-current-buffer buf
1451 (ggtags-delay-finish-functions
1452 (let ((compilation-auto-jump-to-first-error t))
1453 (with-display-buffer-no-window
1454 (compilation-auto-jump buf pt)))))))
1455 (current-buffer) (point)))
1456 (make-local-variable 'ggtags-global-large-output)
1457 (when (> ggtags-global-output-lines ggtags-global-large-output)
1458 (cl-incf ggtags-global-large-output 500)
1459 (ggtags-echo "Output %d lines (Type `C-c C-k' to cancel)"
1460 ggtags-global-output-lines)))
1461
1462 (defun ggtags-global-handle-exit (buf how)
1463 "A function for `compilation-finish-functions' (which see)."
1464 (cond
1465 (ggtags-global-continuation
1466 (let ((cont (prog1 ggtags-global-continuation
1467 (setq ggtags-global-continuation nil))))
1468 (funcall cont buf how)))
1469 ((string-prefix-p "exited abnormally" how)
1470 ;; If exit abnormally display the buffer for inspection.
1471 (ggtags-global--display-buffer)
1472 (when (save-excursion
1473 (goto-char (point-max))
1474 (re-search-backward
1475 (eval-when-compile
1476 (format "^global: %s not found.$"
1477 (regexp-opt '("GTAGS" "GRTAGS" "GSYMS" "GPATH"))))
1478 nil t))
1479 (ggtags-echo "WARNING: Global tag files missing in `%s'"
1480 ggtags-project-root)
1481 (remhash ggtags-project-root ggtags-projects)))
1482 (ggtags-auto-jump-to-match
1483 (if (pcase (compilation-next-single-property-change
1484 (point-min) 'compilation-message)
1485 ((and pt (guard pt))
1486 (compilation-next-single-property-change
1487 (save-excursion (goto-char pt) (end-of-line) (point))
1488 'compilation-message)))
1489 ;; There are multiple matches so pop up the buffer.
1490 (ggtags-global--display-buffer)
1491 ;; For the `compilation-auto-jump' in idle timer to run.
1492 ;; See also: http://debbugs.gnu.org/13829
1493 (sit-for 0)
1494 (ggtags-navigation-mode -1)
1495 (ggtags-navigation-mode-cleanup buf 0)))))
1496
1497 (defvar ggtags-global-mode-font-lock-keywords
1498 '(("^Global \\(exited abnormally\\|interrupt\\|killed\\|terminated\\)\\(?:.*with code \\([0-9]+\\)\\)?.*"
1499 (1 'compilation-error)
1500 (2 'compilation-error nil t))
1501 ("^Global found \\([0-9]+\\)" (1 compilation-info-face))))
1502
1503 (defvar compilation-always-kill) ;new in 24.3
1504
1505 (define-compilation-mode ggtags-global-mode "Global"
1506 "A mode for showing outputs from gnu global."
1507 ;; Note: Place `ggtags-global-output-format' as first element for
1508 ;; `ggtags-abbreviate-files'.
1509 (setq-local compilation-error-regexp-alist (list ggtags-global-output-format))
1510 (when (markerp ggtags-global-start-marker)
1511 (setq ggtags-project-root
1512 (buffer-local-value 'ggtags-project-root
1513 (marker-buffer ggtags-global-start-marker))))
1514 (pcase ggtags-auto-jump-to-match
1515 (`history (make-local-variable 'ggtags-auto-jump-to-match-target)
1516 (setq-local compilation-auto-jump-to-first-error
1517 (not ggtags-auto-jump-to-match-target)))
1518 (`nil (setq-local compilation-auto-jump-to-first-error nil))
1519 (_ (setq-local compilation-auto-jump-to-first-error t)))
1520 (setq-local compilation-scroll-output nil)
1521 ;; See `compilation-move-to-column' for details.
1522 (setq-local compilation-first-column 0)
1523 (setq-local compilation-error-screen-columns nil)
1524 (setq-local compilation-disable-input t)
1525 (setq-local compilation-always-kill t)
1526 (setq-local compilation-error-face 'compilation-info)
1527 (setq-local compilation-exit-message-function
1528 'ggtags-global-exit-message-function)
1529 ;; See: https://github.com/leoliu/ggtags/issues/26
1530 (setq-local find-file-suppress-same-file-warnings t)
1531 (setq-local truncate-lines t)
1532 (jit-lock-register #'ggtags-abbreviate-files)
1533 (add-hook 'compilation-filter-hook 'ggtags-global-filter nil 'local)
1534 (add-hook 'compilation-finish-functions 'ggtags-global-handle-exit nil t)
1535 (setq-local bookmark-make-record-function #'ggtags-make-bookmark-record)
1536 (setq-local ggtags-enable-navigation-keys nil)
1537 (add-hook 'kill-buffer-hook (lambda () (ggtags-navigation-mode -1)) nil t))
1538
1539 ;; NOTE: Need this to avoid putting menu items in
1540 ;; `emulation-mode-map-alists', which creates double entries. See
1541 ;; http://i.imgur.com/VJJTzVc.png
1542 (defvar ggtags-navigation-map
1543 (let ((map (make-sparse-keymap)))
1544 (define-key map "\M-n" 'next-error)
1545 (define-key map "\M-p" 'previous-error)
1546 (define-key map "\M-}" 'ggtags-navigation-next-file)
1547 (define-key map "\M-{" 'ggtags-navigation-previous-file)
1548 (define-key map "\M->" 'ggtags-navigation-last-error)
1549 (define-key map "\M-<" 'first-error)
1550 ;; Note: shadows `isearch-forward-regexp' but it can be invoked
1551 ;; with C-u C-s instead.
1552 (define-key map "\C-\M-s" 'ggtags-navigation-isearch-forward)
1553 (define-key map "\C-c\C-k"
1554 (lambda () (interactive)
1555 (ggtags-ensure-global-buffer (kill-compilation))))
1556 (define-key map "\M-o" 'ggtags-navigation-visible-mode)
1557 (define-key map [return] 'ggtags-navigation-mode-done)
1558 (define-key map "\r" 'ggtags-navigation-mode-done)
1559 (define-key map [remap pop-tag-mark] 'ggtags-navigation-mode-abort)
1560 map))
1561
1562 (defvar ggtags-mode-map-alist
1563 `((ggtags-enable-navigation-keys . ,ggtags-navigation-map)))
1564
1565 (defvar ggtags-navigation-mode-map
1566 (let ((map (make-sparse-keymap))
1567 (menu (make-sparse-keymap "GG-Navigation")))
1568 ;; Menu items: (info "(elisp)Extended Menu Items")
1569 (define-key map [menu-bar ggtags-navigation] (cons "GG-Navigation" menu))
1570 ;; Ordered backwards
1571 (define-key menu [visible-mode]
1572 '(menu-item "Visible mode" ggtags-navigation-visible-mode
1573 :button (:toggle . (ignore-errors
1574 (ggtags-ensure-global-buffer
1575 visible-mode)))))
1576 (define-key menu [done]
1577 '(menu-item "Finish navigation" ggtags-navigation-mode-done))
1578 (define-key menu [abort]
1579 '(menu-item "Abort" ggtags-navigation-mode-abort))
1580 (define-key menu [last-match]
1581 '(menu-item "Last match" ggtags-navigation-last-error))
1582 (define-key menu [first-match] '(menu-item "First match" first-error))
1583 (define-key menu [previous-file]
1584 '(menu-item "Previous file" ggtags-navigation-previous-file))
1585 (define-key menu [next-file]
1586 '(menu-item "Next file" ggtags-navigation-next-file))
1587 (define-key menu [isearch-forward]
1588 '(menu-item "Find match with isearch" ggtags-navigation-isearch-forward))
1589 (define-key menu [previous]
1590 '(menu-item "Previous match" previous-error))
1591 (define-key menu [next]
1592 '(menu-item "Next match" next-error))
1593 map))
1594
1595 (defun ggtags-move-to-tag (&optional name)
1596 "Move to NAME tag in current line."
1597 (let ((tag (or name ggtags-current-tag-name)))
1598 ;; Do nothing if on the tag already i.e. by `ggtags-global-column'.
1599 (unless (or (not tag) (looking-at (concat (regexp-quote tag) "\\_>")))
1600 (let ((orig (point))
1601 (regexps (mapcar (lambda (fmtstr)
1602 (format fmtstr (regexp-quote tag)))
1603 '("\\_<%s\\_>" "%s\\_>" "%s"))))
1604 (beginning-of-line)
1605 (if (cl-loop for re in regexps
1606 ;; Note: tag might not agree with current
1607 ;; major-mode's symbol, so try harder. For
1608 ;; example, in `php-mode' $cacheBackend is a
1609 ;; symbol, but cacheBackend is a tag.
1610 thereis (re-search-forward re (line-end-position) t))
1611 (goto-char (match-beginning 0))
1612 (goto-char orig))))))
1613
1614 (defun ggtags-navigation-mode-cleanup (&optional buf time)
1615 (let ((buf (or buf ggtags-global-last-buffer)))
1616 (and (buffer-live-p buf)
1617 (with-current-buffer buf
1618 (when (get-buffer-process (current-buffer))
1619 (kill-compilation))
1620 (when (and (derived-mode-p 'ggtags-global-mode)
1621 (get-buffer-window))
1622 (quit-windows-on (current-buffer)))
1623 (and time (run-with-idle-timer time nil #'kill-buffer buf))))))
1624
1625 (defun ggtags-navigation-mode-done ()
1626 (interactive)
1627 (ggtags-navigation-mode -1)
1628 (setq tags-loop-scan t
1629 tags-loop-operate '(ggtags-find-tag-continue))
1630 (ggtags-navigation-mode-cleanup))
1631
1632 (defun ggtags-navigation-mode-abort ()
1633 "Abort navigation and return to where the search was started."
1634 (interactive)
1635 (ggtags-navigation-mode -1)
1636 (ggtags-navigation-mode-cleanup nil 0)
1637 ;; Run after (ggtags-navigation-mode -1) or
1638 ;; ggtags-global-start-marker might not have been saved.
1639 (when (and ggtags-global-start-marker
1640 (not (markerp ggtags-global-start-marker)))
1641 (setq ggtags-global-start-marker nil)
1642 (pop-tag-mark)))
1643
1644 (defun ggtags-navigation-next-file (n)
1645 (interactive "p")
1646 (ggtags-ensure-global-buffer
1647 (compilation-next-file n)
1648 (compile-goto-error)))
1649
1650 (defun ggtags-navigation-previous-file (n)
1651 (interactive "p")
1652 (ggtags-navigation-next-file (- n)))
1653
1654 (defun ggtags-navigation-last-error ()
1655 (interactive)
1656 (ggtags-ensure-global-buffer
1657 (goto-char (point-max))
1658 (compilation-previous-error 1)
1659 (compile-goto-error)))
1660
1661 (defun ggtags-navigation-isearch-forward (&optional regexp-p)
1662 (interactive "P")
1663 (ggtags-ensure-global-buffer
1664 (let ((saved (if visible-mode 1 -1)))
1665 (visible-mode 1)
1666 (with-selected-window (get-buffer-window (current-buffer))
1667 (isearch-forward regexp-p)
1668 (beginning-of-line)
1669 (visible-mode saved)
1670 (compile-goto-error)))))
1671
1672 (defun ggtags-navigation-visible-mode (&optional arg)
1673 (interactive (list (or current-prefix-arg 'toggle)))
1674 (ggtags-ensure-global-buffer
1675 (visible-mode arg)))
1676
1677 (defvar ggtags-global-line-overlay nil)
1678
1679 (defun ggtags-global-next-error-function ()
1680 (when (eq next-error-last-buffer ggtags-global-last-buffer)
1681 (ggtags-move-to-tag)
1682 (ggtags-global-save-start-marker)
1683 (and (ggtags-project-update-mtime-maybe)
1684 (message "File `%s' is newer than GTAGS"
1685 (file-name-nondirectory buffer-file-name)))
1686 (and ggtags-mode-sticky (ggtags-mode 1))
1687 (ignore-errors
1688 (ggtags-ensure-global-buffer
1689 (unless (overlayp ggtags-global-line-overlay)
1690 (setq ggtags-global-line-overlay (make-overlay (point) (point)))
1691 (overlay-put ggtags-global-line-overlay 'face 'ggtags-global-line))
1692 (move-overlay ggtags-global-line-overlay
1693 (line-beginning-position) (line-end-position)
1694 (current-buffer))
1695 ;; Update search history
1696 (let ((id (ggtags-global-search-id (car compilation-arguments)
1697 default-directory)))
1698 (setq ggtags-global-search-history
1699 (cl-remove id ggtags-global-search-history :test #'equal :key #'car))
1700 (add-to-history 'ggtags-global-search-history
1701 (cons id (ggtags-global-current-search))
1702 ggtags-global-history-length))))
1703 (run-hooks 'ggtags-find-tag-hook)))
1704
1705 (put 'ggtags-navigation-mode-lighter 'risky-local-variable t)
1706
1707 (defvar ggtags-navigation-mode-lighter
1708 '(" GG["
1709 (:eval
1710 (if (not (buffer-live-p ggtags-global-last-buffer))
1711 '(:propertize "??" face error help-echo "No Global buffer")
1712 (with-current-buffer ggtags-global-last-buffer
1713 (pcase (or ggtags-global-exit-info '(0 0 ""))
1714 (`(,exit ,count ,db)
1715 `((:propertize ,(pcase db
1716 (`"GTAGS" "D")
1717 (`"GRTAGS" "R")
1718 (`"GSYMS" "S")
1719 (`"GPATH" "F")
1720 (`"ID" "I"))
1721 face success)
1722 (:propertize
1723 ,(pcase (get-text-property (line-beginning-position)
1724 'compilation-message)
1725 (`nil "?")
1726 ;; Assume the first match appears at line 5
1727 (_ (number-to-string (- (line-number-at-pos) 4))))
1728 face success)
1729 "/"
1730 (:propertize ,(number-to-string count) face success)
1731 ,(unless (zerop exit)
1732 `(":" (:propertize ,(number-to-string exit) face error)))))))))
1733 "]")
1734 "Ligher for `ggtags-navigation-mode'; set to nil to disable it.")
1735
1736 (define-minor-mode ggtags-navigation-mode nil
1737 :lighter ggtags-navigation-mode-lighter
1738 :global t
1739 (if ggtags-navigation-mode
1740 (progn
1741 ;; Higher priority for `ggtags-navigation-mode' to avoid being
1742 ;; hijacked by modes such as `view-mode'.
1743 (add-to-list 'emulation-mode-map-alists 'ggtags-mode-map-alist)
1744 (add-hook 'next-error-hook 'ggtags-global-next-error-function)
1745 (add-hook 'minibuffer-setup-hook 'ggtags-minibuffer-setup-function))
1746 (setq emulation-mode-map-alists
1747 (delq 'ggtags-mode-map-alist emulation-mode-map-alists))
1748 (remove-hook 'next-error-hook 'ggtags-global-next-error-function)
1749 (remove-hook 'minibuffer-setup-hook 'ggtags-minibuffer-setup-function)))
1750
1751 (defun ggtags-minibuffer-setup-function ()
1752 ;; Disable ggtags-navigation-mode in minibuffer.
1753 (setq-local ggtags-enable-navigation-keys nil))
1754
1755 (defun ggtags-kill-file-buffers (&optional interactive)
1756 "Kill all buffers visiting files in current project."
1757 (interactive "p")
1758 (ggtags-check-project)
1759 (let ((directories (cons (ggtags-current-project-root) (ggtags-get-libpath)))
1760 (count 0))
1761 (dolist (buf (buffer-list))
1762 (let ((file (and (buffer-live-p buf)
1763 (not (eq buf (current-buffer)))
1764 (buffer-file-name buf))))
1765 (when (and file (cl-some (lambda (dir)
1766 ;; Don't use `file-in-directory-p'
1767 ;; to allow symbolic links.
1768 (string-prefix-p dir file))
1769 directories))
1770 (and (kill-buffer buf) (cl-incf count)))))
1771 (and interactive
1772 (message "%d %s killed" count (if (= count 1) "buffer" "buffers")))))
1773
1774 (defun ggtags-after-save-function ()
1775 (when (ggtags-find-project)
1776 (ggtags-project-update-mtime-maybe)
1777 (and buffer-file-name
1778 (ggtags-update-tags-single buffer-file-name 'nowait))))
1779
1780 (defun ggtags-global-output (buffer cmds callback &optional cutoff)
1781 "Asynchronously pipe the output of running CMDS to BUFFER.
1782 When finished invoke CALLBACK in BUFFER with process exit status."
1783 (or buffer (error "Output buffer required"))
1784 (when (get-buffer-process (get-buffer buffer))
1785 ;; Notice running multiple processes in the same buffer so that we
1786 ;; can fix the caller. See for example `ggtags-eldoc-function'.
1787 (message "Warning: detected %S already running in %S; interrupting..."
1788 (get-buffer-process buffer) buffer)
1789 (interrupt-process (get-buffer-process buffer)))
1790 (let* ((program (car cmds))
1791 (args (cdr cmds))
1792 (cutoff (and cutoff (+ cutoff (if (get-buffer buffer)
1793 (with-current-buffer buffer
1794 (line-number-at-pos (point-max)))
1795 0))))
1796 (proc (apply #'start-file-process program buffer program args))
1797 (filter (lambda (proc string)
1798 (and (buffer-live-p (process-buffer proc))
1799 (with-current-buffer (process-buffer proc)
1800 (goto-char (process-mark proc))
1801 (insert string)
1802 (when (and (> (line-number-at-pos (point-max)) cutoff)
1803 (process-live-p proc))
1804 (interrupt-process (current-buffer)))))))
1805 (sentinel (lambda (proc _msg)
1806 (when (memq (process-status proc) '(exit signal))
1807 (with-current-buffer (process-buffer proc)
1808 (set-process-buffer proc nil)
1809 (funcall callback (process-exit-status proc)))))))
1810 (set-process-query-on-exit-flag proc nil)
1811 (and cutoff (set-process-filter proc filter))
1812 (set-process-sentinel proc sentinel)
1813 proc))
1814
1815 (defun ggtags-get-definition-default (defs)
1816 (and (caar defs)
1817 (concat (caar defs) (and (cdr defs) " [guess]"))))
1818
1819 (defun ggtags-show-definition (name)
1820 (interactive (list (ggtags-read-tag 'definition current-prefix-arg)))
1821 (ggtags-check-project)
1822 (let* ((re (cadr (assq 'grep ggtags-global-error-regexp-alist-alist)))
1823 (current (current-buffer))
1824 (buffer (get-buffer-create " *ggtags-definition*"))
1825 ;; Need these bindings so that let-binding
1826 ;; `ggtags-print-definition-function' can work see
1827 ;; `ggtags-eldoc-function'.
1828 (get-fn ggtags-get-definition-function)
1829 (print-fn ggtags-print-definition-function)
1830 (show (lambda (_status)
1831 (goto-char (point-min))
1832 (let ((defs (cl-loop while (re-search-forward re nil t)
1833 collect (list (buffer-substring (1+ (match-end 2))
1834 (line-end-position))
1835 name
1836 (match-string 1)
1837 (string-to-number (match-string 2))))))
1838 (kill-buffer buffer)
1839 (with-current-buffer current
1840 (funcall print-fn (funcall get-fn defs)))))))
1841 (ggtags-with-current-project
1842 (ggtags-global-output
1843 buffer
1844 (list (ggtags-program-path "global")
1845 "--result=grep" "--path-style=absolute" name)
1846 show 100))))
1847
1848 (defvar ggtags-mode-prefix-map
1849 (let ((m (make-sparse-keymap)))
1850 ;; Globally bound to `M-g p'.
1851 ;; (define-key m "\M-'" 'previous-error)
1852 (define-key m (kbd "M-DEL") 'ggtags-delete-tags)
1853 (define-key m "\M-p" 'ggtags-prev-mark)
1854 (define-key m "\M-n" 'ggtags-next-mark)
1855 (define-key m "\M-f" 'ggtags-find-file)
1856 (define-key m "\M-o" 'ggtags-find-other-symbol)
1857 (define-key m "\M-g" 'ggtags-grep)
1858 (define-key m "\M-i" 'ggtags-idutils-query)
1859 (define-key m "\M-b" 'ggtags-browse-file-as-hypertext)
1860 (define-key m "\M-k" 'ggtags-kill-file-buffers)
1861 (define-key m "\M-h" 'ggtags-view-tag-history)
1862 (define-key m "\M-j" 'ggtags-visit-project-root)
1863 (define-key m "\M-/" 'ggtags-global-rerun-search)
1864 (define-key m (kbd "M-SPC") 'ggtags-save-to-register)
1865 (define-key m (kbd "M-%") 'ggtags-query-replace)
1866 (define-key m "\M-?" 'ggtags-show-definition)
1867 m))
1868
1869 (defvar ggtags-mode-map
1870 (let ((map (make-sparse-keymap))
1871 (menu (make-sparse-keymap "Ggtags")))
1872 (define-key map "\M-." 'ggtags-find-tag-dwim)
1873 (define-key map (kbd "M-]") 'ggtags-find-reference)
1874 (define-key map (kbd "C-M-.") 'ggtags-find-tag-regexp)
1875 (define-key map ggtags-mode-prefix-key ggtags-mode-prefix-map)
1876 ;; Menu items
1877 (define-key map [menu-bar ggtags] (cons "Ggtags" menu))
1878 ;; Ordered backwards
1879 (define-key menu [report-bugs]
1880 `(menu-item "Report bugs"
1881 (lambda () (interactive)
1882 (browse-url ggtags-bug-url)
1883 (message "Please visit %s" ggtags-bug-url))
1884 :help ,(format "Visit %s" ggtags-bug-url)))
1885 (define-key menu [custom-ggtags]
1886 '(menu-item "Customize Ggtags"
1887 (lambda () (interactive) (customize-group 'ggtags))))
1888 (define-key menu [eldoc-mode]
1889 '(menu-item "Toggle eldoc mode" eldoc-mode :button (:toggle . eldoc-mode)))
1890 (define-key menu [save-project]
1891 '(menu-item "Save project settings" ggtags-save-project-settings))
1892 (define-key menu [toggle-read-only]
1893 '(menu-item "Toggle project read-only" ggtags-toggle-project-read-only
1894 :button (:toggle . buffer-read-only)))
1895 (define-key menu [visit-project-root]
1896 '(menu-item "Visit project root" ggtags-visit-project-root))
1897 (define-key menu [sep2] menu-bar-separator)
1898 (define-key menu [browse-hypertext]
1899 '(menu-item "Browse as hypertext" ggtags-browse-file-as-hypertext
1900 :enable (ggtags-find-project)))
1901 (define-key menu [delete-tags]
1902 '(menu-item "Delete tags" ggtags-delete-tags
1903 :enable (ggtags-find-project)
1904 :help "Delete file GTAGS, GRTAGS, GPATH, ID etc."))
1905 (define-key menu [kill-buffers]
1906 '(menu-item "Kill project file buffers" ggtags-kill-file-buffers
1907 :enable (ggtags-find-project)))
1908 (define-key menu [view-tag]
1909 '(menu-item "View tag history" ggtags-view-tag-history))
1910 (define-key menu [pop-mark]
1911 '(menu-item "Pop mark" pop-tag-mark
1912 :help "Pop to previous mark and destroy it"))
1913 (define-key menu [next-mark]
1914 '(menu-item "Next mark" ggtags-next-mark))
1915 (define-key menu [prev-mark]
1916 '(menu-item "Previous mark" ggtags-prev-mark))
1917 (define-key menu [sep1] menu-bar-separator)
1918 (define-key menu [previous-error]
1919 '(menu-item "Previous match" previous-error))
1920 (define-key menu [next-error]
1921 '(menu-item "Next match" next-error))
1922 (define-key menu [rerun-search]
1923 '(menu-item "Re-run past search" ggtags-global-rerun-search))
1924 (define-key menu [save-to-register]
1925 '(menu-item "Save search to register" ggtags-save-to-register))
1926 (define-key menu [find-file]
1927 '(menu-item "Find files" ggtags-find-file))
1928 (define-key menu [query-replace]
1929 '(menu-item "Query replace" ggtags-query-replace))
1930 (define-key menu [idutils]
1931 '(menu-item "Query idutils DB" ggtags-idutils-query))
1932 (define-key menu [grep]
1933 '(menu-item "Grep" ggtags-grep))
1934 (define-key menu [find-symbol]
1935 '(menu-item "Find other symbol" ggtags-find-other-symbol
1936 :help "Find references without definition"))
1937 (define-key menu [find-tag-regexp]
1938 '(menu-item "Find tag matching regexp" ggtags-find-tag-regexp))
1939 (define-key menu [show-definition]
1940 '(menu-item "Show definition" ggtags-show-definition))
1941 (define-key menu [find-reference]
1942 '(menu-item "Find reference" ggtags-find-reference))
1943 (define-key menu [find-tag-continue]
1944 '(menu-item "Continue find tag" tags-loop-continue))
1945 (define-key menu [find-tag]
1946 '(menu-item "Find tag" ggtags-find-tag-dwim))
1947 (define-key menu [update-tags]
1948 '(menu-item "Update tag files" ggtags-update-tags
1949 :visible (ggtags-find-project)))
1950 (define-key menu [run-gtags]
1951 '(menu-item "Run gtags" ggtags-create-tags
1952 :visible (not (ggtags-find-project))))
1953 map))
1954
1955 (defvar ggtags-mode-line-project-keymap
1956 (let ((map (make-sparse-keymap)))
1957 (define-key map [mode-line mouse-1] 'ggtags-visit-project-root)
1958 map))
1959
1960 (put 'ggtags-mode-line-project-name 'risky-local-variable t)
1961 (defvar ggtags-mode-line-project-name
1962 '("[" (:eval (let ((name (if (stringp ggtags-project-root)
1963 (file-name-nondirectory
1964 (directory-file-name ggtags-project-root))
1965 "?")))
1966 (propertize
1967 name 'face compilation-info-face
1968 'help-echo (if (stringp ggtags-project-root)
1969 (concat "mouse-1 to visit " ggtags-project-root)
1970 "mouse-1 to set project")
1971 'mouse-face 'mode-line-highlight
1972 'keymap ggtags-mode-line-project-keymap)))
1973 "]")
1974 "Mode line construct for displaying current project name.
1975 The value is the name of the project root directory. Setting it
1976 to nil disables displaying this information.")
1977
1978 ;;;###autoload
1979 (define-minor-mode ggtags-mode nil
1980 :lighter (:eval (if ggtags-navigation-mode "" " GG"))
1981 (unless (timerp ggtags-highlight-tag-timer)
1982 (setq ggtags-highlight-tag-timer
1983 (run-with-idle-timer
1984 ggtags-highlight-tag-delay t #'ggtags-highlight-tag-at-point)))
1985 (if ggtags-mode
1986 (progn
1987 (add-hook 'after-save-hook 'ggtags-after-save-function nil t)
1988 ;; Append to serve as a fallback method.
1989 (add-hook 'completion-at-point-functions
1990 #'ggtags-completion-at-point t t)
1991 (unless (memq 'ggtags-mode-line-project-name
1992 mode-line-buffer-identification)
1993 (setq mode-line-buffer-identification
1994 (append mode-line-buffer-identification
1995 '(ggtags-mode-line-project-name)))))
1996 (remove-hook 'after-save-hook 'ggtags-after-save-function t)
1997 (remove-hook 'completion-at-point-functions #'ggtags-completion-at-point t)
1998 (setq mode-line-buffer-identification
1999 (delq 'ggtags-mode-line-project-name mode-line-buffer-identification))
2000 (and (overlayp ggtags-highlight-tag-overlay)
2001 (delete-overlay ggtags-highlight-tag-overlay))
2002 (setq ggtags-highlight-tag-overlay nil)))
2003
2004 (defvar ggtags-highlight-tag-map
2005 (let ((map (make-sparse-keymap)))
2006 ;; Bind down- events so that the global keymap won't ``shine
2007 ;; through''. See `mode-line-buffer-identification-keymap' for
2008 ;; similar workaround.
2009 (define-key map [S-mouse-1] 'ggtags-find-tag-dwim)
2010 (define-key map [S-down-mouse-1] 'ignore)
2011 (define-key map [S-mouse-3] 'ggtags-find-reference)
2012 (define-key map [S-down-mouse-3] 'ignore)
2013 map)
2014 "Keymap used for valid tag at point.")
2015
2016 (put 'ggtags-active-tag 'face 'ggtags-highlight)
2017 (put 'ggtags-active-tag 'keymap ggtags-highlight-tag-map)
2018 ;; (put 'ggtags-active-tag 'mouse-face 'match)
2019 (put 'ggtags-active-tag 'help-echo
2020 "S-mouse-1 for definitions\nS-mouse-3 for references")
2021
2022 (defun ggtags-highlight-tag-at-point ()
2023 (when (and ggtags-mode ggtags-project-root (ggtags-find-project))
2024 (unless (overlayp ggtags-highlight-tag-overlay)
2025 (setq ggtags-highlight-tag-overlay (make-overlay (point) (point) nil t))
2026 (overlay-put ggtags-highlight-tag-overlay 'modification-hooks
2027 (list (lambda (o after &rest _args)
2028 (and (not after) (delete-overlay o))))))
2029 (let ((bounds (funcall ggtags-bounds-of-tag-function))
2030 (o ggtags-highlight-tag-overlay))
2031 (cond
2032 ((and bounds
2033 (eq (overlay-buffer o) (current-buffer))
2034 (= (overlay-start o) (car bounds))
2035 (= (overlay-end o) (cdr bounds)))
2036 ;; Overlay matches current tag so do nothing.
2037 nil)
2038 ((and bounds (let ((completion-ignore-case nil))
2039 (test-completion
2040 (buffer-substring (car bounds) (cdr bounds))
2041 ggtags-completion-table)))
2042 (move-overlay o (car bounds) (cdr bounds) (current-buffer))
2043 (overlay-put o 'category 'ggtags-active-tag))
2044 (t (move-overlay o
2045 (or (car bounds) (point))
2046 (or (cdr bounds) (point))
2047 (current-buffer))
2048 (overlay-put o 'category nil))))))
2049
2050 ;;; eldoc
2051
2052 (defvar-local ggtags-eldoc-cache nil)
2053
2054 (declare-function eldoc-message "eldoc")
2055 (defun ggtags-eldoc-function ()
2056 "A function suitable for `eldoc-documentation-function' (which see)."
2057 (pcase (ggtags-tag-at-point)
2058 (`nil nil)
2059 (tag (if (equal tag (car ggtags-eldoc-cache))
2060 (cadr ggtags-eldoc-cache)
2061 (and ggtags-project-root (ggtags-find-project)
2062 (let* ((ggtags-print-definition-function
2063 (lambda (s)
2064 (setq ggtags-eldoc-cache (list tag s))
2065 (eldoc-message s))))
2066 ;; Prevent multiple runs of ggtags-show-definition
2067 ;; for the same tag.
2068 (setq ggtags-eldoc-cache (list tag))
2069 (ggtags-show-definition tag)
2070 nil))))))
2071
2072 ;;; imenu
2073
2074 (defun ggtags-goto-imenu-index (name line &rest _args)
2075 (ggtags-forward-to-line line)
2076 (ggtags-move-to-tag name))
2077
2078 ;;;###autoload
2079 (defun ggtags-build-imenu-index ()
2080 "A function suitable for `imenu-create-index-function'."
2081 (let ((file (and buffer-file-name (file-relative-name buffer-file-name))))
2082 (and file (with-temp-buffer
2083 (when (with-demoted-errors "ggtags-build-imenu-index: %S"
2084 (zerop (ggtags-with-current-project
2085 (process-file (ggtags-program-path "global")
2086 nil t nil "-x" "-f" file))))
2087 (goto-char (point-min))
2088 (cl-loop while (re-search-forward
2089 "^\\([^ \t]+\\)[ \t]+\\([0-9]+\\)" nil t)
2090 collect (list (match-string 1)
2091 (string-to-number (match-string 2))
2092 'ggtags-goto-imenu-index)))))))
2093
2094 ;;; hippie-expand
2095
2096 ;;;###autoload
2097 (defun ggtags-try-complete-tag (old)
2098 "A function suitable for `hippie-expand-try-functions-list'."
2099 (eval-and-compile (require 'hippie-exp))
2100 (unless old
2101 (he-init-string (or (car (funcall ggtags-bounds-of-tag-function)) (point))
2102 (point))
2103 (setq he-expand-list
2104 (and (not (equal he-search-string ""))
2105 (ggtags-find-project)
2106 (sort (all-completions he-search-string
2107 ggtags-completion-table)
2108 #'string-lessp))))
2109 (if (null he-expand-list)
2110 (progn
2111 (if old (he-reset-string))
2112 nil)
2113 (he-substitute-string (car he-expand-list))
2114 (setq he-expand-list (cdr he-expand-list))
2115 t))
2116
2117 (defun ggtags-reload (&optional force)
2118 (interactive "P")
2119 (unload-feature 'ggtags force)
2120 (require 'ggtags))
2121
2122 (provide 'ggtags)
2123 ;;; ggtags.el ends here