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