;;; go-mode.el --- Major mode for the Go programming language
-;;; Commentary:
+;; Copyright 2013 The Go Authors. All rights reserved.
+;; Use of this source code is governed by a BSD-style
+;; license that can be found in the LICENSE file.
-;; For installation instructions, see go-mode-load.el
+(require 'cl)
+(require 'diff-mode)
+(require 'ffap)
+(require 'find-lisp)
+(require 'url)
-;;; To do:
+(defconst go-dangling-operators-regexp "[^-]-\\|[^+]\\+\\|[/*&><.=|^]")
+(defconst gofmt-stdin-tag "<standard input>")
+(defconst go-identifier-regexp "[[:word:][:multibyte:]_]+")
+(defconst go-label-regexp go-identifier-regexp)
+(defconst go-type-regexp "[[:word:][:multibyte:]_*]+")
+(defconst go-func-regexp (concat "\\<func\\>\\s *\\(" go-identifier-regexp "\\)"))
+(defconst go-func-meth-regexp (concat "\\<func\\>\\s *\\(?:(\\s *" go-identifier-regexp "\\s +" go-type-regexp "\\s *)\\s *\\)?\\(" go-identifier-regexp "\\)("))
+(defconst go-builtins
+ '("append" "cap" "close" "complex" "copy"
+ "delete" "imag" "len" "make" "new"
+ "panic" "print" "println" "real" "recover")
+ "All built-in functions in the Go language. Used for font locking.")
+
+(defconst go-mode-keywords
+ '("break" "default" "func" "interface" "select"
+ "case" "defer" "go" "map" "struct"
+ "chan" "else" "goto" "package" "switch"
+ "const" "fallthrough" "if" "range" "type"
+ "continue" "for" "import" "return" "var")
+ "All keywords in the Go language. Used for font locking.")
+
+(defconst go-constants '("nil" "true" "false" "iota"))
+(defconst go-type-name-regexp (concat "\\(?:[*(]\\)*\\(?:" go-identifier-regexp "\\.\\)?\\(" go-identifier-regexp "\\)"))
-;; * Indentation is *almost* identical to gofmt
-;; ** We think struct literal keys are labels and outdent them
-;; ** We disagree on the indentation of function literals in arguments
-;; ** There are bugs with the close brace of struct literals
-;; * Highlight identifiers according to their syntactic context: type,
-;; variable, function call, or tag
-;; * Command for adding an import
-;; ** Check if it's already there
-;; ** Factor/unfactor the import line
-;; ** Alphabetize
-;; * Remove unused imports
-;; ** This is hard, since I have to be aware of shadowing to do it
-;; right
-;; * Format region using gofmt
+(defvar go-dangling-cache)
-;;; Code:
+(defgroup go nil
+ "Major mode for editing Go code"
+ :group 'languages)
-(eval-when-compile (require 'cl))
+(defcustom go-fontify-function-calls t
+ "Fontify function and method calls if this is non-nil."
+ :type 'boolean
+ :group 'go)
(defvar go-mode-syntax-table
(let ((st (make-syntax-table)))
- ;; Add _ to :word: character class
- (modify-syntax-entry ?_ "w" st)
-
- ;; Operators (punctuation)
(modify-syntax-entry ?+ "." st)
(modify-syntax-entry ?- "." st)
- (modify-syntax-entry ?* "." st)
- (modify-syntax-entry ?/ "." st)
(modify-syntax-entry ?% "." st)
(modify-syntax-entry ?& "." st)
(modify-syntax-entry ?| "." st)
(modify-syntax-entry ?= "." st)
(modify-syntax-entry ?< "." st)
(modify-syntax-entry ?> "." st)
-
- ;; Strings and comments are font-locked separately.
- (modify-syntax-entry ?\" "." st)
- (modify-syntax-entry ?\' "." st)
- (modify-syntax-entry ?` "." st)
- (modify-syntax-entry ?\\ "." st)
+ (modify-syntax-entry ?/ ". 124b" st)
+ (modify-syntax-entry ?* ". 23" st)
+ (modify-syntax-entry ?\n "> b" st)
+ (modify-syntax-entry ?\" "\"" st)
+ (modify-syntax-entry ?\' "\"" st)
+ (modify-syntax-entry ?` "\"" st)
+ (modify-syntax-entry ?\\ "\\" st)
+ (modify-syntax-entry ?_ "_" st)
st)
"Syntax table for Go mode.")
-(defvar go-mode-keywords
- '("break" "default" "func" "interface" "select"
- "case" "defer" "go" "map" "struct"
- "chan" "else" "goto" "package" "switch"
- "const" "fallthrough" "if" "range" "type"
- "continue" "for" "import" "return" "var")
- "All keywords in the Go language. Used for font locking and
-some syntax analysis.")
-
-(defvar go-mode-font-lock-keywords
- (let ((builtins '("append" "cap" "close" "complex" "copy" "delete" "imag" "len"
- "make" "new" "panic" "print" "println" "real" "recover"))
- (constants '("nil" "true" "false" "iota"))
- (type-name "\\s *\\(?:[*(]\\s *\\)*\\(?:\\w+\\s *\\.\\s *\\)?\\(\\w+\\)")
- )
- `((go-mode-font-lock-cs-comment 0 font-lock-comment-face t)
- (go-mode-font-lock-cs-string 0 font-lock-string-face t)
- (,(regexp-opt go-mode-keywords 'words) . font-lock-keyword-face)
- (,(regexp-opt builtins 'words) . font-lock-builtin-face)
- (,(regexp-opt constants 'words) . font-lock-constant-face)
- ;; Function names in declarations
- ("\\<func\\>\\s *\\(\\w+\\)" 1 font-lock-function-name-face)
- ;; Function names in methods are handled by function call pattern
- ;; Function names in calls
- ;; XXX Doesn't match if function name is surrounded by parens
- ("\\(\\w+\\)\\s *(" 1 font-lock-function-name-face)
- ;; Type names
- ("\\<type\\>\\s *\\(\\w+\\)" 1 font-lock-type-face)
- (,(concat "\\<type\\>\\s *\\w+\\s *" type-name) 1 font-lock-type-face)
- ;; Arrays/slices/map value type
- ;; XXX Wrong. Marks 0 in expression "foo[0] * x"
- ;; (,(concat "]" type-name) 1 font-lock-type-face)
- ;; Map key type
- (,(concat "\\<map\\s *\\[" type-name) 1 font-lock-type-face)
- ;; Channel value type
- (,(concat "\\<chan\\>\\s *\\(?:<-\\)?" type-name) 1 font-lock-type-face)
- ;; new/make type
- (,(concat "\\<\\(?:new\\|make\\)\\>\\(?:\\s \\|)\\)*(" type-name) 1 font-lock-type-face)
- ;; Type conversion
- (,(concat "\\.\\s *(" type-name) 1 font-lock-type-face)
- ;; Method receiver type
- (,(concat "\\<func\\>\\s *(\\w+\\s +" type-name) 1 font-lock-type-face)
- ;; Labels
- ;; XXX Not quite right. Also marks compound literal fields.
- ("^\\s *\\(\\w+\\)\\s *:\\(\\S.\\|$\\)" 1 font-lock-constant-face)
- ("\\<\\(goto\\|break\\|continue\\)\\>\\s *\\(\\w+\\)" 2 font-lock-constant-face)))
- "Basic font lock keywords for Go mode. Highlights keywords,
-built-ins, functions, and some types.")
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Key map
-;;
+(defun go--build-font-lock-keywords ()
+ (append
+ `((,(regexp-opt go-mode-keywords 'symbols) . font-lock-keyword-face)
+ (,(regexp-opt go-builtins 'symbols) . font-lock-builtin-face)
+ (,(regexp-opt go-constants 'symbols) . font-lock-constant-face)
+ (,go-func-regexp 1 font-lock-function-name-face)) ;; function (not method) name
+
+ (if go-fontify-function-calls
+ `((,(concat "\\(" go-identifier-regexp "\\)[[:space:]]*(") 1 font-lock-function-name-face) ;; function call/method name
+ (,(concat "(\\(" go-identifier-regexp "\\))[[:space:]]*(") 1 font-lock-function-name-face)) ;; bracketed function call
+ `((,go-func-meth-regexp 1 font-lock-function-name-face))) ;; method name
+
+ `(
+ ("\\<type\\>[[:space:]]*\\([^[:space:]]+\\)" 1 font-lock-type-face) ;; types
+ (,(concat "\\<type\\>[[:space:]]*" go-identifier-regexp "[[:space:]]*" go-type-name-regexp) 1 font-lock-type-face) ;; types
+ (,(concat "\\(?:[[:space:]]+\\|\\]\\)\\[\\([[:digit:]]+\\|\\.\\.\\.\\)?\\]" go-type-name-regexp) 2 font-lock-type-face) ;; Arrays/slices
+ (,(concat "map\\[[^]]+\\]" go-type-name-regexp) 1 font-lock-type-face) ;; map value type
+ (,(concat "\\(" go-identifier-regexp "\\)" "{") 1 font-lock-type-face)
+ (,(concat "\\<map\\[" go-type-name-regexp) 1 font-lock-type-face) ;; map key type
+ (,(concat "\\<chan\\>[[:space:]]*\\(?:<-\\)?" go-type-name-regexp) 1 font-lock-type-face) ;; channel type
+ (,(concat "\\<\\(?:new\\|make\\)\\>\\(?:[[:space:]]\\|)\\)*(" go-type-name-regexp) 1 font-lock-type-face) ;; new/make type
+ ;; TODO do we actually need this one or isn't it just a function call?
+ (,(concat "\\.\\s *(" go-type-name-regexp) 1 font-lock-type-face) ;; Type conversion
+ (,(concat "\\<func\\>[[:space:]]+(" go-identifier-regexp "[[:space:]]+" go-type-name-regexp ")") 1 font-lock-type-face) ;; Method receiver
+ ;; Like the original go-mode this also marks compound literal
+ ;; fields. There, it was marked as to fix, but I grew quite
+ ;; accustomed to it, so it'll stay for now.
+ (,(concat "^[[:space:]]*\\(" go-label-regexp "\\)[[:space:]]*:\\(\\S.\\|$\\)") 1 font-lock-constant-face) ;; Labels and compound literal fields
+ (,(concat "\\<\\(goto\\|break\\|continue\\)\\>[[:space:]]*\\(" go-label-regexp "\\)") 2 font-lock-constant-face)))) ;; labels in goto/break/continue
(defvar go-mode-map
(let ((m (make-sparse-keymap)))
- (define-key m "}" #'go-mode-insert-and-indent)
- (define-key m ")" #'go-mode-insert-and-indent)
- (define-key m "," #'go-mode-insert-and-indent)
- (define-key m ":" #'go-mode-delayed-electric)
- ;; In case we get : indentation wrong, correct ourselves
- (define-key m "=" #'go-mode-insert-and-indent)
+ (define-key m "}" 'go-mode-insert-and-indent)
+ (define-key m ")" 'go-mode-insert-and-indent)
+ (define-key m "," 'go-mode-insert-and-indent)
+ (define-key m ":" 'go-mode-insert-and-indent)
+ (define-key m "=" 'go-mode-insert-and-indent)
+ (define-key m (kbd "C-c C-a") 'go-import-add)
m)
"Keymap used by Go mode to implement electric keys.")
(call-interactively (lookup-key (current-global-map) key))
(indent-according-to-mode))
-(defvar go-mode-delayed-point nil
- "The point following the previous insertion if the insertion
-was a delayed electric key. Used to communicate between
-`go-mode-delayed-electric' and `go-mode-delayed-electric-hook'.")
-(make-variable-buffer-local 'go-mode-delayed-point)
-
-(defun go-mode-delayed-electric (p)
- "Perform electric insertion, but delayed by one event.
-
-This inserts P into the buffer, as usual, then waits for another key.
-If that second key causes a buffer modification starting at the
-point after the insertion of P, reindents the line containing P."
-
- (interactive "p")
- (self-insert-command p)
- (setq go-mode-delayed-point (point)))
-
-(defun go-mode-delayed-electric-hook (b e l)
- "An after-change-function that implements `go-mode-delayed-electric'."
-
- (when (and go-mode-delayed-point
- (= go-mode-delayed-point b))
- (save-excursion
- (save-match-data
- (goto-char go-mode-delayed-point)
- (indent-according-to-mode))))
- (setq go-mode-delayed-point nil))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Parser
-;;
-
-(defvar go-mode-mark-cs-end 1
- "The point at which the comment/string cache ends. The buffer
-will be marked from the beginning up to this point (that is, up
-to and including character (1- go-mode-mark-cs-end)).")
-(make-variable-buffer-local 'go-mode-mark-cs-end)
-
-(defvar go-mode-mark-string-end 1
- "The point at which the string cache ends. The buffer
-will be marked from the beginning up to this point (that is, up
-to and including character (1- go-mode-mark-string-end)).")
-(make-variable-buffer-local 'go-mode-mark-string-end)
-
-(defvar go-mode-mark-comment-end 1
- "The point at which the comment cache ends. The buffer
-will be marked from the beginning up to this point (that is, up
-to and including character (1- go-mode-mark-comment-end)).")
-(make-variable-buffer-local 'go-mode-mark-comment-end)
-
-(defvar go-mode-mark-nesting-end 1
- "The point at which the nesting cache ends. The buffer will be
-marked from the beginning up to this point.")
-(make-variable-buffer-local 'go-mode-mark-nesting-end)
-
-(defun go-mode-mark-clear-cs (b e l)
- "An after-change-function that removes the go-mode-cs text property"
- (remove-text-properties b e '(go-mode-cs)))
-
-(defun go-mode-mark-clear-cache (b e)
- "A before-change-function that clears the comment/string and
-nesting caches from the modified point on."
-
- (save-restriction
- (widen)
- (when (<= b go-mode-mark-cs-end)
- ;; Remove the property adjacent to the change position.
- ;; It may contain positions pointing beyond the new end mark.
- (let ((b (let ((cs (get-text-property (max 1 (1- b)) 'go-mode-cs)))
- (if cs (car cs) b))))
- (remove-text-properties
- b (min go-mode-mark-cs-end (point-max)) '(go-mode-cs nil))
- (setq go-mode-mark-cs-end b)))
-
- (when (<= b go-mode-mark-string-end)
- ;; Remove the property adjacent to the change position.
- ;; It may contain positions pointing beyond the new end mark.
- (let ((b (let ((cs (get-text-property (max 1 (1- b)) 'go-mode-string)))
- (if cs (car cs) b))))
- (remove-text-properties
- b (min go-mode-mark-string-end (point-max)) '(go-mode-string nil))
- (setq go-mode-mark-string-end b)))
- (when (<= b go-mode-mark-comment-end)
- ;; Remove the property adjacent to the change position.
- ;; It may contain positions pointing beyond the new end mark.
- (let ((b (let ((cs (get-text-property (max 1 (1- b)) 'go-mode-comment)))
- (if cs (car cs) b))))
- (remove-text-properties
- b (min go-mode-mark-string-end (point-max)) '(go-mode-comment nil))
- (setq go-mode-mark-comment-end b)))
-
- (when (< b go-mode-mark-nesting-end)
- (remove-text-properties b (min go-mode-mark-nesting-end (point-max)) '(go-mode-nesting nil))
- (setq go-mode-mark-nesting-end b))))
-
-(defmacro go-mode-parser (&rest body)
- "Evaluate BODY in an environment set up for parsers that use
-text properties to mark text. This inhibits changes to the undo
-list or the buffer's modification status and inhibits calls to
-the modification hooks. It also saves the excursion and
-restriction and widens the buffer, since most parsers are
-context-sensitive."
-
- (let ((modified-var (make-symbol "modified")))
- `(let ((buffer-undo-list t)
- (,modified-var (buffer-modified-p))
- (inhibit-modification-hooks t)
- (inhibit-read-only t))
- (save-excursion
- (save-restriction
- (widen)
- (unwind-protect
- (progn ,@body)
- (set-buffer-modified-p ,modified-var)))))))
-
-(defun go-mode-cs (&optional pos)
- "Return the comment/string state at point POS. If point is
-inside a comment or string (including the delimiters), this
-returns a pair (START . END) indicating the extents of the
-comment or string."
-
- (unless pos
- (setq pos (point)))
- (when (>= pos go-mode-mark-cs-end)
- (go-mode-mark-cs (1+ pos)))
- (get-text-property pos 'go-mode-cs))
-
-(defun go-mode-mark-cs (end)
- "Mark comments and strings up to point END. Don't call this
-directly; use `go-mode-cs'."
- (setq end (min end (point-max)))
- (go-mode-parser
- (save-match-data
- (let ((pos
- ;; Back up to the last known state.
- (let ((last-cs
- (and (> go-mode-mark-cs-end 1)
- (get-text-property (1- go-mode-mark-cs-end)
- 'go-mode-cs))))
- (if last-cs
- (car last-cs)
- (max 1 (1- go-mode-mark-cs-end))))))
- (while (< pos end)
- (goto-char pos)
- (let ((cs-end ; end of the text property
- (cond
- ((looking-at "//")
- (end-of-line)
- (1+ (point)))
- ((looking-at "/\\*")
- (goto-char (+ pos 2))
- (if (search-forward "*/" (1+ end) t)
- (point)
- end))
- ((looking-at "\"")
- (goto-char (1+ pos))
- (if (looking-at "[^\"\n\\\\]*\\(\\\\.[^\"\n\\\\]*\\)*\"")
- (match-end 0)
- (end-of-line)
- (point)))
- ((looking-at "'")
- (goto-char (1+ pos))
- (if (looking-at "[^'\n\\\\]*\\(\\\\.[^'\n\\\\]*\\)*'")
- (match-end 0)
- (end-of-line)
- (point)))
- ((looking-at "`")
- (goto-char (1+ pos))
- (while (if (search-forward "`" end t)
- (if (eq (char-after) ?`)
- (goto-char (1+ (point))))
- (goto-char end)
- nil))
- (point)))))
- (cond
- (cs-end
- (put-text-property pos cs-end 'go-mode-cs (cons pos cs-end))
- (setq pos cs-end))
- ((re-search-forward "[\"'`]\\|/[/*]" end t)
- (setq pos (match-beginning 0)))
- (t
- (setq pos end)))))
- (setq go-mode-mark-cs-end pos)))))
-
-(defun go-mode-in-comment (&optional pos)
- "Return the comment/string state at point POS. If point is
-inside a comment (including the delimiters), this
-returns a pair (START . END) indicating the extents of the
-comment or string."
-
- (unless pos
- (setq pos (point)))
- (when (> pos go-mode-mark-comment-end)
- (go-mode-mark-comment pos))
- (get-text-property pos 'go-mode-comment))
-
-(defun go-mode-mark-comment (end)
- "Mark comments up to point END. Don't call this directly; use `go-mode-in-comment'."
- (setq end (min end (point-max)))
- (go-mode-parser
- (save-match-data
- (let ((pos
- ;; Back up to the last known state.
- (let ((last-comment
- (and (> go-mode-mark-comment-end 1)
- (get-text-property (1- go-mode-mark-comment-end)
- 'go-mode-comment))))
- (if last-comment
- (car last-comment)
- (max 1 (1- go-mode-mark-comment-end))))))
- (while (< pos end)
- (goto-char pos)
- (let ((comment-end ; end of the text property
- (cond
- ((looking-at "//")
- (end-of-line)
- (1+ (point)))
- ((looking-at "/\\*")
- (goto-char (+ pos 2))
- (if (search-forward "*/" (1+ end) t)
- (point)
- end)))))
- (cond
- (comment-end
- (put-text-property pos comment-end 'go-mode-comment (cons pos comment-end))
- (setq pos comment-end))
- ((re-search-forward "/[/*]" end t)
- (setq pos (match-beginning 0)))
- (t
- (setq pos end)))))
- (setq go-mode-mark-comment-end pos)))))
-
-(defun go-mode-in-string (&optional pos)
- "Return the string state at point POS. If point is
-inside a string (including the delimiters), this
-returns a pair (START . END) indicating the extents of the
-comment or string."
-
- (unless pos
- (setq pos (point)))
- (when (> pos go-mode-mark-string-end)
- (go-mode-mark-string pos))
- (get-text-property pos 'go-mode-string))
-
-(defun go-mode-mark-string (end)
- "Mark strings up to point END. Don't call this
-directly; use `go-mode-in-string'."
- (setq end (min end (point-max)))
- (go-mode-parser
- (save-match-data
- (let ((pos
- ;; Back up to the last known state.
- (let ((last-cs
- (and (> go-mode-mark-string-end 1)
- (get-text-property (1- go-mode-mark-string-end)
- 'go-mode-string))))
- (if last-cs
- (car last-cs)
- (max 1 (1- go-mode-mark-string-end))))))
- (while (< pos end)
- (goto-char pos)
- (let ((cs-end ; end of the text property
- (cond
- ((looking-at "\"")
- (goto-char (1+ pos))
- (if (looking-at "[^\"\n\\\\]*\\(\\\\.[^\"\n\\\\]*\\)*\"")
- (match-end 0)
- (end-of-line)
- (point)))
- ((looking-at "'")
- (goto-char (1+ pos))
- (if (looking-at "[^'\n\\\\]*\\(\\\\.[^'\n\\\\]*\\)*'")
- (match-end 0)
- (end-of-line)
- (point)))
- ((looking-at "`")
- (goto-char (1+ pos))
- (while (if (search-forward "`" end t)
- (if (eq (char-after) ?`)
- (goto-char (1+ (point))))
- (goto-char end)
- nil))
- (point)))))
- (cond
- (cs-end
- (put-text-property pos cs-end 'go-mode-string (cons pos cs-end))
- (setq pos cs-end))
- ((re-search-forward "[\"'`]" end t)
- (setq pos (match-beginning 0)))
- (t
- (setq pos end)))))
- (setq go-mode-mark-string-end pos)))))
-
-(defun go-mode-font-lock-cs (limit comment)
- "Helper function for highlighting comment/strings. If COMMENT is t,
-set match data to the next comment after point, and advance point
-after it. If COMMENT is nil, use the next string. Returns nil
-if no further tokens of the type exist."
- ;; Ensures that `next-single-property-change' below will work properly.
- (go-mode-cs limit)
- (let (cs next (result 'scan))
- (while (eq result 'scan)
- (if (or (>= (point) limit) (eobp))
- (setq result nil)
- (setq cs (go-mode-cs))
- (if (and cs (>= (car cs) (point)))
- (if (eq (= (char-after (car cs)) ?/) comment)
- ;; If inside the expected comment/string, highlight it.
- (progn
- ;; If the match includes a "\n", we have a
- ;; multi-line construct. Mark it as such.
- (goto-char (car cs))
- (when (search-forward "\n" (cdr cs) t)
- (put-text-property
- (car cs) (cdr cs) 'font-lock-multline t))
- (set-match-data (list (car cs) (copy-marker (cdr cs))))
- (goto-char (cdr cs))
- (setq result t))
- ;; Wrong type. Look for next comment/string after this one.
- (goto-char (cdr cs)))
- ;; Not inside comment/string. Search for next comment/string.
- (setq next (next-single-property-change
- (point) 'go-mode-cs nil limit))
- (if (and next (< next limit))
- (goto-char next)
- (setq result nil)))))
- result))
-
-(defun go-mode-font-lock-cs-string (limit)
- "Font-lock iterator for strings."
- (go-mode-font-lock-cs limit nil))
-
-(defun go-mode-font-lock-cs-comment (limit)
- "Font-lock iterator for comments."
- (go-mode-font-lock-cs limit t))
-
-(defsubst go-mode-nesting (&optional pos)
- "Return the nesting at point POS. The nesting is a list
-of (START . END) pairs for all braces, parens, and brackets
-surrounding POS, starting at the inner-most nesting. START is
-the location of the open character. END is the location of the
-close character or nil if the nesting scanner has not yet
-encountered the close character."
-
- (unless pos
- (setq pos (point)))
- (if (= pos 1)
- '()
- (when (> pos go-mode-mark-nesting-end)
- (go-mode-mark-nesting pos))
- (get-text-property (- pos 1) 'go-mode-nesting)))
-
-(defun go-mode-mark-nesting (pos)
- "Mark nesting up to point END. Don't call this directly; use
-`go-mode-nesting'."
-
- (go-mode-cs pos)
- (go-mode-parser
- ;; Mark depth
- (goto-char go-mode-mark-nesting-end)
- (let ((nesting (go-mode-nesting))
- (last (point)))
- (while (< last pos)
- ;; Find the next depth-changing character
- (skip-chars-forward "^(){}[]" pos)
- ;; Mark everything up to this character with the current
- ;; nesting
- (put-text-property last (point) 'go-mode-nesting nesting)
- (when nil
- (let ((depth (length nesting)))
- (put-text-property last (point) 'face
- `((:background
- ,(format "gray%d" (* depth 10)))))))
- (setq last (point))
- ;; Update nesting
- (unless (eobp)
- (let ((ch (unless (go-mode-cs) (char-after))))
- (forward-char 1)
- (case ch
- ((?\( ?\{ ?\[)
- (setq nesting (cons (cons (- (point) 1) nil)
- nesting)))
- ((?\) ?\} ?\])
- (when nesting
- (setcdr (car nesting) (- (point) 1))
- (setq nesting (cdr nesting))))))))
- ;; Update state
- (setq go-mode-mark-nesting-end last))))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Indentation
-;;
-
-(defvar go-mode-non-terminating-keywords-regexp
- (let* ((kws go-mode-keywords)
- (kws (remove "break" kws))
- (kws (remove "continue" kws))
- (kws (remove "fallthrough" kws))
- (kws (remove "return" kws)))
- (regexp-opt kws 'words))
- "Regular expression matching all Go keywords that *do not*
-implicitly terminate a statement.")
-
-(defun go-mode-semicolon-p ()
- "True iff point immediately follows either an explicit or
-implicit semicolon. Point should immediately follow the last
-token on the line."
-
- ;; #Semicolons
- (case (char-before)
- ((?\;) t)
- ;; String literal
- ((?' ?\" ?`) t)
- ;; One of the operators and delimiters ++, --, ), ], or }
- ((?+) (eq (char-before (1- (point))) ?+))
- ((?-) (eq (char-before (1- (point))) ?-))
- ((?\) ?\] ?\}) t)
- ;; An identifier or one of the keywords break, continue,
- ;; fallthrough, or return or a numeric literal
- (otherwise
- (save-excursion
- (when (/= (skip-chars-backward "[:word:]_") 0)
- (not (looking-at go-mode-non-terminating-keywords-regexp)))))))
-
-(defun go-mode-whitespace-p (char)
- "Is char whitespace in the syntax table for go."
- (eq 32 (char-syntax char)))
-
-(defun go-mode-backward-skip-comments ()
- "Skip backward over comments and whitespace."
- ;; only proceed if point is in a comment or white space
- (if (or (go-mode-in-comment)
- (go-mode-whitespace-p (char-after (point))))
- (let ((loop-guard t))
- (while (and
- loop-guard
- (not (bobp)))
-
- (cond ((go-mode-whitespace-p (char-after (point)))
- ;; moves point back over any whitespace
- (re-search-backward "[^[:space:]]"))
-
- ((go-mode-in-comment)
- ;; move point to char preceeding current comment
- (goto-char (1- (car (go-mode-in-comment)))))
-
- ;; not in a comment or whitespace? we must be done.
- (t (setq loop-guard nil)
- (forward-char 1)))))))
-
-(defun go-mode-indentation ()
- "Compute the ideal indentation level of the current line.
-
-To the first order, this is the brace depth of the current line,
-plus parens that follow certain keywords. case, default, and
-labels are outdented one level, and continuation lines are
-indented one level."
-
+(defmacro go-paren-level ()
+ `(car (syntax-ppss)))
+
+(defmacro go-in-string-or-comment-p ()
+ `(nth 8 (syntax-ppss)))
+
+(defmacro go-in-string-p ()
+ `(nth 3 (syntax-ppss)))
+
+(defmacro go-in-comment-p ()
+ `(nth 4 (syntax-ppss)))
+
+(defmacro go-goto-beginning-of-string-or-comment ()
+ `(goto-char (nth 8 (syntax-ppss))))
+
+(defun go--backward-irrelevant (&optional stop-at-string)
+ "Skips backwards over any characters that are irrelevant for
+indentation and related tasks.
+
+It skips over whitespace, comments, cases and labels and, if
+STOP-AT-STRING is not true, over strings."
+
+ (let (pos (start-pos (point)))
+ (skip-chars-backward "\n[:blank:]")
+ (if (and (save-excursion (beginning-of-line) (go-in-string-p)) (looking-back "`") (not stop-at-string))
+ (backward-char))
+ (if (and (go-in-string-p) (not stop-at-string))
+ (go-goto-beginning-of-string-or-comment))
+ (if (looking-back "\\*/")
+ (backward-char))
+ (if (go-in-comment-p)
+ (go-goto-beginning-of-string-or-comment))
+ (setq pos (point))
+ (beginning-of-line)
+ (if (or (looking-at (concat "^" go-label-regexp ":")) (looking-at "^[[:space:]]*\\(case .+\\|default\\):"))
+ (end-of-line 0)
+ (goto-char pos))
+ (if (/= start-pos (point))
+ (go--backward-irrelevant stop-at-string))
+ (/= start-pos (point))))
+
+(defun go--buffer-narrowed-p ()
+ "Return non-nil if the current buffer is narrowed."
+ (/= (buffer-size)
+ (- (point-max)
+ (point-min))))
+
+(defun go-previous-line-has-dangling-op-p ()
+ "Returns non-nil if the current line is a continuation line."
+ (let* ((cur-line (line-number-at-pos))
+ (val (gethash cur-line go-dangling-cache 'nope)))
+ (if (or (go--buffer-narrowed-p) (equal val 'nope))
+ (save-excursion
+ (beginning-of-line)
+ (go--backward-irrelevant t)
+ (setq val (looking-back go-dangling-operators-regexp))
+ (if (not (go--buffer-narrowed-p))
+ (puthash cur-line val go-dangling-cache))))
+ val))
+
+(defun go-goto-opening-parenthesis (&optional char)
+ (let ((start-nesting (go-paren-level)))
+ (while (and (not (bobp))
+ (>= (go-paren-level) start-nesting))
+ (if (zerop (skip-chars-backward
+ (if char
+ (case char (?\] "^[") (?\} "^{") (?\) "^("))
+ "^[{(")))
+ (if (go-in-string-or-comment-p)
+ (go-goto-beginning-of-string-or-comment)
+ (backward-char))))))
+
+(defun go-indentation-at-point ()
(save-excursion
- (back-to-indentation)
- (let ((cs (go-mode-cs)))
- ;; Treat comments and strings differently only if the beginning
- ;; of the line is contained within them
- (when (and cs (= (point) (car cs)))
- (setq cs nil))
- ;; What type of context am I in?
+ (let (start-nesting (outindent 0))
+ (back-to-indentation)
+ (setq start-nesting (go-paren-level))
+
(cond
- ((and cs (save-excursion
- (goto-char (car cs))
- (looking-at "`")))
- ;; Inside a multi-line string. Don't mess with indentation.
- nil)
- (cs
- ;; Inside a general comment
- (goto-char (car cs))
- (forward-char 1)
- (current-column))
+ ((go-in-string-p)
+ (current-indentation))
+ ((looking-at "[])}]")
+ (go-goto-opening-parenthesis (char-after))
+ (if (go-previous-line-has-dangling-op-p)
+ (- (current-indentation) tab-width)
+ (current-indentation)))
+ ((progn (go--backward-irrelevant t) (looking-back go-dangling-operators-regexp))
+ ;; only one nesting for all dangling operators in one operation
+ (if (go-previous-line-has-dangling-op-p)
+ (current-indentation)
+ (+ (current-indentation) tab-width)))
+ ((zerop (go-paren-level))
+ 0)
+ ((progn (go-goto-opening-parenthesis) (< (go-paren-level) start-nesting))
+ (if (go-previous-line-has-dangling-op-p)
+ (current-indentation)
+ (+ (current-indentation) tab-width)))
(t
- ;; Not in a multi-line string or comment
- (let ((indent 0)
- (inside-indenting-paren nil))
- ;; Count every enclosing brace, plus parens that follow
- ;; import, const, var, or type and indent according to
- ;; depth. This simple rule does quite well, but also has a
- ;; very large extent. It would be better if we could mimic
- ;; some nearby indentation.
- (save-excursion
- (skip-chars-forward "})")
- (let ((first t))
- (dolist (nest (go-mode-nesting))
- (case (char-after (car nest))
- ((?\{)
- (incf indent tab-width))
- ((?\()
- (goto-char (car nest))
- (go-mode-backward-skip-comments)
- (backward-char)
- ;; Really just want the token before
- (when (looking-back "\\<import\\|const\\|var\\|type\\|package"
- (max (- (point) 7) (point-min)))
- (incf indent tab-width)
- (when first
- (setq inside-indenting-paren t)))))
- (setq first nil))))
-
- ;; case, default, and labels are outdented 1 level
- (when (looking-at "\\<case\\>\\|\\<default\\>\\|\\w+\\s *:\\(\\S.\\|$\\)")
- (decf indent tab-width))
-
- (when (looking-at "\\w+\\s *:.+,\\s *$")
- (incf indent tab-width))
-
- ;; Continuation lines are indented 1 level
- (beginning-of-line) ; back up to end of previous line
- (backward-char)
- (go-mode-backward-skip-comments) ; back up past any comments
- (when (case (char-before)
- ((nil ?\{ ?:)
- ;; At the beginning of a block or the statement
- ;; following a label.
- nil)
- ((?\()
- ;; Usually a continuation line in an expression,
- ;; unless this paren is part of a factored
- ;; declaration.
- (not inside-indenting-paren))
- ((?,)
- ;; Could be inside a literal. We're a little
- ;; conservative here and consider any comma within
- ;; curly braces (as opposed to parens) to be a
- ;; literal separator. This will fail to recognize
- ;; line-breaks in parallel assignments as
- ;; continuation lines.
- (let ((depth (go-mode-nesting)))
- (and depth
- (not (eq (char-after (caar depth)) ?\{)))))
- (t
- ;; We're in the middle of a block. Did the
- ;; previous line end with an implicit or explicit
- ;; semicolon?
- (not (go-mode-semicolon-p))))
- (incf indent tab-width))
-
- (max indent 0)))))))
+ (current-indentation))))))
(defun go-mode-indent-line ()
- "Indent the current line according to `go-mode-indentation'."
(interactive)
-
- ;; turn off case folding to distinguish keywords from identifiers
- ;; e.g. "default" is a keyword; "Default" can be a variable name.
- (let ((case-fold-search nil))
- (let ((col (go-mode-indentation)))
- (when col
- (let ((offset (- (current-column) (current-indentation))))
- (indent-line-to col)
- (when (> offset 0)
- (forward-char offset)))))))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-;; Go mode
-;;
+ (let (indent
+ shift-amt
+ end
+ (pos (- (point-max) (point)))
+ (point (point))
+ (beg (line-beginning-position)))
+ (back-to-indentation)
+ (if (go-in-string-or-comment-p)
+ (goto-char point)
+ (setq indent (go-indentation-at-point))
+ (if (looking-at (concat go-label-regexp ":\\([[:space:]]*/.+\\)?$\\|case .+:\\|default:"))
+ (decf indent tab-width))
+ (setq shift-amt (- indent (current-column)))
+ (if (zerop shift-amt)
+ nil
+ (delete-region beg (point))
+ (indent-to indent))
+ ;; If initial point was within line's indentation,
+ ;; position after the indentation. Else stay at same point in text.
+ (if (> (- (point-max) pos) (point))
+ (goto-char (- (point-max) pos))))))
+
+(defun go-beginning-of-defun (&optional count)
+ (unless count (setq count 1))
+ (let ((first t) failure)
+ (dotimes (i (abs count))
+ (while (and (not failure)
+ (or first (go-in-string-or-comment-p)))
+ (if (>= count 0)
+ (progn
+ (go--backward-irrelevant)
+ (if (not (re-search-backward go-func-meth-regexp nil t))
+ (setq failure t)))
+ (if (looking-at go-func-meth-regexp)
+ (forward-char))
+ (if (not (re-search-forward go-func-meth-regexp nil t))
+ (setq failure t)))
+ (setq first nil)))
+ (if (< count 0)
+ (beginning-of-line))
+ (not failure)))
+
+(defun go-end-of-defun ()
+ (let (orig-level)
+ ;; It can happen that we're not placed before a function by emacs
+ (if (not (looking-at "func"))
+ (go-beginning-of-defun -1))
+ (skip-chars-forward "^{")
+ (forward-char)
+ (setq orig-level (go-paren-level))
+ (while (>= (go-paren-level) orig-level)
+ (skip-chars-forward "^}")
+ (forward-char))))
;;;###autoload
-(define-derived-mode go-mode nil "Go"
+(define-derived-mode go-mode fundamental-mode "Go"
"Major mode for editing Go source text.
-This provides basic syntax highlighting for keywords, built-ins,
-functions, and some types. It also provides indentation that is
-\(almost) identical to gofmt."
+This mode provides (not just) basic editing capabilities for
+working with Go code. It offers almost complete syntax
+highlighting, indentation that is almost identical to gofmt and
+proper parsing of the buffer content to allow features such as
+navigation by function, manipulation of comments or detection of
+strings.
+
+In addition to these core features, it offers various features to
+help with writing Go code. You can directly run buffer content
+through gofmt, read godoc documentation from within Emacs, modify
+and clean up the list of package imports or interact with the
+Playground (uploading and downloading pastes).
+
+The following extra functions are defined:
+
+- `gofmt'
+- `godoc'
+- `go-import-add'
+- `go-remove-unused-imports'
+- `go-goto-imports'
+- `go-play-buffer' and `go-play-region'
+- `go-download-play'
+
+If you want to automatically run `gofmt' before saving a file,
+add the following hook to your emacs configuration:
+
+\(add-hook 'before-save-hook 'gofmt-before-save)
+
+If you're looking for even more integration with Go, namely
+on-the-fly syntax checking, auto-completion and snippets, it is
+recommended that you look at goflymake
+\(https://github.com/dougm/goflymake), gocode
+\(https://github.com/nsf/gocode) and yasnippet-go
+\(https://github.com/dominikh/yasnippet-go)"
;; Font lock
(set (make-local-variable 'font-lock-defaults)
- '(go-mode-font-lock-keywords nil nil nil nil))
-
- ;; Remove stale text properties
- (save-restriction
- (widen)
- (let ((modified (buffer-modified-p)))
- (remove-text-properties 1 (point-max)
- '(go-mode-cs nil go-mode-nesting nil))
- ;; remove-text-properties marks the buffer modified. undo that if it
- ;; wasn't originally marked modified.
- (set-buffer-modified-p modified)))
-
- ;; Reset the syntax mark caches
- (setq go-mode-mark-cs-end 1
- go-mode-mark-nesting-end 1)
- (add-hook 'before-change-functions #'go-mode-mark-clear-cache nil t)
- (add-hook 'after-change-functions #'go-mode-mark-clear-cs nil t)
+ '(go--build-font-lock-keywords))
;; Indentation
- (set (make-local-variable 'indent-line-function)
- #'go-mode-indent-line)
- (add-hook 'after-change-functions #'go-mode-delayed-electric-hook nil t)
+ (set (make-local-variable 'indent-line-function) 'go-mode-indent-line)
;; Comments
(set (make-local-variable 'comment-start) "// ")
(set (make-local-variable 'comment-end) "")
- (set (make-local-variable 'comment-use-syntax) nil)
- (set (make-local-variable 'comment-start-skip) "\\([ \t]*\\)// ")
+ (set (make-local-variable 'comment-use-syntax) t)
+ (set (make-local-variable 'comment-start-skip) "\\(//+\\|/\\*+\\)\\s *")
+
+ (set (make-local-variable 'beginning-of-defun-function) 'go-beginning-of-defun)
+ (set (make-local-variable 'end-of-defun-function) 'go-end-of-defun)
+
+ (set (make-local-variable 'parse-sexp-lookup-properties) t)
+ (if (boundp 'syntax-propertize-function)
+ (set (make-local-variable 'syntax-propertize-function) 'go-propertize-syntax))
+
+ (set (make-local-variable 'go-dangling-cache) (make-hash-table :test 'eql))
+ (add-hook 'before-change-functions (lambda (x y) (setq go-dangling-cache (make-hash-table :test 'eql))) t t)
+
+
+ (setq imenu-generic-expression
+ '(("type" "^type *\\([^ \t\n\r\f]*\\)" 1)
+ ("func" "^func *\\(.*\\) {" 1)))
+ (imenu-add-to-menubar "Index")
;; Go style
(setq indent-tabs-mode t)
;; those alists are traversed in *reverse* order:
;; http://lists.gnu.org/archive/html/bug-gnu-emacs/2001-12/msg00674.html
(when (and (boundp 'compilation-error-regexp-alist)
- (boundp 'compilation-error-regexp-alist-alist))
- (add-to-list 'compilation-error-regexp-alist 'go-test t)
- (add-to-list 'compilation-error-regexp-alist-alist
- '(go-test . ("^\t+\\([^()\t\n]+\\):\\([0-9]+\\):? .*$" 1 2)) t)))
+ (boundp 'compilation-error-regexp-alist-alist))
+ (add-to-list 'compilation-error-regexp-alist 'go-test t)
+ (add-to-list 'compilation-error-regexp-alist-alist
+ '(go-test . ("^\t+\\([^()\t\n]+\\):\\([0-9]+\\):? .*$" 1 2)) t)))
;;;###autoload
-(add-to-list 'auto-mode-alist (cons "\\.go$" #'go-mode))
+(add-to-list 'auto-mode-alist (cons "\\.go\\'" 'go-mode))
-(defun go-mode-reload ()
- "Reload go-mode.el and put the current buffer into Go mode.
-Useful for development work."
-
- (interactive)
- (unload-feature 'go-mode)
- (require 'go-mode)
- (go-mode))
-
-;;;###autoload
(defun gofmt ()
"Pipe the current buffer through the external tool `gofmt`.
Replace the current buffer on success; display errors on failure."
(patchbuf (get-buffer-create "*Gofmt patch*")))
(with-current-buffer patchbuf
(let ((errbuf (get-buffer-create "*Gofmt Errors*"))
- (coding-system-for-read 'utf-8) ;; use utf-8 with subprocesses
+ ;; use utf-8 with subprocesses
+ (coding-system-for-read 'utf-8)
(coding-system-for-write 'utf-8))
(with-current-buffer errbuf
- (toggle-read-only 0)
+ (setq buffer-read-only nil)
(erase-buffer))
(with-current-buffer srcbuf
(save-restriction
(let (deactivate-mark)
(widen)
- ; If this is a new file, diff-mode can't apply a
- ; patch to a non-exisiting file, so replace the buffer
- ; completely with the output of 'gofmt'.
- ; If the file exists, patch it to keep the 'undo' list happy.
+ ;; If this is a new file, diff-mode can't apply a
+ ;; patch to a non-exisiting file, so replace the buffer
+ ;; completely with the output of 'gofmt'.
+ ;; If the file exists, patch it to keep the 'undo' list happy.
(let* ((newfile (not (file-exists-p filename)))
- (flag (if newfile "" " -d")))
- (if (= 0 (shell-command-on-region (point-min) (point-max)
- (concat "gofmt" flag)
- patchbuf nil errbuf))
- ; gofmt succeeded: replace buffer or apply patch hunks.
+ (flag (if newfile "" " -d")))
+
+ ;; diff-mode doesn't work too well with missing
+ ;; end-of-file newline, so add one
+ (if (/= (char-after (1- (point-max))) ?\n)
+ (save-excursion
+ (goto-char (point-max))
+ (insert ?\n)))
+
+ (if (zerop (shell-command-on-region (point-min) (point-max)
+ (concat "gofmt" flag)
+ patchbuf nil errbuf))
+ ;; gofmt succeeded: replace buffer or apply patch hunks.
(let ((old-point (point))
(old-mark (mark t)))
(kill-buffer errbuf)
(if newfile
- ; New file, replace it (diff-mode won't work)
- (gofmt-replace-buffer srcbuf patchbuf)
- ; Existing file, patch it
- (gofmt-apply-patch filename srcbuf patchbuf))
+ ;; New file, replace it (diff-mode won't work)
+ (gofmt--replace-buffer srcbuf patchbuf)
+ ;; Existing file, patch it
+ (gofmt--apply-patch filename srcbuf patchbuf))
(goto-char (min old-point (point-max)))
;; Restore the mark and point
(if old-mark (push-mark (min old-mark (point-max)) t))
(set-window-configuration currconf))
- ;; gofmt failed: display the errors
- (gofmt-process-errors filename errbuf))))))
+ ;; gofmt failed: display the errors
+ (message "Could not apply gofmt. Check errors for details")
+ (gofmt--process-errors filename errbuf))))))
;; Collapse any window opened on outbuf if shell-command-on-region
;; displayed it.
(delete-windows-on patchbuf)))
(kill-buffer patchbuf))))
-(defun gofmt-replace-buffer (srcbuf patchbuf)
+(defun gofmt--replace-buffer (srcbuf patchbuf)
(with-current-buffer srcbuf
(erase-buffer)
- (insert-buffer-substring patchbuf)))
+ (insert-buffer-substring patchbuf))
+ (message "Applied gofmt"))
-(defconst gofmt-stdin-tag "<standard input>")
-
-(defun gofmt-apply-patch (filename srcbuf patchbuf)
- (require 'diff-mode)
+(defun gofmt--apply-patch (filename srcbuf patchbuf)
;; apply all the patch hunks
- (with-current-buffer patchbuf
- (goto-char (point-min))
- ;; The .* is for TMPDIR, but to avoid dealing with TMPDIR
- ;; having a trailing / or not, it's easier to just search for .*
- ;; especially as we're only replacing the first instance.
- (if (re-search-forward "^--- \\(.*/gofmt[0-9]*\\)" nil t)
- (replace-match filename nil nil nil 1))
- (condition-case nil
- (while t
- (diff-hunk-next)
- (diff-apply-hunk))
- ;; When there's no more hunks, diff-hunk-next signals an error, ignore it
- (error nil))))
-
-(defun gofmt-process-errors (filename errbuf)
+ (let (changed)
+ (with-current-buffer patchbuf
+ (goto-char (point-min))
+ ;; The .* is for TMPDIR, but to avoid dealing with TMPDIR
+ ;; having a trailing / or not, it's easier to just search for .*
+ ;; especially as we're only replacing the first instance.
+ (if (re-search-forward "^--- \\(.*/gofmt[0-9]*\\)" nil t)
+ (replace-match filename nil nil nil 1))
+ (condition-case nil
+ (while t
+ (diff-hunk-next)
+ (diff-apply-hunk)
+ (setq changed t))
+ ;; When there's no more hunks, diff-hunk-next signals an error, ignore it
+ (error nil)))
+ (if changed (message "Applied gofmt") (message "Buffer was already gofmted"))))
+
+(defun gofmt--process-errors (filename errbuf)
;; Convert the gofmt stderr to something understood by the compilation mode.
(with-current-buffer errbuf
(goto-char (point-min))
(insert "gofmt errors:\n")
(if (search-forward gofmt-stdin-tag nil t)
- (replace-match (file-name-nondirectory filename) nil t))
+ (replace-match (file-name-nondirectory filename) nil t))
(display-buffer errbuf)
(compilation-mode)))
;;;###autoload
(defun gofmt-before-save ()
"Add this to .emacs to run gofmt on the current buffer when saving:
- (add-hook 'before-save-hook #'gofmt-before-save)"
+ (add-hook 'before-save-hook 'gofmt-before-save).
+
+Note that this will cause go-mode to get loaded the first time
+you save any file, kind of defeating the point of autoloading."
(interactive)
(when (eq major-mode 'go-mode) (gofmt)))
-(defun godoc-read-query ()
+(defun godoc--read-query ()
"Read a godoc query from the minibuffer."
;; Compute the default query as the symbol under the cursor.
;; TODO: This does the wrong thing for e.g. multipart.NewReader (it only grabs
"godoc: ")
nil nil symbol)))
-(defun godoc-get-buffer (query)
+(defun godoc--get-buffer (query)
"Get an empty buffer for a godoc query."
(let* ((buffer-name (concat "*godoc " query "*"))
(buffer (get-buffer buffer-name)))
(when buffer (kill-buffer buffer))
(get-buffer-create buffer-name)))
-(defun godoc-buffer-sentinel (proc event)
+(defun godoc--buffer-sentinel (proc event)
"Sentinel function run when godoc command completes."
(with-current-buffer (process-buffer proc)
(cond ((string= event "finished\n") ;; Successful exit.
(goto-char (point-min))
- (view-buffer (current-buffer) 'kill-buffer))
- ((not (= (process-exit-status proc) 0)) ;; Error exit.
+ (view-mode 1)
+ (display-buffer (current-buffer) t))
+ ((/= (process-exit-status proc) 0) ;; Error exit.
(let ((output (buffer-string)))
(kill-buffer (current-buffer))
(message (concat "godoc: " output)))))))
;;;###autoload
(defun godoc (query)
"Show go documentation for a query, much like M-x man."
- (interactive (list (godoc-read-query)))
+ (interactive (list (godoc--read-query)))
(unless (string= query "")
(set-process-sentinel
- (start-process-shell-command "godoc" (godoc-get-buffer query)
+ (start-process-shell-command "godoc" (godoc--get-buffer query)
(concat "godoc " query))
- 'godoc-buffer-sentinel)
+ 'godoc--buffer-sentinel)
nil))
+(defun go-goto-imports ()
+ "Move point to the block of imports.
+
+If using
+
+ import (
+ \"foo\"
+ \"bar\"
+ )
+
+it will move point directly behind the last import.
+
+If using
+
+ import \"foo\"
+ import \"bar\"
+
+it will move point to the next line after the last import.
+
+If no imports can be found, point will be moved after the package
+declaration."
+ (interactive)
+ ;; FIXME if there's a block-commented import before the real
+ ;; imports, we'll jump to that one.
+
+ ;; Generally, this function isn't very forgiving. it'll bark on
+ ;; extra whitespace. It works well for clean code.
+ (let ((old-point (point)))
+ (goto-char (point-min))
+ (cond
+ ((re-search-forward "^import ([^)]+)" nil t)
+ (backward-char 2)
+ 'block)
+ ((re-search-forward "\\(^import \\([^\"]+ \\)?\"[^\"]+\"\n?\\)+" nil t)
+ 'single)
+ ((re-search-forward "^[[:space:]\n]*package .+?\n" nil t)
+ (message "No imports found, moving point after package declaration")
+ 'none)
+ (t
+ (goto-char old-point)
+ (message "No imports or package declaration found. Is this really a Go file?")
+ 'fail))))
+
+(defun go-play-buffer ()
+ "Like `go-play-region', but acts on the entire buffer."
+ (interactive)
+ (go-play-region (point-min) (point-max)))
+
+(defun go-play-region (start end)
+ "Send the region to the Playground and stores the resulting
+link in the kill ring."
+ (interactive "r")
+ (let* ((url-request-method "POST")
+ (url-request-extra-headers
+ '(("Content-Type" . "application/x-www-form-urlencoded")))
+ (url-request-data (buffer-substring-no-properties start end))
+ (content-buf (url-retrieve
+ "http://play.golang.org/share"
+ (lambda (arg)
+ (cond
+ ((equal :error (car arg))
+ (signal 'go-play-error (cdr arg)))
+ (t
+ (re-search-forward "\n\n")
+ (kill-new (format "http://play.golang.org/p/%s" (buffer-substring (point) (point-max))))
+ (message "http://play.golang.org/p/%s" (buffer-substring (point) (point-max)))))))))))
+
+;;;###autoload
+(defun go-download-play (url)
+ "Downloads a paste from the playground and inserts it in a Go
+buffer. Tries to look for a URL at point."
+ (interactive (list (read-from-minibuffer "Playground URL: " (ffap-url-p (ffap-string-at-point 'url)))))
+ (with-current-buffer
+ (let ((url-request-method "GET") url-request-data url-request-extra-headers)
+ (url-retrieve-synchronously (concat url ".go")))
+ (let ((buffer (generate-new-buffer (concat (car (last (split-string url "/"))) ".go"))))
+ (goto-char (point-min))
+ (re-search-forward "\n\n")
+ (copy-to-buffer buffer (point) (point-max))
+ (kill-buffer)
+ (with-current-buffer buffer
+ (go-mode)
+ (switch-to-buffer buffer)))))
+
+(defun go-propertize-syntax (start end)
+ (save-excursion
+ (goto-char start)
+ (while (search-forward "\\" end t)
+ (put-text-property (1- (point)) (point) 'syntax-table (if (= (char-after) ?`) '(1) '(9))))))
+
+;; ;; Commented until we actually make use of this function
+;; (defun go--common-prefix (sequences)
+;; ;; mismatch and reduce are cl
+;; (assert sequences)
+;; (flet ((common-prefix (s1 s2)
+;; (let ((diff-pos (mismatch s1 s2)))
+;; (if diff-pos (subseq s1 0 diff-pos) s1))))
+;; (reduce #'common-prefix sequences)))
+
+(defun go-import-add (arg import)
+ "Add a new import to the list of imports.
+
+When called with a prefix argument asks for an alternative name
+to import the package as.
+
+If no list exists yet, one will be created if possible.
+
+If an identical import has been commented, it will be
+uncommented, otherwise a new import will be added."
+
+ ;; - If there's a matching `// import "foo"`, uncomment it
+ ;; - If we're in an import() block and there's a matching `"foo"`, uncomment it
+ ;; - Otherwise add a new import, with the appropriate syntax
+ (interactive
+ (list
+ current-prefix-arg
+ (replace-regexp-in-string "^[\"']\\|[\"']$" "" (completing-read "Package: " (go-packages)))))
+ (save-excursion
+ (let (as line import-start)
+ (if arg
+ (setq as (read-from-minibuffer "Import as: ")))
+ (if as
+ (setq line (format "%s \"%s\"" as import))
+ (setq line (format "\"%s\"" import)))
+
+ (goto-char (point-min))
+ (if (re-search-forward (concat "^[[:space:]]*//[[:space:]]*import " line "$") nil t)
+ (uncomment-region (line-beginning-position) (line-end-position))
+ (case (go-goto-imports)
+ ('fail (message "Could not find a place to add import."))
+ ('block
+ (save-excursion
+ (re-search-backward "^import (")
+ (setq import-start (point)))
+ (if (re-search-backward (concat "^[[:space:]]*//[[:space:]]*" line "$") import-start t)
+ (uncomment-region (line-beginning-position) (line-end-position))
+ (insert "\n\t" line)))
+ ('single (insert "import " line "\n"))
+ ('none (insert "\nimport (\n\t" line "\n)\n")))))))
+
+(defun go-root-and-paths ()
+ (let* ((output (process-lines "go" "env" "GOROOT" "GOPATH"))
+ (root (car output))
+ (paths (split-string (car (cdr output)) ":")))
+ (append (list root) paths)))
+
+(defun go-packages ()
+ (sort
+ (delete-dups
+ (mapcan
+ (lambda (topdir)
+ (let ((pkgdir (concat topdir "/pkg/")))
+ (mapcan (lambda (dir)
+ (mapcar (lambda (file)
+ (let ((sub (substring file (length pkgdir) -2)))
+ (unless (or (string-prefix-p "obj/" sub) (string-prefix-p "tool/" sub))
+ (mapconcat 'identity (cdr (split-string sub "/")) "/"))))
+ (if (file-directory-p dir)
+ (directory-files dir t "\\.a$"))))
+ (if (file-directory-p pkgdir)
+ (find-lisp-find-files-internal pkgdir 'find-lisp-file-predicate-is-directory 'find-lisp-default-directory-predicate)))))
+ (go-root-and-paths)))
+ 'string<))
+
+(defun go-unused-imports-lines ()
+ ;; FIXME Technically, -o /dev/null fails in quite some cases (on
+ ;; Windows, when compiling from within GOPATH). Practically,
+ ;; however, it has the same end result: There won't be a
+ ;; compiled binary/archive, and we'll get our import errors when
+ ;; there are any.
+ (reverse (remove nil
+ (mapcar
+ (lambda (line)
+ (if (string-match "^\\(.+\\):\\([[:digit:]]+\\): imported and not used: \".+\"$" line)
+ (if (string= (file-truename (match-string 1 line)) (file-truename buffer-file-name))
+ (string-to-number (match-string 2 line)))))
+ (split-string (shell-command-to-string
+ (if (string-match "_test\.go$" buffer-file-truename)
+ "go test -c"
+ "go build -o /dev/null")) "\n")))))
+
+(defun go-remove-unused-imports (arg)
+ "Removes all unused imports. If ARG is non-nil, unused imports
+will be commented, otherwise they will be removed completely."
+ (interactive "P")
+ (save-excursion
+ (let ((cur-buffer (current-buffer)) flymake-state lines)
+ (when (boundp 'flymake-mode)
+ (setq flymake-state flymake-mode)
+ (flymake-mode-off))
+ (save-some-buffers nil (lambda () (equal cur-buffer (current-buffer))))
+ (if (buffer-modified-p)
+ (message "Cannot operate on unsaved buffer")
+ (setq lines (go-unused-imports-lines))
+ (dolist (import lines)
+ (goto-char (point-min))
+ (forward-line (1- import))
+ (beginning-of-line)
+ (if arg
+ (comment-region (line-beginning-position) (line-end-position))
+ (let ((kill-whole-line t))
+ (kill-line))))
+ (message "Removed %d imports" (length lines)))
+ (if flymake-state (flymake-mode-on)))))
+
(provide 'go-mode)