431 lines
16 KiB
EmacsLisp

;;; go-eldoc.el --- eldoc for go-mode -*- lexical-binding: t; -*-
;; Copyright (C) 2017 by Syohei YOSHIDA
;; Author: Syohei YOSHIDA <syohex@gmail.com>
;; URL: https://github.com/syohex/emacs-go-eldoc
;; Package-Version: 20170305.627
;; Version: 0.30
;; Package-Requires: ((emacs "24.3") (go-mode "1.0.0"))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; `go-eldoc.el' provides eldoc for Go language. `go-eldoc.el' shows type information
;; for variable, functions and current argument position of function.
;; To use this package, add these lines to your init.el file:
;;
;; (require 'go-eldoc)
;; (add-hook 'go-mode-hook 'go-eldoc-setup)
;;
;;; Code:
(require 'cl-lib)
(require 'eldoc)
(require 'go-mode)
(require 'thingatpt)
(defgroup go-eldoc nil
"Eldoc for golang"
:group 'go
:prefix "go-eldoc-")
(defcustom go-eldoc-gocode "gocode"
"gocode path"
:type 'string)
(defcustom go-eldoc-gocode-args nil
"Additional arguments to pass to `gocode'"
:type '(repeat string))
(defvar go-eldoc--builtins
'(("append" . "append,,func(slice []Type, elems ...Type) []Type")
("close" . "close,,func(c chan<- Type)")
("delete" . "delete,,func(m map[Type]Type1, key Type)")
("panic" . "panic,,func(v interface{})")
("recover" . "recover,,func() interface{}")
("complex" . "complex,,func(r, i FloatType) ComplexType")
("imag" . "imag,,func(c ComplexType) FloatType")
("real" . "real,,func(c ComplexType) FloatType")
("new" . "new,,func(Type) *Type")
("cap" . "cap,,func(v Type) int")
("copy" . "copy,,func(dst, src []Type) int")
("len" . "len,,func(v Type) int")))
(defun go-eldoc--current-arg-index (curpoint)
(save-excursion
(let ((count 1)
(start-level (go-paren-level)))
(while (search-forward "," curpoint t)
(when (and (not (go-in-string-or-comment-p))
(= start-level (1- (go-paren-level))))
(cl-incf count)))
count)))
(defun go-eldoc--count-string (str from to)
(goto-char from)
(cl-loop while (search-forward str to t)
unless (go-in-string-or-comment-p)
counting 1))
(defun go-eldoc--inside-funcall-p (from to)
(save-excursion
(let ((left-paren (go-eldoc--count-string "(" from to))
(right-paren (go-eldoc--count-string ")" from to)))
(> left-paren right-paren))))
(defsubst go-eldoc--goto-opening-parenthesis ()
(and (ignore-errors (backward-up-list) t)
(eql (char-after) ?\()))
(defun go-eldoc--inside-anon-function-p (from to)
(save-excursion
(goto-char to)
(when (go-eldoc--goto-opening-parenthesis)
(when (char-equal (char-after) ?\{)
(let ((func-start (point))
(case-fold-search nil))
(goto-char from)
(re-search-forward "\\<func\\s-*(" func-start t))))))
(defun go-eldoc--make-type ()
(save-excursion
(let ((cur (point)))
(when (re-search-forward "[,)]" (line-end-position) t)
(backward-char 1)
(skip-chars-backward "[:space:]")
(buffer-substring-no-properties (1+ cur) (point))))))
(defun go-eldoc--make-signature (type index)
(when (or (not type) (string= type ""))
(setq type "Type"))
(if (= index 3)
(format "make,,func(%s, size IntegerType, capacity IntegerType) %s" type type)
(format "make,,func(%s, size IntegerType) %s" type type)))
(defun go-eldoc--search-builtin-functions (symbol curpoint)
(if (string= symbol "make")
(let ((index (go-eldoc--current-arg-index curpoint)))
(go-eldoc--make-signature (go-eldoc--make-type) index))
(assoc-default symbol go-eldoc--builtins)))
(defun go-eldoc--match-candidates (candidates cur-symbol curpoint)
(when (and candidates (stringp candidates))
(let* ((cands (if (string= candidates "")
(go-eldoc--search-builtin-functions cur-symbol curpoint)
candidates))
(regexp (format "^\\(%s,,\\(?:func\\|type\\).+\\)$" cur-symbol))
(case-fold-search nil))
(when (and cands (string-match regexp cands))
(match-string-no-properties 1 cands)))))
(defun go-eldoc--begining-of-funcall-p ()
(and (= (char-after) ?\()
(looking-back (concat go-identifier-regexp "\\s-*") nil)
(not (string= "func" (thing-at-point 'word)))))
(defun go-eldoc--goto-beginning-of-funcall ()
(cl-loop with old-point = (point)
with retval = nil
while (and (go-eldoc--goto-opening-parenthesis)
(not (bobp))
(not (= old-point (point)))
(progn
(setq retval (go-eldoc--begining-of-funcall-p))
(not retval)))
do
(setq old-point (point))
finally return retval))
(defun go-eldoc--invoke-autocomplete ()
(let ((temp-buffer (get-buffer-create "*go-eldoc*"))
(gocode-args (append go-eldoc-gocode-args
(list "-f=emacs"
"autocomplete"
(or (buffer-file-name) "")
(concat "c" (int-to-string (- (point) 1)))))))
(unwind-protect
(progn
(apply #'call-process-region
(point-min)
(point-max)
go-eldoc-gocode
nil
temp-buffer
nil
gocode-args)
(with-current-buffer temp-buffer
(buffer-string)))
(kill-buffer temp-buffer))))
(defsubst go-eldoc--assignment-index (lhs)
(1+ (cl-loop for c across lhs
when (= c ?,)
sum 1)))
(defsubst go-eldoc--has-paren-same-line-p ()
(save-excursion
(re-search-forward "[({\\[]" (line-end-position) t)))
(defun go-eldoc--goto-last-funcall (limit)
(let ((level (car (syntax-ppss)))
pos)
(save-excursion
(while (re-search-forward "[[:word:][:multibyte:]]\\s-*+(" limit t)
(when (= level (1- (car (syntax-ppss))))
(setq pos (point)))))
(when pos
(goto-char pos))))
(defun go-eldoc--goto-statement-end ()
(let ((limit (line-end-position)))
(if (re-search-forward ")\\s-*;" limit t)
(goto-char (match-beginning 0))
(when (and (go-eldoc--has-paren-same-line-p)
(go-eldoc--goto-last-funcall limit))
(go-eldoc--goto-opening-parenthesis)
(forward-list)
(goto-char (1- (point)))))))
(defun go-eldoc--lhs-p (curpoint)
(save-excursion
(let ((limit (line-end-position)))
(when (search-forward ";" limit t)
(setq limit (1- (point))))
(goto-char curpoint)
(and (re-search-forward ":?=" limit t)
(not (go-in-string-or-comment-p))))))
(defun go-eldoc--assignment-p (curpoint)
(when (and (not (looking-at-p "\\s-+")) (go-eldoc--lhs-p curpoint))
(let ((lhs (buffer-substring-no-properties (line-beginning-position) curpoint)))
(when (go-eldoc--goto-statement-end)
(- (go-eldoc--assignment-index lhs))))))
(defun go-eldoc--get-funcinfo ()
(save-excursion
(let ((curpoint (point))
assignment-index)
(if (go-in-string-or-comment-p)
(go-goto-beginning-of-string-or-comment)
(when (setq assignment-index (go-eldoc--assignment-p curpoint))
(setq curpoint (point))))
(when (go-eldoc--goto-beginning-of-funcall)
(when (and (go-eldoc--inside-funcall-p (1- (point)) curpoint)
(not (go-eldoc--inside-anon-function-p (1- (point)) curpoint)))
(let ((matched (go-eldoc--match-candidates
(go-eldoc--invoke-autocomplete) (thing-at-point 'symbol)
curpoint)))
(when (and matched
(string-match "\\`\\(.+?\\),,\\(.+\\)$" matched))
(let ((funcname (match-string-no-properties 1 matched))
(signature (match-string-no-properties 2 matched)))
(list :name funcname :signature signature
:index (or assignment-index
(go-eldoc--current-arg-index curpoint)))))))))))
(defsubst go-eldoc--no-argument-p (arg-type)
(string-match-p "\\`\\s-+\\'" arg-type))
(defconst go-eldoc--argument-type-regexp
(concat
"\\([]{}[:word:][:multibyte:]*.[]+\\)" ;; $1 argname
(format "\\(?: %s%s\\)?"
"\\(\\(?:\\[\\]\\)?\\(?:<-\\)?chan\\(?:<-\\)? \\)?" ;; $2 channel
"\\(?:\\([]{}[:word:][:multibyte:]*.[]+\\)\\)?") ;; $3 argtype
))
(defun go-eldoc--extract-type-name (chan sym)
(when sym
(if (or (not chan) (string= chan ""))
sym
(concat chan sym))))
(defun go-eldoc--split-types-string (arg-type)
(with-temp-buffer
(set-syntax-table go-mode-syntax-table)
(insert arg-type)
(goto-char (point-min))
(let ((name-types nil))
(while (re-search-forward go-eldoc--argument-type-regexp nil t)
(let* ((name (match-string-no-properties 1))
(type (go-eldoc--extract-type-name
(match-string-no-properties 2)
(match-string-no-properties 3)))
(name-type (if type
(concat name " " type)
name))
(end (match-end 0)))
(when (or (string= type "func") (and (not type) (string= name "func")))
(forward-list)
(cond ((looking-at (concat "\\s-*" go-eldoc--argument-type-regexp))
(goto-char (match-end 0)))
((looking-at-p "\\s-*(")
(skip-chars-forward " \t")
(forward-list)))
(setq name-type (concat name-type
(buffer-substring-no-properties end (point)))))
(push name-type name-types)))
(reverse name-types))))
(defsubst go-eldoc--has-spaces (str)
(string-match-p "[[:space:]]" str))
(defun go-eldoc--wrap-parenthesis (str len rettype)
;; Don't wrap if return value is only one
(if (and rettype (<= len 1) (not (go-eldoc--has-spaces str)))
str
(concat "(" str ")")))
(defun go-eldoc--highlight-index-position (types-str index &optional rettype-p)
(cl-loop with types = (go-eldoc--split-types-string types-str)
with highlight-done = nil
with len = (length types)
with last-index = (1- len)
for i from 0 below len
for type in types
if (and (not highlight-done)
(or (= i (1- index))
(and (= i last-index)
(string-match-p "\\.\\{3\\}" type))))
collect
(progn
(setq highlight-done t)
(propertize type 'face 'eldoc-highlight-function-argument)) into args
else
collect type into args
finally return (go-eldoc--wrap-parenthesis
(mapconcat 'identity args ", ") len rettype-p)))
(defun go-eldoc--highlight-argument (signature index)
(let* ((arg-type (plist-get signature :arg-type))
(ret-type (plist-get signature :ret-type)))
(if (go-eldoc--no-argument-p arg-type)
(concat "() " ret-type)
(if (> index 0)
(let ((highlighed-args (go-eldoc--highlight-index-position arg-type index)))
(concat highlighed-args " " ret-type))
(let ((highlighed-rets (go-eldoc--highlight-index-position ret-type (- index) t)))
(concat "(" arg-type ") " highlighed-rets))))))
(defun go-eldoc--analyze-func-signature ()
(let (arg-start arg-end)
(when (search-forward "func(" nil t)
(setq arg-start (point))
(backward-char 1)
(when (ignore-errors (forward-list) t)
(setq arg-end (1- (point)))
(skip-chars-forward " \t")
(list :type 'function
:arg-type (buffer-substring-no-properties arg-start arg-end)
:ret-type (buffer-substring-no-properties (point) (point-max)))))))
(defun go-eldoc--analyze-type-signature ()
(when (search-forward "type " nil t)
(list :type 'type
:real-type (buffer-substring-no-properties (point) (point-max)))))
(defun go-eldoc--analyze-signature (signature)
(with-temp-buffer
(set-syntax-table go-mode-syntax-table)
(insert signature)
(goto-char (point-min))
(let ((word (thing-at-point 'word)))
(cond ((string= "func" word)
(go-eldoc--analyze-func-signature))
((string= "type" word)
(go-eldoc--analyze-type-signature))))))
(defun go-eldoc--format-signature (funcinfo)
(let ((name (plist-get funcinfo :name))
(signature (go-eldoc--analyze-signature (plist-get funcinfo :signature)))
(index (plist-get funcinfo :index)))
(when signature
(cl-case (plist-get signature :type)
(function
(format "%s: %s"
(propertize name 'face 'font-lock-function-name-face)
(go-eldoc--highlight-argument signature index)))
(type
(format "%s: %s"
(propertize name 'face 'font-lock-type-face)
(plist-get signature :real-type)))))))
(defun go-eldoc--retrieve-type (typeinfo symbol)
(let ((case-fold-search nil))
(cond ((string-match (format "^%s,,var \\(.+\\)$" symbol) typeinfo)
(match-string-no-properties 1 typeinfo))
((string-match-p (format "\\`%s,,package\\s-*$" symbol) typeinfo)
"package")
((string-match (format "^%s,,\\(func.+\\)$" symbol) typeinfo)
(match-string-no-properties 1 typeinfo))
((string-match (format "^%s,,\\(.+\\)$" symbol) typeinfo)
(match-string-no-properties 1 typeinfo)))))
(defun go-eldoc--get-cursor-info (bounds)
(save-excursion
(goto-char (cdr bounds))
(go-eldoc--retrieve-type
(go-eldoc--invoke-autocomplete)
(buffer-substring-no-properties (car bounds) (cdr bounds)))))
(defun go-eldoc--retrieve-concrete-name (bounds)
(save-excursion
(goto-char (car bounds))
(while (looking-back "\\." (1- (point)))
(backward-char 1)
(skip-chars-backward "[:word:][:multibyte:]\\[\\]"))
(buffer-substring-no-properties (point) (cdr bounds))))
(defun go-eldoc--bounds-of-go-symbol ()
(save-excursion
(let (start)
(skip-chars-backward "[:word:][:multibyte:]")
(setq start (point))
(skip-chars-forward "[:word:][:multibyte:]")
(unless (= start (point))
(cons start (point))))))
(defsubst go-eldoc--propertize-cursor-thing (bounds)
(propertize (go-eldoc--retrieve-concrete-name bounds)
'face 'font-lock-variable-name-face))
(defun go-eldoc--documentation-function ()
(let ((funcinfo (go-eldoc--get-funcinfo)))
(if funcinfo
(go-eldoc--format-signature funcinfo)
(let ((bounds (go-eldoc--bounds-of-go-symbol)))
(when bounds
(let ((curinfo (go-eldoc--get-cursor-info bounds)))
(when curinfo
(format "%s: %s"
(go-eldoc--propertize-cursor-thing bounds)
curinfo))))))))
;;;###autoload
(defun go-eldoc-setup ()
"Set up eldoc function and enable eldoc-mode."
(interactive)
(setq-local eldoc-documentation-function #'go-eldoc--documentation-function)
(eldoc-mode +1))
(provide 'go-eldoc)
;;; go-eldoc.el ends here