1 ;;; gnat-inspect.el --- minor-mode for navigating sources using the
2 ;;; AdaCore cross reference tool gnatinspect.
4 ;;; gnatinspect supports Ada and any gcc language that supports the
5 ;;; -fdump-xref switch (which includes C, C++).
7 ;;; Copyright (C) 2013, 2014 Free Software Foundation, Inc.
9 ;; Author: Stephen Leake <stephen_leake@member.fsf.org>
10 ;; Maintainer: Stephen Leake <stephen_leake@member.fsf.org>
13 ;; This file is part of GNU Emacs.
15 ;; GNU Emacs is free software: you can redistribute it and/or modify
16 ;; it under the terms of the GNU General Public License as published by
17 ;; the Free Software Foundation, either version 3 of the License, or
18 ;; (at your option) any later version.
20 ;; GNU Emacs is distributed in the hope that it will be useful,
21 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 ;; GNU General Public License for more details.
25 ;; You should have received a copy of the GNU General Public License
26 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
32 (require 'ada-mode) ;; for ada-prj-*, some other things
38 ;; gnatinspect reads the project files and the database at startup,
39 ;; which is noticeably slow for a reasonably sized project. But
40 ;; running queries after startup is fast. So we leave gnatinspect
41 ;; running, and send it new queries via stdin, getting responses via
44 ;; We maintain a cache of active sessions, one per gnat project.
46 (cl-defstruct (gnat-inspect--session)
47 (process nil) ;; running gnatinspect
48 (buffer nil) ;; receives output of gnatinspect
52 (defconst gnat-inspect-buffer-name-prefix " *gnatinspect-")
54 (defun gnat-inspect--start-process (session)
55 "Start the session process running gnatinspect."
56 (unless (buffer-live-p (gnat-inspect--session-buffer session))
57 ;; user may have killed buffer
58 (setf (gnat-inspect--session-buffer session) (gnat-run-buffer gnat-inspect-buffer-name-prefix)))
60 (with-current-buffer (gnat-inspect--session-buffer session)
61 (let ((process-environment (ada-prj-get 'proc_env)) ;; for GPR_PROJECT_PATH
63 ;; WORKAROUND: gnatinspect from gnatcoll-1.6w-20130902 can't handle aggregate projects; M910-032
64 (project-file (file-name-nondirectory
65 (ada-prj-get 'gpr_file))))
66 (erase-buffer); delete any previous messages, prompt
67 (setf (gnat-inspect--session-process session)
68 ;; FIXME: need good error message on bad project file:
69 ;; "can't handle aggregate projects?")
70 (start-process (concat "gnatinspect " (buffer-name))
71 (gnat-inspect--session-buffer session)
73 (concat "--project=" project-file)))
74 (set-process-query-on-exit-flag (gnat-inspect--session-process session) nil)
75 (gnat-inspect-session-wait session)
78 (defun gnat-inspect--make-session ()
79 "Create and return a session for the current project file."
81 (make-gnat-inspect--session
82 :buffer (gnat-run-buffer gnat-inspect-buffer-name-prefix))))
83 (gnat-inspect--start-process session)
86 (defvar gnat-inspect--sessions '()
87 "Assoc list of sessions, indexed by absolute GNAT project file name.")
89 (defun gnat-inspect-cached-session ()
90 "Return a session for the current project file, creating it if necessary."
91 (gnat-inspect-ensure-gpr)
93 (let* ((session (cdr (assoc ada-prj-current-file gnat-inspect--sessions))))
96 (unless (process-live-p (gnat-inspect--session-process session))
97 (gnat-inspect--start-process session))
101 (setq session (gnat-inspect--make-session))
102 (setq gnat-inspect--sessions
103 (cl-acons ada-prj-current-file session gnat-inspect--sessions))))
106 (defun gnat-inspect-show-session-buffer ()
108 (pop-to-buffer (gnat-inspect-cached-session)))
110 (defconst gnat-inspect-prompt "^>>> $"
111 ;; gnatinspect output ends with this
112 "Regexp matching gnatinspect prompt; indicates previous command is complete.")
114 (defun gnat-inspect-session-wait (session)
115 "Wait for the current command to complete."
116 (unless (process-live-p (gnat-inspect--session-process session))
117 (error "gnatinspect process failed"))
119 (with-current-buffer (gnat-inspect--session-buffer session)
120 (let ((process (gnat-inspect--session-process session))
121 (search-start (point-min))
124 ;; process output is inserted before point, so move back over it to search it
125 (goto-char search-start)
126 (not (re-search-forward gnat-inspect-prompt (point-max) 1)));; don't search same text again
127 (setq search-start (point))
128 (message (concat "running gnatinspect ..." (make-string wait-count ?.)))
129 (accept-process-output process 1.0)
130 (setq wait-count (1+ wait-count)))
131 (message (concat "running gnatinspect ... done"))
134 (defun gnat-inspect-session-send (cmd wait)
135 "Send CMD to gnatinspect session for current project.
136 If WAIT is non-nil, wait for command to complete.
137 Return buffer that holds output."
138 (let ((session (gnat-inspect-cached-session)))
139 (with-current-buffer (gnat-inspect--session-buffer session)
141 (process-send-string (gnat-inspect--session-process session)
144 (gnat-inspect-session-wait session))
148 (defun gnat-inspect-kill-all-sessions ()
151 (mapc (lambda (assoc)
152 (let ((session (cdr assoc)))
153 (when (process-live-p (gnat-inspect--session-process session))
154 (setq count (1+ count))
155 (process-send-string (gnat-inspect--session-process session) "exit\n")
157 gnat-inspect--sessions)
158 (message "Killed %d sessions" count)
163 (defun gnat-inspect-ensure-gpr ()
164 (unless (ada-prj-get 'gpr_file)
165 (error "no gpr file specified")))
167 (defconst gnat-inspect-ident-file-regexp
168 ;; Write_Message:C:\Projects\GDS\work_dscovr_release\common\1553\gds-mil_std_1553-utf.ads:252:25
169 ;; Write_Message:/Projects/GDS/work_dscovr_release/common/1553/gds-mil_std_1553-utf.ads:252:25
170 "\\([^:]*\\):\\(\\(?:.:\\\|/\\)[^:]*\\):\\([0123456789]+\\):\\([0123456789]+\\)"
171 "Regexp matching <identifier>:<file>:<line>:<column>")
173 (defconst gnat-inspect-ident-file-regexp-alist
174 (list (concat "^" gnat-inspect-ident-file-regexp) 2 3 4)
175 "For compilation-error-regexp-alist, matching `gnatinspect overriding_recursive' output")
177 (defconst gnat-inspect-ident-file-type-regexp
178 (concat gnat-inspect-ident-file-regexp " (\\(.*\\))")
179 "Regexp matching <identifier>:<file>:<line>:<column> (<type>)")
181 (defconst gnat-inspect-ident-file-scope-regexp-alist
182 ;; RX_Enable:C:\common\1553\gds-hardware-bus_1553-raw_read_write.adb:163:13 (write reference) scope=New_Packet_TX:C:\common\1553\gds-hardware-bus_1553-raw_read_write.adb:97:14
185 gnat-inspect-ident-file-regexp
188 gnat-inspect-ident-file-regexp
190 2 3 4;; file line column
193 ;; (list 4 'gnat-inspect-scope-secondary-error)
195 "For compilation-error-regexp-alist, matching `gnatinspect refs' output")
198 ;; in *compilation-gnatinspect-refs*, run
199 ;; (progn (set-text-properties (point-min)(point-max) nil)(compilation-parse-errors (point-min)(point-max) gnat-inspect-ident-file-scope-regexp-alist))
201 (defun gnat-inspect-compilation (identifier file line col cmd comp-err)
202 "Run gnatinspect IDENTIFIER:FILE:LINE:COL CMD,
203 set compilation-mode with compilation-error-regexp-alist set to COMP-ERR."
204 (gnat-inspect-ensure-gpr)
206 (let ((cmd-1 (format "%s %s:%s:%d:%d" cmd identifier file line col))
209 (with-current-buffer (gnat-inspect--session-buffer (gnat-inspect-cached-session))
211 (setq buffer-read-only nil)
212 (set (make-local-variable 'compilation-error-regexp-alist) (list comp-err))
213 (gnat-inspect-session-send cmd-1 t)
214 ;; at EOB. gnatinspect returns one line per result
215 (setq result-count (- (line-number-at-pos) 1))
216 (font-lock-fontify-buffer)
217 ;; font-lock-fontify-buffer applies compilation-message text properties
218 ;; IMPROVEME: for some reason, next-error works, but the font
219 ;; colors are not right (no koolaid!)
220 (goto-char (point-min))
222 (cl-case result-count
224 (error "gnatinspect returned no results"))
226 ;; just go there, don't display session-buffer. We have to
227 ;; fetch the compilation-message while in the session-buffer.
228 (let* ((msg (compilation-next-error 0 nil (point-min)))
229 (loc (compilation--message->loc msg)))
230 (setq file (caar (compilation--loc->file-struct loc))
231 line (caar (cddr (compilation--loc->file-struct loc)))
232 column (1- (compilation--loc->col loc)))
235 ));; case, with-currrent-buffer
237 ;; compilation-next-error-function assumes there is not at error
238 ;; at point-min; work around that by moving forward 0 errors for
240 (if (> result-count 1)
241 ;; more than one result; display session buffer
243 ;; else don't display
244 (ada-goto-source file line column nil))
247 (defun gnat-inspect-dist (found-line line found-col col)
248 "Return non-nil if found-line, -col is closer to line, col than min-distance."
249 (+ (abs (- found-line line))
250 (* (abs (- found-col col)) 250)))
252 ;;;;; user interface functions
254 (defun gnat-inspect-refresh ()
255 "For `ada-xref-refresh-function', using gnatinspect."
257 (gnat-inspect-session-send "refresh" t))
259 (defun gnat-inspect-other (identifier file line col)
260 "For `ada-xref-other-function', using gnatinspect."
261 (when (eq ?\" (aref identifier 0))
262 ;; gnatinspect wants the quotes stripped
264 (setq identifier (substring identifier 1 (1- (length identifier))))
267 (let ((cmd (format "refs %s:%s:%d:%d" identifier (file-name-nondirectory file) line col))
271 (min-distance (1- (expt 2 29)))
274 (with-current-buffer (gnat-inspect-session-send cmd t)
275 ;; 'gnatinspect refs' returns a list containing the declaration,
276 ;; the body, and all the references, in no particular order.
278 ;; We search the list, looking for the input location,
279 ;; declaration and body, then return the declaration or body as
282 ;; the format of each line is name:file:line:column (type) scope=name:file:line:column
288 ;; full declaration (for a private type)
289 ;; implicit reference
293 ;; Module_Type:/home/Projects/GDS/work_stephe_2/common/1553/gds-hardware-bus_1553-wrapper.ads:171:9 (full declaration) scope=Wrapper:/home/Projects/GDS/work_stephe_2/common/1553/gds-hardware-bus_1553-wrapper.ads:49:31
295 ;; itc_assert:/home/Projects/GDS/work_stephe_2/common/itc/opsim/itc_dscovr_gdsi/Gds1553/src/Gds1553.cpp:830:9 (reference) scope=Gds1553WriteSubaddress:/home/Projects/GDS/work_stephe_2/common/itc/opsim/itc_dscovr_gdsi/Gds1553/inc/Gds1553.hpp:173:24
297 (message "parsing result ...")
299 (goto-char (point-min))
303 ((looking-at gnat-inspect-ident-file-type-regexp)
305 (let* ((found-file (expand-file-name (match-string 2)));; converts Windows to normal
306 (found-line (string-to-number (match-string 3)))
307 (found-col (string-to-number (match-string 4)))
308 (found-type (match-string 5))
309 (dist (gnat-inspect-dist found-line line found-col col))
312 (when (string-equal found-type "declaration")
313 (setq decl-loc (list found-file found-line (1- found-col))))
316 (string-equal found-type "body")
317 (string-equal found-type "full declaration"))
318 (setq body-loc (list found-file found-line (1- found-col))))
321 ;; In general, we don't know where in the gnatinspect
322 ;; output the search item occurs, so we search for it.
324 ;; We use the same distance algorithm as gnatinspect
325 ;; to allow a fuzzy match on edited code.
326 (and (equal found-file file)
327 (< dist min-distance))
328 (setq min-distance dist)
329 (setq search-type found-type))
334 ;; This skips GPR_PROJECT_PATH and echoed command at start of buffer.
336 ;; It also skips warning lines. For example,
337 ;; gnatcoll-1.6w-20130902 can't handle the Auto_Text_IO
338 ;; language, because it doesn't use the gprconfig
339 ;; configuration project. That gives lines like:
341 ;; common_text_io.gpr:15:07: language unknown for "gds-hardware-bus_1553-time_tone.ads"
343 ;; There are probably other warnings that might be reported as well.
351 (error "gnatinspect did not return other item; refresh?"))
354 (string-equal search-type "declaration")
356 (setq result body-loc))
359 (setq result decl-loc))
363 (error "gnatinspect did not return other item; refresh?"))
365 (message "parsing result ... done")
368 (defun gnat-inspect-all (identifier file line col)
369 "For `ada-xref-all-function', using gnatinspect."
370 ;; This will in general return a list of references, so we use
371 ;; `compilation-start' to run gnatinspect, so the user can navigate
372 ;; to each result in turn via `next-error'.
373 (gnat-inspect-compilation identifier file line col "refs" 'gnat-inspect-ident-file))
375 (defun gnat-inspect-parents (identifier file line col)
376 "For `ada-xref-parent-function', using gnatinspect."
377 (gnat-inspect-compilation identifier file line col "parent_types" 'gnat-inspect-ident-file))
379 (defun gnat-inspect-overriding (identifier file line col)
380 "For `ada-xref-overriding-function', using gnatinspect."
381 (gnat-inspect-compilation identifier file line col "overridden_recursive" 'gnat-inspect-ident-file))
383 (defun gnat-inspect-overridden-1 (identifier file line col)
384 "For `ada-xref-overridden-function', using gnatinspect."
385 (unless (ada-prj-get 'gpr_file)
386 (error "no gnat project file defined."))
388 (when (eq ?\" (aref identifier 0))
389 ;; gnatinspect wants the quotes stripped
391 (setq identifier (substring identifier 1 (1- (length identifier))))
394 (let ((cmd (format "overrides %s:%s:%d:%d" identifier (file-name-nondirectory file) line col))
396 (with-current-buffer (gnat-inspect-session-send cmd t)
398 (goto-char (point-min))
399 (when (looking-at gnat-inspect-ident-file-regexp)
403 (string-to-number (match-string 3))
404 (string-to-number (match-string 4)))))
407 (error "gnatinspect did not return other item; refresh?"))
409 (message "parsing result ... done")
412 (defun gnat-inspect-overridden (other-window)
413 "Move to the overridden declaration of the identifier around point.
414 If OTHER-WINDOW (set by interactive prefix) is non-nil, show the
415 buffer in another window."
419 (gnat-inspect-overridden-1
420 (thing-at-point 'symbol)
424 (goto-char (car (bounds-of-thing-at-point 'symbol)))
425 (1+ (current-column)))
428 (ada-goto-source (nth 0 target)
434 (defun gnat-inspect-goto-declaration (other-window)
435 "Move to the declaration or body of the identifier around point.
436 If at the declaration, go to the body, and vice versa. If at a
437 reference, goto the declaration.
439 If OTHER-WINDOW (set by interactive prefix) is non-nil, show the
440 buffer in another window."
445 (thing-at-point 'symbol)
449 (goto-char (car (bounds-of-thing-at-point 'symbol)))
450 (1+ (current-column)))
453 (ada-goto-source (nth 0 target)
459 (defvar gnat-inspect-map
460 (let ((map (make-sparse-keymap)))
461 ;; C-c C-i prefix for gnat-inspect minor mode
463 (define-key map "\C-c\C-i\C-d" 'gnat-inspect-goto-declaration)
464 (define-key map "\C-c\C-i\C-p" 'ada-build-prompt-select-prj-file)
465 (define-key map "\C-c\C-i\C-q" 'gnat-inspect-refresh)
466 (define-key map "\C-c\C-i\C-r" 'gnat-inspect-all)
468 ) "Local keymap used for GNAT inspect minor mode.")
470 (defvar gnat-inspect-menu (make-sparse-keymap "gnat-inspect"))
471 (easy-menu-define gnat-inspect-menu gnat-inspect-map "Menu keymap for gnat-inspect minor mode"
473 ["Find and select project ..." ada-build-prompt-select-prj-file t]
474 ["Select project ..." ada-prj-select t]
475 ["Show current project" ada-prj-show t]
476 ["Next compilation error" next-error t]
477 ["Show secondary error" ada-show-secondary-error t]
478 ["Refresh cross reference cache" gnat-inspect-refresh t]
481 (define-minor-mode gnat-inspect
482 "Minor mode for navigating sources using GNAT cross reference tool.
483 Enable mode if ARG is positive"
485 :lighter " gnat-inspect" ;; mode line
487 ;; just enable the menu and keymap
490 ;;;;; support for Ada mode
492 (defun ada-gnat-inspect-select-prj ()
493 (setq ada-file-name-from-ada-name 'ada-gnat-file-name-from-ada-name)
494 (setq ada-ada-name-from-file-name 'ada-gnat-ada-name-from-file-name)
495 (setq ada-make-package-body 'ada-gnat-make-package-body)
497 (add-hook 'ada-syntax-propertize-hook 'gnatprep-syntax-propertize)
499 ;; must be after indentation engine setup, because that resets the
500 ;; indent function list.
501 (add-hook 'ada-mode-hook 'ada-gnat-inspect-setup t)
503 (setq ada-xref-refresh-function 'gnat-inspect-refresh)
504 (setq ada-xref-all-function 'gnat-inspect-all)
505 (setq ada-xref-other-function 'gnat-inspect-other)
506 (setq ada-xref-parent-function 'gnat-inspect-parents)
507 (setq ada-xref-all-function 'gnat-inspect-all)
508 (setq ada-xref-overriding-function 'gnat-inspect-overriding)
509 (setq ada-xref-overridden-function 'gnat-inspect-overridden-1)
510 (setq ada-show-xref-tool-buffer 'gnat-inspect-show-session-buffer)
512 (add-to-list 'completion-ignored-extensions ".ali") ;; gnat library files, used for cross reference
515 (defun ada-gnat-inspect-deselect-prj ()
516 (setq ada-file-name-from-ada-name nil)
517 (setq ada-ada-name-from-file-name nil)
518 (setq ada-make-package-body nil)
520 (setq ada-syntax-propertize-hook (delq 'gnatprep-syntax-propertize ada-syntax-propertize-hook))
521 (setq ada-mode-hook (delq 'ada-gnat-inspect-setup ada-mode-hook))
523 (setq ada-xref-other-function nil)
524 (setq ada-xref-parent-function nil)
525 (setq ada-xref-all-function nil)
526 (setq ada-xref-overriding-function nil)
527 (setq ada-xref-overridden-function nil)
528 (setq ada-show-xref-tool-buffer nil)
530 (setq completion-ignored-extensions (delete ".ali" completion-ignored-extensions))
533 (defun ada-gnat-inspect-setup ()
534 (when (boundp 'wisi-indent-calculate-functions)
535 (add-to-list 'wisi-indent-calculate-functions 'gnatprep-indent))
538 (defun ada-gnat-inspect ()
539 "Set Ada mode global vars to use gnatinspect."
540 (add-to-list 'ada-prj-parser-alist '("gpr" . gnat-parse-gpr))
541 (add-to-list 'ada-select-prj-xref-tool '(gnat_inspect . ada-gnat-inspect-select-prj))
542 (add-to-list 'ada-deselect-prj-xref-tool '(gnat_inspect . ada-gnat-inspect-deselect-prj))
546 (font-lock-add-keywords 'ada-mode
547 ;; gnatprep preprocessor line
548 (list (list "^[ \t]*\\(#.*\n\\)" '(1 font-lock-type-face t))))
550 (add-hook 'ada-gnat-fix-error-hook 'ada-gnat-fix-error)
553 (provide 'gnat-inspect)
554 (provide 'ada-xref-tool)
556 (add-to-list 'compilation-error-regexp-alist-alist
557 (cons 'gnat-inspect-ident-file gnat-inspect-ident-file-regexp-alist))
558 (add-to-list 'compilation-error-regexp-alist-alist
559 (cons 'gnat-inspect-ident-file-scope gnat-inspect-ident-file-scope-regexp-alist))
561 (unless (and (boundp 'ada-xref-tool)
562 (default-value 'ada-xref-tool))
563 (setq ada-xref-tool 'gnat_inspect))