Commit 77ddbf1f authored by Dominik Honnef's avatar Dominik Honnef Committed by Alan Donovan

misc/emacs: Rewrite gofmt to use own function for applying patch instead of using diff-mode.

    Instead of relying on gofmt's diff output (which is a unified
    diff), we manually invoke diff -n and produce an RCS format diff,
    which can easily be parsed in Emacs, with the go--apply-rcs-patch
    function.

    This fixes undocumented issues with the old implementation such as
    skipping over hunks of changes, and it fixes the documented issue
    of not being able to handle file names that include whitespace.

    It can also apply the patch on a buffer that has no file name
    attached at all.

    Last but not least, it greatly simplifies the gofmt function
    itself.

Fixes #4766.
Fixes #4475.

R=adonovan, cw, patrick.allen.higgins, sameer
CC=golang-dev
https://golang.org/cl/7516046
parent b000f228
......@@ -5,7 +5,6 @@
;; license that can be found in the LICENSE file.
(require 'cl)
(require 'diff-mode)
(require 'ffap)
(require 'url)
......@@ -64,7 +63,6 @@
(concat "\\_<" s "\\_>")))
(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:]*]+")
......@@ -418,99 +416,91 @@ recommended that you look at goflymake
;;;###autoload
(add-to-list 'auto-mode-alist (cons "\\.go\\'" 'go-mode))
(defun go--apply-rcs-patch (patch-buffer)
"Apply an RCS-formatted diff from PATCH-BUFFER to the current
buffer."
(let ((target-buffer (current-buffer))
;; Relative offset between buffer line numbers and line numbers
;; in patch.
;;
;; Line numbers in the patch are based on the source file, so
;; we have to keep an offset when making changes to the
;; buffer.
;;
;; Appending lines decrements the offset (possibly making it
;; negative), deleting lines increments it. This order
;; simplifies the forward-line invocations.
(line-offset 0))
(save-excursion
(with-current-buffer patch-buffer
(goto-char (point-min))
(while (not (eobp))
(unless (looking-at "^\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)")
(error "invalid rcs patch or internal error in go--apply-rcs-patch"))
(forward-line)
(let ((action (match-string 1))
(from (string-to-number (match-string 2)))
(len (string-to-number (match-string 3))))
(cond
((equal action "a")
(let ((start (point)))
(forward-line len)
(let ((text (buffer-substring start (point))))
(with-current-buffer target-buffer
(decf line-offset len)
(goto-char (point-min))
(forward-line (- from len line-offset))
(insert text)))))
((equal action "d")
(with-current-buffer target-buffer
(goto-char (point-min))
(forward-line (- from line-offset 1))
(incf line-offset len)
(go--kill-whole-line len)))
(t
(error "invalid rcs patch or internal error in go--apply-rcs-patch")))))))))
(defun gofmt ()
"Pipe the current buffer through the external tool `gofmt`.
Replace the current buffer on success; display errors on failure."
"Formats the current buffer according to the gofmt tool."
(interactive)
(let ((currconf (current-window-configuration)))
(let ((srcbuf (current-buffer))
(filename buffer-file-name)
(patchbuf (get-buffer-create "*Gofmt patch*")))
(with-current-buffer patchbuf
(let ((errbuf (get-buffer-create "*Gofmt Errors*"))
;; use utf-8 with subprocesses
(coding-system-for-read 'utf-8)
(coding-system-for-write 'utf-8))
(with-current-buffer errbuf
(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.
(let* ((newfile (not (file-exists-p filename)))
(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))
(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
(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)
(with-current-buffer srcbuf
(erase-buffer)
(insert-buffer-substring patchbuf))
(message "Applied gofmt"))
(defun gofmt--apply-patch (filename srcbuf patchbuf)
;; apply all the patch hunks
(let (changed)
(let ((tmpfile (make-temp-file "gofmt" nil ".go"))
(patchbuf (get-buffer-create "*Gofmt patch*"))
(errbuf (get-buffer-create "*Gofmt Errors*"))
(coding-system-for-read 'utf-8)
(coding-system-for-write 'utf-8))
(with-current-buffer errbuf
(setq buffer-read-only nil)
(erase-buffer))
(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)
(erase-buffer))
(write-region nil nil tmpfile)
;; We're using errbuf for the mixed stdout and stderr output. This
;; is not an issue because gofmt -w does not produce any stdout
;; output in case of success.
(if (zerop (call-process "gofmt" nil errbuf nil "-w" tmpfile))
(if (zerop (call-process-region (point-min) (point-max) "diff" nil patchbuf nil "-n" "-" tmpfile))
(message "Buffer is already gofmted")
(go--apply-rcs-patch patchbuf)
(kill-buffer errbuf)
(message "Applied gofmt"))
(message "Could not apply gofmt. Check errors for details")
(gofmt--process-errors (buffer-file-name) tmpfile errbuf))
(kill-buffer patchbuf)
(delete-file tmpfile)))
(defun gofmt--process-errors (filename tmpfile 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))
(while (search-forward-regexp (concat "^\\(" (regexp-quote tmpfile) "\\):") nil t)
(replace-match (file-name-nondirectory filename) t t nil 1))
(display-buffer errbuf)
(compilation-mode)))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment