From eb5987e9dd40ce1e27c9c07e41d09571f1bd876e Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Fri, 25 Aug 2023 11:15:12 -0700 Subject: feat: first working version --- lua/conform/formatters/autoflake.lua | 9 + lua/conform/formatters/autopep8.lua | 9 + lua/conform/formatters/black.lua | 19 ++ lua/conform/formatters/clang_format.lua | 9 + lua/conform/formatters/cljstyle.lua | 9 + lua/conform/formatters/cmake_format.lua | 9 + lua/conform/formatters/dart_format.lua | 9 + lua/conform/formatters/dfmt.lua | 8 + lua/conform/formatters/elm_format.lua | 9 + lua/conform/formatters/erb_format.lua | 9 + lua/conform/formatters/eslint_d.lua | 13 ++ lua/conform/formatters/gdformat.lua | 9 + lua/conform/formatters/gofmt.lua | 8 + lua/conform/formatters/gofumpt.lua | 8 + lua/conform/formatters/goimports.lua | 9 + lua/conform/formatters/htmlbeautifier.lua | 8 + lua/conform/formatters/isort.lua | 24 +++ lua/conform/formatters/jq.lua | 8 + lua/conform/formatters/nixfmt.lua | 8 + lua/conform/formatters/nixpkgs_fmt.lua | 8 + lua/conform/formatters/ocamlformat.lua | 9 + lua/conform/formatters/pg_format.lua | 8 + lua/conform/formatters/prettier.lua | 24 +++ lua/conform/formatters/prettierd.lua | 24 +++ lua/conform/formatters/rubocop.lua | 16 ++ lua/conform/formatters/rustfmt.lua | 9 + lua/conform/formatters/scalafmt.lua | 9 + lua/conform/formatters/shfmt.lua | 9 + lua/conform/formatters/sql_formatter.lua | 8 + lua/conform/formatters/stylua.lua | 9 + lua/conform/formatters/swift_format.lua | 8 + lua/conform/formatters/swiftformat.lua | 9 + lua/conform/formatters/terraform_fmt.lua | 9 + lua/conform/formatters/uncrustify.lua | 11 ++ lua/conform/formatters/xmlformat.lua | 9 + lua/conform/formatters/yamlfix.lua | 9 + lua/conform/formatters/yamlfmt.lua | 9 + lua/conform/formatters/yapf.lua | 9 + lua/conform/formatters/zigfmt.lua | 9 + lua/conform/fs.lua | 18 ++ lua/conform/health.lua | 34 ++++ lua/conform/init.lua | 291 ++++++++++++++++++++++++++++++ lua/conform/log.lua | 108 +++++++++++ lua/conform/runner.lua | 283 +++++++++++++++++++++++++++++ lua/conform/util.lua | 75 ++++++++ 45 files changed, 1218 insertions(+) create mode 100644 lua/conform/formatters/autoflake.lua create mode 100644 lua/conform/formatters/autopep8.lua create mode 100644 lua/conform/formatters/black.lua create mode 100644 lua/conform/formatters/clang_format.lua create mode 100644 lua/conform/formatters/cljstyle.lua create mode 100644 lua/conform/formatters/cmake_format.lua create mode 100644 lua/conform/formatters/dart_format.lua create mode 100644 lua/conform/formatters/dfmt.lua create mode 100644 lua/conform/formatters/elm_format.lua create mode 100644 lua/conform/formatters/erb_format.lua create mode 100644 lua/conform/formatters/eslint_d.lua create mode 100644 lua/conform/formatters/gdformat.lua create mode 100644 lua/conform/formatters/gofmt.lua create mode 100644 lua/conform/formatters/gofumpt.lua create mode 100644 lua/conform/formatters/goimports.lua create mode 100644 lua/conform/formatters/htmlbeautifier.lua create mode 100644 lua/conform/formatters/isort.lua create mode 100644 lua/conform/formatters/jq.lua create mode 100644 lua/conform/formatters/nixfmt.lua create mode 100644 lua/conform/formatters/nixpkgs_fmt.lua create mode 100644 lua/conform/formatters/ocamlformat.lua create mode 100644 lua/conform/formatters/pg_format.lua create mode 100644 lua/conform/formatters/prettier.lua create mode 100644 lua/conform/formatters/prettierd.lua create mode 100644 lua/conform/formatters/rubocop.lua create mode 100644 lua/conform/formatters/rustfmt.lua create mode 100644 lua/conform/formatters/scalafmt.lua create mode 100644 lua/conform/formatters/shfmt.lua create mode 100644 lua/conform/formatters/sql_formatter.lua create mode 100644 lua/conform/formatters/stylua.lua create mode 100644 lua/conform/formatters/swift_format.lua create mode 100644 lua/conform/formatters/swiftformat.lua create mode 100644 lua/conform/formatters/terraform_fmt.lua create mode 100644 lua/conform/formatters/uncrustify.lua create mode 100644 lua/conform/formatters/xmlformat.lua create mode 100644 lua/conform/formatters/yamlfix.lua create mode 100644 lua/conform/formatters/yamlfmt.lua create mode 100644 lua/conform/formatters/yapf.lua create mode 100644 lua/conform/formatters/zigfmt.lua create mode 100644 lua/conform/fs.lua create mode 100644 lua/conform/health.lua create mode 100644 lua/conform/init.lua create mode 100644 lua/conform/log.lua create mode 100644 lua/conform/runner.lua create mode 100644 lua/conform/util.lua (limited to 'lua') diff --git a/lua/conform/formatters/autoflake.lua b/lua/conform/formatters/autoflake.lua new file mode 100644 index 0000000..04daf71 --- /dev/null +++ b/lua/conform/formatters/autoflake.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/PyCQA/autoflake", + description = "Removes unused imports and unused variables as reported by pyflakes.", + }, + command = "autoflake", + args = { "--stdin-display-name", "$FILENAME", "-" }, +} diff --git a/lua/conform/formatters/autopep8.lua b/lua/conform/formatters/autopep8.lua new file mode 100644 index 0000000..5ed2f83 --- /dev/null +++ b/lua/conform/formatters/autopep8.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/hhatto/autopep8", + description = "A tool that automatically formats Python code to conform to the PEP 8 style guide.", + }, + command = "autopep8", + args = { "-" }, +} diff --git a/lua/conform/formatters/black.lua b/lua/conform/formatters/black.lua new file mode 100644 index 0000000..0d892a2 --- /dev/null +++ b/lua/conform/formatters/black.lua @@ -0,0 +1,19 @@ +local util = require("conform.util") +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/psf/black", + description = "The uncompromising Python code formatter.", + }, + command = "black", + args = { + "--stdin-filename", + "$FILENAME", + "--quiet", + "-", + }, + cwd = util.root_file({ + -- https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html#configuration-via-a-file + "pyproject.toml", + }), +} diff --git a/lua/conform/formatters/clang_format.lua b/lua/conform/formatters/clang_format.lua new file mode 100644 index 0000000..d2c6d41 --- /dev/null +++ b/lua/conform/formatters/clang_format.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://www.kernel.org/doc/html/latest/process/clang-format.html", + description = "Tool to format C/C++/… code according to a set of rules and heuristics.", + }, + command = "clang-format", + args = { "-assume-filename", "$FILENAME" }, +} diff --git a/lua/conform/formatters/cljstyle.lua b/lua/conform/formatters/cljstyle.lua new file mode 100644 index 0000000..ffd8061 --- /dev/null +++ b/lua/conform/formatters/cljstyle.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/greglook/cljstyle", + description = "Formatter for Clojure code.", + }, + command = "cljstyle", + args = { "pipe" }, +} diff --git a/lua/conform/formatters/cmake_format.lua b/lua/conform/formatters/cmake_format.lua new file mode 100644 index 0000000..563bcae --- /dev/null +++ b/lua/conform/formatters/cmake_format.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/cheshirekow/cmake_format", + description = "Parse cmake listfiles and format them nicely.", + }, + command = "cmake-format", + args = { "-" }, +} diff --git a/lua/conform/formatters/dart_format.lua b/lua/conform/formatters/dart_format.lua new file mode 100644 index 0000000..eb9b39c --- /dev/null +++ b/lua/conform/formatters/dart_format.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://dart.dev/tools/dart-format", + description = "Replace the whitespace in your program with formatting that follows Dart guidelines.", + }, + command = "dart", + args = { "format" }, +} diff --git a/lua/conform/formatters/dfmt.lua b/lua/conform/formatters/dfmt.lua new file mode 100644 index 0000000..49c99cb --- /dev/null +++ b/lua/conform/formatters/dfmt.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/dlang-community/dfmt", + description = "Formatter for D source code.", + }, + command = "dfmt", +} diff --git a/lua/conform/formatters/elm_format.lua b/lua/conform/formatters/elm_format.lua new file mode 100644 index 0000000..23f1408 --- /dev/null +++ b/lua/conform/formatters/elm_format.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/avh4/elm-format", + description = "elm-format formats Elm source code according to a standard set of rules based on the official [Elm Style Guide](https://elm-lang.org/docs/style-guide).", + }, + command = "elm-format", + args = { "--stdin" }, +} diff --git a/lua/conform/formatters/erb_format.lua b/lua/conform/formatters/erb_format.lua new file mode 100644 index 0000000..dad08ca --- /dev/null +++ b/lua/conform/formatters/erb_format.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/nebulab/erb-formatter", + description = "Format ERB files with speed and precision.", + }, + command = "erb-format", + args = { "--stdin" }, +} diff --git a/lua/conform/formatters/eslint_d.lua b/lua/conform/formatters/eslint_d.lua new file mode 100644 index 0000000..e036aae --- /dev/null +++ b/lua/conform/formatters/eslint_d.lua @@ -0,0 +1,13 @@ +local util = require("conform.util") +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/mantoni/eslint_d.js/", + description = "Like ESLint, but faster.", + }, + command = util.from_node_modules("eslint_d"), + args = { "--fix-to-stdout", "--stdin", "--stdin-filename", "$FILENAME" }, + cwd = util.root_file({ + "package.json", + }), +} diff --git a/lua/conform/formatters/gdformat.lua b/lua/conform/formatters/gdformat.lua new file mode 100644 index 0000000..914bb89 --- /dev/null +++ b/lua/conform/formatters/gdformat.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/Scony/godot-gdscript-toolkit", + description = "A formatter for Godot's gdscript.", + }, + command = "gdformat", + args = { "-" }, +} diff --git a/lua/conform/formatters/gofmt.lua b/lua/conform/formatters/gofmt.lua new file mode 100644 index 0000000..b0b81c3 --- /dev/null +++ b/lua/conform/formatters/gofmt.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://pkg.go.dev/cmd/gofmt", + description = "Formats go programs.", + }, + command = "gofmt", +} diff --git a/lua/conform/formatters/gofumpt.lua b/lua/conform/formatters/gofumpt.lua new file mode 100644 index 0000000..79fb4dc --- /dev/null +++ b/lua/conform/formatters/gofumpt.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/mvdan/gofumpt", + description = "Enforce a stricter format than gofmt, while being backwards compatible. That is, gofumpt is happy with a subset of the formats that gofmt is happy with.", + }, + command = "gofumpt", +} diff --git a/lua/conform/formatters/goimports.lua b/lua/conform/formatters/goimports.lua new file mode 100644 index 0000000..2361e43 --- /dev/null +++ b/lua/conform/formatters/goimports.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://pkg.go.dev/golang.org/x/tools/cmd/goimports", + description = "Updates your Go import lines, adding missing ones and removing unreferenced ones.", + }, + command = "goimports", + args = { "-srcdir", "$DIRNAME" }, +} diff --git a/lua/conform/formatters/htmlbeautifier.lua b/lua/conform/formatters/htmlbeautifier.lua new file mode 100644 index 0000000..3182f99 --- /dev/null +++ b/lua/conform/formatters/htmlbeautifier.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/threedaymonk/htmlbeautifier", + description = "A normaliser/beautifier for HTML that also understands embedded Ruby. Ideal for tidying up Rails templates.", + }, + command = "htmlbeautifier", +} diff --git a/lua/conform/formatters/isort.lua b/lua/conform/formatters/isort.lua new file mode 100644 index 0000000..f6c6e3d --- /dev/null +++ b/lua/conform/formatters/isort.lua @@ -0,0 +1,24 @@ +local util = require("conform.util") +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/PyCQA/isort", + description = "Python utility / library to sort imports alphabetically and automatically separate them into sections and by type.", + }, + command = "isort", + args = { + "--stdout", + "--filename", + "$FILENAME", + "-", + }, + cwd = util.root_file({ + -- https://pycqa.github.io/isort/docs/configuration/config_files.html + ".isort.cfg", + "pyproject.toml", + "setup.py", + "setup.cfg", + "tox.ini", + ".editorconfig", + }), +} diff --git a/lua/conform/formatters/jq.lua b/lua/conform/formatters/jq.lua new file mode 100644 index 0000000..50a905d --- /dev/null +++ b/lua/conform/formatters/jq.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/stedolan/jq", + description = "Command-line JSON processor.", + }, + command = "jq", +} diff --git a/lua/conform/formatters/nixfmt.lua b/lua/conform/formatters/nixfmt.lua new file mode 100644 index 0000000..6c5001a --- /dev/null +++ b/lua/conform/formatters/nixfmt.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/serokell/nixfmt", + description = "nixfmt is a formatter for Nix code, intended to apply a uniform style.", + }, + command = "nixfmt", +} diff --git a/lua/conform/formatters/nixpkgs_fmt.lua b/lua/conform/formatters/nixpkgs_fmt.lua new file mode 100644 index 0000000..6685fe8 --- /dev/null +++ b/lua/conform/formatters/nixpkgs_fmt.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/nix-community/nixpkgs-fmt", + description = "nixpkgs-fmt is a Nix code formatter for nixpkgs.", + }, + command = "nixpkgs-fmt", +} diff --git a/lua/conform/formatters/ocamlformat.lua b/lua/conform/formatters/ocamlformat.lua new file mode 100644 index 0000000..4ea1b49 --- /dev/null +++ b/lua/conform/formatters/ocamlformat.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/ocaml-ppx/ocamlformat", + description = "Auto-formatter for OCaml code.", + }, + command = "ocamlformat", + args = { "--enable-outside-detected-project", "--name", "$FILENAME", "-" }, +} diff --git a/lua/conform/formatters/pg_format.lua b/lua/conform/formatters/pg_format.lua new file mode 100644 index 0000000..87aff66 --- /dev/null +++ b/lua/conform/formatters/pg_format.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/darold/pgFormatter", + description = "PostgreSQL SQL syntax beautifier.", + }, + command = "pg_format", +} diff --git a/lua/conform/formatters/prettier.lua b/lua/conform/formatters/prettier.lua new file mode 100644 index 0000000..6f4bbfb --- /dev/null +++ b/lua/conform/formatters/prettier.lua @@ -0,0 +1,24 @@ +local util = require("conform.util") +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/prettier/prettier", + description = [[Prettier is an opinionated code formatter. It enforces a consistent style by parsing your code and re-printing it with its own rules that take the maximum line length into account, wrapping code when necessary.]], + }, + command = util.from_node_modules("prettier"), + args = { "--stdin-filepath", "$FILENAME" }, + cwd = util.root_file({ + -- https://prettier.io/docs/en/configuration.html + ".prettierrc", + ".prettierrc.json", + ".prettierrc.yml", + ".prettierrc.yaml", + ".prettierrc.json5", + ".prettierrc.js", + ".prettierrc.cjs", + ".prettierrc.toml", + "prettier.config.js", + "prettier.config.cjs", + "package.json", + }), +} diff --git a/lua/conform/formatters/prettierd.lua b/lua/conform/formatters/prettierd.lua new file mode 100644 index 0000000..0af6baf --- /dev/null +++ b/lua/conform/formatters/prettierd.lua @@ -0,0 +1,24 @@ +local util = require("conform.util") +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/fsouza/prettierd", + description = "prettier, as a daemon, for ludicrous formatting speed.", + }, + command = util.from_node_modules("prettierd"), + args = { "$FILENAME" }, + cwd = util.root_file({ + -- https://prettier.io/docs/en/configuration.html + ".prettierrc", + ".prettierrc.json", + ".prettierrc.yml", + ".prettierrc.yaml", + ".prettierrc.json5", + ".prettierrc.js", + ".prettierrc.cjs", + ".prettierrc.toml", + "prettier.config.js", + "prettier.config.cjs", + "package.json", + }), +} diff --git a/lua/conform/formatters/rubocop.lua b/lua/conform/formatters/rubocop.lua new file mode 100644 index 0000000..492f379 --- /dev/null +++ b/lua/conform/formatters/rubocop.lua @@ -0,0 +1,16 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/rubocop/rubocop", + description = "Ruby static code analyzer and formatter, based on the community Ruby style guide.", + }, + command = "rubocop", + args = { + "-a", + "-f", + "quiet", + "--stderr", + "--stdin", + "$FILENAME", + }, +} diff --git a/lua/conform/formatters/rustfmt.lua b/lua/conform/formatters/rustfmt.lua new file mode 100644 index 0000000..7b5e322 --- /dev/null +++ b/lua/conform/formatters/rustfmt.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/rust-lang/rustfmt", + description = "A tool for formatting rust code according to style guidelines.", + }, + command = "rustfmt", + args = { "--emit=stdout" }, +} diff --git a/lua/conform/formatters/scalafmt.lua b/lua/conform/formatters/scalafmt.lua new file mode 100644 index 0000000..2b9e451 --- /dev/null +++ b/lua/conform/formatters/scalafmt.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/scalameta/scalafmt", + description = "Code formatter for Scala.", + }, + command = "scalafmt", + args = { "--stdin" }, +} diff --git a/lua/conform/formatters/shfmt.lua b/lua/conform/formatters/shfmt.lua new file mode 100644 index 0000000..6e40f0c --- /dev/null +++ b/lua/conform/formatters/shfmt.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/mvdan/sh", + description = "A shell parser, formatter, and interpreter with `bash` support.", + }, + command = "shfmt", + args = { "-filename", "$FILENAME" }, +} diff --git a/lua/conform/formatters/sql_formatter.lua b/lua/conform/formatters/sql_formatter.lua new file mode 100644 index 0000000..bd92851 --- /dev/null +++ b/lua/conform/formatters/sql_formatter.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/sql-formatter-org/sql-formatter", + description = "A whitespace formatter for different query languages.", + }, + command = "sql-formatter", +} diff --git a/lua/conform/formatters/stylua.lua b/lua/conform/formatters/stylua.lua new file mode 100644 index 0000000..f2cc412 --- /dev/null +++ b/lua/conform/formatters/stylua.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/JohnnyMorganz/StyLua", + description = "An opinionated code formatter for Lua.", + }, + command = "stylua", + args = { "--search-parent-directories", "--stdin-filepath", "$FILENAME", "-" }, +} diff --git a/lua/conform/formatters/swift_format.lua b/lua/conform/formatters/swift_format.lua new file mode 100644 index 0000000..2b81297 --- /dev/null +++ b/lua/conform/formatters/swift_format.lua @@ -0,0 +1,8 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/apple/swift-format", + description = "Swift formatter from apple. Requires building from source with `swift build`.", + }, + command = "swift-format", +} diff --git a/lua/conform/formatters/swiftformat.lua b/lua/conform/formatters/swiftformat.lua new file mode 100644 index 0000000..821a010 --- /dev/null +++ b/lua/conform/formatters/swiftformat.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/nicklockwood/SwiftFormat", + description = "SwiftFormat is a code library and command-line tool for reformatting `swift` code on macOS or Linux.", + }, + command = "swiftformat", + args = { "--stdinpath", "$FILENAME" }, +} diff --git a/lua/conform/formatters/terraform_fmt.lua b/lua/conform/formatters/terraform_fmt.lua new file mode 100644 index 0000000..44edc55 --- /dev/null +++ b/lua/conform/formatters/terraform_fmt.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://www.terraform.io/docs/cli/commands/fmt.html", + description = "The terraform-fmt command rewrites `terraform` configuration files to a canonical format and style.", + }, + command = "terraform", + args = { "fmt", "-" }, +} diff --git a/lua/conform/formatters/uncrustify.lua b/lua/conform/formatters/uncrustify.lua new file mode 100644 index 0000000..3430063 --- /dev/null +++ b/lua/conform/formatters/uncrustify.lua @@ -0,0 +1,11 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/uncrustify/uncrustify", + description = "A source code beautifier for C, C++, C#, ObjectiveC, D, Java, Pawn and Vala.", + }, + command = "uncrustify", + args = function(ctx) + return { "-q", "-l", vim.bo[ctx.buf].filetype:upper() } + end, +} diff --git a/lua/conform/formatters/xmlformat.lua b/lua/conform/formatters/xmlformat.lua new file mode 100644 index 0000000..25b48e2 --- /dev/null +++ b/lua/conform/formatters/xmlformat.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/pamoller/xmlformatter", + description = "xmlformatter is an Open Source Python package, which provides formatting of XML documents.", + }, + command = "xmlformat", + args = { "-" }, +} diff --git a/lua/conform/formatters/yamlfix.lua b/lua/conform/formatters/yamlfix.lua new file mode 100644 index 0000000..1b00e01 --- /dev/null +++ b/lua/conform/formatters/yamlfix.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/lyz-code/yamlfix", + description = "A configurable YAML formatter that keeps comments.", + }, + command = "yamlfix", + args = { "-" }, +} diff --git a/lua/conform/formatters/yamlfmt.lua b/lua/conform/formatters/yamlfmt.lua new file mode 100644 index 0000000..56c6cb6 --- /dev/null +++ b/lua/conform/formatters/yamlfmt.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/google/yamlfmt", + description = "yamlfmt is an extensible command line tool or library to format yaml files.", + }, + command = "yamlfmt", + args = { "-" }, +} diff --git a/lua/conform/formatters/yapf.lua b/lua/conform/formatters/yapf.lua new file mode 100644 index 0000000..5d7e866 --- /dev/null +++ b/lua/conform/formatters/yapf.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/google/yapf", + description = "Yet Another Python Formatter.", + }, + command = "yapf", + args = { "--quiet" }, +} diff --git a/lua/conform/formatters/zigfmt.lua b/lua/conform/formatters/zigfmt.lua new file mode 100644 index 0000000..9c93c0b --- /dev/null +++ b/lua/conform/formatters/zigfmt.lua @@ -0,0 +1,9 @@ +---@type conform.FormatterConfig +return { + meta = { + url = "https://github.com/ziglang/zig", + description = "Reformat Zig source into canonical form.", + }, + command = "zig", + args = { "fmt", "--stdin" }, +} diff --git a/lua/conform/fs.lua b/lua/conform/fs.lua new file mode 100644 index 0000000..d303dbd --- /dev/null +++ b/lua/conform/fs.lua @@ -0,0 +1,18 @@ +local M = {} + +local uv = vim.uv or vim.loop + +---@type boolean +M.is_windows = uv.os_uname().version:match("Windows") + +M.is_mac = uv.os_uname().sysname == "Darwin" + +---@type string +M.sep = M.is_windows and "\\" or "/" + +---@param ... string +M.join = function(...) + return table.concat({ ... }, M.sep) +end + +return M diff --git a/lua/conform/health.lua b/lua/conform/health.lua new file mode 100644 index 0000000..76309be --- /dev/null +++ b/lua/conform/health.lua @@ -0,0 +1,34 @@ +local M = {} + +M.check = function() + local conform = require("conform") + vim.health.report_start("conform.nvim report") + + local log = require("conform.log") + vim.health.info(string.format("Log file: %s", log.get_logfile())) + + local all_formatters = conform.list_all_formatters() + for _, formatter in ipairs(all_formatters) do + if not formatter.available then + vim.health.report_warn( + string.format("%s unavailable: %s", formatter.name, formatter.available_msg) + ) + else + local filetypes = {} + for filetype, formatters in pairs(conform.formatters_by_ft) do + if not vim.tbl_islist(formatters) then + formatters = formatters.formatters + end + if vim.tbl_contains(formatters, formatter.name) then + table.insert(filetypes, filetype) + end + end + + vim.health.report_ok( + string.format("%s ready (%s)", formatter.name, table.concat(filetypes, ", ")) + ) + end + end +end + +return M diff --git a/lua/conform/init.lua b/lua/conform/init.lua new file mode 100644 index 0000000..f721587 --- /dev/null +++ b/lua/conform/init.lua @@ -0,0 +1,291 @@ +local M = {} + +---@class (exact) conform.FormatterInfo +---@field name string +---@field command string +---@field cwd? string +---@field available boolean +---@field available_msg? string + +---@class (exact) conform.FormatterConfig +---@field meta conform.FormatterMeta +---@field command string|fun(ctx: conform.Context): string +---@field args? string[]|fun(ctx: conform.Context): string[] +---@field cwd? fun(ctx: conform.Context): nil|string +---@field require_cwd? boolean When cwd is not found, don't run the formatter (default false) +---@field stdin? boolean Send buffer contents to stdin (default true) +---@field condition? fun(ctx: conform.Context): boolean +---@field exit_codes? integer[] Exit codes that indicate success (default {0}) + +---@class (exact) conform.FormatterMeta +---@field url string +---@field description string +--- +---@class (exact) conform.Context +---@field buf integer +---@field filename string +---@field dirname string + +---@class (exact) conform.RunOptions +---@field run_all_formatters nil|boolean Run all listed formatters instead of stopping at the first one. +---@field format_on_save nil|boolean Run these formatters in the built-in format_on_save autocmd. + +---@class (exact) conform.FormatterList : conform.RunOptions +---@field formatters string[] + +---@type table +M.formatters_by_ft = {} + +---@type table +M.formatters = {} + +M.setup = function(opts) + opts = opts or {} + + M.formatters = vim.tbl_extend("force", M.formatters, opts.formatters or {}) + M.formatters_by_ft = vim.tbl_extend("force", M.formatters_by_ft, opts.formatters_by_ft or {}) + + if opts.log_level then + require("conform.log").level = opts.log_level + end + + if opts.format_on_save then + if type(opts.format_on_save) == "boolean" then + opts.format_on_save = {} + end + local aug = vim.api.nvim_create_augroup("Conform", { clear = true }) + vim.api.nvim_create_autocmd("BufWritePre", { + pattern = "*", + group = aug, + callback = function(args) + local format_opts = vim.tbl_deep_extend("keep", opts.format_on_save, { + buf = args.buf, + }) + local filetypes = vim.split(vim.bo[args.buf].filetype, ".", { plain = true }) + for _, ft in ipairs(filetypes) do + local ft_formatters = M.formatters_by_ft[ft] + if ft_formatters and ft_formatters.format_on_save == false then + return + end + end + M.format(format_opts) + end, + }) + end +end + +---Format a buffer +---@param opts? table +--- timeout_ms nil|integer Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. +--- bufnr nil|integer Format this buffer (default 0) +--- async nil|boolean If true the method won't block. Defaults to false. +--- formatters nil|string[] List of formatters to run. Defaults to all formatters for the buffer filetype. +--- lsp_fallback nil|boolean Attempt LSP formatting if no formatters are available. Defaults to false. +---@return boolean True if any formatters were attempted +M.format = function(opts) + opts = vim.tbl_extend("keep", opts or {}, { + timeout_ms = 1000, + bufnr = 0, + async = false, + lsp_fallback = false, + }) + + local formatters = {} + if opts.formatters then + for _, formatter in ipairs(opts.formatters) do + local info = M.get_formatter_info(formatter) + if info.available then + table.insert(formatters, info) + else + vim.notify( + string.format("Formatter '%s' unavailable: %s", info.name, info.available_msg), + vim.log.levels.WARN + ) + end + end + else + formatters = M.list_formatters(opts.bufnr) + end + local any_formatters = not vim.tbl_isempty(formatters) + if any_formatters then + if opts.async then + require("conform.runner").format_async(opts.bufnr, formatters) + else + require("conform.runner").format_sync(opts.bufnr, formatters, opts.timeout_ms) + end + end + + if not any_formatters and opts.lsp_fallback then + local supports_lsp_formatting = false + for _, client in ipairs(vim.lsp.get_active_clients({ bufnr = opts.bufnr })) do + if client.server_capabilities.documentFormattingProvider then + supports_lsp_formatting = true + break + end + end + + if supports_lsp_formatting then + local restore = require("conform.util").save_win_positions(opts.bufnr) + vim.lsp.buf.format(opts) + restore() + end + else + vim.notify("No formatters found for buffer. See :checkhealth conform", vim.log.levels.WARN) + end + + return any_formatters +end + +---Retried the available formatters for a buffer +---@param bufnr? integer +---@return conform.FormatterInfo[] +M.list_formatters = function(bufnr) + if not bufnr or bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local formatters = {} + local run_options = { + run_all_formatters = false, + format_on_save = true, + } + local filetypes = vim.split(vim.bo[bufnr].filetype, ".", { plain = true }) + table.insert(filetypes, "*") + for _, filetype in ipairs(filetypes) do + local ft_formatters = M.formatters_by_ft[filetype] + if ft_formatters then + if not vim.tbl_islist(ft_formatters) then + for k, v in pairs(ft_formatters) do + if k ~= "formatters" then + run_options[k] = v + end + end + ft_formatters = ft_formatters.formatters + end + for _, formatter in ipairs(ft_formatters) do + formatters[formatter] = true + end + end + end + + ---@type conform.FormatterInfo[] + local all_info = {} + for formatter in pairs(formatters) do + local info = M.get_formatter_info(formatter) + if info.available then + table.insert(all_info, assert(info)) + if not run_options.run_all_formatters then + break + end + else + vim.notify_once( + string.format("conform.nvim: missing configuration for formatter '%s'", formatter), + vim.log.levels.WARN + ) + end + end + + return all_info +end + +---List information about all filetype-configured formatters +---@return conform.FormatterInfo[] +M.list_all_formatters = function() + local formatters = {} + for _, ft_formatters in pairs(M.formatters_by_ft) do + if not vim.tbl_islist(ft_formatters) then + ft_formatters = ft_formatters.formatters + end + for _, formatter in ipairs(ft_formatters) do + formatters[formatter] = true + end + end + + ---@type conform.FormatterInfo[] + local all_info = {} + for formatter in pairs(formatters) do + local info = M.get_formatter_info(formatter) + table.insert(all_info, info) + end + + table.sort(all_info, function(a, b) + return a.name < b.name + end) + return all_info +end + +---@private +---@param formatter string +---@return nil|conform.FormatterConfig +M.get_formatter_config = function(formatter) + local config = M.formatters[formatter] + if not config then + local ok + ok, config = pcall(require, "conform.formatters." .. formatter) + if not ok then + return nil + end + end + if type(config) == "function" then + config = config() + end + + if config.stdin == nil then + config.stdin = true + end + return config +end + +---@private +---@param formatter string +---@param bufnr? integer +---@return conform.FormatterInfo +M.get_formatter_info = function(formatter, bufnr) + if not bufnr or bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local config = M.get_formatter_config(formatter) + if not config then + return { + name = formatter, + command = formatter, + available = false, + available_msg = "No config found", + } + end + + local ctx = require("conform.runner").build_context(bufnr, config) + + local command = config.command + if type(command) == "function" then + command = command(ctx) + end + + local available = true + local available_msg = nil + if vim.fn.executable(command) == 0 then + available = false + available_msg = "Command not found" + elseif config.condition and not config.condition(ctx) then + available = false + available_msg = "Condition failed" + end + local cwd = nil + if config.cwd then + cwd = config.cwd(ctx) + if available and not cwd and config.require_cwd then + available = false + available_msg = "Root directory not found" + end + end + + ---@type conform.FormatterInfo + return { + name = formatter, + command = command, + cwd = cwd, + available = available, + available_msg = available_msg, + } +end + +return M diff --git a/lua/conform/log.lua b/lua/conform/log.lua new file mode 100644 index 0000000..b42d1f8 --- /dev/null +++ b/lua/conform/log.lua @@ -0,0 +1,108 @@ +local uv = vim.uv or vim.loop +local levels = vim.deepcopy(vim.log.levels) +vim.tbl_add_reverse_lookup(levels) + +local Log = {} + +---@type integer +Log.level = vim.log.levels.ERROR + +---@return string +Log.get_logfile = function() + local fs = require("conform.fs") + + local ok, stdpath = pcall(vim.fn.stdpath, "log") + if not ok then + stdpath = vim.fn.stdpath("cache") + end + return fs.join(stdpath, "conform.log") +end + +---@param level integer +---@param msg string +---@param ... any[] +---@return string +local function format(level, msg, ...) + local args = vim.F.pack_len(...) + for i = 1, args.n do + local v = args[i] + if type(v) == "table" then + args[i] = vim.inspect(v) + elseif v == nil then + args[i] = "nil" + end + end + local ok, text = pcall(string.format, msg, vim.F.unpack_len(args)) + if ok then + local str_level = levels[level] + return string.format("[%s] %s", str_level, text) + else + return string.format("[ERROR] error formatting log line: '%s' args %s", msg, vim.inspect(args)) + end +end + +---@param line string +local function write(line) + -- This will be replaced during initialization +end + +local initialized = false +local function initialize() + if initialized then + return + end + initialized = true + local filepath = Log.get_logfile() + + local stat = uv.fs_stat(filepath) + if stat and stat.size > 10 * 1024 * 1024 then + local backup = filepath .. ".1" + uv.fs_unlink(backup) + uv.fs_rename(filepath, backup) + end + + local parent = vim.fs.dirname(filepath) + vim.fn.mkdir(parent, "p") + + local logfile, openerr = io.open(filepath, "a+") + if not logfile then + local err_msg = string.format("Failed to open conform.nvim log file: %s", openerr) + vim.notify(err_msg, vim.log.levels.ERROR) + else + write = function(line) + logfile:write(line) + logfile:write("\n") + logfile:flush() + end + end +end + +function Log.log(level, msg, ...) + if Log.level <= level then + initialize() + local text = format(level, msg, ...) + write(text) + end +end + +function Log.trace(...) + Log.log(vim.log.levels.TRACE, ...) +end + +function Log.debug(...) + Log.log(vim.log.levels.DEBUG, ...) +end + +function Log.info(...) + Log.log(vim.log.levels.INFO, ...) +end + +function Log.warn(...) + Log.log(vim.log.levels.WARN, ...) +end + +function Log.error(...) + Log.log(vim.log.levels.ERROR, ...) +end + +return Log diff --git a/lua/conform/runner.lua b/lua/conform/runner.lua new file mode 100644 index 0000000..fd806ce --- /dev/null +++ b/lua/conform/runner.lua @@ -0,0 +1,283 @@ +local fs = require("conform.fs") +local log = require("conform.log") +local util = require("conform.util") +local uv = vim.uv or vim.loop +local M = {} + +---@param ctx conform.Context +---@param config conform.FormatterConfig +local function build_cmd(ctx, config) + local command = config.command + if type(command) == "function" then + command = command(ctx) + end + local cmd = { command } + if config.args then + local args = config.args + if type(config.args) == "function" then + args = config.args(ctx) + end + ---@cast args string[] + for _, v in ipairs(args) do + if v == "$FILENAME" then + v = ctx.filename + elseif v == "$DIRNAME" then + v = vim.fs.dirname(ctx.filename) + end + table.insert(cmd, v) + end + end + return cmd +end + +---@param bufnr integer +---@param original_lines string[] +---@param new_lines string[] +local function apply_format(bufnr, original_lines, new_lines) + local restore = util.save_win_positions(bufnr) + + local original_text = table.concat(original_lines, "\n") + local new_text = table.concat(new_lines, "\n") + local indices = vim.diff(original_text, new_text, { + result_type = "indices", + algorithm = "minimal", + }) + assert(indices) + for i = #indices, 1, -1 do + local start_a, count_a, start_b, count_b = unpack(indices[i]) + -- When count_a is 0, the diff is an insert after the line + if count_a == 0 then + start_a = start_a + 1 + end + local replacement = util.tbl_slice(new_lines, start_b, start_b + count_b - 1) + vim.api.nvim_buf_set_lines(bufnr, start_a - 1, start_a - 1 + count_a, true, replacement) + end + + restore() +end + +---@param bufnr integer +---@param formatter conform.FormatterInfo +---@param input_lines string[] +---@param callback fun(err?: string, output?: string[]) +---@return integer +local function run_formatter(bufnr, formatter, input_lines, callback) + local config = assert(require("conform").get_formatter_config(formatter.name)) + local ctx = M.build_context(bufnr, config) + local cmd = build_cmd(ctx, config) + local cwd = nil + if config.cwd then + cwd = config.cwd(ctx) + end + log.info("Running formatter %s on buffer %d", formatter.name, bufnr) + if not config.stdin then + log.debug("Creating temp file %s", ctx.filename) + local fd = assert(uv.fs_open(ctx.filename, "w", 448)) -- 0700 + uv.fs_write(fd, table.concat(input_lines, "\n")) + uv.fs_close(fd) + local final_cb = callback + callback = function(...) + log.debug("Cleaning up temp file %s", ctx.filename) + uv.fs_unlink(ctx.filename) + final_cb(...) + end + end + log.debug("Running command: %s", cmd) + if cwd then + log.debug("Running in CWD: %s", cwd) + end + local stdout + local stderr + local exit_codes = config.exit_codes or { 0 } + local jid = vim.fn.jobstart(cmd, { + cwd = cwd, + stdout_buffered = true, + stderr_buffered = true, + stdin = config.stdin and "pipe" or "null", + on_stdout = function(_, data) + if config.stdin then + stdout = data + end + end, + on_stderr = function(_, data) + stderr = data + end, + on_exit = function(_, code) + local output + if not config.stdin then + local fd = assert(uv.fs_open(ctx.filename, "r", 448)) -- 0700 + local stat = assert(uv.fs_fstat(fd)) + local content = assert(uv.fs_read(fd, stat.size)) + uv.fs_close(fd) + output = vim.split(content, "\n", { plain = true }) + else + output = stdout + end + if vim.tbl_contains(exit_codes, code) then + log.debug("Formatter %s exited with code %d", formatter.name, code) + callback(nil, output) + else + log.error("Formatter %s exited with code %d", formatter.name, code) + log.warn("Formatter %s stdout:", formatter.name, stdout) + log.warn("Formatter %s stderr:", formatter.name, stderr) + callback( + string.format("Formatter '%s' error: %s", formatter.name, table.concat(stderr, "\n")) + ) + end + end, + }) + if jid == 0 then + callback(string.format("Formatter '%s' invalid arguments", formatter.name)) + elseif jid == -1 then + callback(string.format("Formatter '%s' command is not executable", formatter.name)) + elseif config.stdin then + local text = table.concat(input_lines, "\n") + vim.api.nvim_chan_send(jid, text) + vim.fn.chanclose(jid, "stdin") + end + vim.b[bufnr].conform_jid = jid + + return jid +end + +---@param bufnr integer +---@param config conform.FormatterConfig +---@return conform.Context +M.build_context = function(bufnr, config) + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local filename = vim.api.nvim_buf_get_name(bufnr) + + -- Hack around checkhealth. For buffers that are not files, we need to fabricate a filename + if vim.bo[bufnr].buftype ~= "" then + filename = "" + end + local dirname + if filename == "" then + dirname = vim.fn.getcwd() + filename = fs.join(dirname, "unnamed_temp") + local ft = vim.bo[bufnr].filetype + if ft and ft ~= "" then + filename = filename .. "." .. ft + end + else + dirname = vim.fs.dirname(filename) + end + + if not config.stdin then + local basename = vim.fs.basename(filename) + local tmpname = string.format(".conform.%d.%s", math.random(1000000, 9999999), basename) + local parent = vim.fs.dirname(filename) + filename = fs.join(parent, tmpname) + end + return { + buf = bufnr, + filename = filename, + dirname = dirname, + } +end + +---@param bufnr integer +---@param formatters conform.FormatterInfo[] +---@param callback? fun(err?: string) +M.format_async = function(bufnr, formatters, callback) + local idx = 1 + local changedtick = vim.b[bufnr].changedtick + local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local input_lines = original_lines + + -- kill previous jobs for buffer + local prev_jid = vim.b[bufnr].conform_jid + if prev_jid then + if vim.fn.jobstop(prev_jid) == 1 then + log.info("Canceled previous format job for buffer %d", bufnr) + end + end + + local function run_next_formatter() + local formatter = formatters[idx] + if not formatter then + -- discard formatting if buffer has changed + if vim.b[bufnr].changedtick == changedtick then + apply_format(bufnr, original_lines, input_lines) + else + log.warn("Async formatter discarding changes for buffer %d: concurrent modification", bufnr) + end + if callback then + callback() + end + return + end + idx = idx + 1 + + local jid + jid = run_formatter(bufnr, formatter, input_lines, function(err, output) + if err then + -- Only display the error if the job wasn't canceled + if jid == vim.b[bufnr].conform_jid then + vim.notify(err, vim.log.levels.ERROR) + end + if callback then + callback(err) + end + return + end + input_lines = output + run_next_formatter() + end) + end + run_next_formatter() +end + +---@param bufnr integer +---@param formatters conform.FormatterInfo[] +---@param timeout_ms integer +M.format_sync = function(bufnr, formatters, timeout_ms) + local start = uv.hrtime() / 1e6 + local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local input_lines = original_lines + + -- kill previous jobs for buffer + local prev_jid = vim.b[bufnr].conform_jid + if prev_jid then + if vim.fn.jobstop(prev_jid) == 1 then + log.info("Canceled previous format job for buffer %d", bufnr) + end + end + + for _, formatter in ipairs(formatters) do + local remaining = timeout_ms - (uv.hrtime() / 1e6 - start) + local done = false + local result = nil + run_formatter(bufnr, formatter, input_lines, function(err, output) + if err then + vim.notify(err, vim.log.levels.ERROR) + end + done = true + result = output + end) + + local wait_result, wait_reason = vim.wait(remaining, function() + return done + end, 5) + + if not wait_result then + if wait_reason == -1 then + vim.notify(string.format("Formatter '%s' timed out", formatter.name), vim.log.levels.WARN) + end + return + end + + if not result then + return + end + + input_lines = result + end + + local final_result = input_lines + apply_format(bufnr, original_lines, final_result) +end + +return M diff --git a/lua/conform/util.lua b/lua/conform/util.lua new file mode 100644 index 0000000..8cbc013 --- /dev/null +++ b/lua/conform/util.lua @@ -0,0 +1,75 @@ +local M = {} + +---@param cmd string +---@return fun(ctx: conform.Context): string +M.from_node_modules = function(cmd) + return function(ctx) + local fs = require("conform.fs") + local found = + vim.fs.find("node_modules", { upward = true, type = "directory", path = ctx.dirname }) + for _, dir in ipairs(found) do + local executable = fs.join(dir, ".bin", cmd) + if vim.fn.executable(executable) == 1 then + return executable + end + end + return cmd + end +end + +---@param files string|string[] +---@return fun(ctx: conform.Context): nil|string +M.root_file = function(files) + return function(ctx) + local found = vim.fs.find(files, { upward = true, path = ctx.dirname })[1] + if found then + return vim.fs.dirname(found) + end + end +end + +---@param bufnr? integer +---@return fun() Function that restores the window positions +M.save_win_positions = function(bufnr) + if bufnr == nil or bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + local win_positions = {} + for _, winid in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_get_buf(winid) == bufnr then + vim.api.nvim_win_call(winid, function() + local view = vim.fn.winsaveview() + win_positions[winid] = view + end) + end + end + + return function() + for winid, view in pairs(win_positions) do + vim.api.nvim_win_call(winid, function() + pcall(vim.fn.winrestview, view) + end) + end + end +end + +---@generic T : any +---@param tbl T[] +---@param start_idx? number +---@param end_idx? number +---@return T[] +M.tbl_slice = function(tbl, start_idx, end_idx) + local ret = {} + if not start_idx then + start_idx = 1 + end + if not end_idx then + end_idx = #tbl + end + for i = start_idx, end_idx do + table.insert(ret, tbl[i]) + end + return ret +end + +return M -- cgit v1.2.3-70-g09d2