go-guru.el (19630B)
1 ;;; go-guru.el --- Integration of the Go 'guru' analysis tool into Emacs. 2 3 ;; Copyright 2016 The Go Authors. All rights reserved. 4 ;; Use of this source code is governed by a BSD-style 5 ;; license that can be found in the LICENSE file. 6 7 ;; Version: 0.1 8 ;; Package-Requires: ((go-mode "1.3.1") (cl-lib "0.5")) 9 ;; Keywords: tools 10 11 ;;; Commentary: 12 13 ;; To enable the Go guru in Emacs, use this command to download, 14 ;; build, and install the tool in $GOROOT/bin: 15 ;; 16 ;; $ go get golang.org/x/tools/cmd/guru 17 ;; 18 ;; Verify that the tool is on your $PATH: 19 ;; 20 ;; $ guru -help 21 ;; Go source code guru. 22 ;; Usage: guru [flags] <mode> <position> 23 ;; ... 24 ;; 25 ;; Then copy this file to a directory on your `load-path', 26 ;; and add this to your ~/.emacs: 27 ;; 28 ;; (require 'go-guru) 29 ;; 30 ;; Inside a buffer of Go source code, select an expression of 31 ;; interest, and type `C-c C-o d' (for "describe") or run one of the 32 ;; other go-guru-xxx commands. If you use `menu-bar-mode', these 33 ;; commands are available from the Guru menu. 34 ;; 35 ;; To enable identifier highlighting mode in a Go source buffer, use: 36 ;; 37 ;; (go-guru-hl-identifier-mode) 38 ;; 39 ;; To enable it automatically in all Go source buffers, 40 ;; add this to your ~/.emacs: 41 ;; 42 ;; (add-hook 'go-mode-hook #'go-guru-hl-identifier-mode) 43 ;; 44 ;; See http://golang.org/s/using-guru for more information about guru. 45 46 ;;; Code: 47 48 (require 'compile) 49 (require 'easymenu) 50 (require 'go-mode) 51 (require 'json) 52 (require 'simple) 53 (require 'cl-lib) 54 55 (defgroup go-guru nil 56 "Options specific to the Go guru." 57 :group 'go) 58 59 (defcustom go-guru-command "guru" 60 "The Go guru command." 61 :type 'string 62 :group 'go-guru) 63 64 (defcustom go-guru-scope "" 65 "The scope of the analysis. See `go-guru-set-scope'." 66 :type 'string 67 :group 'go-guru) 68 69 (defvar go-guru--scope-history 70 nil 71 "History of values supplied to `go-guru-set-scope'.") 72 73 (defcustom go-guru-build-tags '() 74 "Build tags passed to guru." 75 :type '(repeat string) 76 :group 'go-guru) 77 78 (defface go-guru-hl-identifier-face 79 '((t (:inherit highlight))) 80 "Face used for highlighting identifiers in `go-guru-hl-identifier'." 81 :group 'go-guru) 82 83 (defcustom go-guru-debug nil 84 "Print debug messages when running guru." 85 :type 'boolean 86 :group 'go-guru) 87 88 (defcustom go-guru-hl-identifier-idle-time 0.5 89 "How long to wait after user input before highlighting the current identifier." 90 :type 'float 91 :group 'go-guru) 92 93 (defvar go-guru--current-hl-identifier-idle-time 94 0 95 "The current delay for hl-identifier-mode.") 96 97 (defvar go-guru--hl-identifier-timer 98 nil 99 "The global timer used for highlighting identifiers.") 100 101 (defvar go-guru--last-enclosing 102 nil 103 "The remaining enclosing regions of the previous go-expand-region invocation.") 104 105 ;; Extend go-mode-map. 106 (let ((m (define-prefix-command 'go-guru-map))) 107 (define-key m "d" #'go-guru-describe) 108 (define-key m "f" #'go-guru-freevars) 109 (define-key m "i" #'go-guru-implements) 110 (define-key m "c" #'go-guru-peers) ; c for channel 111 (define-key m "r" #'go-guru-referrers) 112 (define-key m "j" #'go-guru-definition) ; j for jump 113 (define-key m "p" #'go-guru-pointsto) 114 (define-key m "s" #'go-guru-callstack) ; s for stack 115 (define-key m "e" #'go-guru-whicherrs) ; e for error 116 (define-key m "<" #'go-guru-callers) 117 (define-key m ">" #'go-guru-callees) 118 (define-key m "x" #'go-guru-expand-region)) ;; x for expand 119 120 (define-key go-mode-map (kbd "C-c C-o") 'go-guru-map) 121 122 (easy-menu-define go-guru-mode-menu go-mode-map 123 "Menu for Go Guru." 124 '("Guru" 125 ["Jump to Definition" go-guru-definition t] 126 ["Show Referrers" go-guru-referrers t] 127 ["Show Free Names" go-guru-freevars t] 128 ["Describe Expression" go-guru-describe t] 129 ["Show Implements" go-guru-implements t] 130 "---" 131 ["Show Callers" go-guru-callers t] 132 ["Show Callees" go-guru-callees t] 133 ["Show Callstack" go-guru-callstack t] 134 "---" 135 ["Show Points-To" go-guru-pointsto t] 136 ["Show Which Errors" go-guru-whicherrs t] 137 ["Show Channel Peers" go-guru-peers t] 138 "---" 139 ["Set pointer analysis scope..." go-guru-set-scope t])) 140 141 (defun go-guru--read-scope () 142 "Read go-guru-scope from the minibuffer." 143 (completing-read-multiple "guru-scope (comma-separated): " 144 (go-packages) nil nil nil 'go-guru--scope-history)) 145 146 (eval-when-compile (require 'subr-x)) 147 148 ;;;###autoload 149 (defun go-guru-set-scope () 150 "Set the scope for the Go guru, prompting the user to edit the previous scope. 151 152 The scope restricts analysis to the specified packages. 153 Its value is a comma-separated list of patterns of these forms: 154 golang.org/x/tools/cmd/guru # a single package 155 golang.org/x/tools/... # all packages beneath dir 156 ... # the entire workspace. 157 158 A pattern preceded by '-' is negative, so the scope 159 encoding/...,-encoding/xml 160 matches all encoding packages except encoding/xml." 161 (interactive) 162 (let ((scope (go-guru--read-scope))) 163 (setq go-guru-scope (string-join scope ",")))) 164 165 (defun go-guru--set-scope-if-empty () 166 (if (string-equal "" go-guru-scope) 167 (go-guru-set-scope))) 168 169 (defun go-guru--json (mode) 170 "Execute the Go guru in the specified MODE, passing it the 171 selected region of the current buffer, requesting JSON output. 172 Parse and return the resulting JSON object." 173 ;; A "what" query works even in a buffer without a file name. 174 (let* ((filename (file-truename (or buffer-file-name "synthetic.go"))) 175 (cmd (go-guru--command mode filename '("-json"))) 176 (buf (current-buffer)) 177 ;; Use temporary buffers to avoid conflict with go-guru--start. 178 (json-buffer (generate-new-buffer "*go-guru-json-output*")) 179 (input-buffer (generate-new-buffer "*go-guru-json-input*"))) 180 (unwind-protect 181 ;; Run guru, feeding it the input buffer (modified files). 182 (with-current-buffer input-buffer 183 (go-guru--insert-modified-files) 184 (unless (buffer-file-name buf) 185 (go-guru--insert-modified-file filename buf)) 186 (let ((exitcode (apply #'call-process-region 187 (append (list (point-min) 188 (point-max) 189 (car cmd) ; guru 190 nil ; delete 191 json-buffer ; output 192 nil) ; display 193 (cdr cmd))))) ; args 194 (with-current-buffer json-buffer 195 (unless (zerop exitcode) 196 ;; Failed: use buffer contents (sans final \n) as an error. 197 (error "%s" (buffer-substring (point-min) (1- (point-max))))) 198 ;; Success: parse JSON. 199 (goto-char (point-min)) 200 (json-read)))) 201 ;; Clean up temporary buffers. 202 (kill-buffer json-buffer) 203 (kill-buffer input-buffer)))) 204 205 (define-compilation-mode go-guru-output-mode "Go guru" 206 "Go guru output mode is a variant of `compilation-mode' for the 207 output of the Go guru tool." 208 (set (make-local-variable 'compilation-error-screen-columns) nil) 209 (set (make-local-variable 'compilation-filter-hook) #'go-guru--compilation-filter-hook) 210 (set (make-local-variable 'compilation-start-hook) #'go-guru--compilation-start-hook)) 211 212 (defun go-guru--compilation-filter-hook () 213 "Post-process a blob of input to the go-guru-output buffer." 214 ;; For readability, truncate each "file:line:col:" prefix to a fixed width. 215 ;; If the prefix is longer than 20, show "…/last/19chars.go". 216 ;; This usually includes the last segment of the package name. 217 ;; Hide the line and column numbers. 218 (let ((start compilation-filter-start) 219 (end (point))) 220 (goto-char start) 221 (unless (bolp) 222 ;; TODO(adonovan): not quite right: the filter may be called 223 ;; with chunks of output containing incomplete lines. Moving to 224 ;; beginning-of-line may cause duplicate post-processing. 225 (beginning-of-line)) 226 (setq start (point)) 227 (while (< start end) 228 (let ((p (search-forward ": " end t))) 229 (if (null p) 230 (setq start end) ; break out of loop 231 (setq p (1- p)) ; exclude final space 232 (let* ((posn (buffer-substring-no-properties start p)) 233 (flen (cl-search ":" posn)) ; length of filename 234 (filename (if (< flen 19) 235 (substring posn 0 flen) 236 (concat "…" (substring posn (- flen 19) flen))))) 237 (put-text-property start p 'display filename) 238 (forward-line 1) 239 (setq start (point)))))))) 240 241 (defun go-guru--compilation-start-hook (proc) 242 "Erase default output header inserted by `compilation-mode'." 243 (with-current-buffer (process-buffer proc) 244 (let ((inhibit-read-only t)) 245 (goto-char (point-min)) 246 (delete-region (point) (point-max))))) 247 248 (defun go-guru--start (mode) 249 "Start an asynchronous Go guru process for the specified query 250 MODE, passing it the selected region of the current buffer, and 251 feeding its standard input with the contents of all modified Go 252 buffers. Its output is handled by `go-guru-output-mode', a 253 variant of `compilation-mode'." 254 (or buffer-file-name 255 (error "Cannot use guru on a buffer without a file name")) 256 (let* ((filename (file-truename buffer-file-name)) 257 (cmd (mapconcat #'shell-quote-argument (go-guru--command mode filename) " ")) 258 (process-connection-type nil) ; use pipe (not pty) so EOF closes stdin 259 (procbuf (compilation-start cmd 'go-guru-output-mode))) 260 (with-current-buffer procbuf 261 (setq truncate-lines t)) ; the output is neater without line wrapping 262 (with-current-buffer (get-buffer-create "*go-guru-input*") 263 (erase-buffer) 264 (go-guru--insert-modified-files) 265 (process-send-region procbuf (point-min) (point-max)) 266 (process-send-eof procbuf)) 267 procbuf)) 268 269 (defun go-guru--command (mode filename &optional flags) 270 "Return a command and argument list for a Go guru query of MODE, passing it 271 the selected region of the current buffer. FILENAME is the 272 effective name of the current buffer." 273 (let* ((posn (if (use-region-p) 274 (format "%s:#%d,#%d" 275 filename 276 (1- (position-bytes (region-beginning))) 277 (1- (position-bytes (region-end)))) 278 (format "%s:#%d" 279 filename 280 (1- (position-bytes (point)))))) 281 (cmd (append (list go-guru-command 282 "-modified" 283 "-scope" go-guru-scope 284 (format "-tags=%s" (mapconcat 'identity go-guru-build-tags " "))) 285 flags 286 (list mode 287 posn)))) 288 ;; Log the command to *Messages*, for debugging. 289 (when go-guru-debug 290 (message "go-guru--command: %s" cmd) 291 (message nil)) ; clear/shrink minibuffer 292 cmd)) 293 294 (defun go-guru--insert-modified-files () 295 "Insert the contents of each modified Go buffer into the 296 current buffer in the format specified by guru's -modified flag." 297 (mapc #'(lambda (b) 298 (and (buffer-modified-p b) 299 (buffer-file-name b) 300 (string= (file-name-extension (buffer-file-name b)) "go") 301 (go-guru--insert-modified-file (buffer-file-name b) b))) 302 (buffer-list))) 303 304 (defun go-guru--insert-modified-file (name buffer) 305 (insert (format "%s\n%d\n" name (go-guru--buffer-size-bytes buffer))) 306 (insert-buffer-substring buffer)) 307 308 (defun go-guru--buffer-size-bytes (&optional buffer) 309 "Return the number of bytes in the current buffer. 310 If BUFFER, return the number of characters in that buffer instead." 311 (with-current-buffer (or buffer (current-buffer)) 312 (string-bytes (buffer-substring (point-min) 313 (point-max))))) 314 315 (defun go-guru--goto-byte (offset) 316 "Go to the OFFSETth byte in the buffer." 317 (goto-char (byte-to-position offset))) 318 319 (defun go-guru--goto-byte-column (offset) 320 "Go to the OFFSETth byte in the current line." 321 (goto-char (byte-to-position (+ (position-bytes (point-at-bol)) (1- offset))))) 322 323 (defun go-guru--goto-pos (posn other-window) 324 "Find the file containing the position POSN (of the form `file:line:col') 325 set the point to it, switching the current buffer." 326 (let ((file-line-pos (split-string posn ":"))) 327 (funcall (if other-window #'find-file-other-window #'find-file) (car file-line-pos)) 328 (goto-char (point-min)) 329 (forward-line (1- (string-to-number (cadr file-line-pos)))) 330 (go-guru--goto-byte-column (string-to-number (cl-caddr file-line-pos))))) 331 332 (defun go-guru--goto-pos-no-file (posn) 333 "Given `file:line:col', go to the line and column. The file 334 component will be ignored." 335 (let ((file-line-pos (split-string posn ":"))) 336 (goto-char (point-min)) 337 (forward-line (1- (string-to-number (cadr file-line-pos)))) 338 (go-guru--goto-byte-column (string-to-number (cl-caddr file-line-pos))))) 339 340 ;;;###autoload 341 (defun go-guru-callees () 342 "Show possible callees of the function call at the current point." 343 (interactive) 344 (go-guru--set-scope-if-empty) 345 (go-guru--start "callees")) 346 347 ;;;###autoload 348 (defun go-guru-callers () 349 "Show the set of callers of the function containing the current point." 350 (interactive) 351 (go-guru--set-scope-if-empty) 352 (go-guru--start "callers")) 353 354 ;;;###autoload 355 (defun go-guru-callstack () 356 "Show an arbitrary path from a root of the call graph to the 357 function containing the current point." 358 (interactive) 359 (go-guru--set-scope-if-empty) 360 (go-guru--start "callstack")) 361 362 ;;;###autoload 363 (defun go-guru-definition (&optional other-window) 364 "Jump to the definition of the selected identifier." 365 (interactive) 366 (or buffer-file-name 367 (error "Cannot use guru on a buffer without a file name")) 368 (let* ((res (go-guru--json "definition")) 369 (desc (cdr (assoc 'desc res)))) 370 (push-mark) 371 (if (eval-when-compile (fboundp 'xref-push-marker-stack)) 372 ;; TODO: Integrate this facility with XRef. 373 (xref-push-marker-stack) 374 (ring-insert find-tag-marker-ring (point-marker))) 375 (go-guru--goto-pos (cdr (assoc 'objpos res)) other-window) 376 (message "%s" desc))) 377 378 ;;;###autoload 379 (defun go-guru-definition-other-window () 380 "Jump to the defintion of the selected identifier in another window" 381 (interactive) 382 (go-guru-definition t)) 383 384 ;;;###autoload 385 (defun go-guru-describe () 386 "Describe the selected syntax, its kind, type and methods." 387 (interactive) 388 (go-guru--start "describe")) 389 390 ;;;###autoload 391 (defun go-guru-pointsto () 392 "Show what the selected expression points to." 393 (interactive) 394 (go-guru--set-scope-if-empty) 395 (go-guru--start "pointsto")) 396 397 ;;;###autoload 398 (defun go-guru-implements () 399 "Describe the 'implements' relation for types in the package 400 containing the current point." 401 (interactive) 402 (go-guru--start "implements")) 403 404 ;;;###autoload 405 (defun go-guru-freevars () 406 "Enumerate the free variables of the current selection." 407 (interactive) 408 (go-guru--start "freevars")) 409 410 ;;;###autoload 411 (defun go-guru-peers () 412 "Enumerate the set of possible corresponding sends/receives for 413 this channel receive/send operation." 414 (interactive) 415 (go-guru--set-scope-if-empty) 416 (go-guru--start "peers")) 417 418 ;;;###autoload 419 (defun go-guru-referrers () 420 "Enumerate all references to the object denoted by the selected 421 identifier." 422 (interactive) 423 (go-guru--start "referrers")) 424 425 ;;;###autoload 426 (defun go-guru-whicherrs () 427 "Show globals, constants and types to which the selected 428 expression (of type 'error') may refer." 429 (interactive) 430 (go-guru--set-scope-if-empty) 431 (go-guru--start "whicherrs")) 432 433 (defun go-guru-what () 434 "Run a 'what' query and return the parsed JSON response as an 435 association list." 436 (go-guru--json "what")) 437 438 (defun go-guru--hl-symbols (posn face id) 439 "Highlight the symbols at the positions POSN by creating 440 overlays with face FACE. The attribute 'go-guru-overlay on the 441 overlays will be set to ID." 442 (save-excursion 443 (mapc (lambda (pos) 444 (go-guru--goto-pos-no-file pos) 445 (let ((x (make-overlay (point) (+ (point) (length (current-word)))))) 446 (overlay-put x 'go-guru-overlay id) 447 (overlay-put x 'face face))) 448 posn))) 449 450 ;;;###autoload 451 (defun go-guru-unhighlight-identifiers () 452 "Remove highlights from previously highlighted identifier." 453 (remove-overlays nil nil 'go-guru-overlay 'sameid)) 454 455 ;;;###autoload 456 (defun go-guru-hl-identifier () 457 "Highlight all instances of the identifier under point. Removes 458 highlights from previously highlighted identifier." 459 (interactive) 460 (go-guru-unhighlight-identifiers) 461 (go-guru--hl-identifier)) 462 463 ;;;###autoload 464 (define-minor-mode go-guru-hl-identifier-mode 465 "Highlight instances of the identifier at point after a short 466 timeout." 467 :group 'go-guru 468 (if go-guru-hl-identifier-mode 469 (progn 470 (go-guru--hl-set-timer) 471 ;; Unhighlight if point moves off identifier 472 (add-hook 'post-command-hook #'go-guru--hl-identifiers-post-command-hook nil t) 473 ;; Unhighlight any time the buffer changes 474 (add-hook 'before-change-functions #'go-guru--hl-identifiers-before-change-function nil t)) 475 (remove-hook 'post-command-hook #'go-guru--hl-identifiers-post-command-hook t) 476 (remove-hook 'before-change-functions #'go-guru--hl-identifiers-before-change-function t) 477 (go-guru-unhighlight-identifiers))) 478 479 (defun go-guru--hl-identifier () 480 "Highlight all instances of the identifier under point." 481 (let ((posn (cdr (assoc 'sameids (go-guru-what))))) 482 (go-guru--hl-symbols posn 'go-guru-hl-identifier-face 'sameid))) 483 484 (defun go-guru--hl-identifiers-function () 485 "Function run after an idle timeout, highlighting the 486 identifier at point, if necessary." 487 (when go-guru-hl-identifier-mode 488 (unless (go-guru--on-overlay-p 'sameid) 489 ;; Ignore guru errors. Otherwise, we might end up with an error 490 ;; every time the timer runs, e.g. because of a malformed 491 ;; buffer. 492 (condition-case nil 493 (go-guru-hl-identifier) 494 (error nil))) 495 (unless (eq go-guru--current-hl-identifier-idle-time go-guru-hl-identifier-idle-time) 496 (go-guru--hl-set-timer)))) 497 498 (defun go-guru--hl-set-timer () 499 (if go-guru--hl-identifier-timer 500 (cancel-timer go-guru--hl-identifier-timer)) 501 (setq go-guru--current-hl-identifier-idle-time go-guru-hl-identifier-idle-time) 502 (setq go-guru--hl-identifier-timer (run-with-idle-timer 503 go-guru-hl-identifier-idle-time 504 t 505 #'go-guru--hl-identifiers-function))) 506 507 (defun go-guru--on-overlay-p (id) 508 "Return whether point is on a guru overlay of type ID." 509 (cl-find-if (lambda (el) (eq (overlay-get el 'go-guru-overlay) id)) (overlays-at (point)))) 510 511 (defun go-guru--hl-identifiers-post-command-hook () 512 (if (and go-guru-hl-identifier-mode 513 (not (go-guru--on-overlay-p 'sameid))) 514 (go-guru-unhighlight-identifiers))) 515 516 (defun go-guru--hl-identifiers-before-change-function (_beg _end) 517 (go-guru-unhighlight-identifiers)) 518 519 ;; TODO(dominikh): a future feature may be to cycle through all uses 520 ;; of an identifier. 521 522 (defun go-guru--enclosing () 523 "Return a list of enclosing regions." 524 (cdr (assoc 'enclosing (go-guru-what)))) 525 526 (defun go-guru--enclosing-unique () 527 "Return a list of enclosing regions, with duplicates removed. 528 Two regions are considered equal if they have the same start and 529 end point." 530 (let ((enclosing (go-guru--enclosing))) 531 (cl-remove-duplicates enclosing 532 :from-end t 533 :test (lambda (a b) 534 (and (= (cdr (assoc 'start a)) 535 (cdr (assoc 'start b))) 536 (= (cdr (assoc 'end a)) 537 (cdr (assoc 'end b)))))))) 538 539 (defun go-guru-expand-region () 540 "Expand region to the next enclosing syntactic unit." 541 (interactive) 542 (let* ((enclosing (if (eq last-command #'go-guru-expand-region) 543 go-guru--last-enclosing 544 (go-guru--enclosing-unique))) 545 (block (if (> (length enclosing) 0) (elt enclosing 0)))) 546 (when block 547 (go-guru--goto-byte (1+ (cdr (assoc 'start block)))) 548 (set-mark (byte-to-position (1+ (cdr (assoc 'end block))))) 549 (setq go-guru--last-enclosing (cl-subseq enclosing 1)) 550 (message "Region: %s" (cdr (assoc 'desc block))) 551 (setq deactivate-mark nil)))) 552 553 554 (provide 'go-guru) 555 556 ;; Local Variables: 557 ;; indent-tabs-mode: t 558 ;; tab-width: 8 559 ;; End: 560 561 ;;; go-guru.el ends here