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:
parent
b0e16dc243
commit
8072762de8
2 changed files with 90 additions and 142 deletions
|
@ -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)
|
||||||
|
(goto-char start)
|
||||||
|
(save-excursion
|
||||||
(insert-buffer-substring-no-properties formatted-buffer)
|
(insert-buffer-substring-no-properties formatted-buffer)
|
||||||
(when callback (funcall callback))
|
(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))))))))
|
||||||
|
|
|
@ -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)
|
||||||
|
(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.
|
"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)
|
||||||
;;
|
|
||||||
;;; Hacks
|
|
||||||
|
|
||||||
(defadvice! +format--inhibit-reformat-on-prefix-arg-a (orig-fn &optional arg)
|
|
||||||
"Make it so \\[save-buffer] with prefix arg inhibits reformatting."
|
"Make it so \\[save-buffer] with prefix arg inhibits reformatting."
|
||||||
:around #'save-buffer
|
:around #'save-buffer
|
||||||
(let ((apheleia-mode (and apheleia-mode (memq arg '(nil 1)))))
|
(let ((apheleia-mode (and apheleia-mode (memq arg '(nil 1)))))
|
||||||
(funcall orig-fn)))
|
(funcall orig-fn)))
|
||||||
|
|
||||||
(add-hook!
|
;; 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
|
'apheleia-post-format-hook
|
||||||
;; HACK `web-mode' doesn't update syntax highlighting after arbitrary buffer
|
(defun +format--update-web-mode-h ()
|
||||||
;; modifications, so we must trigger refontification manually.
|
|
||||||
(defun +format--fix-web-mode-fontification-h ()
|
|
||||||
(when (eq major-mode 'web-mode)
|
(when (eq major-mode 'web-mode)
|
||||||
(setq web-mode-fontification-off nil)
|
(setq web-mode-fontification-off nil)
|
||||||
(when (and web-mode-scan-beg web-mode-scan-end global-font-lock-mode)
|
(when (and web-mode-scan-beg web-mode-scan-end global-font-lock-mode)
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(font-lock-fontify-region web-mode-scan-beg web-mode-scan-end)))))
|
(font-lock-fontify-region web-mode-scan-beg web-mode-scan-end)))))
|
||||||
|
(defun +format--update-vc-gutter-h ()
|
||||||
(defun +format--refresh-vc-gutter-h ()
|
|
||||||
(when (fboundp '+vc-gutter-update-h)
|
(when (fboundp '+vc-gutter-update-h)
|
||||||
(+vc-gutter-update-h))))
|
(+vc-gutter-update-h)))))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue