diff --git a/format-all.el b/format-all.el index 5744bd12..e7d765c0 100644 --- a/format-all.el +++ b/format-all.el @@ -273,7 +273,11 @@ language. Each formatter is either: * a symbol (e.g. black, clang-format, rufo) * a list whose first item is that symbol, and any remaining items - are extra command line arguments to pass to the formatter + are extra command line arguments to pass to the formatter; if + it starts with the symbol `:executable' the following item is + the program name that overrides EXECUTABLE and can be a + relative path from the project root, and the rest items are its + arguments. Those arguments should not be shell-quoted. If more than one formatter is given for the same language, all of them are run as a chain, with the code from each formatter passed @@ -305,7 +309,9 @@ the rules for an entire source tree can be given in one file.") (error "Formatter name missing")) (unless (symbolp (car formatter)) (error "Formatter name is not a symbol: %S" (car formatter))) - (unless (cl-every #'stringp (cdr formatter)) + (unless (or (and (eq (cadr formatter) :executable) + (cl-every #'stringp (cddr formatter))) + (cl-every #'stringp (cdr formatter))) (error "Formatter command line arguments are not all strings: %S" formatter)) formatter)) @@ -326,12 +332,9 @@ the rules for an entire source tree can be given in one file.") (stringp (car chain)) (cl-every (lambda (formatter) - (and (not (null formatter)) - (or (symbolp formatter) - (and (format-all--proper-list-p formatter) - (and (symbolp (car formatter)) - (not (null (car formatter)))) - (cl-every #'stringp (cdr formatter)))))) + (condition-case nil + (format-all--normalize-formatter formatter) + (error nil))) (cdr chain)))) formatters))) @@ -482,8 +485,11 @@ OK-STATUSES. OK-STATUSES and ERROR-REGEXP are hacks to work around formatter programs that don't make sensible use of their exit status. -If ARGS are given, those are arguments to EXECUTABLE. They should -not be shell-quoted. +If ARGS are given, those are arguments to EXECUTABLE. If it +starts with the symbol `:executable' the following item is the +program name that overrides EXECUTABLE and can be a relative path +from the project root, and the rest items are its arguments. +Those arguments should not be shell-quoted. If ROOT-FILES are given, the working directory of the formatter will be the deepest directory (starting from the file being @@ -522,8 +528,11 @@ unformatted code from stdin, write its formatted equivalent to stdout, write errors/warnings to stderr, and exit zero/non-zero on success/failure. -If ARGS are given, those are arguments to EXECUTABLE. They don't -need to be shell-quoted." +If ARGS are given, those are arguments to EXECUTABLE. If it +starts with the symbol `:executable' the following item is the +program name that overrides EXECUTABLE and can be a relative path +from the project root, and the rest items are its arguments. +Those arguments should not be shell-quoted." (apply 'format-all--buffer-hard nil nil nil executable args)) (defun format-all--ruby-gem-bundled-p (gem-name) @@ -1578,6 +1587,31 @@ STATUS and ERROR-OUTPUT come from the formatter." (set-marker (mark-marker) old-mark (current-buffer)) (setq mark-ring (mapcar #'copy-marker old-mark-ring)))) +(defun format-all--project-root () + "Internal function to get the project root for the current buffer." + (or (and (fboundp 'projectile-project-root) + (ignore-errors (projectile-project-root))) + (ignore-errors (project-root (project-current))) + default-directory)) + +(defun format-all--command-args (formatter) + "Internal function to get the full command line arguments for FORMATTER." + (let* ((name (car formatter)) + (args (cdr formatter))) + (if (eq (car args) :executable) + (let* ((executable (cadr args)) + (args (cddr args)) + (executable + (or (let ((project-executable (expand-file-name executable (format-all--project-root)))) + (and (file-executable-p project-executable) + (file-regular-p project-executable) + project-executable)) + (executable-find executable) + (signal 'format-all-executable-not-found + (list (format "You need the %s command." executable)))))) + (cons executable args)) + (cons (format-all--formatter-executable name) args)))) + (defun format-all--run-chain (language chain region) "Internal function to run a formatter CHAIN on the current buffer. @@ -1608,9 +1642,10 @@ entire buffer." (cl-return)) (let* ((formatter (car chain-tail)) (f-name (car formatter)) - (f-args (cdr formatter)) - (f-function (gethash f-name format-all--format-table)) - (f-executable (format-all--formatter-executable f-name))) + (f-command-args (format-all--command-args formatter)) + (f-executable (car f-command-args)) + (f-args (cdr f-command-args)) + (f-function (gethash f-name format-all--format-table))) (when format-all-debug (message "Format-All: Formatting %s as %s using %S%s"