Giter Club home page Giter Club logo

radian-software / apheleia Goto Github PK

View Code? Open in Web Editor NEW
546.0 9.0 74.0 456 KB

🌷 Run code formatter on buffer contents without moving point, using RCS patches and dynamic programming.

License: MIT License

Emacs Lisp 77.52% Makefile 2.62% Shell 11.28% Dockerfile 0.43% Python 2.21% Haskell 0.75% C 0.36% Go 0.46% Java 0.18% TeX 0.23% Erlang 0.06% OCaml 0.17% CSS 0.30% HTML 0.54% JavaScript 0.90% SCSS 0.39% TypeScript 0.65% Rust 0.20% HCL 0.31% Dart 0.46%

apheleia's Introduction

Apheleia

Good code is automatically formatted by tools like Black or Prettier so that you and your team spend less time on formatting and more time on building features. It's best if your editor can run code formatters each time you save a file, so that you don't have to look at badly formatted code or get surprised when things change just before you commit. However, running a code formatter on save suffers from the following two problems:

  1. It takes some time (e.g. around 200ms for Black on an empty file), which makes the editor feel less responsive.
  2. It invariably moves your cursor (point) somewhere unexpected if the changes made by the code formatter are too close to point's position.

Apheleia is an Emacs package which solves both of these problems comprehensively for all languages, allowing you to say goodbye to language-specific packages such as Blacken and prettier-js.

The approach is as follows:

  1. Run code formatters on after-save-hook, rather than before-save-hook, and do so asynchronously. Once the formatter has finished running, check if the buffer has been modified since it started; only apply the changes if not.
  2. After running the code formatter, generate an RCS patch showing the changes and then apply it to the buffer. This prevents changes elsewhere in the buffer from moving point. If a patch region happens to include point, then use a dynamic programming algorithm for string alignment to determine where point should be moved so that it remains in the same place relative to its surroundings. Finally, if the vertical position of point relative to the window has changed, adjust the scroll position to maintain maximum visual continuity. (This includes iterating through all windows displaying the buffer, if there are more than one.) The dynamic programming algorithm runs in quadratic time, which is why it is only applied if necessary and to a single patch region.

Installation

Apheleia is available on MELPA. It is easiest to install it using straight.el:

