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