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