]> code.delx.au - gnu-emacs-elpa/blob - ggtags.el
Remove side-effect from ggtags-with-process-environment
[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.7.9
7 ;; Keywords: tools, convenience
8 ;; Created: 2013-01-29
9 ;; URL: https://github.com/leoliu/ggtags
10 ;; Package-Requires: ((emacs "24"))
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 ;; Type `M-x ggtags-mode' to enable the minor mode, or as usual enable
33 ;; it in your desired major mode hooks. When the mode is on the symbol
34 ;; at point is underlined if it is a valid (definition) tag.
35 ;;
36 ;; `M-.' finds definition or references according to the context at
37 ;; point, i.e. if point is at a definition tag find references and
38 ;; vice versa. `M-]' finds references.
39 ;;
40 ;; If multiple matches are found, navigation mode is entered, the
41 ;; mode-line lighter changed, and a navigation menu-bar entry
42 ;; presented. In this mode, `M-n' and `M-p' moves to next and previous
43 ;; match, `M-}' and `M-{' to next and previous file respectively.
44 ;; `M-o' toggles between full and abbreviated displays of file names
45 ;; in the auxiliary popup window. When you locate the right match,
46 ;; press RET to finish which hides the auxiliary window and exits
47 ;; navigation mode. You can continue the search using `M-,'. To abort
48 ;; the search press `M-*'.
49 ;;
50 ;; Normally after a few searches a dozen buffers are created visiting
51 ;; files tracked by GNU Global. `C-c M-k' helps clean them up.
52 ;;
53 ;; Check the menu-bar entry `Ggtags' for other useful commands.
54
55 ;;; Code:
56
57 (eval-when-compile
58 (require 'cl)
59 (require 'url-parse))
60
61 (require 'compile)
62 (require 'etags)
63
64 (eval-when-compile
65 (unless (fboundp 'setq-local)
66 (defmacro setq-local (var val)
67 (list 'set (list 'make-local-variable (list 'quote var)) val)))
68
69 (unless (fboundp 'defvar-local)
70 (defmacro defvar-local (var val &optional docstring)
71 (declare (debug defvar) (doc-string 3))
72 (list 'progn (list 'defvar var val docstring)
73 (list 'make-variable-buffer-local (list 'quote var)))))
74
75 (defmacro* when-let ((var exp) &rest body)
76 "A macro that combines `let' and `when'."
77 (declare (indent 1) (debug ((sexp form) body)))
78 `(let ((,var ,exp)) (when ,var ,@body))))
79
80 (eval-and-compile
81 (or (fboundp 'user-error)
82 (defalias 'user-error 'error)))
83
84 (defgroup ggtags nil
85 "GNU Global source code tagging system."
86 :group 'tools)
87
88 (defface ggtags-highlight '((t (:underline t)))
89 "Face used to highlight a valid tag at point."
90 :group 'ggtags)
91
92 (defface ggtags-global-line '((t (:inherit secondary-selection)))
93 "Face used to highlight matched line in Global buffer."
94 :group 'ggtags)
95
96 (defcustom ggtags-oversize-limit (* 10 1024 1024)
97 "The over size limit for the GTAGS file.
98 For large source trees, running 'global -u' can be expensive.
99 Thus when GTAGS file is larger than this limit, ggtags
100 automatically switches to 'global --single-update'."
101 :safe 'numberp
102 :type '(choice (const :tag "None" nil)
103 (const :tag "Always" t)
104 number)
105 :group 'ggtags)
106
107 (defcustom ggtags-project-duration 3600
108 "Seconds to keep information of a project in memory."
109 :type 'number
110 :group 'ggtags)
111
112 (defcustom ggtags-process-environment nil
113 "Similar to `process-environment' with higher precedence.
114 Elements are run through `substitute-env-vars' before use.
115 GTAGSROOT will always be expanded to current project root
116 directory. This is intended for project-wise ggtags-specific
117 process environment settings."
118 :safe 'ggtags-list-of-string-p
119 :type '(repeat string)
120 :group 'ggtags)
121
122 (defcustom ggtags-auto-jump-to-first-match t
123 "Non-nil to automatically jump to the first match."
124 :type 'boolean
125 :group 'ggtags)
126
127 (defcustom ggtags-global-window-height 8 ; ggtags-global-mode
128 "Number of lines for the 'global' popup window.
129 If nil, use Emacs default."
130 :type '(choice (const :tag "Default" nil) integer)
131 :group 'ggtags)
132
133 (defcustom ggtags-global-abbreviate-filename 35
134 "Non-nil to display file names abbreviated e.g. \"/u/b/env\".
135 If an integer abbreviate only names longer than that number."
136 :type '(choice (const :tag "No" nil)
137 (const :tag "Always" t)
138 integer)
139 :group 'ggtags)
140
141 (defcustom ggtags-split-window-function split-window-preferred-function
142 "A function to control how ggtags pops up the auxiliary window."
143 :type 'function
144 :group 'ggtags)
145
146 (defcustom ggtags-use-idutils (and (executable-find "mkid") t)
147 "Non-nil to also generate the idutils DB."
148 :type 'boolean
149 :group 'ggtags)
150
151 (defcustom ggtags-global-output-format 'grep
152 "The output format for the 'global' command."
153 :type '(choice (const path)
154 (const ctags)
155 (const ctags-x)
156 (const grep)
157 (const cscope))
158 :group 'ggtags)
159
160 (defcustom ggtags-global-ignore-case nil
161 "Non-nil if Global should ignore case."
162 :safe 'booleanp
163 :type 'boolean
164 :group 'ggtags)
165
166 (defcustom ggtags-global-treat-text nil
167 "Non-nil if Global should include matches from text files."
168 :safe 'booleanp
169 :type 'boolean
170 :group 'ggtags)
171
172 (defcustom ggtags-global-large-output 1000
173 "Number of lines in the Global buffer to indicate large output."
174 :type 'number
175 :group 'ggtags)
176
177 (defcustom ggtags-mode-prefix-key "\C-c"
178 "Key binding used for `ggtags-mode-prefix-map'.
179 Users should change the value using `customize-variable' to
180 properly update `ggtags-mode-map'."
181 :set (lambda (sym value)
182 (when (bound-and-true-p ggtags-mode-map)
183 (when-let (old (and (boundp sym) (symbol-value sym)))
184 (define-key ggtags-mode-map old nil))
185 (and value
186 (bound-and-true-p ggtags-mode-prefix-map)
187 (define-key ggtags-mode-map value ggtags-mode-prefix-map)))
188 (set-default sym value))
189 :type 'key-sequence
190 :group 'ggtags)
191
192 (defcustom ggtags-completing-read-function completing-read-function
193 "Ggtags specific `completing-read-function' (which see)."
194 :type 'function
195 :group 'ggtags)
196
197 (defcustom ggtags-highlight-tag-delay 0.25
198 "Time in seconds before highlighting tag at point."
199 :set (lambda (sym value)
200 (when (bound-and-true-p ggtags-highlight-tag-timer)
201 (timer-set-idle-time ggtags-highlight-tag-timer value t))
202 (set-default sym value))
203 :type 'number
204 :group 'ggtags)
205
206 (defcustom ggtags-bounds-of-tag-function (lambda ()
207 (bounds-of-thing-at-point 'symbol))
208 "Function to get the start and end locations of the tag at point."
209 :type 'function
210 :group 'ggtags)
211
212 (defvar ggtags-bug-url "https://github.com/leoliu/ggtags/issues")
213
214 (defvar ggtags-global-last-buffer nil)
215
216 (defvar ggtags-current-tag-name nil)
217
218 (defvar ggtags-highlight-tag-overlay nil)
219
220 (defvar ggtags-highlight-tag-timer nil)
221
222 ;; Used by ggtags-global-mode
223 (defvar ggtags-global-error "match"
224 "Stem of message to print when no matches are found.")
225
226 ;; http://thread.gmane.org/gmane.comp.gnu.global.bugs/1518
227 (defvar ggtags-global-has-path-style ; introduced in global 6.2.8
228 (with-demoted-errors ; in case `global' not found
229 (zerop (process-file "global" nil nil nil
230 "--path-style" "shorter" "--help")))
231 "Non-nil if `global' supports --path-style switch.")
232
233 ;; http://thread.gmane.org/gmane.comp.gnu.global.bugs/1542
234 (defvar ggtags-global-has-color
235 (with-demoted-errors
236 (zerop (process-file "global" nil nil nil "--color" "--help"))))
237
238 (defmacro ggtags-ensure-global-buffer (&rest body)
239 (declare (indent 0))
240 `(progn
241 (or (and (buffer-live-p ggtags-global-last-buffer)
242 (with-current-buffer ggtags-global-last-buffer
243 (derived-mode-p 'ggtags-global-mode)))
244 (error "No global buffer found"))
245 (with-current-buffer ggtags-global-last-buffer ,@body)))
246
247 (defun ggtags-list-of-string-p (xs)
248 "Return non-nil if XS is a list of strings."
249 (if (null xs)
250 t
251 (and (stringp (car xs))
252 (ggtags-list-of-string-p (cdr xs)))))
253
254 (defun ggtags-get-libpath ()
255 (when-let (path (ggtags-with-process-environment (getenv "GTAGSLIBPATH")))
256 (split-string path (regexp-quote path-separator) t)))
257
258 (defun ggtags-process-string (program &rest args)
259 (with-temp-buffer
260 (let ((exit (apply #'process-file program nil t nil args))
261 (output (progn
262 (goto-char (point-max))
263 (skip-chars-backward " \t\n")
264 (buffer-substring (point-min) (point)))))
265 (or (zerop exit)
266 (error "`%s' non-zero exit: %s" program output))
267 output)))
268
269 (defun ggtags-tag-at-point ()
270 (when-let (bounds (funcall ggtags-bounds-of-tag-function))
271 (buffer-substring (car bounds) (cdr bounds))))
272
273 ;;; Store for project info and settings
274
275 (defvar ggtags-projects (make-hash-table :size 7 :test #'equal))
276
277 (defstruct (ggtags-project (:constructor ggtags-project--make)
278 (:copier nil)
279 (:type vector)
280 :named)
281 root tag-size has-rtags dirty-p timestamp)
282
283 (defun ggtags-make-project (root)
284 "Create or update project info for ROOT."
285 (check-type root string)
286 (let* ((default-directory (file-name-as-directory root))
287 (tag-size (or (nth 7 (file-attributes "GTAGS")) -1))
288 (rtags-size (nth 7 (file-attributes "GRTAGS")))
289 (has-rtags
290 (when rtags-size
291 (or (> rtags-size (* 32 1024))
292 (with-demoted-errors
293 (not (equal "" (ggtags-process-string "global" "-crs")))))))
294 (project (or (gethash default-directory ggtags-projects)
295 (puthash default-directory
296 (ggtags-project--make :root default-directory)
297 ggtags-projects))))
298 (setf (ggtags-project-has-rtags project) has-rtags
299 (ggtags-project-tag-size project) tag-size
300 (ggtags-project-timestamp project) (float-time))
301 project))
302
303 (defvar-local ggtags-project 'unset)
304
305 (defun ggtags-project-expired-p (project)
306 (or (< (ggtags-project-timestamp project) 0)
307 (> (- (float-time)
308 (ggtags-project-timestamp project))
309 ggtags-project-duration)))
310
311 (defun ggtags-project-oversize-p (&optional project)
312 (pcase ggtags-oversize-limit
313 (`nil nil)
314 (`t t)
315 (size (when-let (project (or project (ggtags-find-project)))
316 (> (ggtags-project-tag-size project) size)))))
317
318 ;;;###autoload
319 (defun ggtags-find-project ()
320 (if (ggtags-project-p ggtags-project)
321 (if (ggtags-project-expired-p ggtags-project)
322 ;; Update the project info by side-effect.
323 (ggtags-make-project (ggtags-project-root ggtags-project))
324 ggtags-project)
325 (let ((root (or (ignore-errors (file-name-as-directory
326 ;; Resolves symbolic links
327 (ggtags-process-string "global" "-pr")))
328 ;; 'global -pr' resolves symlinks before checking
329 ;; the GTAGS file which could cause issues such as
330 ;; https://github.com/leoliu/ggtags/issues/22, so
331 ;; let's help it out.
332 (when-let (gtags (locate-dominating-file
333 default-directory "GTAGS"))
334 (file-truename gtags)))))
335 (setq ggtags-project
336 (and root (or (gethash root ggtags-projects)
337 (ggtags-make-project root))))
338 (and ggtags-project (ggtags-find-project)))))
339
340 (defun ggtags-current-project-root ()
341 (and (ggtags-find-project)
342 (ggtags-project-root (ggtags-find-project))))
343
344 (defun ggtags-check-project ()
345 (or (ggtags-find-project) (error "File GTAGS not found")))
346
347 (defun ggtags-ensure-project ()
348 (or (ggtags-find-project)
349 (when (or (yes-or-no-p "File GTAGS not found; run gtags? ")
350 (user-error "Aborted"))
351 (call-interactively #'ggtags-create-tags)
352 ;; Need checking because `ggtags-create-tags' can create tags
353 ;; in any directory.
354 (ggtags-check-project))))
355
356 (defun ggtags-save-project-settings (&optional noconfirm)
357 "Save Gnu Global's specific environment variables."
358 (interactive "P")
359 (ggtags-check-project)
360 (let* ((inhibit-read-only t) ; for `add-dir-local-variable'
361 (default-directory (ggtags-current-project-root))
362 ;; Not using `ggtags-with-process-environment' to preserve
363 ;; environment variables that may be present in
364 ;; `ggtags-process-environment'.
365 (process-environment
366 (append ggtags-process-environment
367 process-environment
368 (and (not (ggtags-project-has-rtags (ggtags-find-project)))
369 (list "GTAGSLABEL=ctags"))))
370 (envlist (delete-dups
371 (loop for x in process-environment
372 when (string-match
373 "^\\(GTAGS[^=\n]*\\|MAKEOBJDIRPREFIX\\)=" x)
374 ;; May have duplicates thus `delete-dups'.
375 collect (concat (match-string 1 x)
376 "="
377 (getenv (match-string 1 x))))))
378 (help-form (format "y: save\nn: don't save\n=: diff\n?: help\n")))
379 (add-dir-local-variable nil 'ggtags-process-environment envlist)
380 ;; Remove trailing newlines by `add-dir-local-variable'.
381 (let ((delete-trailing-lines t)) (delete-trailing-whitespace))
382 (or noconfirm
383 (while (pcase (read-char-choice
384 (format "Save `%s'? (y/n/=/?) " buffer-file-name)
385 '(?y ?n ?= ??))
386 ;; ` required for 24.1 and 24.2
387 (`?n (user-error "Aborted"))
388 (`?y nil)
389 (`?= (diff-buffer-with-file) 'loop)
390 (`?? (help-form-show) 'loop))))
391 (save-buffer)
392 (kill-buffer)))
393
394 (defun ggtags-toggle-project-read-only ()
395 (interactive)
396 (ggtags-check-project)
397 (let ((inhibit-read-only t) ; for `add-dir-local-variable'
398 (val (not buffer-read-only))
399 (default-directory (ggtags-current-project-root)))
400 (add-dir-local-variable nil 'buffer-read-only val)
401 (save-buffer)
402 (kill-buffer)
403 (when buffer-file-name
404 (setq buffer-read-only val))
405 (when (called-interactively-p 'interactive)
406 (message "Project read-only-mode is %s" (if val "on" "off")))
407 val))
408
409 (defmacro ggtags-with-process-environment (&rest body)
410 (declare (debug t))
411 (let ((gtagsroot (make-symbol "-gtagsroot-"))
412 (ggproj (make-symbol "-ggtags-project-")))
413 `(let* ((,ggproj ggtags-project)
414 (,gtagsroot (when (ggtags-find-project)
415 (directory-file-name (ggtags-current-project-root))))
416 (process-environment
417 (append (let ((process-environment process-environment))
418 (and ,gtagsroot (setenv "GTAGSROOT" ,gtagsroot))
419 (mapcar #'substitute-env-vars ggtags-process-environment))
420 process-environment
421 (and ,gtagsroot (list (concat "GTAGSROOT=" ,gtagsroot)))
422 (and (ggtags-find-project)
423 (not (ggtags-project-has-rtags (ggtags-find-project)))
424 (list "GTAGSLABEL=ctags")))))
425 (unwind-protect (save-current-buffer ,@body)
426 (setq ggtags-project ,ggproj)))))
427
428 (defun ggtags-create-tags (root)
429 "Run `gtags' in directory ROOT to create tag files."
430 (interactive "DRoot directory: ")
431 (let ((process-environment process-environment))
432 (when (zerop (length root)) (error "No root directory provided"))
433 (setenv "GTAGSROOT"
434 (directory-file-name (file-name-as-directory root)))
435 (ggtags-with-process-environment
436 (and (not (getenv "GTAGSLABEL"))
437 (yes-or-no-p "Use `ctags' backend? ")
438 (setenv "GTAGSLABEL" "ctags"))
439 (with-temp-message "`gtags' in progress..."
440 (let ((default-directory (file-name-as-directory root)))
441 (apply #'ggtags-process-string
442 "gtags" (and ggtags-use-idutils '("--idutils"))))))
443 (message "GTAGS generated in `%s'" root)
444 root))
445
446 (defun ggtags-update-tags (&optional force)
447 "Update GNU Global tag database.
448 Do nothing if GTAGS exceeds the oversize limit unless FORCE is
449 non-nil."
450 (interactive (progn
451 (ggtags-check-project)
452 ;; Mark project info expired.
453 (setf (ggtags-project-timestamp (ggtags-find-project)) -1)
454 (list t)))
455 (when (or force (and (ggtags-find-project)
456 (not (ggtags-project-oversize-p))
457 (ggtags-project-dirty-p (ggtags-find-project))))
458 (ggtags-with-process-environment
459 (with-temp-message "`global -u' in progress..."
460 (ggtags-process-string "global" "-u")
461 (setf (ggtags-project-dirty-p (ggtags-find-project)) nil)))))
462
463 (defvar-local ggtags-completion-cache nil)
464
465 (defvar ggtags-completion-table
466 (completion-table-dynamic
467 (lambda (prefix)
468 (unless (equal prefix (car ggtags-completion-cache))
469 (setq ggtags-completion-cache
470 (cons prefix
471 (ggtags-with-process-environment
472 (split-string
473 (apply #'ggtags-process-string
474 "global"
475 ;; Note -c alone returns only definitions
476 (if completion-ignore-case
477 (list "--ignore-case" "-Tc" prefix)
478 (list "-Tc" prefix)))
479 "\n" t)))))
480 (cdr ggtags-completion-cache))))
481
482 (defun ggtags-completion-at-point ()
483 "A function for `completion-at-point-functions'."
484 (when-let (bounds (funcall ggtags-bounds-of-tag-function))
485 (and (< (car bounds) (cdr bounds))
486 (list (car bounds) (cdr bounds) ggtags-completion-table))))
487
488 (defun ggtags-read-tag ()
489 (ggtags-ensure-project)
490 (let ((default (ggtags-tag-at-point))
491 (completing-read-function ggtags-completing-read-function))
492 (setq ggtags-current-tag-name
493 (cond (current-prefix-arg
494 (ggtags-update-tags)
495 (completing-read
496 (format (if default "Tag (default %s): " "Tag: ") default)
497 ggtags-completion-table nil t nil nil default))
498 ((not default)
499 (user-error "No tag at point"))
500 (t (substring-no-properties default))))))
501
502 (defun ggtags-global-build-command (cmd &rest args)
503 ;; CMD can be definition, reference, symbol, grep, idutils
504 (let ((xs (append (list "global" "-v"
505 (format "--result=%s" ggtags-global-output-format)
506 (and ggtags-global-ignore-case "--ignore-case")
507 (and ggtags-global-has-color "--color")
508 (and ggtags-global-has-path-style
509 "--path-style=shorter")
510 (and ggtags-global-treat-text "--other")
511 (pcase cmd
512 ((pred stringp) cmd)
513 (`definition "") ;-d not supported by Global 5.7.1
514 (`reference "-r")
515 (`symbol "-s")
516 (`path "--path")
517 (`grep "--grep")
518 (`idutils "--idutils")))
519 args)))
520 (mapconcat #'identity (delq nil xs) " ")))
521
522 ;; takes three values: nil, t and a marker
523 (defvar ggtags-global-start-marker nil)
524
525 (defvar ggtags-global-exit-status 0)
526 (defvar ggtags-global-match-count 0)
527
528 (defvar ggtags-tag-ring-index nil)
529
530 (defun ggtags-global-save-start-marker ()
531 (when (markerp ggtags-global-start-marker)
532 (setq ggtags-tag-ring-index nil)
533 (ring-insert find-tag-marker-ring ggtags-global-start-marker)
534 (setq ggtags-global-start-marker t)))
535
536 (defun ggtags-global-start (command &optional root)
537 (let* ((default-directory (or root (ggtags-current-project-root)))
538 (split-window-preferred-function ggtags-split-window-function)
539 ;; See http://debbugs.gnu.org/13594
540 (display-buffer-overriding-action
541 (if (and ggtags-auto-jump-to-first-match
542 ;; Appeared in emacs 24.4.
543 (fboundp 'display-buffer-no-window))
544 (list #'display-buffer-no-window)
545 display-buffer-overriding-action)))
546 (setq ggtags-global-start-marker (point-marker))
547 (ggtags-navigation-mode +1)
548 (setq ggtags-global-exit-status 0
549 ggtags-global-match-count 0)
550 (ggtags-update-tags)
551 (ggtags-with-process-environment
552 (setq ggtags-global-last-buffer
553 (compilation-start command 'ggtags-global-mode)))))
554
555 (defun ggtags-find-tag-continue ()
556 (interactive)
557 (ggtags-ensure-global-buffer
558 (ggtags-navigation-mode +1)
559 (let ((split-window-preferred-function ggtags-split-window-function))
560 (ignore-errors (compilation-next-error 1))
561 (compile-goto-error))))
562
563 (defun ggtags-find-tag (cmd name)
564 (ggtags-check-project)
565 (ggtags-global-start (ggtags-global-build-command cmd name)))
566
567 ;;;###autoload
568 (defun ggtags-find-tag-dwim (name &optional definition)
569 "Find definitions or references of tag NAME by context.
570 If point is at a definition tag, find references, and vice versa.
571 With a prefix arg (non-nil DEFINITION) always find definitions."
572 (interactive (list (ggtags-read-tag) current-prefix-arg))
573 (ggtags-check-project) ; for `ggtags-current-project-root' below
574 (if (or definition
575 (not buffer-file-name)
576 (and (ggtags-find-project)
577 (not (ggtags-project-has-rtags (ggtags-find-project)))))
578 (ggtags-find-tag 'definition name)
579 (ggtags-find-tag
580 (format "--from-here=%d:%s"
581 (line-number-at-pos)
582 (shell-quote-argument
583 ;; Note `ggtags-global-start' binds default-directory to
584 ;; project root.
585 (file-relative-name
586 buffer-file-name
587 (if (string-prefix-p (ggtags-current-project-root)
588 buffer-file-name)
589 (ggtags-current-project-root)
590 (locate-dominating-file buffer-file-name "GTAGS")))))
591 name)))
592
593 (defun ggtags-find-reference (name)
594 (interactive (list (ggtags-read-tag)))
595 (ggtags-find-tag 'reference name))
596
597 (defun ggtags-find-other-symbol (name)
598 "Find tag NAME that is a reference without a definition."
599 (interactive (list (ggtags-read-tag)))
600 (ggtags-find-tag 'symbol name))
601
602 (defun ggtags-read-string (prompt)
603 "Like `read-string' but handle default automatically."
604 (ggtags-ensure-project)
605 (let ((prompt (if (string-match ": *\\'" prompt)
606 (substring prompt 0 (match-beginning 0))
607 prompt))
608 (default (ggtags-tag-at-point)))
609 (read-string (format (if default "%s (default `%s'): "
610 "%s: ")
611 prompt default)
612 nil nil (and default (substring-no-properties default)))))
613
614 (defun ggtags-grep (pattern &optional invert-match)
615 "Use `global --grep' to search for lines matching PATTERN.
616 Invert the match when called with a prefix arg \\[universal-argument]."
617 (interactive (list (ggtags-read-string (if current-prefix-arg
618 "Inverted grep pattern"
619 "Grep pattern"))
620 current-prefix-arg))
621 (ggtags-find-tag 'grep (format "%s--regexp %S"
622 (if invert-match "--invert-match " "")
623 pattern)))
624
625 (defun ggtags-idutils-query (pattern)
626 (interactive (list (ggtags-read-string "ID query pattern")))
627 (ggtags-find-tag 'idutils (format "--regexp %S" pattern)))
628
629 (defun ggtags-find-file (pattern &optional invert-match)
630 (interactive (list (ggtags-read-string (if current-prefix-arg
631 "Inverted path pattern"
632 "Path pattern"))
633 current-prefix-arg))
634 (let ((ggtags-global-output-format 'path))
635 (ggtags-find-tag 'path (format "%s--regexp %S"
636 (if invert-match "--invert-match " "")
637 pattern))))
638
639 ;; NOTE: Coloured output in grep requested: http://goo.gl/Y9IcX
640 (defun ggtags-find-tag-regexp (regexp directory)
641 "List tags matching REGEXP in DIRECTORY (default to project root)."
642 (interactive
643 (list (ggtags-read-string "POSIX regexp")
644 (if current-prefix-arg
645 (read-directory-name "Directory: " nil nil t)
646 (ggtags-current-project-root))))
647 (ggtags-check-project)
648 (let ((root (file-name-as-directory directory))
649 (cmd (ggtags-global-build-command
650 nil nil "-l" "--regexp" (prin1-to-string regexp))))
651 (ggtags-global-start cmd root)))
652
653 (defun ggtags-query-replace (from to &optional delimited)
654 "Query replace FROM with TO on files in the Global buffer.
655 If not in navigation mode, do a grep on FROM first.
656
657 Note: the regular expression FROM must be supported by both
658 Global and Emacs."
659 (interactive (query-replace-read-args "Query replace (regexp)" t t))
660 (unless (bound-and-true-p ggtags-navigation-mode)
661 (let ((ggtags-auto-jump-to-first-match nil))
662 (ggtags-grep from)))
663 (let ((file-form
664 '(let ((files))
665 (ggtags-ensure-global-buffer
666 (with-temp-message "Waiting for Grep to finish..."
667 (while (get-buffer-process (current-buffer))
668 (sit-for 0.2)))
669 (goto-char (point-min))
670 (while (ignore-errors (compilation-next-file 1) t)
671 (let ((m (get-text-property (point) 'compilation-message)))
672 (push (expand-file-name
673 (caar (compilation--loc->file-struct
674 (compilation--message->loc m))))
675 files))))
676 (ggtags-navigation-mode -1)
677 (nreverse files))))
678 (tags-query-replace from to delimited file-form)))
679
680 (defun ggtags-delete-tag-files ()
681 "Delete the tag files generated by gtags."
682 (interactive (ignore (ggtags-check-project)))
683 (when (ggtags-current-project-root)
684 (let ((files (directory-files
685 (ggtags-current-project-root) t
686 (concat "\\`" (regexp-opt '("GPATH" "GRTAGS" "GTAGS" "ID"))
687 "\\'")))
688 (buffer "*GTags File List*"))
689 (or files (user-error "No tag files found"))
690 (with-output-to-temp-buffer buffer
691 (dolist (file files)
692 (princ file)
693 (princ "\n")))
694 (let ((win (get-buffer-window buffer)))
695 (unwind-protect
696 (progn
697 (fit-window-to-buffer win)
698 (when (yes-or-no-p "Remove GNU Global tag files? ")
699 (mapc #'delete-file files)
700 (remhash (ggtags-current-project-root) ggtags-projects)
701 (and (overlayp ggtags-highlight-tag-overlay)
702 (delete-overlay ggtags-highlight-tag-overlay))
703 (kill-local-variable 'ggtags-project)))
704 (when (window-live-p win)
705 (quit-window t win)))))))
706
707 (defun ggtags-browse-file-as-hypertext (file line)
708 "Browse FILE in hypertext (HTML) form."
709 (interactive (if (or current-prefix-arg (not buffer-file-name))
710 (list (read-file-name "Browse file: " nil nil t)
711 (read-number "Line: " 1))
712 (list buffer-file-name (line-number-at-pos))))
713 (check-type line integer)
714 (or (and file (file-exists-p file)) (error "File `%s' doesn't exist" file))
715 (ggtags-check-project)
716 (or (file-exists-p (expand-file-name "HTML" (ggtags-current-project-root)))
717 (if (yes-or-no-p "No hypertext form exists; run htags? ")
718 (let ((default-directory (ggtags-current-project-root)))
719 (ggtags-with-process-environment (ggtags-process-string "htags")))
720 (user-error "Aborted")))
721 (let ((url (ggtags-process-string "gozilla" "-p" (format "+%d" line)
722 (file-relative-name file))))
723 (or (equal (file-name-extension
724 (url-filename (url-generic-parse-url url))) "html")
725 (user-error "No hypertext form for `%s'" file))
726 (when (called-interactively-p 'interactive)
727 (message "Browsing %s" url))
728 (browse-url url)))
729
730 (defun ggtags-next-mark (&optional arg)
731 "Move to the next (newer) mark in the tag marker ring."
732 (interactive)
733 (and (ring-empty-p find-tag-marker-ring) (user-error "Tag ring empty"))
734 (setq ggtags-tag-ring-index
735 ;; Note `ring-minus1' gets newer item.
736 (funcall (if arg #'ring-plus1 #'ring-minus1)
737 (or ggtags-tag-ring-index
738 (progn
739 (ring-insert find-tag-marker-ring (point-marker))
740 0))
741 (ring-length find-tag-marker-ring)))
742 (let ((m (ring-ref find-tag-marker-ring ggtags-tag-ring-index))
743 (i (- (ring-length find-tag-marker-ring) ggtags-tag-ring-index))
744 (message-log-max nil))
745 (message "%d%s marker%s" i (pcase (mod i 10)
746 ;; ` required for 24.1 and 24.2
747 (`1 "st")
748 (`2 "nd")
749 (`3 "rd")
750 (_ "th"))
751 (if (marker-buffer m) "" " (dead)"))
752 (if (not (marker-buffer m))
753 (ding)
754 (switch-to-buffer (marker-buffer m))
755 (goto-char m))))
756
757 (defun ggtags-prev-mark ()
758 "Move to the previous (older) mark in the tag marker ring."
759 (interactive)
760 (ggtags-next-mark 'previous))
761
762 (defun ggtags-view-tag-history ()
763 (interactive)
764 (and (ring-empty-p find-tag-marker-ring)
765 (user-error "Tag ring empty"))
766 (let ((split-window-preferred-function ggtags-split-window-function)
767 (inhibit-read-only t))
768 (pop-to-buffer "*Tag Ring*")
769 (erase-buffer)
770 (tabulated-list-mode)
771 (setq tabulated-list-entries
772 ;; Use a function so that revert can work properly.
773 (lambda ()
774 (let ((counter (ring-length find-tag-marker-ring))
775 (elements (or (ring-elements find-tag-marker-ring)
776 (user-error "Tag ring empty")))
777 (action
778 (lambda (button) (interactive)
779 (let ((m (button-get button 'marker)))
780 (or (markerp m) (user-error "Marker dead"))
781 (setq ggtags-tag-ring-index
782 (ring-member find-tag-marker-ring m))
783 (pop-to-buffer (marker-buffer m))
784 (goto-char (marker-position m)))))
785 (get-line
786 (lambda (m)
787 (with-current-buffer (marker-buffer m)
788 (save-excursion
789 (goto-char m)
790 (buffer-substring (line-beginning-position)
791 (line-end-position)))))))
792 (setq tabulated-list-format
793 `[("ID" ,(max (1+ (floor (log counter 10))) 2)
794 (lambda (x y) (< (car x) (car y))))
795 ("Buffer" ,(max (loop for m in elements
796 for b = (marker-buffer m)
797 maximize
798 (length (and b (buffer-name b))))
799 6)
800 t :right-align t)
801 ("Position" ,(max (loop for m in elements
802 for p = (or (marker-position m) 1)
803 maximize (1+ (floor (log p 10))))
804 8)
805 (lambda (x y)
806 (< (string-to-number (aref (cadr x) 2))
807 (string-to-number (aref (cadr y) 2))))
808 :right-align t)
809 ("Contents" 100 t)])
810 (tabulated-list-init-header)
811 (mapcar (lambda (x)
812 (prog1
813 (list counter
814 (if (marker-buffer x)
815 (vector (number-to-string counter)
816 `(,(buffer-name (marker-buffer x))
817 face link
818 follow-link t
819 marker ,x
820 action ,action)
821 (number-to-string (marker-position x))
822 (funcall get-line x))
823 (vector (number-to-string counter)
824 "(dead)" "?" "?")))
825 (decf counter)))
826 elements))))
827 (setq tabulated-list-sort-key '("ID" . t))
828 (tabulated-list-print)
829 (fit-window-to-buffer)))
830
831 (defun ggtags-global-exit-message-function (_process-status exit-status msg)
832 (setq ggtags-global-exit-status exit-status)
833 (pcase-let ((`(,count . ,db)
834 (save-excursion
835 (goto-char (point-max))
836 (if (re-search-backward
837 "^\\w+ \\(not found\\)\\|^\\([0-9]+\\) \\w+ located" nil t)
838 (cons (or (and (match-string 1) 0)
839 (string-to-number (match-string 2)))
840 (when (re-search-forward
841 "using \\(?:\\(idutils\\)\\|'[^']*/\\(\\w+\\)'\\)"
842 (line-end-position)
843 t)
844 (or (and (match-string 1) "ID")
845 (match-string 2))))
846 (cons 0 nil)))))
847 (setq ggtags-global-match-count count)
848 ;; Clear the start marker in case of zero matches.
849 (and (zerop count)
850 (markerp ggtags-global-start-marker)
851 (setq ggtags-global-start-marker nil))
852 (cons (if (> exit-status 0)
853 msg
854 (format "found %d %s"
855 count
856 (funcall (if (= count 1) #'car #'cadr)
857 (pcase db
858 ;; ` required for 24.1 and 24.2
859 (`"GTAGS" '("definition" "definitions"))
860 (`"GSYMS" '("symbol" "symbols"))
861 (`"GRTAGS" '("reference" "references"))
862 (`"ID" '("identifier" "identifiers"))
863 (_ '("match" "matches"))))))
864 exit-status)))
865
866 ;;; NOTE: Must not match the 'Global started at Mon Jun 3 10:24:13'
867 ;;; line or `compilation-auto-jump' will jump there and fail. See
868 ;;; comments before the 'gnu' entry in
869 ;;; `compilation-error-regexp-alist-alist'.
870 (defvar ggtags-global-error-regexp-alist-alist
871 (append
872 '((path "^\\(?:[^/\n]*/\\)?[^ )\t\n]+$" 0)
873 ;; ACTIVE_ESCAPE src/dialog.cc 172
874 (ctags "^\\([^ \t\n]+\\)[ \t]+\\(.*?\\)[ \t]+\\([0-9]+\\)$"
875 2 3 nil nil 2 (1 font-lock-function-name-face))
876 ;; ACTIVE_ESCAPE 172 src/dialog.cc #undef ACTIVE_ESCAPE
877 (ctags-x "^\\([^ \t\n]+\\)[ \t]+\\([0-9]+\\)[ \t]+\\(\\(?:[^/\n]*/\\)?[^ \t\n]+\\)"
878 3 2 nil nil 3 (1 font-lock-function-name-face))
879 ;; src/dialog.cc:172:#undef ACTIVE_ESCAPE
880 (grep "^\\(.+?\\):\\([0-9]+\\):\\(?:$\\|[^0-9\n]\\|[0-9][^0-9\n]\\|[0-9][0-9].\\)"
881 1 2 nil nil 1)
882 ;; src/dialog.cc ACTIVE_ESCAPE 172 #undef ACTIVE_ESCAPE
883 (cscope "^\\(.+?\\)[ \t]+\\([^ \t\n]+\\)[ \t]+\\([0-9]+\\).*\\(?:[^0-9\n]\\|[^0-9\n][0-9]\\|[^:\n][0-9][0-9]\\)$"
884 1 3 nil nil 1 (2 font-lock-function-name-face)))
885 compilation-error-regexp-alist-alist))
886
887 (defun ggtags-abbreviate-file (start end)
888 (let ((inhibit-read-only t)
889 (amount (if (numberp ggtags-global-abbreviate-filename)
890 (- (- end start) ggtags-global-abbreviate-filename)
891 999))
892 (advance-word (lambda ()
893 "Return the length of the text made invisible."
894 (let ((wend (min end (progn (forward-word 1) (point))))
895 (wbeg (max start (progn (backward-word 1) (point)))))
896 (goto-char wend)
897 (if (<= (- wend wbeg) 1)
898 0
899 (put-text-property (1+ wbeg) wend 'invisible t)
900 (1- (- wend wbeg)))))))
901 (goto-char start)
902 (while (and (> amount 0) (> end (point)))
903 (decf amount (funcall advance-word)))))
904
905 (defun ggtags-abbreviate-files (start end)
906 (goto-char start)
907 (let* ((error-re (cdr (assq ggtags-global-output-format
908 ggtags-global-error-regexp-alist-alist)))
909 (sub (cadr error-re)))
910 (when (and ggtags-global-abbreviate-filename error-re)
911 (while (re-search-forward (car error-re) end t)
912 (when (and (or (not (numberp ggtags-global-abbreviate-filename))
913 (> (length (match-string sub))
914 ggtags-global-abbreviate-filename))
915 ;; Ignore bogus file lines such as:
916 ;; Global found 2 matches at Thu Jan 31 13:45:19
917 (get-text-property (match-beginning sub) 'compilation-message))
918 (ggtags-abbreviate-file (match-beginning sub) (match-end sub)))))))
919
920 (defvar-local ggtags-global-output-lines 0)
921
922 (defun ggtags-global--display-buffer (&optional buffer)
923 (let ((buffer (or buffer (current-buffer))))
924 (unless (get-buffer-window buffer)
925 (let* ((split-window-preferred-function ggtags-split-window-function)
926 (w (display-buffer (current-buffer) '(nil (allow-no-window . t)))))
927 (and w (compilation-set-window-height w))))))
928
929 (defun ggtags-global-filter ()
930 "Called from `compilation-filter-hook' (which see)."
931 ;; Get rid of line "Using config file '/PATH/TO/.globalrc'." or
932 ;; "Using default configuration."
933 (when (re-search-backward
934 "^ *Using \\(?:config file '.*\\|default configuration.\\)\n"
935 compilation-filter-start t)
936 (replace-match ""))
937 (ansi-color-apply-on-region compilation-filter-start (point))
938 (incf ggtags-global-output-lines
939 (count-lines compilation-filter-start (point)))
940 (when (> ggtags-global-output-lines 5)
941 (ggtags-global--display-buffer))
942 (make-local-variable 'ggtags-global-large-output)
943 (when (> ggtags-global-output-lines ggtags-global-large-output)
944 (incf ggtags-global-large-output 500)
945 (let ((message-log-max nil))
946 (message "Output %d lines (Type `C-c C-k' to cancel)"
947 ggtags-global-output-lines))))
948
949 (defun ggtags-handle-single-match (buf _how)
950 (if (not (zerop ggtags-global-exit-status))
951 ;; If exit abnormally display the buffer for inspection.
952 (ggtags-global--display-buffer)
953 (when (and ggtags-auto-jump-to-first-match
954 (save-excursion
955 (goto-char (point-min))
956 (not (ignore-errors
957 (goto-char (compilation-next-single-property-change
958 (point) 'compilation-message))
959 (end-of-line)
960 (compilation-next-single-property-change
961 (point) 'compilation-message)))))
962 ;; For the `compilation-auto-jump' in idle timer to run. See also:
963 ;; http://debbugs.gnu.org/13829
964 (sit-for 0)
965 (ggtags-navigation-mode -1)
966 (ggtags-navigation-mode-cleanup buf 0))))
967
968 (defvar ggtags-global-mode-font-lock-keywords
969 '(("^Global \\(exited abnormally\\|interrupt\\|killed\\|terminated\\)\\(?:.*with code \\([0-9]+\\)\\)?.*"
970 (1 'compilation-error)
971 (2 'compilation-error nil t))
972 ("^Global found \\([0-9]+\\)" (1 compilation-info-face))))
973
974 (define-compilation-mode ggtags-global-mode "Global"
975 "A mode for showing outputs from gnu global."
976 ;; Make it buffer local for `ggtags-abbreviate-files'.
977 (make-local-variable 'ggtags-global-output-format)
978 (setq-local compilation-error-regexp-alist
979 (list ggtags-global-output-format))
980 (setq-local compilation-auto-jump-to-first-error
981 ggtags-auto-jump-to-first-match)
982 (setq-local compilation-scroll-output 'first-error)
983 (setq-local compilation-disable-input t)
984 (setq-local compilation-always-kill t)
985 (setq-local compilation-error-face 'compilation-info)
986 (setq-local compilation-exit-message-function
987 'ggtags-global-exit-message-function)
988 ;; See: https://github.com/leoliu/ggtags/issues/26
989 (setq-local find-file-suppress-same-file-warnings t)
990 (setq-local truncate-lines t)
991 (jit-lock-register #'ggtags-abbreviate-files)
992 (add-hook 'compilation-filter-hook 'ggtags-global-filter nil 'local)
993 (add-hook 'compilation-finish-functions 'ggtags-handle-single-match nil t)
994 (add-hook 'kill-buffer-hook (lambda () (ggtags-navigation-mode -1)) nil t)
995 (define-key ggtags-global-mode-map "\M-o" 'visible-mode))
996
997 ;; NOTE: Need this to avoid putting menu items in
998 ;; `emulation-mode-map-alists', which creates double entries. See
999 ;; http://i.imgur.com/VJJTzVc.png
1000 (defvar ggtags-navigation-map
1001 (let ((map (make-sparse-keymap)))
1002 (define-key map "\M-n" 'next-error)
1003 (define-key map "\M-p" 'previous-error)
1004 (define-key map "\M-}" 'ggtags-navigation-next-file)
1005 (define-key map "\M-{" 'ggtags-navigation-previous-file)
1006 (define-key map "\M->" 'ggtags-navigation-last-error)
1007 (define-key map "\M-<" 'ggtags-navigation-first-error)
1008 (define-key map "\C-c\C-k"
1009 (lambda () (interactive)
1010 (ggtags-ensure-global-buffer (kill-compilation))))
1011 (define-key map "\M-o" 'ggtags-navigation-visible-mode)
1012 (define-key map [return] 'ggtags-navigation-mode-done)
1013 (define-key map "\r" 'ggtags-navigation-mode-done)
1014 (define-key map [remap pop-tag-mark] 'ggtags-navigation-mode-abort)
1015 map))
1016
1017 (defvar ggtags-mode-map-alist
1018 `((ggtags-navigation-mode . ,ggtags-navigation-map)))
1019
1020 ;; Higher priority for `ggtags-navigation-mode' to avoid being
1021 ;; hijacked by modes such as `view-mode'.
1022 (add-to-list 'emulation-mode-map-alists 'ggtags-mode-map-alist)
1023
1024 (defvar ggtags-navigation-mode-map
1025 (let ((map (make-sparse-keymap))
1026 (menu (make-sparse-keymap "GG-Navigation")))
1027 ;; Menu items: (info "(elisp)Extended Menu Items")
1028 (define-key map [menu-bar ggtags-navigation] (cons "GG-Navigation" menu))
1029 ;; Ordered backwards
1030 (define-key menu [visible-mode]
1031 '(menu-item "Visible mode" ggtags-navigation-visible-mode
1032 :button (:toggle . (ignore-errors
1033 (ggtags-ensure-global-buffer
1034 visible-mode)))))
1035 (define-key menu [done]
1036 '(menu-item "Finish navigation" ggtags-navigation-mode-done))
1037 (define-key menu [abort]
1038 '(menu-item "Abort" ggtags-navigation-mode-abort))
1039 (define-key menu [last-error]
1040 '(menu-item "Last error" ggtags-navigation-last-error))
1041 (define-key menu [fist-error]
1042 '(menu-item "Fist error" ggtags-navigation-first-error))
1043 (define-key menu [previous-file]
1044 '(menu-item "Previous file" ggtags-navigation-previous-file))
1045 (define-key menu [next-file]
1046 '(menu-item "Next file" ggtags-navigation-next-file))
1047 (define-key menu [previous]
1048 '(menu-item "Previous match" previous-error))
1049 (define-key menu [next]
1050 '(menu-item "Next match" next-error))
1051 map))
1052
1053 (defun ggtags-move-to-tag (&optional name)
1054 "Move to NAME tag in current line."
1055 (let ((orig (point))
1056 (tag (or name ggtags-current-tag-name)))
1057 (beginning-of-line)
1058 (if (and tag (re-search-forward
1059 (concat "\\_<" (regexp-quote tag) "\\_>")
1060 (line-end-position)
1061 t))
1062 (goto-char (match-beginning 0))
1063 (goto-char orig))))
1064
1065 (defun ggtags-navigation-mode-cleanup (&optional buf time)
1066 (let ((buf (or buf ggtags-global-last-buffer)))
1067 (and (buffer-live-p buf)
1068 (with-current-buffer buf
1069 (when (get-buffer-process (current-buffer))
1070 (kill-compilation))
1071 (when (and (derived-mode-p 'ggtags-global-mode)
1072 (get-buffer-window))
1073 (quit-window nil (get-buffer-window)))
1074 (and time (run-with-idle-timer time nil #'kill-buffer buf))))))
1075
1076 (defun ggtags-navigation-mode-done ()
1077 (interactive)
1078 (ggtags-navigation-mode -1)
1079 (setq tags-loop-scan t
1080 tags-loop-operate '(ggtags-find-tag-continue))
1081 (ggtags-navigation-mode-cleanup))
1082
1083 (defun ggtags-navigation-mode-abort ()
1084 (interactive)
1085 (ggtags-navigation-mode -1)
1086 ;; Run after (ggtags-navigation-mode -1) or
1087 ;; ggtags-global-start-marker might not have been saved.
1088 (when (and ggtags-global-start-marker
1089 (not (markerp ggtags-global-start-marker)))
1090 (setq ggtags-global-start-marker nil)
1091 (pop-tag-mark))
1092 (ggtags-navigation-mode-cleanup nil 0))
1093
1094 (defun ggtags-navigation-next-file (n)
1095 (interactive "p")
1096 (ggtags-ensure-global-buffer
1097 (compilation-next-file n)
1098 (compile-goto-error)))
1099
1100 (defun ggtags-navigation-previous-file (n)
1101 (interactive "p")
1102 (ggtags-navigation-next-file (- n)))
1103
1104 (defun ggtags-navigation-first-error ()
1105 (interactive)
1106 (ggtags-ensure-global-buffer
1107 (goto-char (point-min))
1108 (compilation-next-error 1)
1109 (compile-goto-error)))
1110
1111 (defun ggtags-navigation-last-error ()
1112 (interactive)
1113 (ggtags-ensure-global-buffer
1114 (goto-char (point-max))
1115 (compilation-previous-error 1)
1116 (compile-goto-error)))
1117
1118 (defun ggtags-navigation-visible-mode (&optional arg)
1119 (interactive (list (or current-prefix-arg 'toggle)))
1120 (ggtags-ensure-global-buffer
1121 (visible-mode arg)))
1122
1123 (defvar ggtags-global-line-overlay nil)
1124
1125 (defun ggtags-global-next-error-hook ()
1126 (ggtags-move-to-tag)
1127 (ggtags-global-save-start-marker)
1128 (ignore-errors
1129 (ggtags-ensure-global-buffer
1130 (unless (overlayp ggtags-global-line-overlay)
1131 (setq ggtags-global-line-overlay (make-overlay (point) (point)))
1132 (overlay-put ggtags-global-line-overlay 'face 'ggtags-global-line))
1133 (move-overlay ggtags-global-line-overlay
1134 (line-beginning-position) (line-end-position)
1135 (current-buffer)))))
1136
1137 (define-minor-mode ggtags-navigation-mode nil
1138 :lighter
1139 (" GG[" (:eval
1140 (ignore-errors
1141 (ggtags-ensure-global-buffer
1142 (let ((index (when (get-text-property (line-beginning-position)
1143 'compilation-message)
1144 ;; Assume the first match appears at line 5
1145 (- (line-number-at-pos) 4))))
1146 `((:propertize ,(if index
1147 (number-to-string (max index 0))
1148 "?") face success) "/")))))
1149 (:propertize (:eval (number-to-string ggtags-global-match-count))
1150 face success)
1151 (:eval
1152 (unless (zerop ggtags-global-exit-status)
1153 `(":" (:propertize ,(number-to-string ggtags-global-exit-status)
1154 face error))))
1155 "]")
1156 :global t
1157 (if ggtags-navigation-mode
1158 (progn
1159 (add-hook 'next-error-hook 'ggtags-global-next-error-hook)
1160 (add-hook 'minibuffer-setup-hook 'ggtags-minibuffer-setup-function))
1161 (remove-hook 'next-error-hook 'ggtags-global-next-error-hook)
1162 (remove-hook 'minibuffer-setup-hook 'ggtags-minibuffer-setup-function)))
1163
1164 (defun ggtags-minibuffer-setup-function ()
1165 ;; Disable ggtags-navigation-mode in minibuffer.
1166 (setq-local ggtags-navigation-mode nil))
1167
1168 (defun ggtags-kill-file-buffers (&optional interactive)
1169 "Kill all buffers visiting files in current project."
1170 (interactive "p")
1171 (ggtags-check-project)
1172 (let ((directories (cons (ggtags-current-project-root) (ggtags-get-libpath)))
1173 (count 0)
1174 (some (lambda (pred list)
1175 (loop for x in list when (funcall pred x) return it))))
1176 (dolist (buf (buffer-list))
1177 (let ((file (and (buffer-live-p buf)
1178 (not (eq buf (current-buffer)))
1179 (buffer-file-name buf))))
1180 (when (and file (funcall some
1181 (lambda (dir)
1182 ;; Don't use `file-in-directory-p'
1183 ;; to allow symbolic links.
1184 (string-prefix-p dir file))
1185 directories))
1186 (and (kill-buffer buf) (incf count)))))
1187 (and interactive
1188 (message "%d %s killed" count (if (= count 1) "buffer" "buffers")))))
1189
1190 (defun ggtags-after-save-function ()
1191 (when (ggtags-find-project)
1192 (setf (ggtags-project-dirty-p (ggtags-find-project)) t)
1193 ;; When oversize update on a per-save basis.
1194 (when (and buffer-file-name (ggtags-project-oversize-p))
1195 (ggtags-with-process-environment
1196 (process-file "global" nil 0 nil "--single-update"
1197 (file-relative-name buffer-file-name))))))
1198
1199 (defvar ggtags-mode-prefix-map
1200 (let ((m (make-sparse-keymap)))
1201 (define-key m "\M-'" 'previous-error)
1202 (define-key m (kbd "M-DEL") 'ggtags-delete-tag-files)
1203 (define-key m "\M-p" 'ggtags-prev-mark)
1204 (define-key m "\M-n" 'ggtags-next-mark)
1205 (define-key m "\M-f" 'ggtags-find-file)
1206 (define-key m "\M-o" 'ggtags-find-other-symbol)
1207 (define-key m "\M-g" 'ggtags-grep)
1208 (define-key m "\M-i" 'ggtags-idutils-query)
1209 (define-key m "\M-b" 'ggtags-browse-file-as-hypertext)
1210 (define-key m "\M-k" 'ggtags-kill-file-buffers)
1211 (define-key m "\M-h" 'ggtags-view-tag-history)
1212 (define-key m (kbd "M-%") 'ggtags-query-replace)
1213 m))
1214
1215 (defvar ggtags-mode-map
1216 (let ((map (make-sparse-keymap))
1217 (menu (make-sparse-keymap "Ggtags")))
1218 (define-key map "\M-." 'ggtags-find-tag-dwim)
1219 (define-key map (kbd "M-]") 'ggtags-find-reference)
1220 (define-key map (kbd "C-M-.") 'ggtags-find-tag-regexp)
1221 (define-key map ggtags-mode-prefix-key ggtags-mode-prefix-map)
1222 ;; Menu items
1223 (define-key map [menu-bar ggtags] (cons "Ggtags" menu))
1224 ;; Ordered backwards
1225 (define-key menu [report-bugs]
1226 `(menu-item "Report bugs"
1227 (lambda () (interactive)
1228 (browse-url ggtags-bug-url)
1229 (message "Please visit %s" ggtags-bug-url))
1230 :help ,(format "Visit %s" ggtags-bug-url)))
1231 (define-key menu [custom-ggtags]
1232 '(menu-item "Customize Ggtags"
1233 (lambda () (interactive) (customize-group 'ggtags))))
1234 (define-key menu [save-project]
1235 '(menu-item "Save project settings" ggtags-save-project-settings))
1236 (define-key menu [toggle-read-only]
1237 '(menu-item "Toggle project read-only" ggtags-toggle-project-read-only
1238 :button (:toggle . buffer-read-only)))
1239 (define-key menu [sep2] menu-bar-separator)
1240 (define-key menu [browse-hypertext]
1241 '(menu-item "Browse as hypertext" ggtags-browse-file-as-hypertext
1242 :enable (ggtags-find-project)))
1243 (define-key menu [delete-tags]
1244 '(menu-item "Delete tag files" ggtags-delete-tag-files
1245 :enable (ggtags-find-project)))
1246 (define-key menu [kill-buffers]
1247 '(menu-item "Kill project file buffers" ggtags-kill-file-buffers
1248 :enable (ggtags-find-project)))
1249 (define-key menu [view-tag]
1250 '(menu-item "View tag history" ggtags-view-tag-history))
1251 (define-key menu [pop-mark]
1252 '(menu-item "Pop mark" pop-tag-mark
1253 :help "Pop to previous mark and destroy it"))
1254 (define-key menu [next-mark]
1255 '(menu-item "Next mark" ggtags-next-mark))
1256 (define-key menu [prev-mark]
1257 '(menu-item "Previous mark" ggtags-prev-mark))
1258 (define-key menu [sep1] menu-bar-separator)
1259 (define-key menu [previous-error]
1260 '(menu-item "Previous match" previous-error))
1261 (define-key menu [next-error]
1262 '(menu-item "Next match" next-error))
1263 (define-key menu [find-file]
1264 '(menu-item "Find files" ggtags-find-file))
1265 (define-key menu [query-replace]
1266 '(menu-item "Query replace" ggtags-query-replace))
1267 (define-key menu [idutils]
1268 '(menu-item "Query idutils DB" ggtags-idutils-query))
1269 (define-key menu [grep]
1270 '(menu-item "Grep" ggtags-grep))
1271 (define-key menu [find-symbol]
1272 '(menu-item "Find other symbol" ggtags-find-other-symbol))
1273 (define-key menu [find-tag-regexp]
1274 '(menu-item "Find tag matching regexp" ggtags-find-tag-regexp))
1275 (define-key menu [find-reference]
1276 '(menu-item "Find reference" ggtags-find-reference))
1277 (define-key menu [find-tag-continue]
1278 '(menu-item "Continue find tag" tags-loop-continue))
1279 (define-key menu [find-tag]
1280 '(menu-item "Find tag" ggtags-find-tag-dwim))
1281 (define-key menu [update-tags]
1282 '(menu-item "Update tag files" ggtags-update-tags
1283 :visible (ggtags-find-project)))
1284 (define-key menu [run-gtags]
1285 '(menu-item "Run gtags" ggtags-create-tags
1286 :visible (not (ggtags-find-project))))
1287 map))
1288
1289 ;;;###autoload
1290 (define-minor-mode ggtags-mode nil
1291 :lighter (:eval (if ggtags-navigation-mode "" " GG"))
1292 (unless (timerp ggtags-highlight-tag-timer)
1293 (setq ggtags-highlight-tag-timer
1294 (run-with-idle-timer
1295 ggtags-highlight-tag-delay t #'ggtags-highlight-tag-at-point)))
1296 (if ggtags-mode
1297 (progn
1298 (add-hook 'after-save-hook 'ggtags-after-save-function nil t)
1299 ;; Append to serve as a fallback method.
1300 (add-hook 'completion-at-point-functions
1301 #'ggtags-completion-at-point t t))
1302 (remove-hook 'after-save-hook 'ggtags-after-save-function t)
1303 (remove-hook 'completion-at-point-functions #'ggtags-completion-at-point t)
1304 (and (overlayp ggtags-highlight-tag-overlay)
1305 (delete-overlay ggtags-highlight-tag-overlay))
1306 (setq ggtags-highlight-tag-overlay nil)))
1307
1308 (defvar ggtags-highlight-tag-map
1309 (let ((map (make-sparse-keymap)))
1310 (define-key map [S-down-mouse-1] 'ggtags-find-tag-dwim)
1311 (define-key map [S-down-mouse-3] 'ggtags-find-reference)
1312 map)
1313 "Keymap used for valid tag at point.")
1314
1315 (put 'ggtags-active-tag 'face 'ggtags-highlight)
1316 (put 'ggtags-active-tag 'keymap ggtags-highlight-tag-map)
1317 ;; (put 'ggtags-active-tag 'mouse-face 'match)
1318 (put 'ggtags-active-tag 'help-echo
1319 "S-down-mouse-1 for definitions\nS-down-mouse-3 for references")
1320
1321 (defun ggtags-highlight-tag-at-point ()
1322 (when (and ggtags-mode (eq ggtags-project 'unset))
1323 (ggtags-find-project))
1324 (when (and ggtags-mode ggtags-project)
1325 (unless (overlayp ggtags-highlight-tag-overlay)
1326 (setq ggtags-highlight-tag-overlay (make-overlay (point) (point) nil t))
1327 (overlay-put ggtags-highlight-tag-overlay 'modification-hooks
1328 (list (lambda (o after &rest _args)
1329 (and (not after) (delete-overlay o))))))
1330 (let ((bounds (funcall ggtags-bounds-of-tag-function))
1331 (o ggtags-highlight-tag-overlay))
1332 (cond
1333 ((and bounds
1334 (eq (overlay-buffer o) (current-buffer))
1335 (= (overlay-start o) (car bounds))
1336 (= (overlay-end o) (cdr bounds)))
1337 ;; Overlay matches current tag so do nothing.
1338 nil)
1339 ((and bounds (let ((completion-ignore-case nil))
1340 (ignore-errors
1341 ;; May throw: global: only name char is
1342 ;; allowed with -c option
1343 (test-completion
1344 (buffer-substring (car bounds) (cdr bounds))
1345 ggtags-completion-table))))
1346 (move-overlay o (car bounds) (cdr bounds) (current-buffer))
1347 (overlay-put o 'category 'ggtags-active-tag))
1348 (t (move-overlay o
1349 (or (car bounds) (point))
1350 (or (cdr bounds) (point))
1351 (current-buffer))
1352 (overlay-put o 'category nil))))))
1353
1354 ;;; imenu
1355
1356 (defun ggtags-goto-imenu-index (name line &rest _args)
1357 (save-restriction
1358 (widen)
1359 (goto-char (point-min))
1360 (forward-line (1- line))
1361 (ggtags-move-to-tag name)))
1362
1363 ;;;###autoload
1364 (defun ggtags-build-imenu-index ()
1365 "A function suitable for `imenu-create-index-function'."
1366 (when-let (file (and buffer-file-name (file-relative-name buffer-file-name)))
1367 (with-temp-buffer
1368 (when (with-demoted-errors
1369 (zerop (ggtags-with-process-environment
1370 (process-file "global" nil t nil "-x" "-f" file))))
1371 (goto-char (point-min))
1372 (loop while (re-search-forward
1373 "^\\([^ \t]+\\)[ \t]+\\([0-9]+\\)" nil t)
1374 collect (list (match-string 1)
1375 (string-to-number (match-string 2))
1376 'ggtags-goto-imenu-index))))))
1377
1378 ;;; hippie-expand
1379
1380 ;;;###autoload
1381 (defun try-complete-ggtags-tag (old)
1382 "A function suitable for `hippie-expand-try-functions-list'."
1383 (with-no-warnings ; to avoid loading hippie-exp
1384 (unless old
1385 (he-init-string (if (looking-back "\\_<.*" (line-beginning-position))
1386 (match-beginning 0)
1387 (point))
1388 (point))
1389 (setq he-expand-list
1390 (and (not (equal he-search-string ""))
1391 (ggtags-find-project)
1392 (sort (all-completions he-search-string
1393 ggtags-completion-table)
1394 #'string-lessp))))
1395 (if (null he-expand-list)
1396 (progn
1397 (if old (he-reset-string))
1398 nil)
1399 (he-substitute-string (car he-expand-list))
1400 (setq he-expand-list (cdr he-expand-list))
1401 t)))
1402
1403 (defun ggtags-reload (&optional force)
1404 (interactive "P")
1405 (unload-feature 'ggtags force)
1406 (require 'ggtags))
1407
1408 (defun ggtags-unload-function ()
1409 (setq emulation-mode-map-alists
1410 (delq 'ggtags-mode-map-alist emulation-mode-map-alists))
1411 nil)
1412
1413 (provide 'ggtags)
1414 ;;; ggtags.el ends here