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