diff --git a/modules/editor/format/autoload/settings.el b/modules/editor/format/autoload/settings.el index cfb162943..3d8183e1c 100644 --- a/modules/editor/format/autoload/settings.el +++ b/modules/editor/format/autoload/settings.el @@ -22,22 +22,28 @@ return (cadr choice))) ;;;###autodef -(cl-defun set-formatter! (modes formatter &key - name - install - filter - ok-statuses - error-regexp) - "Define a FORMATTER for MODES. +(cl-defun set-formatter! + (modes-or-name formatter + &key + name + modes + install + filter + ok-statuses + error-regexp) + "Define a formatter. -MODES can be a major mode symbol, a list of major modes, or a list of -two-element lists made up of (MAJOR-MODE FORM). FORM is evaluated when the -buffer is formatted and its return value serves two roles: +MODES-OR-NAME can either be a major mode symbol (or list thereof), a unique name +for the formatter being defined (also a symbol), or a special list of +two-element sublists with the structure: (MAJOR-MODE FORM). -1. It is a predicate for this formatter. If it returns non-nil (and MAJOR-MODE - matches the current mode), that formatter is used. -2. Its return value is stored in the `mode-result' variable for FORMATTER (if - it's a function). +FORM is evaluated when the buffer is formatted and its return value serves two +purposes: + +1. It is a predicate for this formatter. Assuming the MAJOR-MODE matches the + current mode, if FORM evaluates to nil, the formatter is skipped. +2. It's return value is made available to FORMATTER if it is a function or list + of shell arguments via the `mode-result' variable. FORMATTER can be a function, string or nested list. @@ -50,18 +56,23 @@ FORMATTER can be a function, string or nested list. string and ARG is both a predicate and argument for STRING. If ARG is nil, STRING will be omitted from the vector. -NAME is the identifier for this formatter. If FORMATTER is a lambda, NAME is -required. +NAME is a symbol that identifies this formatter. If NAME isn't specified and +FORMATTER is a function symbol, the symbol's name is used. If FORMATTER is a +string or list of strings, the executable is extracted from it and used as the +NAME. If FORMATTER is a lambda, NAME is required and will error if omitted. Note +that any formatters with the same NAME will be overwritten by FORMATTER. INSTALL is a string representing the shell command to install this formatter's -dependencies. INSTALL can also be a list of lists made up of two items: (OS -COMMAND). OS can be windows, macos, linux, freebsd, openbsd or netbsd. +dependencies. INSTALL can also be a two-element list: (OS COMMAND), or a list of +these. OS can be windows, macos, linux, freebsd, openbsd or netbsd. FILTER is a function that takes three arguments: the formatted output, any error -output and the position of the first change, and must return these three after -making whatever changes you like to them. This might be useful if the output -contains ANSI color codes that need to be stripped out (as is the case with -elm-format). +output and the position of the first change. This function must return these +three after making whatever changes you like to them. This might be useful if +the output contains ANSI color codes that need to be stripped out (as is the +case with elm-format). + +OK-STATUSES and ERROR-REGEXP are ignored if FORMATTER is not a shell command. OK-STATUSES is a list of integer exit codes that should be treated as success codes. However, if ERROR-REGEXP is given, and the program's stderr contains that @@ -72,6 +83,7 @@ Basic examples: (set-formatter! '(asm-mode nasm-mode) \"asmfmt\") (set-formatter! 'python-mode \"black -q -\" :install \"pip install black\") + (set-formatter! 'tidy \"tidy -q -indent\" :modes '(html-mode web-mode)) Advanced examples: @@ -94,6 +106,16 @@ Advanced examples: :ok-statuses '(0 1) :install '(macos \"brew install tidy-html5\")) + (set-formatter! 'html-tidy ; overwrite predefined html-tidy formatter + '(\"tidy\" \"-q\" \"-indent\" + \"--tidy-mark\" \"no\" + \"--drop-empty-elements\" \"no\" + \"--show-body-only\" \"auto\" + (\"--indent-spaces\" \"%d\" tab-width) + (\"--indent-with-tabs\" \"%s\" (if indent-tabs-mode \"yes\" \"no\")) + (\"-xml\" (memq major-mode '(nxml-mode xml-mode)))) + :ok-statuses '(0 1))) + (set-formatter! 'elm-mode \"elm-format --yes --stdin\" :install '(macos \"brew install elm\") @@ -104,6 +126,11 @@ Advanced examples: first-diff)))" (declare (indent defun)) (cl-check-type name (or symbol null)) + ;; Determine if MODES-OR-NAME means MODES or NAMES + (if (and (symbolp modes-or-name) + (not (string-match-p "-mode$" (symbol-name modes-or-name)))) + (setq name modes-or-name) + (setq modes (doom-enlist modes-or-name))) (let* ((command-list (cond ((stringp formatter) ; shell command (split-string formatter " " t)) ((listp formatter) ; shell command in lists @@ -112,46 +139,55 @@ Advanced examples: ((car command-list) (intern (car command-list))) ((symbolp formatter) formatter) ((user-error "Anonymous formatter requires a :name")))) - (fn (lambda (executable mode-result) - (let ((result - (cond ((commandp formatter) - (let ((mode major-mode) - (file buffer-file-name) - (dir default-directory)) - (format-all-buffer-thunk - (lambda (input) - (with-silent-modifications - (setq buffer-file-name file - default-directory dir) - (delay-mode-hooks (funcall mode)) - (insert input) - (condition-case e - (progn - (call-interactively formatter) - (list nil "")) - (error (list t (error-message-string e))))))))) - ((functionp formatter) - (format-all-buffer-thunk formatter)) - ((cl-loop for arg in command-list - if (stringp arg) - collect arg into args - else if (eval (cadr arg) t) - collect (format (car arg) it) into args - finally do - (if (or ok-statuses error-regexp) - (apply #'format-all-buffer-hard ok-statuses error-regexp args) - (apply #'format-all-buffer-easy args))))))) - (if filter - (apply filter result) - result)))) + (formatter + (cond ((commandp formatter) + `(format-all-buffer-thunk + (lambda (input) + (with-silent-modifications + (setq buffer-file-name ,(buffer-file-name (buffer-base-buffer)) + default-directory ,default-directory) + (delay-mode-hooks (funcall ',major-mode)) + (insert input) + (condition-case e + (progn (call-interactively #',formatter) + (list nil "")) + (error (list t (error-message-string e)))))))) + ((functionp formatter) + `(format-all-buffer-thunk #',formatter)) + (`(let (args) + (dolist (arg ',command-list) + (cond ((stringp arg) (push arg args)) + ((listp arg) + (catch 'skip + (let (subargs this) + (while (setq this (pop arg)) + (cond ((not (stringp (car arg))) + (let ((val (eval (pop arg) t))) + (unless val (throw 'skip nil)) + (push (format this val) subargs))) + ((stringp this) + (push this subargs)))) + (setq args (append subargs args))))))) + (if ,(and (or ok-statuses error-regexp) t) + (apply #'format-all-buffer-hard + ',ok-statuses ,error-regexp + (reverse args)) + (apply #'format-all-buffer-easy (reverse args))))))) + (fn + `(lambda (executable mode-result) + (let ((result ,formatter)) + ,(if filter + `(apply #',filter result) + 'result)))) (install (cond ((null install) install) ((listp install) (cdr (assq (+format--resolve-system) install))) (install)))) (after! format-all - (puthash name fn format-all-format-table) - (puthash name install format-all-install-table) + (puthash name (eval fn t) format-all-format-table) (puthash name (car command-list) format-all-executable-table) + (puthash name (or install (gethash name format-all-install-table)) + format-all-install-table) (dolist (mode (doom-enlist modes)) (cl-destructuring-bind (m &optional probe) (doom-enlist mode)