267 lines
9.2 KiB
EmacsLisp

;;; company-go.el --- company-mode backend for Go (using gocode)
;; Copyright (C) 2012
;; Author: nsf <no.smile.face@gmail.com>
;; Keywords: languages
;; Package-Version: 20170825.943
;; Package-Requires: ((company "0.8.0") (go-mode "1.0.0"))
;; No license, this code is under public domain, do whatever you want.
;;; Code:
(require 'company-template)
(eval-when-compile
(require 'cl))
(require 'go-mode)
(require 'company)
;; Close gocode daemon at exit unless it was already running
(eval-after-load "go-mode"
'(progn
(let* ((user (or (getenv "USER") "all"))
(sock (format (concat temporary-file-directory "gocode-daemon.%s") user)))
(unless (file-exists-p sock)
(add-hook 'kill-emacs-hook #'(lambda ()
(ignore-errors
(call-process company-go-gocode-command nil nil nil "close"))))))))
(defgroup company-go nil
"Completion back-end for Go."
:group 'company)
(defcustom company-go-show-annotation nil
"Show an annotation inline with the candidate."
:group 'company-go
:type 'boolean)
(defcustom company-go-begin-after-member-access t
"When non-nil, automatic completion will start whenever the current
symbol is preceded by a \".\", ignoring `company-minimum-prefix-length'."
:group 'company-go
:type 'boolean)
(defcustom company-go-insert-arguments t
"When non-nil, insert function or method arguments as a template after completion."
:group 'company-go
:type 'boolean)
(defcustom company-go-gocode-command "gocode"
"The command to invoke `gocode'"
:group 'company-go
:type 'string)
(defcustom company-go-gocode-args nil
"Additional arguments to pass to `gocode'"
:group 'company-go
:type '(repeat string))
(defcustom company-go-godoc-command "go doc"
"The command to invoke `go doc' with."
:group 'company-go
:type 'string)
(defcustom company-go-godoc-args "-u"
"Arguments to pass to `go doc'."
:group 'company-go
:type 'string)
(defun company-go--invoke-autocomplete ()
(let ((code-buffer (current-buffer))
(gocode-args (append company-go-gocode-args
(list "-f=csv-with-package"
"autocomplete"
(or (buffer-file-name) "")
(concat "c" (int-to-string (- (point) 1)))))))
(with-temp-buffer
(let ((temp-buffer (current-buffer)))
(with-current-buffer code-buffer
(apply #'call-process-region
(point-min)
(point-max)
company-go-gocode-command
nil
temp-buffer
nil
gocode-args))
(buffer-string)))))
(defun company-go--format-meta (candidate)
(let ((class (nth 0 candidate))
(name (nth 1 candidate))
(type (nth 2 candidate)))
(setq type (if (string-prefix-p "func" type)
(substring type 4 nil)
(concat " " type)))
(concat class " " name type)))
(defun company-go--get-candidates (strings)
(mapcar (lambda (str)
(let ((candidate (split-string str ",,")))
(propertize (nth 1 candidate)
'meta (company-go--format-meta candidate)
'package (nth 3 candidate))))
strings))
(defun company-go--candidates ()
(let ((candidates (company-go--get-candidates (split-string (company-go--invoke-autocomplete) "\n" t))))
(if (equal candidates '("PANIC"))
(error "GOCODE PANIC: Please check your code by \"go build\"")
candidates)))
(defun company-go--location (arg)
(when (require 'go-mode nil t)
(company-go--location-1 arg)))
(defun company-go--location-1 (arg)
(let* ((temp (make-temp-file
(directory-file-name
(expand-file-name "company-go--location"))))
(buffer (current-buffer))
(prefix-len (length company-prefix))
(point (point))
(temp-buffer (find-file-noselect temp)))
(unwind-protect
(progn
(with-current-buffer temp-buffer
(insert-buffer-substring buffer)
(goto-char point)
(insert (substring arg prefix-len))
(goto-char point)
(company-go--godef-jump point)))
(ignore-errors
(with-current-buffer temp-buffer
(set-buffer-modified-p nil))
(kill-buffer temp-buffer)
(delete-file temp)))))
(defun company-go--prefix ()
"Returns the symbol to complete. Also, if point is on a dot,
triggers a completion immediately."
(if company-go-begin-after-member-access
(company-grab-symbol-cons "\\." 1)
(company-grab-symbol)))
(defun company-go--godef-jump (point)
(condition-case nil
(let ((file (car (godef--call point))))
(cond
((string= "-" file)
(message "company-go: expression is not defined anywhere") nil)
((string= "company-go: no identifier found" file)
(message "%s" file) nil)
((go--string-prefix-p "godef: no declaration found for " file)
(message "%s" file) nil)
((go--string-prefix-p "error finding import path for " file)
(message "%s" file) nil)
(t (if (not (string-match "\\(.+\\):\\([0-9]+\\):\\([0-9]+\\)" file))
(cons (find-file-noselect file) 0)
(let ((filename (match-string 1 file))
(line (string-to-number (match-string 2 file)))
(column (string-to-number (match-string 3 file))))
(with-current-buffer (find-file-noselect filename)
(go--goto-line line)
(beginning-of-line)
(forward-char (1- column))
(cons (current-buffer) (point))))))))
(file-error (message "company-go: Could not run godef binary") nil)))
(defun company-go--insert-arguments (meta)
"Insert arguments when META is a function or a method."
(when (string-match "^func\\s *[^(]+\\(.*\\)" meta)
(let ((args (company-go--extract-arguments (match-string 1 meta))))
(insert args)
(company-template-c-like-templatify args))))
(defun company-go--extract-arguments (str)
"Extract arguments with parentheses from STR."
(let ((len (length str))
(pos 1)
(pirs-paren 1))
(while (and (/= pirs-paren 0) (< pos len))
(let ((c (substring-no-properties str pos (1+ pos))))
(cond
((string= c "(") (setq pirs-paren (1+ pirs-paren)))
((string= c ")") (setq pirs-paren (1- pirs-paren))))
(setq pos (1+ pos))))
(substring-no-properties str 0 pos)))
; Uses meta as-is if annotation alignment is enabled. Otherwise removes first
; two words from the meta, which are usually the class and the name of the
; entity, the rest is the function signature or type. That's how annotations are
; supposed to be used.
(defun company-go--extract-annotation (meta)
"Extract annotation from META."
(if company-tooltip-align-annotations
meta
(save-match-data
(and (string-match "\\w+ \\w+\\(.+\\)" meta)
(match-string 1 meta)))))
(defun company-go--in-num-literal-p ()
"Returns t if point is in a numeric literal."
(let ((word (company-grab-word)))
(when word
(string-match-p "^0x\\|^[0-9]+" word))))
(defun company-go--syntax-highlight (str)
"Apply syntax highlighting to STR."
;; If the user has disabled font-lock, respect that.
(if global-font-lock-mode
(with-temp-buffer
(insert str)
(delay-mode-hooks (go-mode))
(if (fboundp 'font-lock-ensure)
(font-lock-ensure)
(with-no-warnings
(font-lock-fontify-buffer)))
(buffer-string))
str))
(defun company-go--godoc-as-buffer (arg)
"Return Go documentation for QUERY as a buffer."
(unless (string= arg "")
(let* ((package (get-text-property 0 'package arg))
(query (if (string= package "")
arg
(format "%s.%s" package arg)))
(buf (godoc--get-buffer query))
(exit-code (call-process-shell-command
(concat company-go-godoc-command " " company-go-godoc-args " " query)
nil buf nil)))
(if (zerop exit-code)
buf
(kill-buffer buf)
nil))))
;;;###autoload
(defun company-go (command &optional arg &rest ignored)
(interactive (list 'interactive))
(case command
(interactive (company-begin-backend 'company-go))
(prefix (and (derived-mode-p 'go-mode)
(not (company-in-string-or-comment))
(not (company-go--in-num-literal-p))
(or (company-go--prefix) 'stop)))
(candidates (company-go--candidates))
(meta
(company-go--syntax-highlight (get-text-property 0 'meta arg)))
(annotation
(when company-go-show-annotation
(company-go--extract-annotation (get-text-property 0 'meta arg))))
(location (company-go--location arg))
(doc-buffer
(company-go--godoc-as-buffer arg))
(sorted t)
(post-completion
(when (and company-go-insert-arguments
(not (char-equal ?\( (following-char))))
(company-go--insert-arguments
(get-text-property 0 'meta arg))))))
(provide 'company-go)
;;; company-go.el ends here