refactor!(format): simplify & gate lsp/eglot impl behind +lsp

BREAKING CHANGE: This commit introduces two changes, a breaking, one
not.

The breaking change: `+format-with-lsp` used to control lsp-mode/eglot
integration for this module, but it is now gated behind a new +lsp flag.
Users will need to add it to their `doom!` blocks (in $DOOMDIR/init.el)
to restore the integration.

The other change: I've merged the former `lsp` and `eglot` formatters
into a single `lsp` formatter that dispatches to the appropriate
backend, as well as wrapping this integration in a
`+format-with-lsp-mode` minor mode, so it can be toggled at will; mainly
to assist in debugging formatter behavior.

A unified formatter makes it easier for folks to configure
`+format-with` on a per-project/directory/file basis, without needing to
know what backend this module uses, and opens us up to integrating other
LSP backends in the future (like lsp-bridge).
This commit is contained in:
Henrik Lissner 2024-07-07 20:44:33 -04:00
parent cca40c0277
commit fd1941b95f
No known key found for this signature in database
GPG key ID: B60957CA074D39A3
3 changed files with 70 additions and 59 deletions

View file

@ -27,6 +27,9 @@ be formatted and returned to the buffer using
Enable reformatting of a buffer when it is saved. See
[[var:+format-on-save-disabled-modes]] to disable format on save for certain
major modes.
- +lsp ::
Use ~lsp-mode~'s or ~eglot~'s formatters, instead of Apheleia's, if the active
client has the capabilities.
** Packages
- [[doom-package:apheleia]]

View file

