230 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			EmacsLisp
		
	
	
	
			
		
		
	
	
			230 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			EmacsLisp
		
	
	
	
| ;;;
 | |
| ;;; Integration of the Go 'oracle' analysis tool into Emacs.
 | |
| ;;;
 | |
| ;;; To install the Go oracle, run:
 | |
| ;;; % export GOROOT=... GOPATH=...
 | |
| ;;; % go get golang.org/x/tools/cmd/oracle
 | |
| ;;; % mv $GOPATH/bin/oracle $GOROOT/bin/
 | |
| ;;;
 | |
| ;;; Load this file into Emacs and set go-oracle-scope to your
 | |
| ;;; configuration. Then, find a file of Go source code, enable
 | |
| ;;; go-oracle-mode, select an expression of interest, and press `C-c C-o d'
 | |
| ;;; (for "describe") or run one of the other go-oracle-xxx commands.
 | |
| ;;;
 | |
| ;;; TODO(adonovan): simplify installation and configuration by making
 | |
| ;;; oracle a subcommand of 'go tool'.
 | |
| 
 | |
| (require 'compile)
 | |
| (require 'go-mode)
 | |
| (require 'cl)
 | |
| 
 | |
| (defgroup go-oracle nil
 | |
|   "Options specific to the Go oracle."
 | |
|   :group 'go)
 | |
| 
 | |
| (defcustom go-oracle-command (concat (car (go-root-and-paths)) "/bin/oracle")
 | |
|   "The Go oracle command; the default is $GOROOT/bin/oracle."
 | |
|   :type 'string
 | |
|   :group 'go-oracle)
 | |
| 
 | |
| (defcustom go-oracle-scope ""
 | |
|   "The scope of the analysis.  See `go-oracle-set-scope'."
 | |
|   :type 'string
 | |
|   :group 'go-oracle)
 | |
| 
 | |
| (defvar go-oracle--scope-history
 | |
|   nil
 | |
|   "History of values supplied to `go-oracle-set-scope'.")
 | |
| 
 | |
| ;; TODO(adonovan): I'd like to get rid of this separate mode since it
 | |
| ;; makes it harder to use the oracle.
 | |
