From fd1941b95f22ce3649ef7c5b28b9b5c982586bb4 Mon Sep 17 00:00:00 2001 From: Henrik Lissner Date: Sun, 7 Jul 2024 20:44:33 -0400 Subject: [PATCH] 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). --- modules/editor/format/README.org | 3 + modules/editor/format/autoload/lsp.el | 99 ++++++++++++++++----------- modules/editor/format/config.el | 27 +++----- 3 files changed, 70 insertions(+), 59 deletions(-) diff --git a/modules/editor/format/README.org b/modules/editor/format/README.org index d5c855bac..7be3facdc 100644 --- a/modules/editor/format/README.org +++ b/modules/editor/format/README.org @@ -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]] diff --git a/modules/editor/format/autoload/lsp.el b/modules/editor/format/autoload/lsp.el index a3cd82505..0235b0763 100644 --- a/modules/editor/format/autoload/lsp.el +++ b/modules/editor/format/autoload/lsp.el @@ -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 diff --git a/modules/editor/format/config.el b/modules/editor/format/config.el index 88d6d58b3..bef0b8737 100644 --- a/modules/editor/format/config.el +++ b/modules/editor/format/config.el @@ -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)