diff --git a/modules/editor/format/autoload/format.el b/modules/editor/format/autoload/format.el index d0379c6a7..5428d8ec4 100644 --- a/modules/editor/format/autoload/format.el +++ b/modules/editor/format/autoload/format.el @@ -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) - (insert-buffer-substring-no-properties formatted-buffer) - (when callback (funcall callback)) + (goto-char start) + (save-excursion + (insert-buffer-substring-no-properties formatted-buffer) + (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)))))))) diff --git a/modules/editor/format/config.el b/modules/editor/format/config.el index 8f4826c8d..76ca73ae6 100644 --- a/modules/editor/format/config.el +++ b/modules/editor/format/config.el @@ -1,87 +1,79 @@ ;;; 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 -(when (modulep! +onsave) - (add-hook 'doom-first-file-hook #'apheleia-global-mode)) - -(defun +format-maybe-inhibit-h () - "Check if formatting should be disabled for current buffer. +(use-package! apheleia + :defer t + :init + (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. 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))))) + (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))))))) - -(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))) + ;; 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." + :around #'save-buffer + (let ((apheleia-mode (and apheleia-mode (memq arg '(nil 1))))) + (funcall orig-fn))) -;; -;;; Hacks - -(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))) - -(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 () - (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)))) + ;; 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 + (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--update-vc-gutter-h () + (when (fboundp '+vc-gutter-update-h) + (+vc-gutter-update-h)))))