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 @@ ...@@ -5,7 +5,6 @@
;; license that can be found in the LICENSE file. ;; license that can be found in the LICENSE file.
(require 'cl) (require 'cl)
(require 'diff-mode)
(require 'ffap) (require 'ffap)
(require 'url) (require 'url)
...@@ -64,7 +63,6 @@ ...@@ -64,7 +63,6 @@
(concat "\\_<" s "\\_>"))) (concat "\\_<" s "\\_>")))
(defconst go-dangling-operators-regexp "[^-]-\\|[^+]\\+\\|[/*&><.=|^]") (defconst go-dangling-operators-regexp "[^-]-\\|[^+]\\+\\|[/*&><.=|^]")
(defconst gofmt-stdin-tag "<standard input>")
(defconst go-identifier-regexp "[[:word:][:multibyte:]]+") (defconst go-identifier-regexp "[[:word:][:multibyte:]]+")
(defconst go-label-regexp go-identifier-regexp) (defconst go-label-regexp go-identifier-regexp)
(defconst go-type-regexp "[[:word:][:multibyte:]*]+") (defconst go-type-regexp "[[:word:][:multibyte:]*]+")
...@@ -418,99 +416,91 @@ recommended that you look at goflymake ...@@ -418,99 +416,91 @@ recommended that you look at goflymake
;;;###autoload ;;;###autoload
(add-to-list 'auto-mode-alist (cons "\\.go\\'" 'go-mode)) (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 () (defun gofmt ()
"Pipe the current buffer through the external tool `gofmt`. "Formats the current buffer according to the gofmt tool."
Replace the current buffer on success; display errors on failure."
(interactive) (interactive)
(let ((currconf (current-window-configuration))) (let ((tmpfile (make-temp-file "gofmt" nil ".go"))
(let ((srcbuf (current-buffer)) (patchbuf (get-buffer-create "*Gofmt patch*"))
(filename buffer-file-name) (errbuf (get-buffer-create "*Gofmt Errors*"))
(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-read 'utf-8)
(coding-system-for-write 'utf-8)) (coding-system-for-write 'utf-8))
(with-current-buffer errbuf (with-current-buffer errbuf
(setq buffer-read-only nil) (setq buffer-read-only nil)
(erase-buffer)) (erase-buffer))
(with-current-buffer srcbuf (with-current-buffer patchbuf
(save-restriction (erase-buffer))
(let (deactivate-mark)
(widen) (write-region nil nil tmpfile)
;; If this is a new file, diff-mode can't apply a
;; patch to a non-exisiting file, so replace the buffer ;; We're using errbuf for the mixed stdout and stderr output. This
;; completely with the output of 'gofmt'. ;; is not an issue because gofmt -w does not produce any stdout
;; If the file exists, patch it to keep the 'undo' list happy. ;; output in case of success.
(let* ((newfile (not (file-exists-p filename))) (if (zerop (call-process "gofmt" nil errbuf nil "-w" tmpfile))
(flag (if newfile "" " -d"))) (if (zerop (call-process-region (point-min) (point-max) "diff" nil patchbuf nil "-n" "-" tmpfile))
(message "Buffer is already gofmted")
;; diff-mode doesn't work too well with missing (go--apply-rcs-patch patchbuf)
;; 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) (kill-buffer errbuf)
(if newfile (message "Applied gofmt"))
;; 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") (message "Could not apply gofmt. Check errors for details")
(gofmt--process-errors filename errbuf)))))) (gofmt--process-errors (buffer-file-name) tmpfile errbuf))
;; Collapse any window opened on outbuf if shell-command-on-region (kill-buffer patchbuf)
;; displayed it. (delete-file tmpfile)))
(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) (defun gofmt--process-errors (filename tmpfile errbuf)
;; apply all the patch hunks
(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. ;; Convert the gofmt stderr to something understood by the compilation mode.
(with-current-buffer errbuf (with-current-buffer errbuf
(goto-char (point-min)) (goto-char (point-min))
(insert "gofmt errors:\n") (insert "gofmt errors:\n")
(if (search-forward gofmt-stdin-tag nil t) (while (search-forward-regexp (concat "^\\(" (regexp-quote tmpfile) "\\):") nil t)
(replace-match (file-name-nondirectory filename) nil t)) (replace-match (file-name-nondirectory filename) t t nil 1))
(display-buffer errbuf) (display-buffer errbuf)
(compilation-mode))) (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