refactor(format): redesign module

Rather than wrap Apheleia in custom formatting logic, I now use
Apheleia's own machinary to integrate into LSP and Eglot, which is less
complexity to maintain. It also makes settings +format-with a more
reliable option for per-project or per-file configuration.

This also adds a +format/org-src-block command, which I'll incorporate
into the org module in a follow-up commit.

Ref: #7685
This commit is contained in:
Henrik Lissner 2024-07-06 19:54:55 -04:00
parent b0e16dc243
commit 8072762de8
No known key found for this signature in database
GPG key ID: B60957CA074D39A3
2 changed files with 90 additions and 142 deletions

View file

@ -6,6 +6,9 @@
(skip-chars-forward " \t\n")
(current-indentation)))
;;;###autoload (autoload 'apheleia--get-formatters "apheleia-formatters")
;;;###autoload
(defun +format-region (start end &optional callback)
"Format from START to END with `apheleia'."
(when-let* ((command (apheleia--get-formatters
@ -42,15 +45,16 @@
(lambda ()
(with-current-buffer formatted-buffer
(when (> indent 0)
;; restore indentation without affecting new
;; indentation
;; restore indentation without affecting new indentation
(indent-rigidly (point-min) (point-max)
(max 0 (- indent (+format--current-indentation)))))
(set-buffer-modified-p nil))
(with-current-buffer cur-buffer
(delete-region start end)
(goto-char start)
(save-excursion
(insert-buffer-substring-no-properties formatted-buffer)
(when callback (funcall callback))
(when callback (funcall callback)))
(kill-buffer formatted-buffer)))))))
@ -58,22 +62,17 @@
;;; Commands
;;;###autoload
(defun +format/buffer (&optional arg)
"Reformat the current buffer using LSP or `format-all-buffer'."
(interactive "P")
(or (run-hook-with-args-until-success '+format-functions (point-min) (point-max) 'buffer)
(call-interactively #'apheleia-format-buffer)))
(defalias '+format/buffer #'apheleia-format-buffer)
;;;###autoload
(defun +format/region (beg end &optional arg)
(defun +format/region (beg end &optional _arg)
"Runs the active formatter on the lines within BEG and END.
WARNING: this may not work everywhere. It will throw errors if the region
contains a syntax error in isolation. It is mostly useful for formatting
snippets or single lines."
(interactive "rP")
(or (run-hook-with-args-until-success '+format-functions beg end 'region)
(+format-region beg end)))
(+format-region beg end))
;;;###autoload
(defun +format/region-or-buffer ()
@ -85,72 +84,29 @@ is selected)."
#'+format/region
#'+format/buffer)))
;;
;;; Specialized formatters
;;;###autoload
(defun +format-with-lsp-fn (beg end op)
"Format the region/buffer using any available lsp-mode formatter.
Does nothing if `+format-with-lsp' is nil or the active server doesn't support
the requested feature."
(and +format-with-lsp
(bound-and-true-p lsp-mode)
(pcase op
('buffer (condition-case _
;; Avoid lsp-feature? checks for this, since
;; `lsp-format-buffer' does its own, and allows clients
;; without formatting support (but with rangeFormatting,
;; for some reason) to work.
(always (lsp-format-buffer))
('lsp-capability-not-supported nil)))
('region (if (lsp-feature? "textDocument/rangeFormatting")
(always (lsp-format-region beg end))))
(_ (error "Invalid formatter operation: %s" op)))))
;;;###autoload
(defun +format-with-eglot-fn (beg end op)
"Format the region/buffer using any available eglot formatter.
Does nothing if `+format-with-lsp' is nil or the active server doesn't support
the requested feature."
(and +format-with-lsp
(bound-and-true-p eglot--managed-mode)
(pcase op
('buffer (if (eglot--server-capable :documentFormattingProvider)
(always (eglot-format-buffer))))
('region (if (eglot--server-capable :documentRangeFormattingProvider)
(always (eglot-format beg end))))
(_ (error "Invalid formatter operation: %s" op)))))
;;;###autoload
(defun +format-in-org-src-blocks-fn (beg end _op)
"Reformat org src blocks with apheleia as if they were independent buffers."
(when (derived-mode-p 'org-mode)
(goto-char beg)
(while (re-search-forward org-babel-src-block-regexp end t)
(let* ((element (org-element-at-point))
(block-beg (save-excursion
(goto-char (org-babel-where-is-src-block-head element))
(line-beginning-position 2)))
(block-end (save-excursion
(goto-char (org-element-property :end element))
(skip-chars-backward " \t\n")
(line-beginning-position)))
(beg (max beg block-beg))
(end (min end block-end))
(lang (org-element-property :language element))
(major-mode (org-src-get-lang-mode lang)))
(defun +format/org-block (point)
"Reformat the org src block at POINT with a mode approriate formatter."
(interactive (list (point)))
(unless (derived-mode-p 'org-mode)
(user-error "Not an org-mode buffer!"))
(let ((element (org-element-at-point point)))
(unless (org-in-src-block-p nil element)
(user-error "Not in an org src block"))
(cl-destructuring-bind (beg end _) (org-src--contents-area element)
(let* ((lang (org-element-property :language element))
(mode (org-src-get-lang-mode lang)))
(save-excursion
(if (eq major-mode 'org-mode)
(user-error "Cannot reformat an org src block in org-mode")
;; Determine formatter based on language and format the region
(let ((formatter (apheleia--get-formatters 'interactive)))
(unless formatter
(setq formatter (apheleia--get-formatters 'prompt))
(unless formatter
(user-error "No formatter configured for language: %s" lang)))
(let ((apheleia-formatter formatter))
(+format-region beg end)))))))
t))
(if (provided-mode-derived-p mode 'org-mode)
(user-error "Cannot reformat an org-mode or org-derived src block")
(let* ((major-mode mode)
(after-change-functions
;; HACK: Silence excessive and unhelpful warnings about
;; 'org-element-at-point being used in non-org-mode
;; buffers'.
(remq 'org-indent-refresh-maybe after-change-functions))
(apheleia-formatter
(or (apheleia--get-formatters 'interactive)
(apheleia--get-formatters 'prompt)
(user-error "No formatter configured for language: %s" lang))))
(+format-region beg end))))))))

View file

@ -1,69 +1,62 @@
;;; editor/format/config.el -*- lexical-binding: t; -*-
(defvar +format-on-save-disabled-modes
(defcustom +format-on-save-disabled-modes
'(sql-mode ; sqlformat is currently broken
tex-mode ; latexindent is broken
latex-mode
org-msg-edit-mode) ; doesn't need a formatter
"A list of major modes in which to not reformat the buffer upon saving.
If it is t, it is disabled in all modes, the same as if the +onsave flag
wasn't used at all.
If nil, formatting is enabled in all modes.
Irrelevant if you do not have the +onsave flag enabled for this module.")
(defvar +format-preserve-indentation t
"If non-nil, the leading indentation is preserved when formatting the whole
buffer. This is particularly useful for partials.
If it is t, it is disabled in all modes, the same as if the +onsave flag wasn't
used at all.
If nil, formatting is enabled in all modes."
:type '(list symbol))
Indentation is always preserved when formatting regions.")
(defvar +format-with-lsp t
(defcustom +format-with-lsp t
"If non-nil, format with LSP formatter if it's available.
LSP formatter is provided by either `lsp-mode' or `eglot'.
This can be set buffer-locally with `setq-hook!' to disable LSP formatting in
select buffers.
This has no effect on the +onsave flag, apheleia will always be used there.")
select buffers, from a project's .dir-locals.el file, or as a file-local
variable."
:type 'boolean
:safe 'booleanp)
(defvaralias '+format-with 'apheleia-formatter
"Set this to explicitly use a certain formatter for the current buffer.")
(defvaralias '+format-with 'apheleia-formatter)
(defvar +format-functions
'(+format-in-org-src-blocks-fn
+format-with-lsp-fn
+format-with-eglot-fn)
"A list of functions to run when formatting a buffer or region.
Each function is given three arguments: the starting point, end point, and a
symbol indicating the type of operation being requested (as a symbol: either
`region' or `buffer').
The first function to return non-nil will abort all functions after it,
including Apheleia itself.")
;;
;;; Bootstrap
(use-package! apheleia
:defer t
:init
(when (modulep! +onsave)
(add-hook 'doom-first-file-hook #'apheleia-global-mode))
(add-hook 'doom-first-file-hook #'apheleia-global-mode)
;; apheleia autoloads `apheleia-inhibit-functions' so it will be immediately
;; available to mutate early.
(add-hook! 'apheleia-inhibit-functions
(defun +format-maybe-inhibit-h ()
"Check if formatting should be disabled for current buffer.
This is controlled by `+format-on-save-disabled-modes'."
(or (eq major-mode 'fundamental-mode)
(string-blank-p (buffer-name))
(eq +format-on-save-disabled-modes t)
(not (null (memq major-mode +format-on-save-disabled-modes)))))
(not (null (memq major-mode +format-on-save-disabled-modes)))))))
(after! apheleia
:config
(add-to-list 'doom-debug-variables '(apheleia-log-only-errors . nil))
(when (modulep! +onsave)
(add-to-list 'apheleia-inhibit-functions #'+format-maybe-inhibit-h)))
;;
;;; Hacks
;; Use the formatter provided by lsp-mode and eglot, if they are available and
;; `+format-with-lsp' is non-nil.
(cond ((modulep! :tools lsp +eglot)
(add-to-list 'apheleia-formatters '(eglot . +format-eglot-buffer))
(add-hook 'eglot-managed-hook #'+format-toggle-eglot-formatter-h))
((modulep! :tools lsp)
(add-to-list 'apheleia-formatters '(lsp . +format-lsp-buffer))
(add-hook 'lsp-configure-hook #'+format-enable-lsp-formatter-h)
(add-hook 'lsp-unconfigure-hook #'+format-disable-lsp-formatter-h)))
(defadvice! +format--inhibit-reformat-on-prefix-arg-a (orig-fn &optional arg)
"Make it so \\[save-buffer] with prefix arg inhibits reformatting."
@ -71,17 +64,16 @@ This is controlled by `+format-on-save-disabled-modes'."
(let ((apheleia-mode (and apheleia-mode (memq arg '(nil 1)))))
(funcall orig-fn)))
;; HACK: Apheleia suppresses notifications that the current buffer has
;; changed, so plugins that listen for them need to be manually informed:
(add-hook!
'apheleia-post-format-hook
;; HACK `web-mode' doesn't update syntax highlighting after arbitrary buffer
;; modifications, so we must trigger refontification manually.
(defun +format--fix-web-mode-fontification-h ()
(defun +format--update-web-mode-h ()
(when (eq major-mode 'web-mode)
(setq web-mode-fontification-off nil)
(when (and web-mode-scan-beg web-mode-scan-end global-font-lock-mode)
(save-excursion
(font-lock-fontify-region web-mode-scan-beg web-mode-scan-end)))))
(defun +format--refresh-vc-gutter-h ()
(defun +format--update-vc-gutter-h ()
(when (fboundp '+vc-gutter-update-h)
(+vc-gutter-update-h))))
(+vc-gutter-update-h)))))