From 9b745df2fabff6edad07f0c50cb6821d6d48d1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ellis=20Keny=C5=91?= Date: Sat, 3 Sep 2022 19:22:35 +0100 Subject: [PATCH] Add emacs-lisp formatting (#102) * feat: add emacs-lisp formatting * Disable indent-tabs-mode * Add stub file for installation * Fix lint errors * fix: correctly format based on previous mode * Formatting * Fix weird indent * Add checkindent target * Update changelog * Long line * Empty commit * fix ci * revert changelog reformatting * more changelog * more Co-authored-by: Radon Rosborough Co-authored-by: Radon Rosborough --- .github/workflows/lint.yml | 2 +- CHANGELOG.md | 2 + Makefile | 23 +++- apheleia.el | 69 +++++++--- scripts/apheleia-indent.el | 6 + test/formatters/apheleia-ft.el | 127 ++++++++++-------- test/formatters/installers/lisp-indent.bash | 1 + test/formatters/samplecode/lisp-indent/in.el | 5 + test/formatters/samplecode/lisp-indent/out.el | 5 + 9 files changed, 162 insertions(+), 78 deletions(-) create mode 100644 scripts/apheleia-indent.el create mode 100644 test/formatters/installers/lisp-indent.bash create mode 100644 test/formatters/samplecode/lisp-indent/in.el create mode 100644 test/formatters/samplecode/lisp-indent/out.el diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e06e4a7..eb79979 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - emacs_version: [26, 27, "master"] + emacs_version: [26, 27, 28, "master"] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 90b9f49..d87b17e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,11 @@ The format is based on [Keep a Changelog]. * [bean-format](https://github.com/beancount/beancount) for Beancount ([#101]). * [stylua](https://github.com/JohnnyMorganz/StyLua) for Lua ([#105]). +* Native Emacs indentation of Emacs Lisp code as a formatter ([#102]). [#100]: https://github.com/radian-software/apheleia/pull/100 [#101]: https://github.com/radian-software/apheleia/pull/101 +[#102]: https://github.com/radian-software/apheleia/pull/102 [#105]: https://github.com/radian-software/apheleia/pull/105 [#109]: https://github.com/radian-software/apheleia/issues/109 [#110]: https://github.com/radian-software/apheleia/pull/110 diff --git a/Makefile b/Makefile index 1f3ccd2..b5aea8f 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +SHELL := bash + VERSION ?= CMD ?= @@ -8,6 +10,7 @@ TAG ?= latest # The order is important for compilation. for_compile := *.el for_checkdoc := *.el +for_checkindent := *.el .PHONY: help help: ## Show this message @@ -19,7 +22,7 @@ help: ## Show this message column -t -s'|' >&2 .PHONY: lint -lint: compile checkdoc longlines fmt-lint ## Build project and run all linters +lint: compile checkdoc longlines checkindent fmt-lint ## Run all fast linters .PHONY: compile compile: ## Check for byte-compiler errors @@ -42,6 +45,24 @@ checkdoc: ## Check for missing or poorly formatted docstrings | grep . && exit 1 || true ;\ done +.PHONY: checkindent +checkindent: ## Ensure that indentation is correct + @tmpdir="$$(mktemp -d)"; for file in $(for_checkindent); do \ + echo "[checkindent] $$file" >&2; \ + emacs -Q --batch \ + -l scripts/apheleia-indent.el \ + --eval "(setq inhibit-message t)" \ + --eval "(load (expand-file-name \"apheleia.el\") nil t)" \ + --eval "(find-file \"$$file\")" \ + --eval "(indent-region (point-min) (point-max))" \ + --eval "(write-file \"$$tmpdir/$$file\")"; \ + (diff <(cat "$$file" | nl -v1 -ba | \ + sed "s/\t/: /" | sed "s/^ */$$file:/") \ + <(cat "$$tmpdir/$$file" | nl -v1 -ba | \ + sed "s/\t/: /" | sed "s/^ */$$file:/") ) \ + | grep -F ">" | grep -o "[a-z].*" | grep . && exit 1 || true; \ + done + .PHONY: longlines longlines: ## Check for long lines @scripts/check-line-length.bash diff --git a/apheleia.el b/apheleia.el index a0acc54..df89471 100644 --- a/apheleia.el +++ b/apheleia.el @@ -363,20 +363,21 @@ NO-QUERY, and CONNECTION-TYPE." (stderr-file (apheleia--make-temp-file run-on-remote "apheleia")) (args (append - (list (car command) ; argv[0] - (not stdin) ; If stdin we don't delete the STDIN - ; buffer text with - ; `call-process-region'. Otherwise we - ; send no INFILE argument to - ; `call-process'. - `(,stdout ,stderr-file) ; stdout buffer and stderr file. - ; `call-process' cannot capture - ; stderr into a separate buffer, the - ; best we can do is save and read - ; from a file. - nil) ; Do not re/display stdout as output - ; is recieved. - (cdr command)))) ; argv[1:] + (list + ;; argv[0] + (car command) + ;; If stdin we don't delete the STDIN buffer text with + ;; `call-process-region'. Otherwise we send no INFILE + ;; argument to `call-process'. + (not stdin) + ;; stdout buffer and stderr file. `call-process' cannot + ;; capture stderr into a separate buffer, the best we can + ;; do is save and read from a file. + `(,stdout ,stderr-file) + ;; Do not re/display stdout as output is recieved. + nil) + ;; argv[1:] + (cdr command)))) (unwind-protect (let ((exit-status (cl-letf* ((message (symbol-function #'message)) @@ -656,7 +657,9 @@ See `apheleia--run-formatters' for a description of REMOTE." (clear-files nil) (run-on-remote (and (eq apheleia-remote-algorithm 'remote) remote))) - (cl-labels ((apheleia--make-temp-file-for-rcs-patch + (cl-labels ((;; Weird indentation because of differences in Emacs + ;; indentation algorithm between 27 and 28 + apheleia--make-temp-file-for-rcs-patch (buffer &optional fname) ;; Ensure there's a file with the contents of `buffer' on the ;; target machine. `fname', if given, refers to an existing @@ -826,12 +829,12 @@ machine from the machine file is available on")) arg (eval arg))) if val - if (and (consp val) - (cl-every #'stringp val)) - append val - else if (stringp val) - collect val - else do (error "Result of command evaluation must be a string \ + if (and (consp val) + (cl-every #'stringp val)) + append val + else if (stringp val) + collect val + else do (error "Result of command evaluation must be a string \ or list of strings: %S" arg))) `(,input-fname ,output-fname ,stdin ,@command)))) @@ -928,6 +931,7 @@ being run, for diagnostic purposes." (gofmt . ("gofmt")) (google-java-format . ("google-java-format" "-")) (isort . ("isort" "-")) + (lisp-indent . apheleia-indent-lisp-buffer) (ktlint . ("ktlint" "--stdin" "-F")) (latexindent . ("latexindent" "--logfile=/dev/null")) (mix-format . ("mix" "format" "-")) @@ -1000,6 +1004,26 @@ rather than using this system." (const :tag "Name of temporary file used for output" output))) (function :tag "Formatter function")))) +(cl-defun apheleia-indent-lisp-buffer + (&key buffer scratch callback &allow-other-keys) + "Format a Lisp BUFFER. +Use SCRATCH as a temporary buffer and CALLBACK to apply the +transformation. + +For more implementation detail, see +`apheleia--run-formatter-function'." + (with-current-buffer scratch + (setq-local indent-line-function + (buffer-local-value 'indent-line-function buffer)) + (setq-local lisp-indent-function + (buffer-local-value 'lisp-indent-function buffer)) + (funcall (with-current-buffer buffer major-mode)) + (goto-char (point-min)) + (let ((inhibit-message t) + (message-log-max nil)) + (indent-region (point-min) (point-max))) + (funcall callback))) + (defun apheleia--run-formatters (formatters buffer remote callback &optional stdin) "Run one or more code formatters on the current buffer. @@ -1054,8 +1078,10 @@ function: %s" command))) (c-mode . clang-format) (c++-mode . clang-format) (caml-mode . ocamlformat) + (common-lisp-mode . lisp-indent) (css-mode . prettier) (dart-mode . dart-format) + (emacs-lisp-mode . lisp-indent) (elixir-mode . mix-format) (elm-mode . elm-format) (fish-mode . fish-indent) @@ -1070,6 +1096,7 @@ function: %s" command))) (latex-mode . latexindent) (LaTeX-mode . latexindent) (lua-mode . stylua) + (lisp-mode . lisp-indent) (nix-mode . nixfmt) (python-mode . black) (ruby-mode . prettier) diff --git a/scripts/apheleia-indent.el b/scripts/apheleia-indent.el new file mode 100644 index 0000000..039cf51 --- /dev/null +++ b/scripts/apheleia-indent.el @@ -0,0 +1,6 @@ +;; This file has code that is evaluated in CI before the indentation +;; of apheleia.el is checked. This is helpful because it allows us to +;; ensure that various things are indented correctly if they require +;; some setup for Emacs to know how to do the right thing. + +;; Nothing here yet though! ^_^ diff --git a/test/formatters/apheleia-ft.el b/test/formatters/apheleia-ft.el index 26279ad..db2422b 100755 --- a/test/formatters/apheleia-ft.el +++ b/test/formatters/apheleia-ft.el @@ -218,65 +218,82 @@ environment variable, defaulting to all formatters." (out-file (replace-regexp-in-string "/in\\([^/]+\\)" "/out\\1" in-file 'fixedcase)) (exec-path - (append `(,(expand-file-name - "scripts/formatters" - (file-name-directory - (file-truename - ;; Borrowed with love from Magit - (let ((load-suffixes '(".el"))) - (locate-library "apheleia")))))) - exec-path))) - (mapc - (lambda (arg) - (when (memq arg '(file filepath input output inplace)) - (cl-pushnew arg syms))) - command) - (when (or (memq 'file syms) (memq 'filepath syms)) - (setq in-temp-real-file (apheleia-ft--write-temp-file - in-text extension))) - (when (or (memq 'input syms) (memq 'inplace syms)) - (setq in-temp-file (apheleia-ft--write-temp-file - in-text extension)) - (when (memq 'inplace syms) - (setq out-temp-file in-temp-file))) - (when (memq 'output syms) - (setq out-temp-file (apheleia-ft--write-temp-file - "" extension))) - (setq command - (mapcar - (lambda (arg) - (pcase arg - ((or `file `filepath) - in-temp-real-file) - ((or `input `inplace) - in-temp-file) - (`output - out-temp-file) - (_ arg))) - command)) - (setq command (delq 'npx command)) + (append `(,(expand-file-name + "scripts/formatters" + (file-name-directory + (file-truename + ;; Borrowed with love from Magit + (let ((load-suffixes '(".el"))) + (locate-library "apheleia")))))) + exec-path))) (setq stdout-buffer (get-buffer-create (format "*apheleia-ft-stdout-%S" formatter))) (with-current-buffer stdout-buffer (erase-buffer)) - (setq exit-status - (apply - #'call-process - (car command) - (unless (or (memq 'file syms) - (memq 'input syms) - (memq 'inplace syms)) - in-file) - (list stdout-buffer stderr-file) - nil - (cdr command))) - ;; Verify that formatter succeeded. - (unless (zerop exit-status) - (with-temp-buffer - (insert-file-contents stderr-file) - (princ (buffer-string))) - (error - "Formatter %s exited with status %S" formatter exit-status)) + (if (functionp command) + (progn + (setq in-temp-file (apheleia-ft--write-temp-file + in-text extension)) + (with-current-buffer (find-file-noselect in-temp-file) + (funcall command + :buffer (current-buffer) + :scratch (current-buffer) + :formatter formatter + :callback (lambda ())) + (copy-to-buffer stdout-buffer (point-min) (point-max)))) + (progn + + (with-current-buffer stdout-buffer + (erase-buffer)) + (mapc + (lambda (arg) + (when (memq arg '(file filepath input output inplace)) + (cl-pushnew arg syms))) + command) + (when (or (memq 'file syms) (memq 'filepath syms)) + (setq in-temp-real-file (apheleia-ft--write-temp-file + in-text extension))) + (when (or (memq 'input syms) (memq 'inplace syms)) + (setq in-temp-file (apheleia-ft--write-temp-file + in-text extension)) + (when (memq 'inplace syms) + (setq out-temp-file in-temp-file))) + (when (memq 'output syms) + (setq out-temp-file (apheleia-ft--write-temp-file + "" extension))) + (setq command + (mapcar + (lambda (arg) + (pcase arg + ((or `file `filepath) + in-temp-real-file) + ((or `input `inplace) + in-temp-file) + (`output + out-temp-file) + (_ arg))) + command)) + (setq command (delq 'npx command)) + (setq stdout-buffer (get-buffer-create + (format "*apheleia-ft-stdout-%S" formatter))) + (setq exit-status + (apply + #'call-process + (car command) + (unless (or (memq 'file syms) + (memq 'input syms) + (memq 'inplace syms)) + in-file) + (list stdout-buffer stderr-file) + nil + (cdr command))) + ;; Verify that formatter succeeded. + (unless (zerop exit-status) + (with-temp-buffer + (insert-file-contents stderr-file) + (princ (buffer-string))) + (error + "Formatter %s exited with status %S" formatter exit-status)))) ;; Verify that formatter has not touched original file. (when in-temp-real-file (let ((in-text-now (apheleia-ft--read-file in-temp-real-file))) diff --git a/test/formatters/installers/lisp-indent.bash b/test/formatters/installers/lisp-indent.bash new file mode 100644 index 0000000..97dbfb0 --- /dev/null +++ b/test/formatters/installers/lisp-indent.bash @@ -0,0 +1 @@ +# Nothing to do here, this formatter is pure Emacs! diff --git a/test/formatters/samplecode/lisp-indent/in.el b/test/formatters/samplecode/lisp-indent/in.el new file mode 100644 index 0000000..3e9200d --- /dev/null +++ b/test/formatters/samplecode/lisp-indent/in.el @@ -0,0 +1,5 @@ +;; -*- indent-tabs-mode: nil -*- +(if (and (< 3 5) + (= 1 1)) + (message "true") +(message "false")) diff --git a/test/formatters/samplecode/lisp-indent/out.el b/test/formatters/samplecode/lisp-indent/out.el new file mode 100644 index 0000000..2b8c5a2 --- /dev/null +++ b/test/formatters/samplecode/lisp-indent/out.el @@ -0,0 +1,5 @@ +;; -*- indent-tabs-mode: nil -*- +(if (and (< 3 5) + (= 1 1)) + (message "true") + (message "false"))