diff --git a/misc/emacs/go-mode.el b/misc/emacs/go-mode.el index 973ee876a6b..68ded4906f6 100644 --- a/misc/emacs/go-mode.el +++ b/misc/emacs/go-mode.el @@ -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 "") (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"))) + (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)) - ;; 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) + (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")))) + (erase-buffer)) -(defun gofmt--process-errors (filename errbuf) + (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)))