From b302d68ebcc2500e6e013ff5fed9c70b0e1ff3a4 Mon Sep 17 00:00:00 2001 From: Dominik Honnef Date: Tue, 26 Feb 2013 13:48:32 -0500 Subject: [PATCH] misc/emacs: Greatly improve go-mode for Emacs. The original go-mode is plagued with odd behaviour, lack of behaviour typical to modes in Emacs and bugs. This change rewrites great parts of go-mode (basically only keeping the gofmt and godoc functions). Additionally it adds new features such as manipulating package imports. For more information please see https://groups.google.com/group/golang-nuts/browse_frm/thread/3a9d6dae3369c0b5/1efe65e2f7afb190 Fixes #3618. Fixes #4240. Fixes #4322. Fixes #4671. Fixes #4726. R=golang-dev, fullung, sameer, cw, arthur, proppy, adonovan, rsc, bradfitz CC=golang-dev https://golang.org/cl/7314113 --- misc/emacs/go-mode-load.el | 88 ++- misc/emacs/go-mode.el | 1261 ++++++++++++++++-------------------- 2 files changed, 610 insertions(+), 739 deletions(-) diff --git a/misc/emacs/go-mode-load.el b/misc/emacs/go-mode-load.el index d453166a45c..3fc35c1169d 100644 --- a/misc/emacs/go-mode-load.el +++ b/misc/emacs/go-mode-load.el @@ -1,50 +1,96 @@ -;;; go-mode-load.el --- Major mode for the Go programming language - +;;; go-mode-load.el --- automatically extracted autoloads ;;; Commentary: ;; To install go-mode, add the following lines to your .emacs file: ;; (add-to-list 'load-path "PATH CONTAINING go-mode-load.el" t) ;; (require 'go-mode-load) +;; ;; After this, go-mode will be used for files ending in '.go'. - +;; ;; To compile go-mode from the command line, run the following ;; emacs -batch -f batch-byte-compile go-mode.el - +;; ;; See go-mode.el for documentation. - -;;; Code: - +;; ;; To update this file, evaluate the following form ;; (let ((generated-autoload-file buffer-file-name)) (update-file-autoloads "go-mode.el")) +;;; Code: + -;;;### (autoloads (gofmt-before-save gofmt go-mode) "go-mode" "go-mode.el" -;;;;;; (19917 17808)) +;;;### (autoloads (go-download-play godoc gofmt-before-save go-mode) +;;;;;; "go-mode" "go-mode.el" (20767 50749)) ;;; Generated autoloads from go-mode.el (autoload 'go-mode "go-mode" "\ 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, +proper parsing of the buffer content to allow features such as +navigation by function, manipulation of comments or detection of +strings. + +Additionally 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 to look at goflymake +\(https://github.com/dougm/goflymake), gocode +\(https://github.com/nsf/gocode) and yasnippet-go +\(https://github.com/dominikh/yasnippet-go) \(fn)" t nil) -(add-to-list 'auto-mode-alist (cons "\\.go$" #'go-mode)) - -(autoload 'gofmt "go-mode" "\ -Pipe the current buffer through the external tool `gofmt`. -Replace the current buffer on success; display errors on failure. - -\(fn)" t nil) +(add-to-list 'auto-mode-alist (cons "\\.go\\'" 'go-mode)) (autoload 'gofmt-before-save "go-mode" "\ 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. \(fn)" t nil) -;;;*** +(autoload 'godoc "go-mode" "\ +Show go documentation for a query, much like M-x man. +\(fn QUERY)" t nil) + +(autoload 'go-download-play "go-mode" "\ +Downloads a paste from the playground and inserts it in a Go +buffer. Tries to look for a URL at point. + +\(fn URL)" t nil) + +;;;*** + (provide 'go-mode-load) +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; coding: utf-8 +;; End: +;;; go-mode-load.el ends here diff --git a/misc/emacs/go-mode.el b/misc/emacs/go-mode.el index 6f680b24ada..8a16d8a4f2e 100644 --- a/misc/emacs/go-mode.el +++ b/misc/emacs/go-mode.el @@ -1,40 +1,54 @@ ;;; 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 "") +(defconst go-identifier-regexp "[[:word:][:multibyte:]_]+") +(defconst go-label-regexp go-identifier-regexp) +(defconst go-type-regexp "[[:word:][:multibyte:]_*]+") +(defconst go-func-regexp (concat "\\\\s *\\(" go-identifier-regexp "\\)")) +(defconst go-func-meth-regexp (concat "\\\\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.") -;; * 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 +(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.") -;;; Code: +(defconst go-constants '("nil" "true" "false" "iota")) +(defconst go-type-name-regexp (concat "\\(?:[*(]\\)*\\(?:" go-identifier-regexp "\\.\\)?\\(" go-identifier-regexp "\\)")) -(eval-when-compile (require 'cl)) +(defvar go-dangling-cache) + +(defgroup go nil + "Major mode for editing Go code" + :group 'languages) + +(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) @@ -43,77 +57,56 @@ (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.") +(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 -(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 - ("\\\\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 - ("\\\\s *\\(\\w+\\)" 1 font-lock-type-face) - (,(concat "\\\\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 "\\\\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 "\\\\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.") + (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 -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Key map -;; + `( + ("\\[[:space:]]*\\([^[:space:]]+\\)" 1 font-lock-type-face) ;; types + (,(concat "\\[[: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 "\\[[: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 "\\[[: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.") @@ -124,607 +117,229 @@ built-ins, functions, and some types.") (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) +(defmacro go-paren-level () + `(car (syntax-ppss))) -(defun go-mode-delayed-electric (p) - "Perform electric insertion, but delayed by one event. +(defmacro go-in-string-or-comment-p () + `(nth 8 (syntax-ppss))) -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." +(defmacro go-in-string-p () + `(nth 3 (syntax-ppss))) - (interactive "p") - (self-insert-command p) - (setq go-mode-delayed-point (point))) +(defmacro go-in-comment-p () + `(nth 4 (syntax-ppss))) -(defun go-mode-delayed-electric-hook (b e l) - "An after-change-function that implements `go-mode-delayed-electric'." +(defmacro go-goto-beginning-of-string-or-comment () + `(goto-char (nth 8 (syntax-ppss)))) - (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)) +(defun go--backward-irrelevant (&optional stop-at-string) + "Skips backwards over any characters that are irrelevant for +indentation and related tasks. -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Parser -;; +It skips over whitespace, comments, cases and labels and, if +STOP-AT-STRING is not true, over strings." -(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) + (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)))) -(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) +(defun go--buffer-narrowed-p () + "Return non-nil if the current buffer is narrowed." + (/= (buffer-size) + (- (point-max) + (point-min)))) -(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) +(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)) -(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." +(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 "\\\\|\\\\|\\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) + (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)))))) - ;; 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))))))) +(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))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Go mode -;; +(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) @@ -737,24 +352,14 @@ functions, and some types. It also provides indentation that is ;; 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." @@ -766,89 +371,103 @@ 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 "") - -(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)))) + (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) +(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 @@ -862,7 +481,7 @@ Replace the current buffer on success; display errors on failure." "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))) @@ -870,13 +489,14 @@ Replace the current buffer on success; display errors on failure." (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))))))) @@ -884,12 +504,217 @@ Replace the current buffer on success; display errors on failure." ;;;###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)