editor/format: rewrite & fix set-formatter!

+ Updated docstring (and added two more examples)
+ Can now be used with shell command strings or string lists. String
  sublists can have more than 2 elements. Non-string items will be
  interpolated into the string before it. If any non-string item is nil,
  its sublist is omitted entirely.
+ Can now be used to redefine formatters predefined by the format-all
  package, by passing the formatter's name (as a symbol) as the first
  argument.
+ Added :modes property for cases when first argument isn't a major mode
  or list of them (when redefining a formatter).
This commit is contained in:
Henrik Lissner 2018-09-03 03:53:07 +02:00
parent e7d5da2686
commit df42d8ce89
No known key found for this signature in database
GPG key ID: 5F6C0EA160557395

View file

@ -22,22 +22,28 @@
return (cadr choice))) return (cadr choice)))
;;;###autodef ;;;###autodef
(cl-defun set-formatter! (modes formatter &key (cl-defun set-formatter!
(modes-or-name formatter
&key
name name
modes
install install
filter filter
ok-statuses ok-statuses
error-regexp) error-regexp)
"Define a FORMATTER for MODES. "Define a formatter.
MODES can be a major mode symbol, a list of major modes, or a list of MODES-OR-NAME can either be a major mode symbol (or list thereof), a unique name
two-element lists made up of (MAJOR-MODE FORM). FORM is evaluated when the for the formatter being defined (also a symbol), or a special list of
buffer is formatted and its return value serves two roles: 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 FORM is evaluated when the buffer is formatted and its return value serves two
matches the current mode), that formatter is used. purposes:
2. Its return value is stored in the `mode-result' variable for FORMATTER (if
it's a function). 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. 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 and ARG is both a predicate and argument for STRING. If ARG is nil,
STRING will be omitted from the vector. STRING will be omitted from the vector.
NAME is the identifier for this formatter. If FORMATTER is a lambda, NAME is NAME is a symbol that identifies this formatter. If NAME isn't specified and
required. 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 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 dependencies. INSTALL can also be a two-element list: (OS COMMAND), or a list of
COMMAND). OS can be windows, macos, linux, freebsd, openbsd or netbsd. these. OS can be windows, macos, linux, freebsd, openbsd or netbsd.
FILTER is a function that takes three arguments: the formatted output, any error 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 output and the position of the first change. This function must return these
making whatever changes you like to them. This might be useful if the output three after making whatever changes you like to them. This might be useful if
contains ANSI color codes that need to be stripped out (as is the case with the output contains ANSI color codes that need to be stripped out (as is the
elm-format). 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 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 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! '(asm-mode nasm-mode) \"asmfmt\")
(set-formatter! 'python-mode \"black -q -\" :install \"pip install black\") (set-formatter! 'python-mode \"black -q -\" :install \"pip install black\")
(set-formatter! 'tidy \"tidy -q -indent\" :modes '(html-mode web-mode))
Advanced examples: Advanced examples:
@ -94,6 +106,16 @@ Advanced examples:
:ok-statuses '(0 1) :ok-statuses '(0 1)
:install '(macos \"brew install tidy-html5\")) :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 (set-formatter! 'elm-mode
\"elm-format --yes --stdin\" \"elm-format --yes --stdin\"
:install '(macos \"brew install elm\") :install '(macos \"brew install elm\")
@ -104,6 +126,11 @@ Advanced examples:
first-diff)))" first-diff)))"
(declare (indent defun)) (declare (indent defun))
(cl-check-type name (or symbol null)) (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 (let* ((command-list (cond ((stringp formatter) ; shell command
(split-string formatter " " t)) (split-string formatter " " t))
((listp formatter) ; shell command in lists ((listp formatter) ; shell command in lists
@ -112,46 +139,55 @@ Advanced examples:
((car command-list) (intern (car command-list))) ((car command-list) (intern (car command-list)))
((symbolp formatter) formatter) ((symbolp formatter) formatter)
((user-error "Anonymous formatter requires a :name")))) ((user-error "Anonymous formatter requires a :name"))))
(fn (lambda (executable mode-result) (formatter
(let ((result
(cond ((commandp formatter) (cond ((commandp formatter)
(let ((mode major-mode) `(format-all-buffer-thunk
(file buffer-file-name)
(dir default-directory))
(format-all-buffer-thunk
(lambda (input) (lambda (input)
(with-silent-modifications (with-silent-modifications
(setq buffer-file-name file (setq buffer-file-name ,(buffer-file-name (buffer-base-buffer))
default-directory dir) default-directory ,default-directory)
(delay-mode-hooks (funcall mode)) (delay-mode-hooks (funcall ',major-mode))
(insert input) (insert input)
(condition-case e (condition-case e
(progn (progn (call-interactively #',formatter)
(call-interactively formatter)
(list nil "")) (list nil ""))
(error (list t (error-message-string e))))))))) (error (list t (error-message-string e))))))))
((functionp formatter) ((functionp formatter)
(format-all-buffer-thunk formatter)) `(format-all-buffer-thunk #',formatter))
((cl-loop for arg in command-list (`(let (args)
if (stringp arg) (dolist (arg ',command-list)
collect arg into args (cond ((stringp arg) (push arg args))
else if (eval (cadr arg) t) ((listp arg)
collect (format (car arg) it) into args (catch 'skip
finally do (let (subargs this)
(if (or ok-statuses error-regexp) (while (setq this (pop arg))
(apply #'format-all-buffer-hard ok-statuses error-regexp args) (cond ((not (stringp (car arg)))
(apply #'format-all-buffer-easy args))))))) (let ((val (eval (pop arg) t)))
(if filter (unless val (throw 'skip nil))
(apply filter result) (push (format this val) subargs)))
result)))) ((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) (install (cond ((null install) install)
((listp install) ((listp install)
(cdr (assq (+format--resolve-system) install))) (cdr (assq (+format--resolve-system) install)))
(install)))) (install))))
(after! format-all (after! format-all
(puthash name fn format-all-format-table) (puthash name (eval fn t) format-all-format-table)
(puthash name install format-all-install-table)
(puthash name (car command-list) format-all-executable-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)) (dolist (mode (doom-enlist modes))
(cl-destructuring-bind (m &optional probe) (cl-destructuring-bind (m &optional probe)
(doom-enlist mode) (doom-enlist mode)