apheleia icon indicating copy to clipboard operation
apheleia copied to clipboard

Feature request: support https://codeberg.org/ideasman42/emacs-elisp-autofmt

Open hab25 opened this issue 2 years ago • 4 comments

Further, when the user has elisp-autofmt installed, apheleia should probably use it by default for elisp; it is much more powerful than lisp-indent .

hab25 avatar Dec 19 '23 05:12 hab25

I'm a bit wary of enabling automatic reformatting for Emacs Lisp by default, as there are not universal style guidelines that everybody agrees on. I might be convinced. In any case, there is nothing wrong with adding a formatter for it, which can be enabled by the user if they want.

raxod502 avatar Dec 22 '23 03:12 raxod502

I'm a bit wary of enabling automatic reformatting

Despite the name, elisp-autofmt provides commands to be used non-automatically, and used in this way it still has the significant advantage over lisp-indent in that the former does much more formatting.

hab25 avatar Dec 22 '23 05:12 hab25

Right, but if we register the formatter in apheleia-mode-alist by default, then it will indeed become automatic. Like I said I am fine with registering it in apheleia-formatters, though, and letting the user decide if they want it to be run automatically. With the formatter registered, it can always be run manually.

raxod502 avatar Dec 30 '23 21:12 raxod502

In case it helps anyone, I'm using this setup to integrate elisp-autofmt with apheleia:

(add-to-list
   'apheleia-formatters
   '(elisp-autofmt
     .
     (
      ;; Evaluates to the main external formatter command that would be run (synchronously) by
      ;; `elisp-autofmt-buffer', as a list of strings. This expression will be evaluated whenever
      ;; apheleia runs the formatter, and "flattened" into the main command definition.
      ;;
      ;; The logic to build the formatting command is rather complicated, so we extract it directly
      ;; from elisp-autofmt by advising some internals and running a "no-op" formatting pass.
      ;;
      ;; The formatting command executes a python script, with a number of flags (to set line width,
      ;; etc) based on values from the current buffer. It reads the source from stdin and outputs
      ;; the formatted contents to stdout, as expected by apheleia by default.
      (let ((command nil))
        (require 'elisp-autofmt)
        (elisp-autofmt--with-advice
         ((
           ;; elisp-autofmt uses this to (synchronously) invoke formatting and other processes.
           #'elisp-autofmt--call-process
           :around
           ;; Intercept the formatting command from elisp-autofmt and store it in the local `command'
           ;; variable.
           (lambda (orig-fn proc-id command-with-args stdin-buffer stdout-buffer)
             ;; elisp-autofmt also sometimes needs to generate cache files via external commands. We
             ;; don't want to interfere with those processes; they need to run before the main
             ;; apheleia formatter command can succeed.
             ;;
             ;; Check if this is the formatting process by looking for a python command which doesn't
             ;; include a "--gen-defs" argument.
             (if (and (string-equal (car command-with-args) elisp-autofmt-python-bin)
                      (not (member "--gen-defs" command-with-args)))
                 (progn
                   ;; Store the command so we can pass it to apheleia.
                   (setq command command-with-args)
                   ;; Fake a "successfully executed" formatting run (response code 2 as expected by
                   ;; elisp-autofmt, and no stderr output).
                   '(2 . nil))
               ;; If this isn't the formatting process (i.e. it's a caching process), just let it run
               ;; normally. This should occur pretty infrequently.
               ;;
               ;; Note these cache commands sometimes fail with broken pipe errors, even when using
               ;; `elisp-autofmt-buffer' directly. But the underlying processes do get triggered, and
               ;; the formatter seems to fix itself after a few attempts.
               (funcall orig-fn proc-id command-with-args stdin-buffer stdout-buffer))))
          ;; Don't let elisp-autofmt actually write to the buffer at this point, as it
          ;; would fill it with empty output.
          (#'elisp-autofmt--replace-region-contents-wrapper :override (lambda (&rest _))))
         ;; Now perform a fake formatting pass to intercept/store the formatting command. This
         ;; should be fast, since the formatting process isn't actually getting run (though note
         ;; that it's possible for cache processes to be run synchronously here, which may cause
         ;; temporary broken pipe issues as described above).
         (elisp-autofmt-buffer)
         (unless (listp command)
           (error "Could not determine elisp-autofmt formatting command"))
         ;; Return the extracted command; apheleia will recognize it as a list of arguments.
         command))
      ;; elisp-autofmt normally forces a non-zero exit code for the formatter's success case, which
      ;; is handled internally during elisp-autofmt processing. For apheleia we need to restore sane
      ;; exit code behavior.
      "--exit-code=0")))

The basic idea is to intercept the (rather complex) python command that would normally be invoked synchronously during elisp-autofmt-buffer, and allow apheleia to invoke it asynchronously instead. Doing it this way respects the current value of things like fill-column (as opposed to using elisp-autofmt's built-in batch formatting script). It also allows elisp-autofmt to generate cache content when necessary.

Obviously this is pretty dependent on elisp-autofmt's internals, but afaict it's the cleanest way to build the command and leverage all the related elisp-autofmt logic.

kmontag avatar Nov 12 '25 22:11 kmontag