]> code.delx.au - gnu-emacs-elpa/blob - packages/ada-mode/gnat-inspect.el
055f942f19f60799f9ee6f5c3ebd0e821997cac6
[gnu-emacs-elpa] / packages / ada-mode / gnat-inspect.el
1 ;;; gnat-inspect.el --- minor-mode for navigating sources using the
2 ;;; AdaCore cross reference tool gnatinspect.
3 ;;;
4 ;;; gnatinspect supports Ada and any gcc language that supports the
5 ;;; -fdump-xref switch (which includes C, C++).
6 ;;
7 ;;; Copyright (C) 2013 Free Software Foundation, Inc.
8
9 ;; Author: Stephen Leake <stephen_leake@member.fsf.org>
10 ;; Maintainer: Stephen Leake <stephen_leake@member.fsf.org>
11 ;; Version: 1.0
12
13 ;; This file is part of GNU Emacs.
14
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.
19
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.
24
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/>.
27
28 ;;; Usage:
29 ;;
30 ;; M-x gnat-inspect
31
32 (require 'compile)
33 (require 'ada-mode) ;; for ada-prj-*, some other things
34 (eval-when-compile (require 'cl-macs))
35
36 ;;;;; sessions
37
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
42 ;; stdout.
43 ;;
44 ;; We maintain a cache of active sessions, one per gnat project.
45
46 (cl-defstruct (gnat-inspect--session)
47 (process nil) ;; running gnatinspect
48 (buffer nil) ;; receives output of gnatinspect
49 (sent-kill-p nil)
50 (closed-p nil))
51
52 (defconst gnat-inspect-buffer-name-prefix " *gnatinspect-")
53
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)))
59
60 (with-current-buffer (gnat-inspect--session-buffer session)
61 (let ((process-environment (ada-prj-get 'proc_env)) ;; for GPR_PROJECT_PATH
62
63 ;; WORKAROUND: gnatinspect from gnatcoll-1.6w-20130902 can't handle aggregate projects; M910-032
64 (project-file (file-name-nondirectory
65 (or (ada-prj-get 'gnat_inspect_gpr_file)
66 (ada-prj-get 'gpr_file)))))
67 (erase-buffer); delete any previous messages, prompt
68 (setf (gnat-inspect--session-process session)
69 ;; FIXME: need good error message on bad project file:
70 ;; "can't handle aggregate projects? - set gnat_inspect_gpr_file")
71 (start-process (concat "gnatinspect " (buffer-name))
72 (gnat-inspect--session-buffer session)
73 "gnatinspect"
74 (concat "--project=" project-file)))
75 (set-process-query-on-exit-flag (gnat-inspect--session-process session) nil)
76 (gnat-inspect-session-wait session)
77 )))
78
79 (defun gnat-inspect--make-session ()
80 "Create and return a session for the current project file."
81 (let ((session
82 (make-gnat-inspect--session
83 :buffer (gnat-run-buffer gnat-inspect-buffer-name-prefix))))
84 (gnat-inspect--start-process session)
85 session))
86
87 (defvar gnat-inspect--sessions '()
88 "Assoc list of sessions, indexed by absolute GNAT project file name.")
89
90 (defun gnat-inspect-cached-session ()
91 "Return a session for the current project file, creating it if necessary."
92 (let* ((session (cdr (assoc ada-prj-current-file gnat-inspect--sessions))))
93 (if session
94 (progn
95 (unless (process-live-p (gnat-inspect--session-process session))
96 (gnat-inspect--start-process session))
97 session)
98 ;; else
99 (prog1
100 (setq session (gnat-inspect--make-session))
101 (setq gnat-inspect--sessions
102 (cl-acons ada-prj-current-file session gnat-inspect--sessions))))
103 ))
104
105 (defconst gnat-inspect-prompt "^>>> $"
106 ;; gnatinspect output ends with this
107 "Regexp matching gnatinspect prompt; indicates previous command is complete.")
108
109 (defun gnat-inspect-session-wait (session)
110 "Wait for the current command to complete."
111 (with-current-buffer (gnat-inspect--session-buffer session)
112 (let ((process (gnat-inspect--session-process session))
113 (search-start (point-min))
114 (wait-count 0))
115 (while (progn
116 ;; process output is inserted before point, so move back over it to search it
117 (goto-char search-start)
118 (not (re-search-forward gnat-inspect-prompt (point-max) 1)));; don't search same text again
119 (setq search-start (point))
120 (message (concat "running gnatinspect ..." (make-string wait-count ?.)))
121 (accept-process-output process 1.0)
122 (setq wait-count (1+ wait-count)))
123 (message (concat "running gnatinspect ... done"))
124 )))
125
126 (defun gnat-inspect-session-send (cmd wait)
127 "Send CMD to gnatinspect session for current project.
128 If WAIT is non-nil, wait for command to complete.
129 Return buffer that holds output."
130 (let ((session (gnat-inspect-cached-session)))
131 (with-current-buffer (gnat-inspect--session-buffer session)
132 (erase-buffer)
133 (process-send-string (gnat-inspect--session-process session)
134 (concat cmd "\n"))
135 (when wait
136 (gnat-inspect-session-wait session))
137 (current-buffer)
138 )))
139
140 (defun gnat-inspect-session-kill (session)
141 (when (process-live-p (gnat-inspect--session-process session))
142 (process-send-string (gnat-inspect--session-process session) "exit\n")))
143
144 (defun gnat-inspect-kill-all-sessions ()
145 (interactive)
146 (mapc (lambda (assoc) (gnat-inspect-session-kill (cdr assoc))) gnat-inspect--sessions))
147
148 ;;;;; utils
149
150 (defconst gnat-inspect-ident-file-regexp
151 ;; Write_Message:C:\Projects\GDS\work_dscovr_release\common\1553\gds-mil_std_1553-utf.ads:252:25
152 ;; Write_Message:/Projects/GDS/work_dscovr_release/common/1553/gds-mil_std_1553-utf.ads:252:25
153 "\\([^:]*\\):\\(\\(?:.:\\\|/\\)[^:]*\\):\\([0123456789]+\\):\\([0123456789]+\\)"
154 "Regexp matching <identifier>:<file>:<line>:<column>")
155
156 (defconst gnat-inspect-ident-file-regexp-alist
157 (list (concat "^" gnat-inspect-ident-file-regexp) 2 3 4)
158 "For compilation-error-regexp-alist, matching `gnatinspect overriding_recursive' output")
159
160 (defconst gnat-inspect-ident-file-type-regexp
161 (concat gnat-inspect-ident-file-regexp " (\\(.*\\))")
162 "Regexp matching <identifier>:<file>:<line>:<column> (<type>)")
163
164 (defconst gnat-inspect-ident-file-scope-regexp-alist
165 ;; 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
166
167 (list (concat
168 gnat-inspect-ident-file-regexp
169 " (.*) "
170 "scope="
171 gnat-inspect-ident-file-regexp
172 )
173 2 3 4;; file line column
174 ;; 2 ;; type = error
175 ;; nil ;; hyperlink
176 ;; (list 4 'gnat-inspect-scope-secondary-error)
177 )
178 "For compilation-error-regexp-alist, matching `gnatinspect refs' output")
179
180 ;; debugging:
181 ;; in *compilation-gnatinspect-refs*, run
182 ;; (progn (set-text-properties (point-min)(point-max) nil)(compilation-parse-errors (point-min)(point-max) gnat-inspect-ident-file-scope-regexp-alist))
183
184 (defun gnat-inspect-compilation (identifier file line col cmd comp-err)
185 "Run gnatinspect IDENTIFIER:FILE:LINE:COL CMD,
186 set compilation-mode with compilation-error-regexp-alist set to COMP-ERR."
187 (let ((cmd-1 (format "%s %s:%s:%d:%d" cmd identifier file line col))
188 (result-count 0)
189 file line column)
190 (with-current-buffer (gnat-inspect--session-buffer (gnat-inspect-cached-session))
191 (compilation-mode)
192 (setq buffer-read-only nil)
193 (set (make-local-variable 'compilation-error-regexp-alist) (list comp-err))
194 (gnat-inspect-session-send cmd-1 t)
195 ;; at EOB. gnatinspect returns one line per result
196 (setq result-count (- (line-number-at-pos) 1))
197 (font-lock-fontify-buffer)
198 ;; font-lock-fontify-buffer applies compilation-message text properties
199 ;; IMPROVEME: for some reason, next-error works, but the font
200 ;; colors are not right (no koolaid!)
201 (goto-char (point-min))
202
203 (cl-case result-count
204 (0
205 (error "gnatinspect returned no results"))
206 (1
207 ;; just go there, don't display session-buffer. We have to
208 ;; fetch the compilation-message while in the session-buffer.
209 (let* ((msg (compilation-next-error 0 nil (point-min)))
210 (loc (compilation--message->loc msg)))
211 (setq file (caar (compilation--loc->file-struct loc))
212 line (caar (cddr (compilation--loc->file-struct loc)))
213 column (1- (compilation--loc->col loc)))
214 ))
215
216 ));; case, with-currrent-buffer
217
218 ;; compilation-next-error-function assumes there is not at error
219 ;; at point-min; work around that by moving forward 0 errors for
220 ;; the first one.
221 (if (> result-count 1)
222 ;; more than one result; display session buffer
223 (next-error 0 t)
224 ;; else don't display
225 (ada-goto-source file line column nil))
226 ))
227
228 (defun gnat-inspect-dist (found-line line found-col col)
229 "Return non-nil if found-line, -col is closer to line, col than min-distance."
230 (+ (abs (- found-line line))
231 (* (abs (- found-col col)) 250)))
232
233 ;;;;; user interface functions
234
235 (defun gnat-inspect-refresh ()
236 "For `ada-xref-refresh-function', using gnatinspect."
237 (with-current-buffer (gnat-inspect-session-send "refresh" t)))
238
239 (defun gnat-inspect-other (identifier file line col)
240 "For `ada-xref-other-function', using gnatinspect."
241 (unless (ada-prj-get 'gpr_file)
242 (error "no gnat project file defined."))
243
244 (when (eq ?\" (aref identifier 0))
245 ;; gnatinspect wants the quotes stripped
246 (setq col (+ 1 col))
247 (setq identifier (substring identifier 1 (1- (length identifier))))
248 )
249
250 (let ((cmd (format "refs %s:%s:%d:%d" identifier (file-name-nondirectory file) line col))
251 (decl-loc nil)
252 (body-loc nil)
253 (search-type nil)
254 (min-distance (1- (expt 2 29)))
255 (result nil))
256
257 (with-current-buffer (gnat-inspect-session-send cmd t)
258 ;; 'gnatinspect refs' returns a list containing the declaration,
259 ;; the body, and all the references, in no particular order.
260 ;;
261 ;; We search the list, looking for the input location,
262 ;; declaration and body, then return the declaration or body as
263 ;; appropriate.
264 ;;
265 ;; the format of each line is name:file:line:column (type) scope=name:file:line:column
266 ;; 1 2 3 4 5
267 ;;
268 ;; 'type' can be:
269 ;; body
270 ;; declaration
271 ;; full declaration (for a private type)
272 ;; implicit reference
273 ;; reference
274 ;; static call
275 ;;
276 ;; 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
277 ;;
278 ;; 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
279
280 (message "parsing result ...")
281
282 (goto-char (point-min))
283
284 (while (not (eobp))
285 (cond
286 ((looking-at gnat-inspect-ident-file-type-regexp)
287 ;; process line
288 (let* ((found-file (file-name-nondirectory (match-string 2)))
289 (found-line (string-to-number (match-string 3)))
290 (found-col (string-to-number (match-string 4)))
291 (found-type (match-string 5))
292 (dist (gnat-inspect-dist found-line line found-col col))
293 )
294
295 (when (string-equal found-type "declaration")
296 (setq decl-loc (list found-file found-line (1- found-col))))
297
298 (when (or
299 (string-equal found-type "body")
300 (string-equal found-type "full declaration"))
301 (setq body-loc (list found-file found-line (1- found-col))))
302
303 (when
304 ;; In general, we don't know where in the gnatinspect
305 ;; output the search item occurs, so we search for it.
306 ;;
307 ;; We use the same distance algorithm as gnatinspect
308 ;; to allow a fuzzy match on edited code.
309 (and (equal found-file file)
310 (< dist min-distance))
311 (setq min-distance dist)
312 (setq search-type found-type))
313 ))
314
315 (t ;; ignore line
316 ;;
317 ;; This skips GPR_PROJECT_PATH and echoed command at start of buffer.
318 ;;
319 ;; It also skips warning lines. For example,
320 ;; gnatcoll-1.6w-20130902 can't handle the Auto_Text_IO
321 ;; language, because it doesn't use the gprconfig
322 ;; configuration project. That gives lines like:
323 ;;
324 ;; common_text_io.gpr:15:07: language unknown for "gds-hardware-bus_1553-time_tone.ads"
325 ;;
326 ;; There are probably other warnings that might be reported as well.
327 )
328 )
329 (forward-line 1)
330 )
331
332 (cond
333 ((null search-type)
334 (pop-to-buffer (current-buffer))
335 (error "gnatinspect did not return other item"))
336
337 ((and
338 (string-equal search-type "declaration")
339 body-loc)
340 (setq result body-loc))
341
342 (decl-loc
343 (setq result decl-loc))
344 )
345
346 (when (null result)
347 (pop-to-buffer (current-buffer))
348 (error "gnatinspect did not return other item"))
349
350 (message "parsing result ... done")
351 result)))
352
353 (defun gnat-inspect-all (identifier file line col)
354 "For `ada-xref-all-function', using gnatinspect."
355 ;; This will in general return a list of references, so we use
356 ;; `compilation-start' to run gnatinspect, so the user can navigate
357 ;; to each result in turn via `next-error'.
358 (gnat-inspect-compilation identifier file line col "refs" 'gnat-inspect-ident-file))
359
360 (defun gnat-inspect-parents (identifier file line col)
361 "For `ada-xref-parent-function', using gnatinspect."
362 (gnat-inspect-compilation identifier file line col "parent_types" 'gnat-inspect-ident-file))
363
364 (defun gnat-inspect-overriding (identifier file line col)
365 "For `ada-xref-overriding-function', using gnatinspect."
366 (gnat-inspect-compilation identifier file line col "overridden_recursive" 'gnat-inspect-ident-file))
367
368 (defun gnat-inspect-overridden-1 (identifier file line col)
369 "For `ada-xref-overridden-function', using gnatinspect."
370 (unless (or (ada-prj-get 'gnat_inspect_gpr_file)
371 (ada-prj-get 'gpr_file))
372 (error "no gnat project file defined."))
373
374 (when (eq ?\" (aref identifier 0))
375 ;; gnatinspect wants the quotes stripped
376 (setq col (+ 1 col))
377 (setq identifier (substring identifier 1 (1- (length identifier))))
378 )
379
380 (let ((cmd (format "overrides %s:%s:%d:%d" identifier (file-name-nondirectory file) line col))
381 result)
382 (with-current-buffer (gnat-inspect-session-send cmd t)
383
384 (goto-char (point-min))
385 (when (looking-at gnat-inspect-ident-file-regexp)
386 (setq result
387 (list
388 (match-string 2)
389 (string-to-number (match-string 3))
390 (string-to-number (match-string 4)))))
391
392 (when (null result)
393 (pop-to-buffer (current-buffer))
394 (error "gnatinspect did not return other item"))
395
396 (message "parsing result ... done")
397 result)))
398
399 (defun gnat-inspect-overridden (other-window)
400 "Move to the overridden declaration of the identifier around point.
401 If OTHER-WINDOW (set by interactive prefix) is non-nil, show the
402 buffer in another window."
403 (interactive "P")
404
405 (let ((target
406 (gnat-inspect-overridden-1
407 (thing-at-point 'symbol)
408 (buffer-file-name)
409 (line-number-at-pos)
410 (save-excursion
411 (goto-char (car (bounds-of-thing-at-point 'symbol)))
412 (1+ (current-column)))
413 )))
414
415 (ada-goto-source (nth 0 target)
416 (nth 1 target)
417 (nth 2 target)
418 other-window)
419 ))
420
421 (defun gnat-inspect-goto-declaration (other-window)
422 "Move to the declaration or body of the identifier around point.
423 If at the declaration, go to the body, and vice versa. If at a
424 reference, goto the declaration.
425
426 If OTHER-WINDOW (set by interactive prefix) is non-nil, show the
427 buffer in another window."
428 (interactive "P")
429
430 (let ((target
431 (gnat-inspect-other
432 (thing-at-point 'symbol)
433 (buffer-file-name)
434 (line-number-at-pos)
435 (save-excursion
436 (goto-char (car (bounds-of-thing-at-point 'symbol)))
437 (1+ (current-column)))
438 )))
439
440 (ada-goto-source (nth 0 target)
441 (nth 1 target)
442 (nth 2 target)
443 other-window)
444 ))
445
446 (defvar gnat-inspect-map
447 (let ((map (make-sparse-keymap)))
448 ;; C-c <letter> are reserved for users
449
450 (define-key map "\C-c\C-d" 'gnat-inspect-goto-declaration)
451 ;; FIXME: (define-key map "\C-c\M-d" 'gnat-inspect-parents)
452 ;; FIXME: overriding
453 (define-key map "\C-c\C-r" 'gnat-inspect-all)
454 map
455 ) "Local keymap used for GNAT inspect minor mode.")
456
457 ;; FIXME: define menu
458
459 (define-minor-mode gnat-inspect
460 "Minor mode for navigating sources using GNAT cross reference tool.
461 Enable mode if ARG is positive"
462 :initial-value t
463 :lighter " gnat-inspect" ;; mode line
464
465 ;; just enable the menu and keymap
466 )
467
468 ;;;;; support for Ada mode
469
470 (defun ada-gnat-inspect-select-prj ()
471 (setq ada-file-name-from-ada-name 'ada-gnat-file-name-from-ada-name)
472 (setq ada-ada-name-from-file-name 'ada-gnat-ada-name-from-file-name)
473 (setq ada-make-package-body 'ada-gnat-make-package-body)
474
475 (add-hook 'ada-syntax-propertize-hook 'gnatprep-syntax-propertize)
476
477 ;; must be after indentation engine setup, because that resets the
478 ;; indent function list.
479 (add-hook 'ada-mode-hook 'ada-gnat-inspect-setup t)
480
481 (setq ada-xref-refresh-function 'gnat-inspect-refresh)
482 (setq ada-xref-all-function 'gnat-inspect-all)
483 (setq ada-xref-other-function 'gnat-inspect-other)
484 (setq ada-xref-parent-function 'gnat-inspect-parents)
485 (setq ada-xref-all-function 'gnat-inspect-all)
486 (setq ada-xref-overriding-function 'gnat-inspect-overriding)
487 (setq ada-xref-overridden-function 'gnat-inspect-overridden-1)
488
489 (add-to-list 'completion-ignored-extensions ".ali") ;; gnat library files, used for cross reference
490 )
491
492 (defun ada-gnat-inspect-deselect-prj ()
493 (setq ada-file-name-from-ada-name nil)
494 (setq ada-ada-name-from-file-name nil)
495 (setq ada-make-package-body nil)
496
497 (setq ada-syntax-propertize-hook (delq 'gnatprep-syntax-propertize ada-syntax-propertize-hook))
498 (setq ada-mode-hook (delq 'ada-gnat-inspect-setup ada-mode-hook))
499
500 (setq ada-xref-other-function nil)
501 (setq ada-xref-parent-function nil)
502 (setq ada-xref-all-function nil)
503 (setq ada-xref-overriding-function nil)
504 (setq ada-xref-overridden-function nil)
505
506 (setq completion-ignored-extensions (delete ".ali" completion-ignored-extensions))
507 )
508
509 (defun ada-gnat-inspect-setup ()
510 (when (boundp 'wisi-indent-calculate-functions)
511 (add-to-list 'wisi-indent-calculate-functions 'gnatprep-indent))
512 )
513
514 (defun ada-gnat-inspect ()
515 "Set Ada mode global vars to use gnatinspect."
516 (add-to-list 'ada-prj-parser-alist '("gpr" . gnat-parse-gpr))
517 (add-to-list 'ada-select-prj-xref-tool '(gnat_inspect . ada-gnat-inspect-select-prj))
518 (add-to-list 'ada-deselect-prj-xref-tool '(gnat_inspect . ada-gnat-inspect-deselect-prj))
519
520 ;; no parse-*-xref
521
522 (font-lock-add-keywords 'ada-mode
523 ;; gnatprep preprocessor line
524 (list (list "^[ \t]*\\(#.*\n\\)" '(1 font-lock-type-face t))))
525
526 (add-hook 'ada-gnat-fix-error-hook 'ada-gnat-fix-error)
527 )
528
529 (provide 'gnat-inspect)
530
531 (add-to-list 'compilation-error-regexp-alist-alist
532 (cons 'gnat-inspect-ident-file gnat-inspect-ident-file-regexp-alist))
533 (add-to-list 'compilation-error-regexp-alist-alist
534 (cons 'gnat-inspect-ident-file-scope gnat-inspect-ident-file-scope-regexp-alist))
535
536 (unless (and (boundp 'ada-xref-tool)
537 (default-value 'ada-xref-tool))
538 (setq ada-xref-tool 'gnat_inspect))
539
540 (ada-gnat-inspect)
541
542 ;;; end of file