| (defvar go-oracle-mode-map
 | |
|   (let ((m (make-sparse-keymap)))
 | |
|     (define-key m (kbd "C-c C-o t") #'go-oracle-describe) ; t for type
 | |
|     (define-key m (kbd "C-c C-o f") #'go-oracle-freevars)
 | |
|     (define-key m (kbd "C-c C-o g") #'go-oracle-callgraph)
 | |
|     (define-key m (kbd "C-c C-o i") #'go-oracle-implements)
 | |
|     (define-key m (kbd "C-c C-o c") #'go-oracle-peers)  ; c for channel
 | |
|     (define-key m (kbd "C-c C-o r") #'go-oracle-referrers)
 | |
|     (define-key m (kbd "C-c C-o d") #'go-oracle-definition)
 | |
|     (define-key m (kbd "C-c C-o p") #'go-oracle-pointsto)
 | |
|     (define-key m (kbd "C-c C-o s") #'go-oracle-callstack)
 | |
|     (define-key m (kbd "C-c C-o <") #'go-oracle-callers)
 | |
|     (define-key m (kbd "C-c C-o >") #'go-oracle-callees)
 | |
|     m))
 | |
| 
 | |
| ;; TODO(dominikh): Rethink set-scope some. Setting it to a file is
 | |
| ;; painful because it doesn't use find-file, and variables/~ aren't
 | |
| ;; expanded. Setting it to an import path is somewhat painful because
 | |
| ;; it doesn't make use of go-mode's import path completion. One option
 | |
| ;; would be having two different functions, but then we can't
 | |
| ;; automatically call it when no scope has been set. Also it wouldn't
 | |
| ;; easily allow specifying more than one file/package.
 | |
| (defun go-oracle-set-scope ()
 | |
|   "Set the scope for the Go oracle, prompting the user to edit the
 | |
| previous scope.
 | |
| 
 | |
| The scope specifies a set of arguments, separated by spaces.
 | |
| It may be:
 | |
| 1) a set of packages whose main() functions will be analyzed.
 | |
| 2) a list of *.go filenames; they will treated like as a single
 | |
|    package (see #3).
 | |
| 3) a single package whose main() function and/or Test* functions
 | |
|    will be analyzed.
 | |
| 
 | |
| In the common case, this is similar to the argument(s) you would
 | |
| specify to 'go build'."
 | |
|   (interactive)
 | |
|   (let ((scope (read-from-minibuffer "Go oracle scope: "
 | |
|                                      go-oracle-scope
 | |
|                                      nil
 | |
|                                      nil
 | |
|                                      'go-oracle--scope-history)))
 | |
|     (if (string-equal "" scope)
 | |
|         (error "You must specify a non-empty scope for the Go oracle"))
 | |
|     (setq go-oracle-scope scope)))
 | |
| 
 | |
| (defun go-oracle--run (mode)
 | |
|   "Run the Go oracle in the specified MODE, passing it the
 | |
| selected region of the current buffer.  Process the output to
 | |
| replace each file name with a small hyperlink.  Display the
 | |
| result."
 | |
|   (if (not buffer-file-name)
 | |
|       (error "Cannot use oracle on a buffer without a file name"))
 | |
|   ;; It's not sufficient to save a modified buffer since if
 | |
|   ;; gofmt-before-save is on the before-save-hook, saving will
 | |
|   ;; disturb the selected region.
 | |
|   (if (buffer-modified-p)
 | |
|       (error "Please save the buffer before invoking go-oracle"))
 | |
|   (if (string-equal "" go-oracle-scope)
 | |
|       (go-oracle-set-scope))
 | |
|   (let* ((filename (file-truename buffer-file-name))
 | |
|          (posflag (if (use-region-p)
 | |
|                       (format "-pos=%s:#%d,#%d"
 | |
|                               filename
 | |
|                               (1- (go--position-bytes (region-beginning)))
 | |
|                               (1- (go--position-bytes (region-end))))
 | |
|                     (format "-pos=%s:#%d"
 | |
|                             filename
 | |
|                             (1- (position-bytes (point))))))
 | |
|          ;; This would be simpler if we could just run 'go tool oracle'.
 | |
|          (env-vars (go-root-and-paths))
 | |
|          (goroot-env (concat "GOROOT=" (car env-vars)))
 | |
|          (gopath-env (concat "GOPATH=" (mapconcat #'identity (cdr env-vars) ":"))))
 | |
|     (with-current-buffer (get-buffer-create "*go-oracle*")
 | |
|       (setq buffer-read-only nil)
 | |
|       (erase-buffer)
 | |
|       (insert "Go Oracle\n")
 | |
|       (let ((args (append (list go-oracle-command nil t nil posflag mode)
 | |
|                           (split-string go-oracle-scope " " t))))
 | |
|         ;; Log the command to *Messages*, for debugging.
 | |
|         (message "Command: %s:" args)
 | |
|         (message nil) ; clears/shrinks minibuffer
 | |
| 
 | |
|         (message "Running oracle...")
 | |
|         ;; Use dynamic binding to modify/restore the environment
 | |
|         (let ((process-environment (list* goroot-env gopath-env process-environment)))
 | |
|             (apply #'call-process args)))
 | |
|       (insert "\n")
 | |
|       (compilation-mode)
 | |
|       (setq compilation-error-screen-columns nil)
 | |
| 
 | |
|       ;; Hide the file/line info to save space.
 | |
|       ;; Replace each with a little widget.
 | |
|       ;; compilation-mode + this loop = slooow.
 | |
|       ;; TODO(adonovan): have oracle give us JSON
 | |
|       ;; and we'll do the markup directly.
 | |
|       (let ((buffer-read-only nil)
 | |
|             (p 1))
 | |
|         (while (not (null p))
 | |
|           (let ((np (compilation-next-single-property-change p 'compilation-message)))
 | |
|             ;; TODO(adonovan): this can be verbose in the *Messages* buffer.
 | |
|             ;; (message "Post-processing link (%d%%)" (/ (* p 100) (point-max)))
 | |
|             (if np
 | |
|                 (when (equal (line-number-at-pos p) (line-number-at-pos np))
 | |
|                   ;; np is (typically) the space following ":"; consume it too.
 | |
|                   (put-text-property p np 'display "▶")
 | |
|                   (goto-char np)
 | |
|                   (insert " ")))
 | |
|             (setq p np)))
 | |
|         (message nil))
 | |
| 
 | |
|       (let ((w (display-buffer (current-buffer))))
 | |
|         (balance-windows)
 | |
|         (shrink-window-if-larger-than-buffer w)
 | |
|         (set-window-point w (point-min))))))
 | |
| 
 | |
| (defun go-oracle-callees ()
 | |
|   "Show possible callees of the function call at the current point."
 | |
|   (interactive)
 | |
|   (go-oracle--run "callees"))
 | |
| 
 | |
| (defun go-oracle-callers ()
 | |
|   "Show the set of callers of the function containing the current point."
 | |
|   (interactive)
 | |
|   (go-oracle--run "callers"))
 | |
| 
 | |
| (defun go-oracle-callgraph ()
 | |
|   "Show the callgraph of the current program."
 | |
|   (interactive)
 | |
|   (go-oracle--run "callgraph"))
 | |
| 
 | |
| (defun go-oracle-callstack ()
 | |
|   "Show an arbitrary path from a root of the call graph to the
 | |
| function containing the current point."
 | |
|   (interactive)
 | |
|   (go-oracle--run "callstack"))
 | |
| 
 | |
| (defun go-oracle-definition ()
 | |
|   "Show the definition of the selected identifier."
 | |
|   (interactive)
 | |
|   (go-oracle--run "definition"))
 | |
| 
 | |
| (defun go-oracle-describe ()
 | |
|   "Describe the selected syntax, its kind, type and methods."
 | |
|   (interactive)
 | |
|   (go-oracle--run "describe"))
 | |
| 
 | |
| (defun go-oracle-pointsto ()
 | |
|   "Show what the selected expression points to."
 | |
|   (interactive)
 | |
|   (go-oracle--run "pointsto"))
 | |
| 
 | |
| (defun go-oracle-implements ()
 | |
|   "Describe the 'implements' relation for types in the package
 | |
| containing the current point."
 | |
|   (interactive)
 | |
|   (go-oracle--run "implements"))
 | |
| 
 | |
| (defun go-oracle-freevars ()
 | |
|   "Enumerate the free variables of the current selection."
 | |
|   (interactive)
 | |
|   (go-oracle--run "freevars"))
 | |
| 
 | |
| (defun go-oracle-peers ()
 | |
|   "Enumerate the set of possible corresponding sends/receives for
 | |
| this channel receive/send operation."
 | |
|   (interactive)
 | |
|   (go-oracle--run "peers"))
 | |
| 
 | |
| (defun go-oracle-referrers ()
 | |
|   "Enumerate all references to the object denoted by the selected
 | |
| identifier."
 | |
|   (interactive)
 | |
|   (go-oracle--run "referrers"))
 | |
| 
 | |
| (defun go-oracle-whicherrs ()
 | |
|   "Show globals, constants and types to which the selected
 | |
| expression (of type 'error') may refer."
 | |
|   (interactive)
 | |
|   (go-oracle--run "whicherrs"))
 | |
| 
 | |
| ;; TODO(dominikh): better docstring
 | |
| (define-minor-mode go-oracle-mode "Oracle minor mode for go-mode
 | |
| 
 | |
| Keys specific to go-oracle-mode:
 | |
| \\{go-oracle-mode-map}"
 | |
|   nil " oracle" go-oracle-mode-map)
 | |
| 
 | |
| (provide 'go-oracle)
 |