@ -1,67 +1,86 @@
;;; editor/format/autoload/lsp.el -*- lexical-binding: t; -*-
;;;###if (modulep! :tools lsp)
(defvar +format-with--last nil)
(defvar +format-lsp--last nil)
;;;###autoload
(defun +format-enable-lsp-formatter-h ()
"TODO"
(when (and +format-with-lsp (null +format-with))
(when (or (lsp-feature? "textDocument/formatting")
(lsp-feature? "textDocument/rangeFormatting"))
(setq-local +format-with--last +format-with
+format-with 'lsp))))
(define-minor-mode +format-with-lsp-mode
"Toggles `lsp-mode'/`eglot' integration with `apheleia-mode' in this buffer.
When enabled, it changes `+format-with' to `lsp', and back to its old value when
disabled. Use `+format-wtih-lsp-maybe-h' to activate it, to ensure it only
activates if lsp-mode/eglot are on and connected to a valid client, and so this
mode won't conflict with pre-existing user config on `+format-with'."
:init-value nil
(unless (local-variable-p '+format-lsp--last)
(setq-local +format-lsp--last +format-with))
(setq-local +format-with
(if +format-with-lsp-mode
(cl-remove-duplicates (cons 'lsp +format-with) :test #'eq)
(prog1 (remq 'lsp (ensure-list +format-lsp--last))
(kill-local-variable '+format-lsp--last)))))
(defvar +format--lsp-alist
'((lsp-managed-mode . +format--with-lsp-mode)
(eglot--managed-mode . +format--with-eglot))
"A list of LSP formatter functions to try on `+format-lsp-buffer'.")
(defun +format--lsp-fn ()
(cl-loop for (sym . fn) in +format--lsp-alist
if (and (boundp sym) (symbol-value sym))
return fn))
;;;###autoload
(defun +format-disable-lsp-formatter-h ()
"TODO"
(when (local-variable-p '+format-with--last)
(kill-local-variable '+format-with--last)
(setq-local +format-with +format-with--last)))
(defun +format-with-lsp-toggle-h ()
"Toggle `+format-with-lsp-mode' depending on the state of lsp-mode/eglot.
;;;###autoload
(defun +format-toggle-eglot-formatter-h ()
"TODO"
(if (bound-and-true-p eglot--managed-mode)
(when (and +format-with-lsp (null +format-with))
(when (or (eglot-server-capable :documentFormattingProvider)
(eglot-server-capable :documentRangeFormattingProvider))
(setq-local +format-with--last +format-with
+format-with 'eglot)))
(when +format-with--last
(kill-local-variable '+format-with--last)
(setq-local +format-with +format-with--last))))
Does not activate the mode if `+format-with' is already set. To activate the
mode unconditionally, call `+format-with-lsp-mode' instead."
(when (or (null +format-with) +format-with-lsp-mode)
(+format-with-lsp-mode (if (+format--lsp-fn) +1 -1))))
;;
;;; Apheleia formatters
;;;###autoload
(cl-defun +format-lsp-buffer (&key buffer scratch callback &allow-other-keys)
(cl-defun +format-lsp-buffer (&rest plist &key callback &allow-other-keys)
"Format the current buffer with any available lsp-mode or eglot formatter."
(if-let* ((fn (+format--lsp-fn))
((apply fn plist)))
(funcall callback)
(funcall callback "LSP server doesn't support formatting")))
(cl-defun +format--with-lsp-mode (&key buffer scratch &allow-other-keys)
"Format the current buffer with any available lsp-mode formatter."
(with-current-buffer buffer
(let ((edits
(cond
((lsp-feature? "textDocument/formatting")
(lsp-request "textDocument/formatting" (lsp--make-document-formatting-params)))
(lsp-request "textDocument/formatting"
(lsp--make-document-formatting-params)))
((lsp-feature? "textDocument/rangeFormatting")
(lsp-request "textDocument/rangeFormatting"
(lsp--make-document-range-formatting-params
(point-min) (point-max)))))))
(unless (seq-empty-p edits)
(with-current-buffer scratch
(lsp--apply-text-edits edits 'format))))
(funcall callback)))
(point-min) (point-max))))
(:err))))
(unless (eq edits :err)
(unless (seq-empty-p edits)
(with-current-buffer scratch
(lsp--apply-text-edits edits 'format)))
t))))
;;;###autoload
(cl-defun +format-eglot-buffer (&key buffer scratch callback &allow-other-keys)
(cl-defun +format--with-eglot (&key buffer scratch &allow-other-keys)
"Format the current buffer with any available eglot formatter."
(with-current-buffer scratch
(setq-local eglot--cached-server
(with-current-buffer buffer
(eglot-current-server)))
(let ((buffer-file-name (buffer-file-name buffer)))
(eglot-format-buffer))
(funcall callback)))
(when (setq-local
eglot--cached-server
(with-current-buffer buffer
(when (or (eglot-server-capable :documentFormattingProvider)
(eglot-server-capable :documentRangeFormattingProvider))
(eglot-current-server))))
(let ((buffer-file-name (buffer-file-name buffer)))
(eglot-format-buffer))
t)))
;;; lsp.el ends here

View file

@ -12,17 +12,6 @@ If it is t, it is disabled in all modes, the same as if the +onsave flag wasn't
If nil, formatting is enabled in all modes."
:type '(list symbol))
(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, from a project's .dir-locals.el file, or as a file-local
variable."
:type 'boolean
:safe 'booleanp)
(defvaralias '+format-with 'apheleia-formatter)
(defvaralias '+format-inhibit 'apheleia-inhibit)
@ -46,19 +35,19 @@ This is controlled by `+format-on-save-disabled-modes'."
(eq +format-on-save-disabled-modes t)
(not (null (memq major-mode +format-on-save-disabled-modes)))))))
;; 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-hook 'eglot-managed-mode-hook #'+format-toggle-eglot-formatter-h))
((modulep! :tools lsp)
(add-hook 'lsp-configure-hook #'+format-enable-lsp-formatter-h)
(add-hook 'lsp-unconfigure-hook #'+format-disable-lsp-formatter-h)))
;; Use the formatter provided by lsp-mode and eglot, if available.
(when (modulep! +lsp)
(add-hook 'eglot-managed-mode-hook #'+format-with-lsp-maybe-h)
(add-hook 'lsp-managed-mode-hook #'+format-with-lsp-maybe-h))
:config
(add-to-list 'doom-debug-variables '(apheleia-log-only-errors . nil))
(add-to-list 'doom-debug-variables '(apheleia-log-debug-info . t))
(add-to-list 'apheleia-formatters '(eglot . +format-eglot-buffer))
;; A psuedo-formatter that dispatches to the appropriate LSP client (via
;; `lsp-mode' or `eglot') that is capable of formatting. Without +lsp, users
;; must manually set `+format-with' to `lsp' to use it, or activate
;; `+format-with-lsp-mode' in the appropriate modes.
(add-to-list 'apheleia-formatters '(lsp . +format-lsp-buffer))
(defadvice! +format--inhibit-reformat-on-prefix-arg-a (orig-fn &optional arg)