feat(emacs): rebuild format.el

This is the first implementation of the fmt-mode built with 'reformatter'. This
removes the CLI tool that was calling all the different formatters and replaces
it with an emacs mode.

The CLI tool was an attempt to create a common CLI tool to format code. In
reality this just became hard to maintain and was only ever used in the emacs
formatter. To format code in the CLI I was just using the upstream tools.

See: https://github.com/purcell/emacs-reformatter
This commit is contained in:
Ade Attwood 2022-03-13 20:36:11 +00:00
parent 7c184d8616
commit 4760acc065

View file

@ -5,131 +5,97 @@
;; Use of this source is governed by a BSD-style ;; Use of this source is governed by a BSD-style
;; licence that can be found in the LICENCE file or at ;; licence that can be found in the LICENCE file or at
;; https://www.practically.io/copyright/ ;; https://www.practically.io/copyright/
;;
;;; Commentary:
;;
;;; Code:
(defun fmt--goto-line (line) ;; Ensure the reformatter package is installed
"Move cursor to line LINE." (quelpa '(reformatter :fetcher git :url "https://github.com/purcell/emacs-reformatter"))
(goto-char (point-min)) (require 'reformatter)
(forward-line (1- line))) (require 'projectile)
(defcustom fmt-show-errors 'buffer (defun fmt--find-prettier ()
"Where to display prettier error output. "Find the 'prittier' executable.
It can either be displayed in its own buffer, in the echo area, or not at all. This will be found from the project node_modules with a fall back to the
Please note that Emacs outputs to the echo area when writing globally installed package"
files and will overwrite prettier's echo output if used from inside (let ((project-prittier (concat (projectile-project-root) "node_modules/.bin/prettier")))
a `before-save-hook'." (message project-prittier)
:type '(choice (if (file-exists-p project-prittier)
(const :tag "Own buffer" buffer) project-prittier
(const :tag "Echo area" echo) "prettier")))
(const :tag "None" nil))
:group 'fmt)
(defun fmt--apply-rcs-patch (patch-buffer) (defun fmt--find-phpcbf ()
"Apply an RCS-formatted diff from PATCH-BUFFER to the current buffer." "Find the phpcbf executable.
(let ((target-buffer (current-buffer)) It will look first in the locally installed version in your vendor folder, if
;; Relative offset between buffer line numbers and line numbers that cant be fond then it will try and use the globally installed executable"
;; in patch. (let ((project-prittier (concat (projectile-project-root) "vendor/bin/phpcbf")))
;; (message project-prittier)
;; Line numbers in the patch are based on the source file, so (if (file-exists-p project-prittier)
;; we have to keep an offset when making changes to the project-prittier
;; buffer. "phpcbf")))
;;
;; 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 fmt--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
(setq line-offset (- line-offset len))
(goto-char (point-min))
(forward-line (- from len line-offset))
(insert text)))))
((equal action "d")
(with-current-buffer target-buffer
(fmt--goto-line (- from line-offset))
(setq line-offset (+ line-offset len))
(let ((beg (point)))
(forward-line len)
(delete-region (point) beg))))
(t
(error "Invalid rcs patch or internal error in fmt--apply-rcs-patch")))))))))
(defun fmt--process-errors (filename errorfile errbuf) (defun fmt--find-php-ruleset ()
"Process errors for FILENAME, using an ERRORFILE and display the output in ERRBUF." "Find the phpcs ruleset for the current project.
(with-current-buffer errbuf If there is a 'ruleset.xml' file in your project root that will be used, if not
(if (eq fmt-show-errors 'echo) then the 'psr2' ruleset will be used"
(progn (let ((project-prittier (concat (projectile-project-root) "ruleset.xml")))
(message "%s" (buffer-string)) (message project-prittier)
(fmt--kill-error-buffer errbuf)) (if (file-exists-p project-prittier)
(insert-file-contents errorfile nil nil nil) project-prittier
;; Convert the prettier stderr to something understood by the compilation mode. "psr2")))
(goto-char (point-min))
(insert "fmt errors:\n")
(while (search-forward-regexp "^stdin" nil t)
(replace-match (file-name-nondirectory filename)))
(compilation-mode)
(display-buffer errbuf))))
(defun fmt--kill-error-buffer (errbuf) (reformatter-define prettier-fmt
"Kill buffer ERRBUF." :program (fmt--find-prettier)
(let ((win (get-buffer-window errbuf))) :args (list "--stdin-filepath" (buffer-file-name))
(if win :group 'fmt
(quit-window t win) :lighter " PrettierFMT")
(with-current-buffer errbuf
(erase-buffer)) (reformatter-define phpcbf-fmt
(kill-buffer errbuf)))) :program (fmt--find-phpcbf)
:args (list (format "--standard=%s" (fmt--find-php-ruleset)) "-")
:group 'fmt
:exit-code-success-p (lambda (number) (= 1 number))
:lighter " PhpCbfFMT")
(reformatter-define clang-fmt
:program "clang-format"
:args (list "--assume-filename" (buffer-file-name))
:group 'fmt
:lighter " ClangFMT")
;; Define our own jsonnet formatter. This is way faster then the
;; 'jsonnet-reformat-buffer command that comes with jsonnet-mode
(reformatter-define jsonnet-fmt
:program "jsonnetfmt"
:args (list "-")
:group 'fmt
:lighter " JsonnetFMT")
;; Set the gofmt command to be goimports we can use the built in functions in
;; go-mode they work just fine
(setq gofmt-command "goimports")
(defun fmt-buffer () (defun fmt-buffer ()
"Format the current buffer according to the fmt tool." "Format the current buffer."
(interactive) (interactive)
(let* ((ext (file-name-extension buffer-file-name t)) (cond
(bufferfile (make-temp-file "fmt" nil ext)) ((eq major-mode 'c++-mode) (clang-fmt))
(outputfile (make-temp-file "fmt" nil ext)) ((eq major-mode 'go-mode) (gofmt))
(errorfile (make-temp-file "fmt" nil ext)) ((eq major-mode 'js2-mode) (prettier-fmt))
(errbuf (if fmt-show-errors (get-buffer-create "*fmt errors*"))) ((eq major-mode 'jsonnet-mode) (jsonnet-fmt))
(patchbuf (get-buffer-create "*fmt patch*")) ((eq major-mode 'markdown-mode) (prettier-fmt))
(coding-system-for-read 'utf-8) ((eq major-mode 'php-mode) (phpcbf-fmt))
(coding-system-for-write 'utf-8)) ((eq major-mode 'typescript-mode) (prettier-fmt))
(unwind-protect ((eq major-mode 'css-mode) (prettier-fmt))
(save-restriction ((eq major-mode 'scss-mode) (prettier-fmt))
(widen) ((eq major-mode 'typescript-tsx-mode) (prettier-fmt))
(write-region nil nil bufferfile) ((eq major-mode 'yaml-mode) (prettier-fmt))
(if errbuf ((eq 1 1) (message "No formatter found"))))
(with-current-buffer errbuf
(setq buffer-read-only nil)
(erase-buffer)))
(with-current-buffer patchbuf
(erase-buffer))
(if (zerop (apply 'call-process "fmtcli" bufferfile (list (list :file outputfile) errorfile) nil (list "-input" bufferfile "-formatting_file" buffer-file-name)))
(progn
(call-process-region (point-min) (point-max) "diff" nil patchbuf nil "-n" "--strip-trailing-cr" "-"
outputfile)
(fmt--apply-rcs-patch patchbuf)
(if errbuf (fmt--kill-error-buffer errbuf)))
(message "Could not apply fmt")
(if errbuf
(fmt--process-errors (buffer-file-name) errorfile errbuf))))
(kill-buffer patchbuf)
(delete-file errorfile)
(delete-file bufferfile)
(delete-file outputfile))))
;;;###autoload ;;;###autoload
(define-minor-mode fmt-mode (define-minor-mode fmt-mode
"Runs fmt on file save when this mode is turned on" "Run fmt on file save when this mode is turned on."
:lighter " fmt" :lighter " fmt"
:global nil :global nil
(if fmt-mode (if fmt-mode
@ -138,3 +104,7 @@ a `before-save-hook'."
(define-globalized-minor-mode global-fmt-mode fmt-mode (define-globalized-minor-mode global-fmt-mode fmt-mode
(lambda () (fmt-mode 1))) (lambda () (fmt-mode 1)))
(provide 'format)
;;; format.el ends here