aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMohsin Kaleem <mohkale@kisara.moe>2021-12-27 18:00:21 +0000
committerGitHub <noreply@github.com>2021-12-27 10:00:21 -0800
commit2e9816513789c233acc79493523a7e1aa5b3eeb6 (patch)
tree26f43ff2238443e4dfc0e2dfb7c85cecb6023d1e
parente700c78a5db4ea9599b2d04bbc8e4c40ce822c37 (diff)
[#62] Support functions as formatters (#63)
* [#62] Support functions as formatters Closes #62. Lets you use a lisp function as a formatter. This gives apheleia a lot more flexibility in regards to what constitutes a formatter. For example you can now plug an external language server or another tool as a formatter for use with apheleia. Here's a very basic example of using indent-line-function with apheleia after merging this commit. Note: this doesn't take into account any special local variables in the original buffer such as lisp-body-indent. It's really just for demonstration purposes and as a proof of concept. ```lisp (defun apheleia-indent-region+ (orig scratch callback) (with-current-buffer scratch (setq-local indent-line-function (buffer-local-value 'indent-line-function orig)) (indent-region (point-min) (point-max)) (funcall callback scratch))) (push '(indent-region . apheleia-indent-region+) apheleia-formatters) (push '(elisp-mode . indent-region) apheleia-mode-alist) (push '(lisp-interaction-mode . indent-region) apheleia-mode-alist) ``` * Fix misc-bugs + prevent race conditions * Update docstring * Reword a bit * Add to README Co-authored-by: Radon Rosborough <radon.neon@gmail.com>
-rw-r--r--CHANGELOG.md4
-rw-r--r--README.md5
-rw-r--r--apheleia.el154
3 files changed, 116 insertions, 47 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db6f76b..d07eea6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ The format is based on [Keep a Changelog].
* Support evaluating items in `apheleia-formatters` to make formatter
commands more dynamic ([#50], [#55]).
* Allow apheleia to format buffers without an underlying file ([#52]).
+* Support functional formatters ([#62]). You can now use a lisp
+ function as a formatter allowing you to plug more powerful
+ formatters into apheleia such as language servers.
### Formatters
* [ClangFormat](https://clang.llvm.org/docs/ClangFormat.html) for
@@ -54,6 +57,7 @@ The format is based on [Keep a Changelog].
[#52]: https://github.com/raxod502/apheleia/issues/52
[#54]: https://github.com/raxod502/apheleia/pull/54
[#55]: https://github.com/raxod502/apheleia/issues/55
+[#62]: https://github.com/raxod502/apheleia/issues/62
[#64]: https://github.com/raxod502/apheleia/issues/64
[#65]: https://github.com/raxod502/apheleia/pull/65
diff --git a/README.md b/README.md
index 00fa7f1..be22b9f 100644
--- a/README.md
+++ b/README.md
@@ -119,6 +119,11 @@ variables:
example a buffer called `*foo-bar.c*` that has no associated
file will have an implicit file-name of `foo-bar.c` and any
temporary files will be suffixed with a `.c` extension.
+ * You can implement formatters as arbitrary Elisp functions which
+ operate directly on a buffer, without needing to invoke an
+ external command. This can be useful to integrate with e.g.
+ language servers. See the docstring for more information on the
+ expected interface for Elisp formatters.
* `apheleia-mode-alist`: Alist mapping major modes and filename
regexps to names of formatters to use in those modes and files. See
the docstring for more information.
diff --git a/apheleia.el b/apheleia.el
index ed2c794..6d46208 100644
--- a/apheleia.el
+++ b/apheleia.el
@@ -520,26 +520,17 @@ sequence unless it's first in the sequence"))
or list of strings: %S" arg)))
`(,input-fname ,output-fname ,stdin ,@command))))
-(defun apheleia--run-formatters (commands buffer callback &optional stdin)
- "Run one or more code formatters on the current buffer.
-The formatter is specified by the COMMANDS list. Each entry in
-COMMANDS should be a list of strings or symbols (see
-`apheleia-format-buffer'). BUFFER is the `current-buffer' when
-this function was first called. Once all the formatters in
-COMMANDS finish succesfully then invoke CALLBACK with one argument,
-a buffer containing the output of all the formatters.
-
-STDIN is a buffer containing the standard input for the first
-formatter in COMMANDS. This should not be supplied by the caller
-and instead is supplied by this command when invoked recursively.
-The stdout of the previous formatter becomes the stdin of the
-next formatter."
+(defun apheleia--run-formatter-command (command buffer callback stdin)
+ "Run a formatter using a shell command.
+COMMAND should be a list of string or symbols for the formatter that
+will format the current buffer. See `apheleia--run-formatters' for a
+description of COMMAND, BUFFER, CALLBACK and STDIN."
;; NOTE: We switch to the original buffer both to format the command
;; correctly and also to ensure any buffer local variables correctly
;; resolve for the whole formatting process (for example
;; `apheleia--current-process').
(with-current-buffer buffer
- (when-let ((ret (apheleia--format-command (car commands) stdin)))
+ (when-let ((ret (apheleia--format-command command stdin)))
(cl-destructuring-bind (input-fname output-fname stdin &rest command) ret
(apheleia--make-process
:command command
@@ -552,12 +543,7 @@ next formatter."
(erase-buffer)
(insert-file-contents-literally output-fname))
- (if (cdr commands)
- ;; Forward current stdout to remaining formatters, passing along
- ;; the current callback and using the current formatters output
- ;; as stdin.
- (apheleia--run-formatters (cdr commands) buffer callback stdout)
- (funcall callback stdout)))
+ (funcall callback stdout))
:ensure
(lambda ()
(ignore-errors
@@ -566,6 +552,69 @@ next formatter."
(when output-fname
(delete-file output-fname)))))))))
+(defun apheleia--run-formatter-function (func buffer callback stdin)
+ "Run a formatter using a Lisp function FUNC.
+See `apheleia--run-formatters' for a description of BUFFER, CALLBACK
+and STDIN."
+ ;; Will be an ugly name if you use a lambda for FUNC, instead of a symbol.
+ (let* ((formatter-name (if (symbolp func) (symbol-name func) "lambda"))
+ (scratch (generate-new-buffer
+ (format " *apheleia-%s-scratch*" formatter-name))))
+ (with-current-buffer scratch
+ ;; We expect FUNC to modify scratch in place so we can't simply pass
+ ;; STDIN to it. When STDIN isn't nil, it's the output of a previous
+ ;; formatter and we want to keep it alive so we can debug any issues
+ ;; with it.
+ (insert-buffer-substring (or stdin buffer))
+ (funcall func
+ ;; Original buffer being formatted.
+ buffer
+ ;; Buffer the formatter should modify.
+ scratch
+ ;; Callback after succesfully formatting.
+ (lambda ()
+ (unwind-protect
+ (funcall callback scratch)
+ (kill-buffer scratch)))
+ ;; Callback when formatting scratch has failed.
+ (apply-partially #'kill-buffer scratch)))))
+
+(defun apheleia--run-formatters (commands buffer callback &optional stdin)
+ "Run one or more code formatters on the current buffer.
+The formatter is specified by the COMMANDS list. Each entry in
+COMMANDS should be a list of strings or symbols or a function
+\(see `apheleia-format-buffer'). BUFFER is the `current-buffer' when
+this function was first called. Once all the formatters in COMMANDS
+finish succesfully then invoke CALLBACK with one argument, a buffer
+containing the output of all the formatters.
+
+STDIN is a buffer containing the standard input for the first
+formatter in COMMANDS. This should not be supplied by the caller
+and instead is supplied by this command when invoked recursively.
+The stdout of the previous formatter becomes the stdin of the
+next formatter."
+ (let ((command (car commands)))
+ (funcall
+ (cond
+ ((consp command)
+ #'apheleia--run-formatter-command)
+ ((or (functionp command)
+ (symbolp command))
+ #'apheleia--run-formatter-function)
+ (t
+ (error "Formatter must be a shell command or a Lisp \
+function: %s" command)))
+ command
+ buffer
+ (lambda (stdout)
+ (if (cdr commands)
+ ;; Forward current stdout to remaining formatters, passing along
+ ;; the current callback and using the current formatters output
+ ;; as stdin.
+ (apheleia--run-formatters (cdr commands) buffer callback stdout)
+ (funcall callback stdout)))
+ stdin)))
+
(defcustom apheleia-formatters
'((black . ("black" "-"))
(brittany . ("brittany"))
@@ -582,38 +631,49 @@ next formatter."
(terraform . ("terraform" "fmt" "-")))
"Alist of code formatting commands.
The keys may be any symbols you want, and the values are
-commands, lists of strings and symbols.
-
-In Lisp code, the format of commands is similar to what you pass to
-`make-process', except as follows. Normally, the contents of the
-current buffer are passed to the command on stdin, and the output
-is read from stdout. However, if you use the symbol `file' as one
-of the elements of commands, then the filename of the current
-buffer is substituted for it. (Use `filepath' instead of `file'
-if you need the filename of the current buffer, but you still
-want its contents to be passed on stdin.) If you instead use the
-symbol `input' as one of the elements of commands, then the
-contents of the current buffer are written to a temporary file
-and its name is substituted for `input'. Also, if you use the
-symbol `output' as one of the elements of commands, then it is
-substituted with the name of a temporary file. In that case, it
-is expected that the command writes to that file, and the file is
-then read into an Emacs buffer. Finally, if you use the symbol
-`npx' as one of the elements of commands, then the first string
+shell commands, lists of strings and symbols, or a function
+symbol.
+
+If the value is a function, the function will be called with four
+arguments to format the current buffer: the original buffer that
+was being formatted (use this to access any relevent local
+variables or options that the formatter needs); a clone of the
+original buffer (that may have been modified by another formatter
+prior to being passed to the function); a callback that should be
+called when formatting is finished; and another callback that
+should be called when an error was raised during formatting.
+
+Otherwise in Lisp code, the format of commands is similar to what
+you pass to `make-process', except as follows. Normally, the contents
+of the current buffer are passed to the command on stdin, and the
+output is read from stdout. However, if you use the symbol `file' as
+one of the elements of commands, then the filename of the current
+buffer is substituted for it. (Use `filepath' instead of `file' if you
+need the filename of the current buffer, but you still want its
+contents to be passed on stdin.) If you instead use the symbol `input'
+as one of the elements of commands, then the contents of the current
+buffer are written to a temporary file and its name is substituted for
+`input'. Also, if you use the symbol `output' as one of the elements
+of commands, then it is substituted with the name of a temporary file.
+In that case, it is expected that the command writes to that file, and
+the file is then read into an Emacs buffer. Finally, if you use the
+symbol `npx' as one of the elements of commands, then the first string
element of the command list is resolved inside node_modules/.bin if
such a directory exists anywhere above the current
`default-directory'."
:type '(alist
:key-type symbol
:value-type
- (repeat
- (choice
- (string :tag "Argument")
- (const :tag "Look for command in node_modules/.bin" npx)
- (const :tag "Name of file being formatted" filepath)
- (const :tag "Name of real file used for input" file)
- (const :tag "Name of temporary file used for input" input)
- (const :tag "Name of temporary file used for output" output)))))
+ (choice
+ (repeat
+ (choice
+ (string :tag "Argument")
+ (const :tag "Look for command in node_modules/.bin" npx)
+ (const :tag "Name of file being formatted" filepath)
+ (const :tag "Name of real file used for input" file)
+ (const :tag "Name of temporary file used for input" input)
+ (const :tag "Name of temporary file used for output" output)))
+ (function :tag "Formatter function"))))
(defcustom apheleia-mode-alist
'((cc-mode . clang-format)