radian-software / apheleia Goto Github PK
View Code? Open in Web Editor NEW๐ท Run code formatter on buffer contents without moving point, using RCS patches and dynamic programming.
License: MIT License
๐ท Run code formatter on buffer contents without moving point, using RCS patches and dynamic programming.
License: MIT License
When formatting fails, we have to:
M-S-; (switch-to-buffer " *apheleia ... stderr") RET
Should there be a less painful way?
emacsrc:
(require 'php-mode)
(require 'apheleia)
(add-to-list 'apheleia-formatters '(cat "cat"))
(add-to-list 'apheleia-mode-alist '(php-mode . cat))
(apheleia-global-mode +1)
(setq debug-on-message "while locally")
test.php:
Something here
Run this (you should have apheleia, php-mode installed with straight, or clone the repos elsewhere and change the paths below):
emacs -Q -L ~/.emacs.d/straight/repos/apheleia -L ~/.emacs.d/straight/repos/php-mode/lisp -l emacsrc test.php
After changing something and saving, you get:
Debugger entered--Lisp error: "Making after-save-hook buffer-local while locally ..."
make-local-variable(after-save-hook)
add-hook(after-save-hook apheleia--format-after-save nil local)
(if apheleia-mode (add-hook 'after-save-hook #'apheleia--format-after-save nil 'local) (remove-hook 'after-save-hook #'apheleia--format-after-save 'local))
(let ((last-message (current-message))) (setq apheleia-mode (if (eq arg 'toggle) (not apheleia-mode) (> (prefix-numeric-value arg) 0))) (if apheleia-mode (add-hook 'after-save-hook #'apheleia--format-after-save nil 'local) (remove-hook 'after-save-hook #'apheleia--format-after-save 'local)) (run-hooks 'apheleia-mode-hook (if apheleia-mode 'apheleia-mode-on-hook 'apheleia-mode-off-hook)) (if (called-interactively-p 'any) (progn nil (if (and (current-message) (not (equal last-message (current-message)))) nil (let ((local " in current buffer")) (message "Apheleia mode %sabled%s" (if apheleia-mode "en" "dis") local))))))
apheleia-mode()
(if apheleia-mode (progn (apheleia-mode -1) (apheleia-mode)) (apheleia-mode))
(if (eq apheleia-mode-major-mode major-mode) nil (if apheleia-mode (progn (apheleia-mode -1) (apheleia-mode)) (apheleia-mode)))
(if apheleia-mode-set-explicitly nil (if (eq apheleia-mode-major-mode major-mode) nil (if apheleia-mode (progn (apheleia-mode -1) (apheleia-mode)) (apheleia-mode))))
(save-current-buffer (set-buffer buf) (if apheleia-mode-set-explicitly nil (if (eq apheleia-mode-major-mode major-mode) nil (if apheleia-mode (progn (apheleia-mode -1) (apheleia-mode)) (apheleia-mode)))) (setq apheleia-mode-major-mode major-mode))
(progn (save-current-buffer (set-buffer buf) (if apheleia-mode-set-explicitly nil (if (eq apheleia-mode-major-mode major-mode) nil (if apheleia-mode (progn (apheleia-mode -1) (apheleia-mode)) (apheleia-mode)))) (setq apheleia-mode-major-mode major-mode)))
(if (buffer-live-p buf) (progn (save-current-buffer (set-buffer buf) (if apheleia-mode-set-explicitly nil (if (eq apheleia-mode-major-mode major-mode) nil (if apheleia-mode (progn (apheleia-mode -1) (apheleia-mode)) (apheleia-mode)))) (setq apheleia-mode-major-mode major-mode))))
(let ((buf (car --dolist-tail--))) (if (buffer-live-p buf) (progn (save-current-buffer (set-buffer buf) (if apheleia-mode-set-explicitly nil (if (eq apheleia-mode-major-mode major-mode) nil (if apheleia-mode (progn ... ...) (apheleia-mode)))) (setq apheleia-mode-major-mode major-mode)))) (setq --dolist-tail-- (cdr --dolist-tail--)))
(while --dolist-tail-- (let ((buf (car --dolist-tail--))) (if (buffer-live-p buf) (progn (save-current-buffer (set-buffer buf) (if apheleia-mode-set-explicitly nil (if (eq apheleia-mode-major-mode major-mode) nil (if apheleia-mode ... ...))) (setq apheleia-mode-major-mode major-mode)))) (setq --dolist-tail-- (cdr --dolist-tail--))))
(let ((--dolist-tail-- buffers)) (while --dolist-tail-- (let ((buf (car --dolist-tail--))) (if (buffer-live-p buf) (progn (save-current-buffer (set-buffer buf) (if apheleia-mode-set-explicitly nil (if ... nil ...)) (setq apheleia-mode-major-mode major-mode)))) (setq --dolist-tail-- (cdr --dolist-tail--)))))
(let ((buffers apheleia-global-mode-buffers)) (setq apheleia-global-mode-buffers nil) (let ((--dolist-tail-- buffers)) (while --dolist-tail-- (let ((buf (car --dolist-tail--))) (if (buffer-live-p buf) (progn (save-current-buffer (set-buffer buf) (if apheleia-mode-set-explicitly nil ...) (setq apheleia-mode-major-mode major-mode)))) (setq --dolist-tail-- (cdr --dolist-tail--))))))
apheleia-global-mode-enable-in-buffers()
run-hooks(after-change-major-mode-hook)
run-mode-hooks(php-mode-hook)
php-mode()
funcall(php-mode)
php-mode-maybe()
set-auto-mode-0(php-mode-maybe t)
set-auto-mode(t)
set-visited-file-name("/tmp/tmp.PViU3Ex3tP/test.php" t)
write-file("/tmp/tmp.PViU3Ex3tP/test.php")
(let ((after-save-hook (remq #'apheleia--format-after-save after-save-hook))) (write-file (or filename buffer-file-name)))
(progn (fset #'message vnew) (let ((after-save-hook (remq #'apheleia--format-after-save after-save-hook))) (write-file (or filename buffer-file-name))))
(unwind-protect (progn (fset #'message vnew) (let ((after-save-hook (remq #'apheleia--format-after-save after-save-hook))) (write-file (or filename buffer-file-name)))) (fset #'message old))
(let* ((vnew #'(lambda (format &rest args) (if (equal format "Saving file %s...") nil (apply message format args)))) (old (symbol-function #'message))) (unwind-protect (progn (fset #'message vnew) (let ((after-save-hook (remq #'apheleia--format-after-save after-save-hook))) (write-file (or filename buffer-file-name)))) (fset #'message old)))
(let ((message (symbol-function #'message))) (let* ((vnew #'(lambda (format &rest args) (if (equal format "Saving file %s...") nil (apply message format args)))) (old (symbol-function #'message))) (unwind-protect (progn (fset #'message vnew) (let ((after-save-hook (remq ... after-save-hook))) (write-file (or filename buffer-file-name)))) (fset #'message old))))
(progn (fset #'write-region vnew) (let ((message (symbol-function #'message))) (let* ((vnew #'(lambda (format &rest args) (if ... nil ...))) (old (symbol-function #'message))) (unwind-protect (progn (fset #'message vnew) (let ((after-save-hook ...)) (write-file (or filename buffer-file-name)))) (fset #'message old)))))
(unwind-protect (progn (fset #'write-region vnew) (let ((message (symbol-function #'message))) (let* ((vnew #'(lambda ... ...)) (old (symbol-function #'message))) (unwind-protect (progn (fset #'message vnew) (let (...) (write-file ...))) (fset #'message old))))) (fset #'write-region old))
(let* ((vnew #'(lambda (start end filename &optional append _visit lockname mustbenew) (apheleia--write-region-silently start end filename append t lockname mustbenew write-region))) (old (symbol-function #'write-region))) (unwind-protect (progn (fset #'write-region vnew) (let ((message (symbol-function #'message))) (let* ((vnew #'...) (old (symbol-function ...))) (unwind-protect (progn (fset ... vnew) (let ... ...)) (fset #'message old))))) (fset #'write-region old)))
(let ((write-region (symbol-function #'write-region))) (let* ((vnew #'(lambda (start end filename &optional append _visit lockname mustbenew) (apheleia--write-region-silently start end filename append t lockname mustbenew write-region))) (old (symbol-function #'write-region))) (unwind-protect (progn (fset #'write-region vnew) (let ((message (symbol-function ...))) (let* ((vnew ...) (old ...)) (unwind-protect (progn ... ...) (fset ... old))))) (fset #'write-region old))))
apheleia--write-file-silently("/tmp/tmp.PViU3Ex3tP/test.php")
(progn (apheleia--write-file-silently buffer-file-name) (run-hooks 'apheleia-post-format-hook))
(condition-case err (progn (apheleia--write-file-silently buffer-file-name) (run-hooks 'apheleia-post-format-hook)) ((debug error) (message "Apheleia: %s" err) nil))
(closure ((command "cat") apheleia-mode t) nil (condition-case err (progn (apheleia--write-file-silently buffer-file-name) (run-hooks 'apheleia-post-format-hook)) ((debug error) (message "Apheleia: %s" err) nil)))()
funcall((closure ((command "cat") apheleia-mode t) nil (condition-case err (progn (apheleia--write-file-silently buffer-file-name) (run-hooks 'apheleia-post-format-hook)) ((debug error) (message "Apheleia: %s" err) nil))))
(progn (funcall callback))
(if callback (progn (funcall callback)))
(progn (apheleia--apply-rcs-patch (current-buffer) patch-buffer) (if callback (progn (funcall callback))))
(if (equal apheleia--buffer-hash (apheleia--buffer-hash)) (progn (apheleia--apply-rcs-patch (current-buffer) patch-buffer) (if callback (progn (funcall callback)))))
(save-current-buffer (set-buffer cur-buffer) (if (equal apheleia--buffer-hash (apheleia--buffer-hash)) (progn (apheleia--apply-rcs-patch (current-buffer) patch-buffer) (if callback (progn (funcall callback))))))
(closure ((formatted-buffer . #<buffer *apheleia-cat-stdout*>) (cur-buffer . #<buffer test.php>) (callback closure ((command "cat") apheleia-mode t) nil (condition-case err (progn (apheleia--write-file-silently buffer-file-name) (run-hooks 'apheleia-post-format-hook)) ((debug error) (message "Apheleia: %s" err) nil))) (command "cat") t) (patch-buffer) (save-current-buffer (set-buffer cur-buffer) (if (equal apheleia--buffer-hash (apheleia--buffer-hash)) (progn (apheleia--apply-rcs-patch (current-buffer) patch-buffer) (if callback (progn (funcall callback)))))))(#<buffer *apheleia-diff-stdout*>)
funcall((closure ((formatted-buffer . #<buffer *apheleia-cat-stdout*>) (cur-buffer . #<buffer test.php>) (callback closure ((command "cat") apheleia-mode t) nil (condition-case err (progn (apheleia--write-file-silently buffer-file-name) (run-hooks 'apheleia-post-format-hook)) ((debug error) (message "Apheleia: %s" err) nil))) (command "cat") t) (patch-buffer) (save-current-buffer (set-buffer cur-buffer) (if (equal apheleia--buffer-hash (apheleia--buffer-hash)) (progn (apheleia--apply-rcs-patch (current-buffer) patch-buffer) (if callback (progn (funcall callback))))))) #<buffer *apheleia-diff-stdout*>)
(progn (funcall callback stdout))
(if callback (progn (funcall callback stdout)))
(if (funcall (or exit-status #'(lambda (status) (= 0 status))) (process-exit-status proc)) (if callback (progn (funcall callback stdout))) (message (concat "Failed to run %s: exit status %s " "(see hidden buffer%s)") (car command) (process-exit-status proc) stderr))
(if (process-live-p proc) nil (save-current-buffer (set-buffer stderr) (if (= 0 (buffer-size)) (progn (insert "[No output received on stderr]\n")))) (if (funcall (or exit-status #'(lambda (status) (= 0 status))) (process-exit-status proc)) (if callback (progn (funcall callback stdout))) (message (concat "Failed to run %s: exit status %s " "(see hidden buffer%s)") (car command) (process-exit-status proc) stderr)))
(closure ((stderr . #<buffer *apheleia-diff-stderr*>) (stdout . #<buffer *apheleia-diff-stdout*>) (name . "diff") (exit-status closure ((new-fname) (old-fname . "/tmp/tmp.PViU3Ex3tP/test.php") (callback closure ((formatted-buffer . #<buffer *apheleia-cat-stdout*>) (cur-buffer . #<buffer test.php>) (callback closure ... nil ...) (command "cat") t) (patch-buffer) (save-current-buffer (set-buffer cur-buffer) (if ... ...))) (new-buffer . #<buffer *apheleia-cat-stdout*>) (old-buffer . #<buffer test.php>) t) (status) (memq status '(0 1))) (callback closure ((formatted-buffer . #<buffer *apheleia-cat-stdout*>) (cur-buffer . #<buffer test.php>) (callback closure ((command "cat") apheleia-mode t) nil (condition-case err (progn ... ...) (... ... nil))) (command "cat") t) (patch-buffer) (save-current-buffer (set-buffer cur-buffer) (if (equal apheleia--buffer-hash (apheleia--buffer-hash)) (progn (apheleia--apply-rcs-patch ... patch-buffer) (if callback ...))))) (stdin . #<buffer *apheleia-cat-stdout*>) (command "diff" "--rcs" "--" "/tmp/tmp.PViU3Ex3tP/test.php" "-") (--cl-rest-- :command ("diff" "--rcs" "--" "/tmp/tmp.PViU3Ex3tP/test.php" "-") :stdin #<buffer *apheleia-cat-stdout*> :callback (closure ((formatted-buffer . #<buffer *apheleia-cat-stdout*>) (cur-buffer . #<buffer test.php>) (callback closure (... apheleia-mode t) nil (condition-case err ... ...)) (command "cat") t) (patch-buffer) (save-current-buffer (set-buffer cur-buffer) (if (equal apheleia--buffer-hash ...) (progn ... ...)))) :exit-status (closure ((new-fname) (old-fname . "/tmp/tmp.PViU3Ex3tP/test.php") (callback closure (... ... ... ... t) (patch-buffer) (save-current-buffer ... ...)) (new-buffer . #<buffer *apheleia-cat-stdout*>) (old-buffer . #<buffer test.php>) t) (status) (memq status '(0 1)))) t) (proc _event) (if (process-live-p proc) nil (save-current-buffer (set-buffer stderr) (if (= 0 (buffer-size)) (progn (insert "[No output received on stderr]\n")))) (if (funcall (or exit-status #'(lambda ... ...)) (process-exit-status proc)) (if callback (progn (funcall callback stdout))) (message (concat "Failed to run %s: exit status %s " "(see hidden buffer%s)") (car command) (process-exit-status proc) stderr))))(#<process aphelieia-diff> "finished\n")
This is due to apheleia let-binding after-save-hook while writing the file. Writing the file triggers auto mode, auto mode triggers php mode, php mode triggers the hook installed by apheleia global mode which should add apheleia's after save hook, and that breaks because after-save-hook is locally let bound at that point.
Instead of binding after-save-hook itself, should we not better bind a flag, eg, apheleia--format-after-save-in-progress
, to break the infinite loop?
I will submit a PR for your consideration.
Atm apheleia has filepath
and file
command specifiers that're replaced with the value of the current buffers file. Quite a lot of the time I'm working with virtual files (such as by creating an org src-block for some c code) and apheliea will refuse to format that buffer (when called interactively through apheleia-format-buffer
) because its been modified and it doesn't have a file-name. There should be a way to format buffers as well as files.
Hi there, very happy with apheleia overall, and I apologize if this isn't really the right spot to make this request / open a discussion about this, but I've found that apheleia doesn't seem to follow along with my .prettierignore
file in my project (using projectile if that's of relevance). It will adhere to the rules in my .pretterrc.js
file in the project root, but happily skips over the ignore file. Is there something I am able to do to get it to adhere to that file as well?
Thanks very much, big fan of your many contributions to the Emacs ecosystem as well!
Hi,
So I tried to get flutter format
working in apheleia. The problem is, this command
directly writes to the file. AFAIK, there is no option to write to stdout.
My question is, is there a way to add a formatter that only writes directly
to the file?
I have alias for latexindent: alias latexindent="~/tools/latexindent.pl/latexindent.pl -l=~/indentconfig.yaml"
I am able to run it as:
~/tools/latexindent.pl/latexindent.pl -l=~/indentconfig.yaml myfile.tex > output.tex
But inside emacs I am having following error related to latexindent:
*apheleia-latexindent-log*
$ latexindent --logfile\=/dev/null
Can't open /dev/null at /usr/local/texlive/2021/texmf-dist/scripts/latexindent/LatexIndent/Document.pm line 165, <> line 328.
Command failed with exit code 2.
Fri Jan 14 12:41:34 2022 :: /Users/user/folder
$ latexindent --logfile\=/dev/null
Can't open /dev/null at /usr/local/texlive/2021/texmf-dist/scripts/latexindent/LatexIndent/Document.pm line 165, <> line 328.
Command failed with exit code 2.
How can I fix this error in order to run latexindent
with my given config file?
From what I can tell, formatter processes that fail never get freed. I do not have clang-format
installed so when saving a C file I the message
Failed to run clang-format: Searching for program: No such file or directory, clang-format
is echoed, which is fine. However, looking at the output of M-x list-processes
it seems that the process list is getting clogged up:
Process [v] PID Status Buffer TTY Thread Command
apheleia-cla... -- open *apheleia-clang-forma... -- Main
apheleia-cla... -- open *apheleia-clang-forma... -- Main
...
apheleia-cla... -- open *apheleia-clang-forma... -- Main
server -- listen -- -- Main (network server on /run/user/1000/emacs/server)
server <1> -- open -- -- Main (network connection to t:/run/user/1000/emacs/server)
I do believe this is what caused my Emacs instance to start failing with "Too many open files" until killed.
For formatters I do have installed this is not an issue.
I am on commit 8e022c6.
Follows from #52, but will require either #43 or the region facing part of #34 before it can be finished.
The gist of the issue is emacs lets you have indirect buffers within a buffer. Such as in org-mode you can edit a src code block and get a new buffer which can be edited and whose changes can be saved back to the original buffer. This issue wants formatting to be possible when commiting an indirect-buffer back to the original buffer.
I am running adamzapasnik/prettier-plugin-eex on an index.html.eex
file which is opened with a major mode of web-mode
. When I edit a file and hit save, the formatter runs great but I am switched into mhtml-mode
(HTML+) after the formatter is run.
Is there some configuration I am missing? Happy to help debug if you can guide me on where/how to look.
Thanks for this great project btw ๐ป This is a great step forward in formatters for the emacs ecosystem
When I add following lines into my init.el file:
(setf (alist-get 'isort apheleia-formatters)
'("isort" "--stdout" "-"))
(setf (alist-get 'python-mode apheleia-mode-alist)
'(isort black))
I am getting following error:
Debugger entered--Lisp error: (void-variable apheleia-formatters)
(assq 'isort apheleia-formatters)
(if (and nil (not (eq nil 'eq))) (assoc 'isort apheleia-formatters nil) (assq 'isort apheleia-formatters))
(let* ((p (if (and nil (not (eq nil 'eq))) (assoc 'isort apheleia-formatters nil) (assq 'isort apheleia-formatters)))) (progn
(if p (setcdr p '("isort" "--stdout" "-")) (setq apheleia-formatters (cons (setq p (cons 'isort '...)) apheleia-formatters)))
'("isort" "--stdout" "-")))
load-with-code-conversion("/home/alper/.emacs" "/home/alper/.emacs" t t)
load("~/.emacs" noerror nomessage)
startup--load-user-init-file(#f(compiled-function () #<bytecode 0x10f36531c68d12d2>) #f(compiled-function () #<bytecode
-0x1f3c686ddc0ca9b5>) t)
command-line()
normal-top-level()
How can I prevent this error?
When a lot of formatting is going on I seem to lose the ability to undo back very far. This is extremely bad for UX. Probably a consequence of undo-limit
and the reformatting generating large volumes of undo data (not sure why).
Thank you for making Apheleia!
I've found a little issue in the way Apheleia is using temporary file. Some formatter reads a configuration from a directory hierarchy until a file is found (e.g. prettier
reads .editorconfig
, and black
reads pyproject.toml
). Right now Apheleia uses make-temp-file
which creates a temporary file in /tmp
, which cause a per-project configuration to not get read during formatting.
Create an .editorconfig
file with the following content:
root = true
[*]
indent_size = 4
Create test.js
with the following content:
function foo() {
return true;
}
Format the file with npx prettier test.js
Format the file with Apheleia
Compare results between (3) and (4)
Apheleia-run formatted file becomes (4 spaces):
function foo() {
return true;
}
Apheleia-run formatted file becomes (2 spaces):
function foo() {
return true;
}
Unfortunately, we get errors
Failed to run prettier: process aphelieia-prettier no longer connected to pipe; closed it
Failed to run prettier: exit status 256 (see hidden buffer *apheleia-prettier-stderr*)
when a buffer's size exceeds 65,536 characters. I am not aware of any workaround other than disabling Apheleia. Note that the error is unrelated to any particular code formatter, since it reproduces with cat
.
Semaphore CI turns out to be unsuitable for open-source (for example, logs are private and you can't build pull requests without leaking any secrets that may be used for the project). Travis CI is likely dying. The best option is CircleCI in my opinion, so I'd like to import my configuration for that from Radian.
Looks like apheleia is on Melpa now. The readme says it is not, this should probably be updated.
Sorry, this is just a question, I'm not reporting a bug or anything.
Emacs 26 and later come with the function replace-buffer-contents which can be used to efficiently replace the contents of the current buffer by the contests of a different buffer, trying to make minimal edits and preserving marks and the point as much as possible. Underneath it uses code from https://github.com/coreutils/gnulib/blob/master/lib/diffseq.h which is based on well knows algorithms.
Is there a reason why this function is not used, instead of writing a new diff implementation or working with RCS patches? It seems much simpler to use replace-buffer-contents.
I use it daily for automatically formatting OCaml code with ocamlformat. I don't remember it ever moved my point where I didn't expect it (for example, I never saw something like issue #2 ) and I never noticed any slowness.
You can see example use here
https://github.com/ocaml-ppx/ocamlformat/blob/02ea48f0d09a48fb2a25fdba75a8bf20872dadb5/emacs/ocamlformat.el#L211
Example:
if (node.type === "CallExpression" && (node.callee.type === "Import" || (node.callee.type === "Identifier" && node.callee.name === "require"))) {
//@
}
with point represented by @
is reformatted by Prettier to:
if (
node.type === "CallExpression" &&
(node.callee.type === "Import" ||
(node.callee.type === "Identifier" && node.callee.name === "require"))
) {@
//
}
instead of:
if (
node.type === "CallExpression" &&
(node.callee.type === "Import" ||
(node.callee.type === "Identifier" && node.callee.name === "require"))
) {
//@
}
I suspect that this problem could be solved by tweaking the weights used in the dynamic programming algorithm.
I noticed that after editing a TSX file in Emacs and saving, a buffer called *jka-compr-wr-temp*
is created and is linked to a file located in undo-fu-session-directory
and when I try and close Emacs, naturally the editor tells me if I want to save that buffer/file before quitting which is annoying:
Save file /home/jorge/.emacs.d/.local/cache/undo-fu-session/60bb5b95f70b5c8292b03b6db987f789ce8c7c76.zst (buffer *jka-compr-wr-temp*)? (y, n, !, ., q, C-r, C-f, d or C-h)
I know this is somehow related to apheleia because if I disable it in my configuration and restart Emacs, try to reproduce and then check the buffer list, the buffer *jka-compr-wr-temp*
is not linked to any file:
this happens with the latest commit f865c16
My config is based on Doom Emacs, but Apheleia is not provided by Doom (I added it myself)
What I have in mind is using something like what highlight-changes-mode
does on save to track the modified lines. We could then use the current RCS patch strategy to match diff hunks to modified regions (essentially if the diff is contained within the modified region plus some configurable context value) and only apply these hunks.
I am happy to develop this but I want to first here this would be of interest to people. Also I am relatively new to elisp so it might take some time and I might need some guidance navigating the existing source.
Hi! Thank you for writing Apheleia!
I was integrating Apheleia with Darker yesterday, a Python reformatter. Darker is just Black, but with the key difference that Darker only reformats the portions of the file you've changed in your local Git repository. This is useful for me while I'm working in a legacy code base that is mostly unformatted today, but which aspires to eventually format everything with Black.
Darker really wants to be given the original input file, in place (so it can look at git diff
I assume), and Apheleia can handle that just fine. However, Darker's only output options are:
1 is not acceptable while you're editing the file, so we're left with 2.
Of course, a third option would be to modify Darker to support writing output to stdout or another file rather than updating the input file. However, I suspect there are other formatters that want to output a diff.
Since Apheleia is conveniently already operating on a diff, would you be interesting in adding support for formatters that output a diff?
I've actually hacked this up alreadyโemphasis on "hacked"โand it seems to work fine in my very limited initial testing. I used el-patch (๐ฅ) on apheleia-format-buffer
to make a case where, if the first element of the formatter's command
is rcs
, the call to diff --rcs
is simply skipped and we go straight to apheleia--apply-rcs-patch
. Of course, Darker can only output unified diff format, so I wrote a little Python script to transform unified diff to RCS diff. I could port that unified โ RCS diff format to Elisp easily enough.
I'd be happy to clean that up and submit a PR, but I won't bore you with it you're not interested.
Also, before I did rip off a PR, I wanted to check in to ask how you'd like the configuration for formatters to work? As I said above, I'm currently doing something like this in my hacky POC:
(defcustom apheleia-formatters
'((darker . (rcs "darker" "--diff" filepath))
...) ...)
I think that's kind of ugly. It also looks too much like the existing npx
semi-special value for my tastes.
Instead, my first choice would be to turn the values of the apheleia-formatters
alist into alists of parameters, something like:
(defcustom apheleia-formatters
'((black . ((:command . ("black" "-"))))
(brittany . ((:command . ("brittany" file))))
(darker . ((:command . ("darker" "--diff" filepath))
(:output-format . unified-diff)))
(prettier . ((:command . (npx "prettier" "--stdin-filepath" filepath))))
(gofmt . ((:command . ("gofmt"))))
(ocamlformat . ((:command . ("ocamlformat" file))))
(terraform . ((:command . ("terraform" "fmt" "-")))))
...)
This would allow for additional configuration parameters for formatters in the future, too. However (1) I don't know how you feel about breaking compatibility, and (2) I don't know if you like the aesthetics of that. Better suggestions very welcome!
I wanted to use Apheleia for Rust code, too, as rustic-mode
's own formatter is rather buggy and nowhere near as good as Apheleia's handling.
However, rustfmt
behaves in a way that confuses Apheleia. The existing PR #24 is not going to integrate it successfully. There are several problems:
rustfmt
defaults to checking all dependent files, not just the file it is passed. This means that its output with the --emit stdout
option contains multiple files. To output only one file, there's an unstable --skip-children
flag, which requires us to also allow such flags.--skip-children
has a bug: it still outputs the file name in the first line, and two additional newlines.Of course, I think that the underlying issue should be patched out of rust-analyzer
. I'll see if I can do that.
In the meantime, I've created a workaround. First, you need to register rustfmt
with Apheleia:
(add-to-list 'apheleia-formatters
'(rustfmt . ("rustfmt-helper" file)))
(add-to-list 'apheleia-mode-alist
'(rustic-mode . rustfmt)
I've made a shell script called rustfmt-helper
to work around the above issues:
#!/bin/bash
rustfmt --emit stdout --unstable-features --skip-children $1 | tail -n +3
exit ${PIPESTATUS[0]}
Put this on your $PATH
and chmod +x
it.
Using the format on save, or manual formatting of the file causes the mode to revert to whatever is the default. i.e. if you open a buffer and set it to web-mode, after the formatting is done it returns to fundamental mode.
Also, it has a bug relating to php-mode. If php-mode is active, Apheleia attempts to format it using clang-formatter, which then obviously causes an error, in my case, it says clang-formatter cannot be found.
With point inside a sufficiently large diff region, Apheleia can cause Emacs to lock up. We should impose a maximum diff region size to use the smart algorithm on, and fall back to a simpler heuristic (preserve offset into the region?) otherwise.
apheleia-hide-old-log-entries
is deleted, apheleia-formatter-exited-hook
is added, you mean apheleia-hide-old-log-entries
is replaced by apheleia-formatter-exited-hook
?
I don't understand how to use apheleia-formatter-exited-hook
.
Could you please give an example how to replace apheleia-hide-old-log-entries
?
I am trying to add a new formatter for eslint_d
and looking at the examples for black
and prettier
I can't understand why it doesn't work.
I have done some step through debugging and see that it creates the process properly and sends the buffer on stdin, only thing I noted there was that the buffer seemed to include faces and font-lock styling etc.
Not sure if this is causing the issue. After it is executed I receive nothing, no output to indicate error or success. I guess if it succeded it would run the diff and apply function next.
The command I have configured is:
(cl-pushnew '(eslint . (npx "eslint_d" "--fix-to-stdout" "--stdin" "--stdin-filename" file)) apheleia-formatters :test #'equal)
What's the reason that default-directory is set to the project root instead of left unchanged as the directory that the file is in? If the project contains different formatter configs in different subfolders, the wrong one (or none) will get used.
In my init.el
, I added:
(defun apheleia--project-root () default-directory)
But I'm wondering whether this functionality is not better removed altogether? Most if not all formatters should automatically find the right config in parent folders.
Do you want me to set up a testing framework for supported formatters? I'm imagining a system similar to flycheck's where a Docker container installs all checkers and runs a test suite against them.
We could discuss details if you think this is something worth working on.
See #50
When undo-tree-auto-save-history
set to t
and apheleia-global-mode
is on, apheleia
and undo-tree
will go into a save buffer race causing the buffer to never be saved.
global-undo-tree-mode
and apheleia-global-mode
undo-tree-auto-save-history
to t
test.py
)The is saved and formatted
The file is formatted, but become modified once again and cannot be saved unless I disable apheleia or undo-tree (or C-u 0 C-x C-s
in case radian's hook)
Reduced init.el
:
(setq user-init-file (or load-file-name (buffer-file-name)))
(setq user-emacs-directory (file-name-directory user-init-file))
(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
(bootstrap-version 5))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
(straight-use-package '(apheleia :host github :repo "raxod502/apheleia"))
(straight-use-package 'undo-tree)
(require 'apheleia)
(require 'undo-tree)
(apheleia-global-mode +1)
(global-undo-tree-mode +1)
(setq undo-tree-auto-save-history t)
(I run this reduced init.el
with: /usr/bin/emacs -nw -q --load "/path/to/init.el"
)
Not sure whose bug this might be, please let me know if it's more appropriate to report to undo-tree instead (and sorry for the noise in such case). As always, HUGE thank you for straight.el, apheleia and radian โค๏ธ
I've noticed apheleia atm doesn't support functional formatters, such as pp-buffer
.
I'd like to add support for using language servers (eglot, or lsp-mode) for formatting a buffer on save through apheleia so I'd like apheleia to support functional formatters.
I'm interested in what your thoughts on this would be. Implementation wise it might be more convenient to clone the current buffer, or copy it and then run the formatter on the cloned buffer and continue with the changes we were already doing. This would also allow chaining with the existing formatter system. So we could say format with lsp-mode, then run python-isort, etc.
Interface wise I was thinking we just allow the cdr of an entry in apheleia-formatters
to be a symbol, and if it is then its a function to call. Luckily atm their all lists, even gofmt.
As noted in the README, TRAMP is currently unsupported. I am considering adding support since I use TRAMP extensively. I'm interested in your thoughts on this before I potentially embark on creating a PR. ๐
I am able to use this package mostly successfully with my Elixir code with the following config:
(add-to-list 'apheleia-formatters '(mix . ("mix" "format" "-")))
(add-to-list 'apheleia-mode-alist '(elixir-mode . mix))
There is one issue though. When I format a file under a project that contains a .formatter.exs
config, the formatter configuration is not respected. In the documentation of mix format
it states that
The formatter will read a .formatter.exs in the current directory for formatter configuration.
So it seems like it was caused by that mix format -
is not executed in the project directory but the directory of the current buffer file.
Is there a way to specify apheleia to run the formatter under the project directory? The project root directory can be found by either (project-root (project-current))
or (projectile-project-root)
.
I've been using apheleia with prettier for a while now and recently started seeing this error on save, and I'm not sure exactly what's changed (I did update apheleia a few weeks ago and maybe I just started noticing now).
Debugger entered--Lisp error: (wrong-type-argument bufferp nil)
buffer-local-value(buffer-file-coding-system nil)
apheleia--make-process(:command ("/Users/[...]/node_module..." "--print-width" "88" "/Users/[...]/resources/j...") :stdin nil :callback #f(compiled-function (stdout) #<bytecode 0x5da82447>) :ensure #f(compiled-function () #<bytecode 0x5da8245f>))
apheleia--run-formatters(((npx "/Users/[...]/node_module..." "--print-width" "88" file)) #<buffer ExpenseForm.js> #f(compiled-function (formatted-buffer) #<bytecode 0x5da823c9>))
apheleia-format-buffer(((npx "/Users/[...]/node_module..." "--print-width" "88" file)) #f(compiled-function () #<bytecode 0x5d56a885>))
apheleia--format-after-save()
run-hooks(after-save-hook)
basic-save-buffer(t)
save-buffer(1)
funcall-interactively(save-buffer 1)
call-interactively(save-buffer nil nil)
command-execute(save-buffer)
Here's my apheleia config:
(defun my-apheleia-mode ()
(interactive)
(if (and (not apheleia-mode)
(buffer-file-name)
(locate-dominating-file (buffer-file-name) ".apheleia"))
(apheleia-mode +1)))
(use-package apheleia
:straight (apheleia
:host github
:repo "raxod502/apheleia")
:diminish "aph"
:commands (apheleia-mode)
:hook ((css-mode js-mode php-mode python-mode rjsx-mode web-mode) . my-apheleia-mode)
:custom
(apheleia-formatters
`((black . ("black" "-l" ,(number-to-string my-python-line-length) "-"))
(prettier . (npx "prettier"
"--print-width" ,(number-to-string my-python-line-length)
file))
(prettier-html . (npx "prettier"
"--parser" "html"
"--print-width" ,(number-to-string my-python-line-length)
file))))
:config
(add-to-list 'apheleia-mode-alist '(php-mode . prettier))
(add-to-list 'apheleia-mode-alist '(rjsx-mode . prettier))
(add-to-list 'apheleia-mode-alist '(web-mode . prettier-html)))
apheleia is actually working when this happens, the buffer does still get formatted properly.
(let ((bootstrap-file (concat user-emacs-directory "straight/repos/straight.el/bootstrap.el"))
(bootstrap-version 5))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
(straight-use-package 'use-package)
(setq straight-use-package-by-default t)
(setq debug-on-error t)
(use-package apheleia
:straight (:repo "raxod502/apheleia" :host github)
:demand t
:config
(apheleia-global-mode 1))
(use-package flycheck
:demand t
:config
(global-flycheck-mode 1))
Repro:
emacs -q -l above-init.el
. The Python-formatter black
should also be installed.C-x C-f hello.py
def hi():
return 42
and then save the file, allowing Apheleia to clean up the extra whitespace.
M-: (buffer-file-name)
. The output is /tmp/flycheckpqHf80/hello.py
, instead of the expected /home/alice/hello.py
.Funny enough, M-x apheleia-format-buffer
on the above whitespace-heavy file does not permanently visit the temp file in this manner. However, manually saving the formatted buffer (which does not trigger an auto-format of the file, since it was just formatted manually) does cause the temp file to be visited.
I can reproduce this on both Emacs 27 (on macOS, installed via Nix) and Emacs 28 (on NixOS).
Thank you for the awesome package. This is the only formatted that doesn't open all my folded code when I run it in the buffer.
I like to use black
with the traditional line length. With blacken, I do this: (setq blacken-line-length 79)
. I tried editing apheleia-formatters
, but after many attempts I wasn't able to get the syntax right.
Hi, first of all thanks for your great work. I have been using this package for a while now. Recently i have also been using it on windows machine. On windows i have msys64 setup for various unix/linux tools, that includes the diff tool as well. so apheleia is using diff from msys setup. The code's line ending is CRLF
, but when i edit some and format code, it inserts ^M
as line ending. its probably the diff tool's doing. i took screenshots from various buffers to show you here, hope you can find out the problem,
main code after formatting -
prettier-stdout buffer -
sorry i can not show you the full code. my emacs config is here - emacs configuration. i am using windows 10, emacs 28, msys64, diff tool was install via base-devel
package of msys64.
First, thank you for this great package.
Unless I'm mistaken, there is currently no way to configure apheleia-mode
to run multiple formatters on save. When working with Python files, I often run isort
followed by black
. It would be nice to automate this workflow using this package. Maybe the apheleia-mode-alist
could be changed to, for instance:
((python-mode . (isort black))
(js-mode . prettier))
and the package would run the list entries sequentially.
Hi, thank you for this package.
I use python black formatter and one day I wanted to edit narrowed region.
class A:
pass
|class B:
pass|
class C:
pass
Select the code inside |
call M-x narrow-to-region
- you will see only:
class B:
pass
After editing and saving the narrowed buffer - the code disappears.
Call M-x widen
and the content of the buffer will be
class A:
pass
class C:
pass
Emacs 27.1, apheleia-20210723.516
If I have configured:
(setq vc-follow-symlinks t)
and are visiting a file under version control and then edit this file and then save with apheleia-mode
enabled it enters some kind of infinite symlink follow recursion.
I will see output like this (and it will continue until I press C-g, at which point I get thrown out from the previous buffer which was formatted successfully):
Followed link to /dev/shm/buffer-content-ui2Z3V
Followed link to /dev/shm/buffer-content-BIEWUp
Followed link to /dev/shm/buffer-content-ORmfZq
Followed link to /dev/shm/buffer-content-MSk9TR
Followed link to /dev/shm/buffer-content-MG1VLT
Followed link to /dev/shm/buffer-content-Hhjuxl
Followed link to /dev/shm/buffer-content-gdLxGo
Followed link to /dev/shm/buffer-content-QsdyEP
Followed link to /dev/shm/buffer-content-sFHU8R
Followed link to /dev/shm/buffer-content-vJWO3i
Followed link to /dev/shm/buffer-content-CPrtWo
Followed link to /dev/shm/buffer-content-yJBDBX
Followed link to /dev/shm/buffer-content-4nU2f1
Followed link to /dev/shm/buffer-content-jTkcmA
Followed link to /dev/shm/buffer-content-Mfbp9E
Followed link to /dev/shm/buffer-content-utF0ud
Followed link to /dev/shm/buffer-content-uCuUYi
Followed link to /dev/shm/buffer-content-RyZ7kO
Followed link to /dev/shm/buffer-content-yhqNTT
Followed link to /dev/shm/buffer-content-CBcCwo
Followed link to /dev/shm/buffer-content-itulmu
Followed link to /dev/shm/buffer-content-R4XDZY
Followed link to /dev/shm/buffer-content-kDbsj5
Followed link to /dev/shm/buffer-content-Cx1QPB
Followed link to /dev/shm/buffer-content-MxSF8J
Followed link to /dev/shm/buffer-content-H3tsTg
Followed link to /dev/shm/buffer-content-MGU45o
Followed link to /dev/shm/buffer-content-AiPN6U
Followed link to /dev/shm/buffer-content-idLNM2
Followed link to /dev/shm/buffer-content-9vFLOy
Followed link to /dev/shm/buffer-content-knWU5C
Followed link to /dev/shm/buffer-content-07XDc6
Followed link to /dev/shm/buffer-content-stIYId
If I don't enable vc-follow-symlinks
it won't happen, but then instead I will end up in a buffer with the correct file-name, but which is not actually the file in the project and it will lose the VC status information.
I am not sure this error stems from apheleia, but it might be a regression introduced in the latest Emacs HEAD, I am currently running:
GNU Emacs 28.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.24.13, cairo version 1.16.0)
on commit https://github.com/emacs-mirror/emacs/tree/ffb89ed5f07491e33fc79d8b4be49d9deba2ad4a
It does not seem to happen when I trigger apheleia-format-buffer
manually.
Any ideas what can be wrong and if this is something that needs to be reported to Emacs itself, which I suspect might be the case.
Thank you for your assistance.
Currently if you save a buffer and then switch to a different buffer before Apheleia can finish reformatting, the contents of the new buffer can get overwritten. We need to save the buffer that was current when apheleia-format-buffer
was called, and be sure to update that buffer.
I am using diminish
for major modes. But I was not able to apply it for apheleia-mode
.
(apheleia-global-mode +1)
(with-eval-after-load 'apheleia
(setf (alist-get 'isort apheleia-formatters)
'("isort" "--stdout" "-"))
(setf (alist-get 'python-mode apheleia-mode-alist)
'(isort black))
)
(diminish 'apheleia-mode)
(diminish 'apheleia-global-mode)
I keep see following Apheleia
at bottom line, how can I suppress it:
Driver.py ~/program/user Git:dev (home) [Py] Top (15, 0) Apheleia------------
I received the error Failed to run rustfmt: exit status 1 (see hidden buffer *apheleia-rustfmt-log*)
.
But I cannot find *apheleia-rustfmt-log*
buffer.
BTW: rustfmt
binary is already installed
Working on a repository with a different style guide results in a lot of noise in the commits as the entire contents of a buffer are being reformatted. What would be the best way to circumvent that?
I would like some functionality similar to ws-butler that would only change lines that have actually been touched by the user.
The best solution would be to autodetect the style and format it accordingly, although that would have to be done on the formatters side and but would also defeat the purpose of it to some extent.
My current solution would be to specify the project or files in that project manually where the formatting should be disabled, although I would prefer an automatic way to do that.
Okay, so I've been using apheleia pretty heavily over the past few days/weeks and more than once I've run into issues with formatters failing or even worse my buffers being overwritten with the contents of both that buffer and another buffer.
a.py
, b.py
, c.py
.:wa
with evil-mode).import csv
# need to define cmp function in Python 3
def cmp(a, b):
return (a > b) - (a < b)
# write stocks data as comma-separated values
with open('stocks.csv', 'w', newline='') as stocksFileW:
writer = csv.writer(stocksFileW)
writer.writerows([
['GOOG', 'Google, Inc.', 505.24, 0.47, 0.09],
['YHOO', 'Yahoo! Inc.', 27.38, 0.33, 1.22],
['CNET', 'CNET Networks, Inc.', 8.62, -0.13, -1.4901]
])
# read stocks data, print status messages
with open('stocks.csv', 'r') as stocksFile:
stocks = csv.reader(stocksFile)
status_labels = {-1: 'down', 0: 'unchanged', 1: 'up'}
for ticker, name, price, change, pct in stocks:
status = status_labels[cmp(float(change), 0.0)]
print ('%s is %s (%.2f)' % (name, status, float(pct)))
Chances are at least one of these attempts to format the buffer on save will fail. In my case they both failed Failed to run isort: exit status 2 (see hidden buffer *apheleia-isort-stderr*) [2 times]
and the stderr buffer for isort contains:
Traceback (most recent call last):
File "/home/mohkale/.local/bin/isort", line 5, in <module>
from isort.main import main
File "/home/mohkale/.local/lib/python3.10/site-packages/isort/main.py", line 2, in <module>
import argparse
File "/usr/lib/python3.10/argparse.py", line 92, in <module>
from gettext import gettext as _, ngettext
File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 674, in _load_unlocked
File "<frozen importlib._bootstrap>", line 577, in module_from_spec
File "<frozen importlib._bootstrap>", line 541, in _init_module_attrs
KeyboardInterrupt
Note: this example also shows how the output of two buffers could end up mixed. I run a formatter, it starts formatting and outputting, then it's interrupted and another formatter is begun outputting to the same stdout buffer, then apheleia concatenates the partial output from the first and the full output of the second into the second buffer.
I think this is pretty clearly because apheleia uses the same buffer-names for every formatters stderr and stdout buffer & we explicitly kill any process in that buffer when running the formatter.
I like that apheleia keeps the stderr and stdout buffer around after the process finishes, it helps with debugging, but the current approach doesn't scale well to many files or workflows that touch many files before saving them. What I'd suggest is 2 things:
So we still keep the *apheleia-isort-stderr*
buffer, but instead of connecting it directly to a process, we give each process a unique stderr buffer and then append the output to this buffer at the end. If there's a worry it's getting too large in the background we can even only add to it if the formatter fails.
This is great, thanks for creating.
Do you have any tips on creating an apheleia-format-region
command?
Currently, if Apheleia reformats a buffer, then any Flycheck diagnostics provided by lsp-mode disappear until the next edit that causes them to be recomputed. I am not sure which package is at fault for this interaction.
I don't particularly know why ktfmt behaves in this way, but if stdin is already formatted properly, nothing will be printed to stdout, causing apheleia to delete the contents of the entire buffer. I see three ways to approach fixing this:
I'm going to be raising an issue on their tracker as well, since I feel like this behavior is unexpected and should probably have an option to disable it. That said, I do think there are probably other formatters that work like this somewhere out there and that apheleia should be able to handle them ideally. I wanted to raise this issue before implementing, since I wanted your feedback before creating a patch.
That all said: maybe we could just not touch the buffer at all if stdout is empty, and the return code is 0? I can't imagine a scenario in which an empty/entirely whitespace buffer would need special formatting at all (unless there's someone silly enough to have written a formatter for the whitespace programming language.
This is a bit of a weird issue, I admit, but after a long debugging session of my Radian config I finally tracked it down to apheleia.
I am using undo-fu-session
which has a before-save-hook
to save the undo data. In this hook, there's code like this:
(with-auto-compression-mode
(with-temp-buffer
...
(write-region nil nil undo-file nil 0)
t))
where the function write-region
is called on a temporary buffer. Note that 0
is passed to visit
of write-region
. The buffer content should be written to a file but the temporary buffer should not visit the file. I was surprised to see that the buffer was in fact visiting the file in undo-file
causing annoying dialogue boxes of "buffer changed.. save anyways?" everytime auto-save runs and if I select Yes to save, the undo file is overwritten!
I found out that the reason is apheleia--write-file-silently
where the function write-region
is bound to a lambda function that ignores the visit
argument and instead always passes t
in its place causing the file to be always visited (even if the buffer is temporary).
I can see that in the commit 8112041 visit
is always set to t
instead of 0
to fix an issue with undo-tree
, but I am wondering why the visit
argument is ignored at all? It seems to me that ignoring an argument of a such a basic function is a radical measure.
It would be nice if this were hosted on MELPA.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.