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

View file

@ -1,87 +1,79 @@
;;; editor/format/config.el -*- lexical-binding: t; -*- ;;; 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 '(sql-mode ; sqlformat is currently broken
tex-mode ; latexindent is broken tex-mode ; latexindent is broken
latex-mode latex-mode
org-msg-edit-mode) ; doesn't need a formatter org-msg-edit-mode) ; doesn't need a formatter
"A list of major modes in which to not reformat the buffer upon saving. "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 it is t, it is disabled in all modes, the same as if the +onsave flag wasn't
"If non-nil, the leading indentation is preserved when formatting the whole used at all.
buffer. This is particularly useful for partials. If nil, formatting is enabled in all modes."
:type '(list symbol))
Indentation is always preserved when formatting regions.") (defcustom +format-with-lsp t
(defvar +format-with-lsp t
"If non-nil, format with LSP formatter if it's available. "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 This can be set buffer-locally with `setq-hook!' to disable LSP formatting in
select buffers. select buffers, from a project's .dir-locals.el file, or as a file-local
This has no effect on the +onsave flag, apheleia will always be used there.") variable."
:type 'boolean
:safe 'booleanp)
(defvaralias '+format-with 'apheleia-formatter (defvaralias '+format-with 'apheleia-formatter)
"Set this to explicitly use a certain formatter for the current buffer.")
(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 ;;; Bootstrap
(when (modulep! +onsave) (use-package! apheleia
(add-hook 'doom-first-file-hook #'apheleia-global-mode)) :defer t
:init
(defun +format-maybe-inhibit-h () (when (modulep! +onsave)
"Check if formatting should be disabled for current buffer. (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'." This is controlled by `+format-on-save-disabled-modes'."
(or (eq major-mode 'fundamental-mode) (or (eq major-mode 'fundamental-mode)
(string-blank-p (buffer-name)) (string-blank-p (buffer-name))
(eq +format-on-save-disabled-modes t) (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)))))))
:config
(after! apheleia
(add-to-list 'doom-debug-variables '(apheleia-log-only-errors . nil)) (add-to-list 'doom-debug-variables '(apheleia-log-only-errors . nil))
(when (modulep! +onsave) ;; Use the formatter provided by lsp-mode and eglot, if they are available and
(add-to-list 'apheleia-inhibit-functions #'+format-maybe-inhibit-h))) ;; `+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."
:around #'save-buffer
(let ((apheleia-mode (and apheleia-mode (memq arg '(nil 1)))))
(funcall orig-fn)))
;; ;; HACK: Apheleia suppresses notifications that the current buffer has
;;; Hacks ;; changed, so plugins that listen for them need to be manually informed:
(add-hook!
(defadvice! +format--inhibit-reformat-on-prefix-arg-a (orig-fn &optional arg) 'apheleia-post-format-hook
"Make it so \\[save-buffer] with prefix arg inhibits reformatting." (defun +format--update-web-mode-h ()
:around #'save-buffer (when (eq major-mode 'web-mode)
(let ((apheleia-mode (and apheleia-mode (memq arg '(nil 1))))) (setq web-mode-fontification-off nil)
(funcall orig-fn))) (when (and web-mode-scan-beg web-mode-scan-end global-font-lock-mode)
(save-excursion
(add-hook! (font-lock-fontify-region web-mode-scan-beg web-mode-scan-end)))))
'apheleia-post-format-hook (defun +format--update-vc-gutter-h ()
;; HACK `web-mode' doesn't update syntax highlighting after arbitrary buffer (when (fboundp '+vc-gutter-update-h)
;; modifications, so we must trigger refontification manually. (+vc-gutter-update-h)))))
(defun +format--fix-web-mode-fontification-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 ()
(when (fboundp '+vc-gutter-update-h)
(+vc-gutter-update-h))))