(straight-use-package 'apheleia)

However, you may install using any other package manager if you prefer.

Dependencies

Emacs 27 or later is supported. Apheleia does not include any formatters. You must install any formatter separately that you wish to use. As long as it is on $PATH then Apheleia will pick it up automatically; missing formatters will silently be skipped, but errors from invoking installed formatters will be reported on buffer save.

It is recommended to have Bash installed, as this is used as a dependency for Apheleia to invoke certain formatters (e.g. Node.js-based formatters).

Windows support is not guaranteed due to lack of support for common open standards on this platform. Pull requests adjusting Apheleia for improved cross-platform portability will be accepted, but no guarantees are made about stability on Windows.

User guide

To your init-file, add the following form:

(apheleia-global-mode +1)

The autoloading has been configured so that this will not cause Apheleia to be loaded until you save a file.

By default, Apheleia is configured to format with Black, Prettier, and Gofmt on save in all relevant major modes. To configure this, you can adjust the values of the following variables:

  • apheleia-formatters: Alist mapping names of formatters (symbols like black and prettier) to commands used to run those formatters (such as ("black" "-") and (npx "prettier" input)). See the docstring for more information.
    • You can manipulate this alist using standard Emacs functions. For example, to add some command-line options to Black, you could use:

      (setf (alist-get 'black apheleia-formatters)
            '("black" "--option" "..." "-"))
    • There are a list of symbols that are interpreted by apheleia specially when formatting a command (example: npx). Any non-string entries in a formatter that doesn't equal one of these symbols is evaluated and replaced in place. This can be used to pass certain flags to the formatter process depending on the state of the current buffer. For example:

      (push '(shfmt . ("beautysh"
                       "-filename" filepath
                       (when-let ((indent (bound-and-true-p sh-basic-offset)))
                         (list "--indent-size" (number-to-string indent)))
                       (when indent-tabs-mode "--tab")
                       "-"))
            apheleia-formatters)

      This adds an entry to apheleia-formatters for the beautysh formatter. The evaluated entries makes it so that the --tab flag is only passed to beautysh when the value of indent-tabs-mode is true. Similarly the indent-size flag is passed the exact value of the sh-basic-offset variable only when it is bound. Observe that one of these evaluations returns a list of flags whereas the other returns a single string. These are substituted into the command as you'd expect.

    • You can also use Apheleia to format buffers that have no underlying files. In this case the value of file and filepath will be the name of the current buffer with any special characters for the file-system (such as * on windows) being stripped out.

      This is also how the extension for any temporary files apheleia might create will be determined. If you're using a formatter that determines the file-type from the extension you should name such buffers such that their suffixed with the extension. For example a buffer called *foo-bar.c* that has no associated file will have an implicit file-name of foo-bar.c and any temporary files will be suffixed with a .c extension.

    • You can implement formatters as arbitrary Elisp functions which operate directly on a buffer, without needing to invoke an external command. This can be useful to integrate with e.g. language servers. See the docstring for more information on the expected interface for Elisp formatters.

  • apheleia-mode-alist: Alist mapping major modes and filename regexps to names of formatters to use in those modes and files. See the docstring for more information.
    • You can use this variable to configure multiple formatters for the same buffer by setting the cdr of an entry to a list of formatters to run instead of a single formatter. For example you may want to run isort and black one after the other.

      (setf (alist-get 'isort apheleia-formatters)
            '("isort" "--stdout" "-"))
      (setf (alist-get 'python-mode apheleia-mode-alist)
            '(isort black))

      This will make apheleia run isort on the current buffer and then black on the result of isort and then use the final output to format the current buffer.

      Warning: At the moment there's no smart or configurable error handling in place. This means if one of the configured formatters fail (for example if isort isn't installed) then apheleia just doesn't format the buffer at all, even if black is installed.

      Warning: If a formatter uses file (rather than filepath or input or none of these keywords), it can't be chained after another formatter, because file implies that the formatter must read from the original file, not an intermediate temporary file. For this reason it's suggested to avoid the use of file in general.

  • apheleia-formatter: Optional buffer-local variable specifying the formatter to use in this buffer. Overrides apheleia-mode-alist. You can set this in a local variables list, or in .dir-locals.el (e.g. ((python-mode . ((apheleia-formatter . (isort black)))))), or in a custom hook of your own that sets the local variable conditionally.
  • apheleia-inhibit: Optional buffer-local variable, if set to non-nil then Apheleia does not turn on automatically even if apheleia-global-mode is on.

You can run M-x apheleia-mode to toggle automatic formatting on save in a single buffer, or M-x apheleia-global-mode to toggle the default setting for all buffers. Also, even if apheleia-mode is not enabled, you can run M-x apheleia-format-buffer to manually invoke the configured formatter for the current buffer. Running with a prefix argument will cause the command to prompt you for which formatter to run.

Apheleia does not currently support TRAMP, and is therefore automatically disabled for remote files.

If an error occurs while formatting, a message is displayed in the echo area. You can jump to the error by invoking M-x apheleia-goto-error, or manually switch to the log buffer mentioned in the message.

You can configure error reporting using the following user options:

  • apheleia-hide-log-buffers: By default, errors from formatters are put in buffers named like *apheleia-cmdname-log*. If you customize this user option to non-nil then a space is prepended to the names of these buffers, hiding them by default in switch-to-buffer (you must type a space to see them).
  • apheleia-log-only-errors: By default, only failed formatter runs are logged. If you customize this user option to nil then all runs are logged, along with whether or not they succeeded. This could be helpful in debugging.

The following user options are also available:

  • apheleia-post-format-hook: Normal hook run after Apheleia formats a buffer. Run if the formatting is successful, even when no changes are made to the buffer.
  • apheleia-max-alignment-size: The maximum number of characters that a diff region can have to be processed using Apheleia's dynamic programming algorithm for point alignment. This cannot be too big or Emacs will hang noticeably on large reformatting operations, since the DP algorithm is quadratic-time.
  • apheleia-mode-lighter: apheleia-mode lighter displayed in the mode-line. If you don't want to display it, use nil. Otherwise, its value must be a string.

Apheleia exposes some hooks for advanced customization:

  • apheleia-formatter-exited-hook: Abnormal hook which is run after a formatter has completely finished running for a buffer. Not run if the formatting was interrupted and no action was taken. Receives two arguments: the symbol for the formatter that was run (e.g. black, or it could be a list if multiple formatters were run in a chain), and a boolean for whether there was an error.

  • apheleia-inhibit-functions: List of functions to run before turning on Apheleia automatically from apheleia-global-mode. If one of these returns non-nil then apheleia-mode is not enabled in the buffer.

Formatter configuration

There is no configuration interface in Apheleia for formatter behavior. The way to configure a formatter is by editing a standard config file that it reads (e.g. .prettierrc.json), or setting an environment variable that it reads, or by changing the entry in apheleia-formatters to customize the command-line arguments.

There is one exception to this, which is that Apheleia's default command-line arguments for the built-in formatters will automatically check Emacs' indentation options for the corresponding major mode, and pass that information to the formatter. This way, the indentation (tabs vs spaces, and how many) applied by the formatter will match what electric indentation in Emacs is doing, preventing a shuffle back and forth as you type.

This behavior can be disabled by setting apheleia-formatters-respect-indent-level to nil.

Troubleshooting

Try running your formatter outside of Emacs to verify it works there. Check what command-line options it is configured with in apheleia-formatters.

To debug internal bugs, race conditions, or performance issues, try setting apheleia-log-debug-info to non-nil and check the contents of *apheleia-debug-log*. It will have detailed trace information about most operations performed by Apheleia.

Known issues

  • process aphelieia-whatever no longer connected to pipe; closed it: This happens on older Emacs versions when formatting a buffer with size greater than 65,536 characters. There is no known workaround besides disabling apheleia-mode for the affected buffer, or upgrading to a more recent version of Emacs. See #20.

Contributing

Please see the contributor guide for my projects for general information, and the following sections for Apheleia-specific details.

There's also a wiki that could do with additions/clarity. Any improvement suggestions should be submitted as an issue.

Adding a formatter

I have done my best to make it straightforward to add a formatter. You just follow these steps:

  1. Install your formatter on your machine so you can test.
  2. Create an entry in apheleia-formatters with how to run it. (See the docstring of this variable for explanation about the available keywords.)
  3. Add entries for the relevant major modes in apheleia-mode-alist.
  4. See if it works for you!
  5. Add a file at test/formatters/installers/yourformatter.bash which explains how to install the formatter on Ubuntu. This will be used by CI.
  6. Test with make fmt-build FORMATTERS=yourformatter to do the installation, then make fmt-docker to start a shell with the formatter available. Verify it runs in this environment.
  7. Add an example input (pre-formatting) and output (post-formatting) file at test/formatters/samplecode/yourformatter/in.whatever and test/formatters/samplecode/yourformatter/out.whatever.
  8. Verify that the tests are passing, using make fmt-test FORMATTERS=yourformatter from inside the fmt-docker shell.
  9. Submit a pull request, CI should now be passing!

Acknowledgements

I got the idea for using RCS patches to avoid moving point too much from prettier-js, although that package does not implement the dynamic programming algorithm which Apheleia uses to guarantee stability of point even within a formatted region.

Note that despite this inspiration, Apheleia is a clean-room implementation which is free of the copyright terms of prettier-js.

apheleia's People

Contributors

adimit avatar alternateved avatar andersk avatar cxa avatar dalugm avatar danielpza avatar dpassen avatar edslocomb avatar elken avatar itshoff avatar jsmestad avatar leonidborisenko avatar leotaku avatar leungbk avatar meliache avatar michzappa avatar mohkale avatar offbyone avatar primarycanary avatar raxod502 avatar scop avatar semeninrussia avatar sgherdao avatar sohumb avatar stonekyx avatar strake7 avatar terlar avatar tpeacock19 avatar xfa25e avatar zeorin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

apheleia's Issues

[help] How can I integrate `latexindent ` into alpheleia?

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?

TRAMP support

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. 🙂

Support formatting when committing indirect buffers

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.

Formatting a buffer changes the mode

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.

Allow running multiple formatters in sequence

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.

Mixed line ending after formatting in windows + msys

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 -

main-code

prettier-stdout buffer -

prettier-stdout

diff-stdout buffer -
diff-stdout

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.

Wrong type argument error on save

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.

Is there a way to add a non stdin/stdout formatter?

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?

Debugger entered--Lisp error: (void-variable apheleia-formatters)

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?

After post-save auto-formatting, buffer visits a Flycheck temp file

(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:

  1. emacs -q -l above-init.el. The Python-formatter black should also be installed.
  2. C-x C-f hello.py
  3. Write
def hi():


    return 42

and then save the file, allowing Apheleia to clean up the extra whitespace.

  1. 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).

Global mode conflicts with php-mode

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.

https://github.com/raxod502/apheleia/blob/8a1e68441ca418c2a277d7aef663790f26208dd8/apheleia.el#L351-L354

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.

apheleia--write-file-silently ignores visit in write-region

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.

Race condition when saving multiple buffers in a short time

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.

Better handling for non-file buffers

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.

Add to MELPA

It would be nice if this were hosted on MELPA.

Integrating `rustfmt`

Background

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.
  • (in my eyes at least) --skip-children has a bug: it still outputs the file name in the first line, and two additional newlines.

Questions to Apheleia

  • Is there a provision to make changes to the output of formatting commands? I'd like to drop the first two lines without shell magic.

Of course, I think that the underlying issue should be patched out of rust-analyzer. I'll see if I can do that.

Workaround

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.

Use replace-buffer-contents?

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

RFC: Support formatting only changed regions

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.

Support formatters that output a diff

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. Update the file in place
  2. Output a diff

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!

Processes that fail stick around

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.

Does apheleia support formatting of narrowed buffer?

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

apheleia-hide-old-log-entries is deleted

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?

Trying to add a new formatter for eslint_d

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)

Apheleia conflicts with undo-tree-auto-save-history

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.


Steps to reproduce:

  1. Enable global-undo-tree-mode and apheleia-global-mode
  2. Set undo-tree-auto-save-history to t
  3. Visit a file with apheleia autoformatting (e.g. test.py)
  4. Type something and save a file (C-x C-s)

Expected behavior:

The is saved and formatted

Actual behavior:

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 ❤️

Cannot save many files at once because process buffers are tied to formatters

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.

Reproduction Instructions

  1. Create a temporary directory foo.
  2. In foo create 3 files: a.py, b.py, c.py.
  3. Open all of them in emacs and enable apheleia-mode.
  4. Insert into each of them the following src-code block.
  5. Save all of them at once (example using :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.

Analysis

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:

  1. Don't keep the stdout buffer around. I don't really see the point of this at least from a user POV. Better to use it to format the current buffer and then delete it.
  2. Create separate buffers for stdout and stderr whenever you run a formatter and then insert any stderr output to a shared stderr buffer for that formatter.

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.

Conflicts with lsp-mode diagnostics

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.

Be less obtrusive in large repos with different styles

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.

Infinite symlink lookup recursion when using apheleia--format-after-save

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.

Automated formatter testing and CI

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.

Point is sometimes moved small distances

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.

Error buffers are hidden, which makes it hard to see the problem

When formatting fails, we have to:

  1. Navigate to *Messages*
  2. Search for "Failed to run ...: exit status ... (see hidden buffer apheleia-...-stderr)"
  3. Copy the " *apheleia-...-stderr" buffer name, including the the initial space
  4. Type M-S-; (switch-to-buffer " *apheleia ... stderr") RET

Should there be a less painful way?

Impose maximum length for dynamic programming algorithm

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.

Don't change default-directory?

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.

Migrate back to CircleCI

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.

Seems to wreck undo history

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).

Run formatter under project root directory

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).

Fails for buffers greater than 2^16 characters

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.

apheleia messes with `*jka-compr-wr-temp*` buffer somehow

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:

image

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)

.pretterignore is ignored

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!

How can I apply diminish-mode to aphelei-mode?

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------------

How can I change line length settings for the black formatter?

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 blackwith 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.

After formatting a file in web-mode, mode is switched to mhtml-mode

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

Confusion about use of log buffer

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

ktfmt and other formatters which output nothing to stdout on success clobber buffer

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:

  • Change apheleia to be able to handle cases like this. (ideal)
  • Add an option to ktfmt to always print something to stdout. (semi-ideal)
  • Write a wrapper script around ktfmt which always prints something. (not very ideal at all)

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.

Functional formatters (for use with lsp-mode/eglot)

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.

Formatters ran by apheleia doesn't respect per-project configuration

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.

Steps to reproduce

  1. Create an .editorconfig file with the following content:

    root = true
    
    [*]
    indent_size = 4
    
  2. Create test.js with the following content:

    function foo() {
    return true;
    }
    
  3. Format the file with npx prettier test.js

  4. Format the file with Apheleia

  5. Compare results between (3) and (4)

Expected behavior

Apheleia-run formatted file becomes (4 spaces):

function foo() {
    return true;
}

Actual behavior

Apheleia-run formatted file becomes (2 spaces):

function foo() {
  return true;
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.