apheleia/apheleia.el

324 lines
13 KiB
EmacsLisp
Raw Normal View History

2019-07-07 14:13:57 -07:00
;;; apheleia.el --- Reformat buffer stably -*- lexical-binding: t -*-
2022-05-09 15:58:41 -07:00
;; Copyright (C) 2019-2022 Radian LLC and contributors
2019-07-07 14:13:57 -07:00
2022-05-09 15:58:41 -07:00
;; Author: Radian LLC <contact+apheleia@radian.codes>
2019-07-07 14:13:57 -07:00
;; Created: 7 Jul 2019
2023-08-26 09:05:35 -07:00
;; Homepage: https://github.com/radian-software/apheleia
2019-07-07 14:13:57 -07:00
;; Keywords: tools
Support formatting remote files with Tramp (#76) * [#33] Support remote buffers and files CLOSES #33 Adds support for formatting remote files. The new `apheleia-remote-algorithm` option configures this. The default behaviour is consistent with what we had before, with `apheleia` aborting a formatting when dealing with a remote file/buffer. Users can customise apheleia to run the formatter on the remote machine, in which case any temporary files or other checks such as `npx` will be fully handled on the remote machine. Users can also make apheleia run the formatter on the local machine. This works exactly like one would expect, except if the formatter requires access to the physical file (meaning it uses 'file in `apheleia-formatters`) because the file isn't available on the local machine. This PR also fixes a bug in apheleia where `input-fname` was assigned in local let scope, instead of being returned by `apheleia--format-command`, meaning any formatters using a 'input file weren't cleaned up after formatting. * Cleanup + run diffs on remote as well * Fix diff uses correct file path for remote files If a file being diffed is remote, but the program is being run locally, then we create a temporary file on the current machine. * Make a few style changes * Drop support for Emacs 25 * Fix GitHub Actions triggers * Don't run tests on Emacs 25 either * Make apheleia run synchronously when running on remote Also added a metadata field to function based formatters. * feat: Suppress meaningless messages while formatting synchronously Also fixed any linter complaints. * bug: Make running formatter locally on remote buffer synchronous More re-entrant tramp issues. * Update apheleia.el * review: Replace custom temp-file logic with make-nearby-temp-file * review: Revamp functional formatter interface * refactor: Rework remote file handling implementation + Reordered parameters to ensure remote always comes before callback. + Updated some docstrings. * bug: Re-add apheleia--make-temp-file We don't always want to create a temporary file on the remote, only when apheleia-remote-algorithm is 'remote. * bug: Prevent repeat diff-file invocation with 2 files Previously we always made a temp-file for diffing the formatted and unformatted buffer leading to a bug when trying to send stdin to the formatter process. Now we only perform this check when running both locally and remotely. * Update changelog Co-authored-by: Radon Rosborough <radon.neon@gmail.com>
2022-04-10 22:21:50 +01:00
;; Package-Requires: ((emacs "26"))
2020-04-04 09:50:44 -06:00
;; SPDX-License-Identifier: MIT
2023-02-25 11:31:26 -08:00
;; Version: 3.2
2019-07-07 14:13:57 -07:00
;;; Commentary:
;; Apheleia is an Emacs Lisp package which allows you to reformat a
;; buffer without moving point. This solves the usual problem of
;; running a tool like Prettier or Black on `before-save-hook', namely
;; that it resets point to the beginning of the buffer. Apheleia
;; maintains the position of point relative to its surrounding text
;; even if the buffer is modified by the reformatting.
2023-08-26 09:05:35 -07:00
;; Please see https://github.com/radian-software/apheleia for more information.
2019-07-07 14:13:57 -07:00
;;; Code:
(require 'apheleia-utils)
2019-07-07 14:13:57 -07:00
(defgroup apheleia nil
"Reformat buffer without moving point."
2019-07-07 14:13:57 -07:00
:group 'external
2023-08-26 09:05:35 -07:00
:link '(url-link :tag "GitHub" "https://github.com/radian-software/apheleia")
2019-07-07 14:13:57 -07:00
:link '(emacs-commentary-link :tag "Commentary" "apheleia"))
(defcustom apheleia-formatters
2023-04-22 10:36:38 +01:00
'((astyle . ("astyle" (apheleia-formatters-locate-file
"--options" ".astylerc")))
(asmfmt . ("asmfmt"))
2023-04-22 10:26:49 +01:00
(bean-format . ("bean-format"))
2023-04-22 11:34:12 +01:00
(beautysh . ("beautysh"
(apheleia-formatters-indent
"--tab" "--indent-size" 'sh-basic-offset)
"-"))
(black . ("black"
(when (apheleia-formatters-extension-p "pyi") "--pyi")
(apheleia-formatters-fill-column "--line-length")
"-"))
(brittany . ("brittany"))
2023-04-22 11:38:15 +01:00
(buildifier . ("buildifier"))
(caddyfmt . ("caddy" "fmt" "-"))
(clang-format . ("clang-format"
"-assume-filename"
(or (buffer-file-name)
(apheleia-formatters-mode-extension)
".c")))
2023-04-22 11:41:37 +01:00
(cmake-format . ("cmake-format" "-"))
(crystal-tool-format . ("crystal" "tool" "format" "-"))
(dart-format . ("dart" "format"))
(elm-format . ("elm-format" "--yes" "--stdin"))
(fish-indent . ("fish_indent"))
(fourmolu . ("fourmolu"))
2023-04-20 14:33:53 +01:00
(gawk . ("gawk" "-f" "-" "--pretty-print=-"))
(gofmt . ("gofmt"))
(gofumpt . ("gofumpt"))
(goimports . ("goimports"))
(google-java-format . ("google-java-format" "-"))
2023-04-22 11:44:44 +01:00
(html-tidy "tidy"
"--quiet" "yes"
"--tidy-mark" "no"
"--vertical-space" "yes"
"-indent"
(when (derived-mode-p 'nxml-mode)
"-xml")
(apheleia-formatters-indent
"--indent-with-tabs"
"--indent-spaces"
(cond
((derived-mode-p 'nxml-mode)
'nxml-child-indent)
((derived-mode-p 'web-mode)
'web-mode-indent-style)))
(apheleia-formatters-fill-column "-wrap"))
(isort . ("isort" "-"))
Run jq without colorization (#213) Set the -M flag to jq: ```text λ jq --help mohkale@mk-desktop ~ jq - commandline JSON processor [version 1.7] Usage: jq [options] <jq filter> [file...] jq [options] --args <jq filter> [strings...] jq [options] --jsonargs <jq filter> [JSON_TEXTS...] jq is a tool for processing JSON inputs, applying the given filter to its JSON text inputs and producing the filter's results as JSON on standard output. The simplest filter is ., which copies jq's input to its output unmodified except for formatting. For more advanced filters see the jq(1) manpage ("man jq") and/or https://jqlang.github.io/jq/. Example: $ echo '{"foo": 0}' | jq . { "foo": 0 } Command options: -n, --null-input use `null` as the single input value; -R, --raw-input read each line as string instead of JSON; -s, --slurp read all inputs into an array and use it as the single input value; -c, --compact-output compact instead of pretty-printed output; -r, --raw-output output strings without escapes and quotes; --raw-output0 implies -r and output NUL after each output; -j, --join-output implies -r and output without newline after each output; -a, --ascii-output output strings by only ASCII characters using escape sequences; -S, --sort-keys sort keys of each object on output; -C, --color-output colorize JSON output; -M, --monochrome-output disable colored output; --tab use tabs for indentation; --indent n use n spaces for indentation (max 7 spaces); --unbuffered flush output stream after each output; --stream parse the input value in streaming fashion; --stream-errors implies --stream and report parse error as an array; --seq parse input/output as application/json-seq; -f, --from-file file load filter from the file; -L directory search modules from the directory; --arg name value set $name to the string value; --argjson name value set $name to the JSON value; --slurpfile name file set $name to an array of JSON values read from the file; --rawfile name file set $name to string contents of file; --args consume remaining arguments as positional string values; --jsonargs consume remaining arguments as positional JSON values; -e, --exit-status set exit status code based on the output; -V, --version show the version; --build-configuration show jq's build configuration; -h, --help show the help; -- terminates argument processing; Named arguments are also available as $ARGS.named[], while positional arguments are available as $ARGS.positional[]. ```
2023-10-01 00:40:18 +01:00
(jq "jq" "." "-M"
2023-04-20 13:50:03 +01:00
(apheleia-formatters-js-indent "--tab" "--indent"))
(lisp-indent . apheleia-indent-lisp-buffer)
(ktlint . ("ktlint" "--log-level=none" "--stdin" "-F" "-"))
(latexindent . ("latexindent" "--logfile=/dev/null"))
(mix-format . ("mix" "format" "-"))
(nixfmt . ("nixfmt"))
(ocamlformat . ("ocamlformat" "-" "--name" filepath
"--enable-outside-detected-project"))
(ormolu . ("ormolu"))
2023-04-22 11:48:38 +01:00
(perltidy . ("perltidy" "--quiet" "--standard-error-output"))
(phpcs . ("apheleia-phpcs"))
(prettier
. (npx "prettier" "--stdin-filepath" filepath
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-css
. (npx "prettier" "--stdin-filepath" filepath "--parser=css"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-html
. (npx "prettier" "--stdin-filepath" filepath "--parser=html"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-graphql
. (npx "prettier" "--stdin-filepath" filepath "--parser=graphql"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-javascript
. (npx "prettier" "--stdin-filepath" filepath "--parser=babel-flow"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-json
. (npx "prettier" "--stdin-filepath" filepath "--parser=json"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-markdown
. (npx "prettier" "--stdin-filepath" filepath "--parser=markdown"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-ruby
. (npx "prettier" "--stdin-filepath" filepath "--parser=ruby"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-scss
. (npx "prettier" "--stdin-filepath" filepath "--parser=scss"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-svelte
. (npx "prettier" "--stdin-filepath" filepath "--parser=svelte"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-typescript
. (npx "prettier" "--stdin-filepath" filepath "--parser=typescript"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(prettier-yaml
. (npx "prettier" "--stdin-filepath" filepath "--parser=yaml"
(apheleia-formatters-js-indent "--use-tabs" "--tab-width")))
(purs-tidy . (npx "purs-tidy" "format"))
2023-04-22 11:53:13 +01:00
(rubocop . ("rubocop" "--stdin" filepath "--auto-correct"
"--stderr" "--format" "quiet" "--fail-level" "fatal"))
(ruby-standard . ("standardrb" "--stdin" filepath "--fix" "--stderr"
"--format" "quiet" "--fail-level" "fatal"))
(shfmt . ("shfmt"
"-filename" filepath
"-ln" (cl-case (bound-and-true-p sh-shell)
(sh "posix")
(t "bash"))
(when apheleia-formatters-respect-indent-level
(list
"-i" (number-to-string
(cond
(indent-tabs-mode 0)
((boundp 'sh-basic-offset)
sh-basic-offset)
(t 4)))))
"-"))
2023-04-22 11:56:29 +01:00
(rufo . ("rufo" "--filename" filepath "--simple-exit"))
(stylua . ("stylua" "-"))
(rustfmt . ("rustfmt" "--quiet" "--emit" "stdout"))
(terraform . ("terraform" "fmt" "-"))
(yapf . ("yapf")))
"Alist of code formatting commands.
The keys may be any symbols you want, and the values are shell
commands, lists of strings and symbols, or a function symbol.
If the value is a function, the function will be called with
keyword arguments (see the implementation of
`apheleia--run-formatter-function' to see which). It should use
`cl-defun' with `&allow-other-keys' for forward compatibility.
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.
If you use the symbol `inplace' as one of the elements of the
list, then the contents of the current buffer are written to a
temporary file and its name is substituted for `inplace'.
However, unlike `input', it is expected that the formatter write
the formatted file back to the same file in place. In other
words, `inplace' is like `input' and `output' together.
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'.
Any list elements that are not strings and not any of the special
symbols mentioned above will be evaluated when the formatter is
invoked, and spliced into the list. A form can evaluate either to
a string or to a list of strings.
The \"scripts/formatters\" subdirectory of the Apheleia source
repository is automatically prepended to $PATH (variable
`exec-path', to be specific) when invoking external formatters.
This is intended for internal use. If you would like to define
your own script, you can simply place it on your normal $PATH
rather than using this system."
:type '(alist
:key-type symbol
:value-type
(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")))
:group 'apheleia)
(defcustom apheleia-mode-alist
'(;; Alphabetical please
2023-04-22 10:26:49 +01:00
(asm-mode . asmfmt)
2023-04-20 14:33:53 +01:00
(awk-mode . gawk)
(bash-ts-mode . shfmt)
2023-04-22 11:38:15 +01:00
(bazel-mode . buildifier)
(beancount-mode . bean-format)
(c++-ts-mode . clang-format)
(caddyfile-mode . caddyfmt)
(cc-mode . clang-format)
(c-mode . clang-format)
(c-ts-mode . clang-format)
(c++-mode . clang-format)
(caml-mode . ocamlformat)
2023-04-22 11:41:37 +01:00
(cmake-mode . cmake-format)
(cmake-ts-mode . cmake-format)
(common-lisp-mode . lisp-indent)
(crystal-mode . crystal-tool-format)
(css-mode . prettier-css)
(css-ts-mode . prettier-css)
(dart-mode . dart-format)
(dart-ts-mode . dart-format)
(elixir-mode . mix-format)
(elixir-ts-mode . mix-format)
(elm-mode . elm-format)
(fish-mode . fish-indent)
(go-mode . gofmt)
(go-ts-mode . gofmt)
(graphql-mode . prettier-graphql)
(haskell-mode . brittany)
(html-mode . prettier-html)
(html-ts-mode . prettier-html)
(java-mode . google-java-format)
(java-ts-mode . google-java-format)
(js3-mode . prettier-javascript)
(js-json-mode . prettier-json)
(js-mode . prettier-javascript)
(js-ts-mode . prettier-javascript)
(json-mode . prettier-json)
(json-ts-mode . prettier-json)
(kotlin-mode . ktlint)
(latex-mode . latexindent)
(LaTeX-mode . latexindent)
(lua-mode . stylua)
(lisp-mode . lisp-indent)
2023-04-22 10:26:49 +01:00
(nasm-mode . asmfmt)
(nix-mode . nixfmt)
2023-04-22 11:48:38 +01:00
(perl-mode . perltidy)
(php-mode . phpcs)
(purescript-mode . purs-tidy)
(python-mode . black)
(python-ts-mode . black)
(ruby-mode . prettier-ruby)
(ruby-ts-mode . prettier-ruby)
(rustic-mode . rustfmt)
(rust-mode . rustfmt)
(rust-ts-mode . rustfmt)
(scss-mode . prettier-scss)
(svelte-mode . prettier-svelte)
(terraform-mode . terraform)
(TeX-latex-mode . latexindent)
(TeX-mode . latexindent)
(tsx-ts-mode . prettier-typescript)
(tuareg-mode . ocamlformat)
(typescript-mode . prettier-typescript)
(typescript-ts-mode . prettier-typescript)
(web-mode . prettier)
(yaml-mode . prettier-yaml)
(yaml-ts-mode . prettier-yaml))
"Alist mapping major mode names to formatters to use in those modes.
This determines what formatter to use in buffers without a
setting for `apheleia-formatter'. The keys are major mode
symbols (matched against `major-mode' with `derived-mode-p') or
strings (matched against value of variable `buffer-file-name'
with `string-match-p'), and the values are symbols with entries
in `apheleia-formatters' (or equivalently, they are allowed
values for `apheleia-formatter'). Values can be a list of such
symnols causing each formatter in the list to be called one after
the other (with the output of the previous formatter).
Earlier entries in this variable take precedence over later ones.
Be careful when writing regexps to include \"\\'\" and to escape
\"\\.\" in order to properly match a file extension. For example,
to match \".jsx\" files you might use \"\\.jsx\\'\".
If a given mode derives from another mode (e.g. `php-mode' and
`cc-mode'), then whichever entry in the alist is more specific
will apply. In the case that multiple modes match
`derived-mode-p' for the current buffer but neither derives from
the other, whichever entry comes first will be used."
:type '(alist
:key-type
(choice (symbol :tag "Major mode")
(string :tag "Buffer name regexp"))
:value-type
(choice (symbol :tag "Formatter")
(repeat
(symbol :tag "Formatter"))))
:group 'apheleia)
2019-07-07 14:13:57 -07:00
(provide 'apheleia)
;;; apheleia.el ends here