aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.envrc2
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.yml112
-rwxr-xr-x.github/generate.py91
-rwxr-xr-x.github/main.py31
m---------.github/nvim_doc_tools0
-rwxr-xr-x.github/pre-commit5
-rwxr-xr-x.github/pre-push7
-rw-r--r--.github/workflows/install_nvim.sh12
-rw-r--r--.github/workflows/tests.yml65
-rw-r--r--.github/workflows/update-docs.yml35
-rw-r--r--.gitignore44
-rw-r--r--.gitmodules3
-rw-r--r--.luacheckrc19
-rw-r--r--.stylua.toml3
-rw-r--r--README.md290
-rw-r--r--doc/conform.txt39
-rw-r--r--lua/conform/formatters/autoflake.lua9
-rw-r--r--lua/conform/formatters/autopep8.lua9
-rw-r--r--lua/conform/formatters/black.lua19
-rw-r--r--lua/conform/formatters/clang_format.lua9
-rw-r--r--lua/conform/formatters/cljstyle.lua9
-rw-r--r--lua/conform/formatters/cmake_format.lua9
-rw-r--r--lua/conform/formatters/dart_format.lua9
-rw-r--r--lua/conform/formatters/dfmt.lua8
-rw-r--r--lua/conform/formatters/elm_format.lua9
-rw-r--r--lua/conform/formatters/erb_format.lua9
-rw-r--r--lua/conform/formatters/eslint_d.lua13
-rw-r--r--lua/conform/formatters/gdformat.lua9
-rw-r--r--lua/conform/formatters/gofmt.lua8
-rw-r--r--lua/conform/formatters/gofumpt.lua8
-rw-r--r--lua/conform/formatters/goimports.lua9
-rw-r--r--lua/conform/formatters/htmlbeautifier.lua8
-rw-r--r--lua/conform/formatters/isort.lua24
-rw-r--r--lua/conform/formatters/jq.lua8
-rw-r--r--lua/conform/formatters/nixfmt.lua8
-rw-r--r--lua/conform/formatters/nixpkgs_fmt.lua8
-rw-r--r--lua/conform/formatters/ocamlformat.lua9
-rw-r--r--lua/conform/formatters/pg_format.lua8
-rw-r--r--lua/conform/formatters/prettier.lua24
-rw-r--r--lua/conform/formatters/prettierd.lua24
-rw-r--r--lua/conform/formatters/rubocop.lua16
-rw-r--r--lua/conform/formatters/rustfmt.lua9
-rw-r--r--lua/conform/formatters/scalafmt.lua9
-rw-r--r--lua/conform/formatters/shfmt.lua9
-rw-r--r--lua/conform/formatters/sql_formatter.lua8
-rw-r--r--lua/conform/formatters/stylua.lua9
-rw-r--r--lua/conform/formatters/swift_format.lua8
-rw-r--r--lua/conform/formatters/swiftformat.lua9
-rw-r--r--lua/conform/formatters/terraform_fmt.lua9
-rw-r--r--lua/conform/formatters/uncrustify.lua11
-rw-r--r--lua/conform/formatters/xmlformat.lua9
-rw-r--r--lua/conform/formatters/yamlfix.lua9
-rw-r--r--lua/conform/formatters/yamlfmt.lua9
-rw-r--r--lua/conform/formatters/yapf.lua9
-rw-r--r--lua/conform/formatters/zigfmt.lua9
-rw-r--r--lua/conform/fs.lua18
-rw-r--r--lua/conform/health.lua34
-rw-r--r--lua/conform/init.lua291
-rw-r--r--lua/conform/log.lua108
-rw-r--r--lua/conform/runner.lua283
-rw-r--r--lua/conform/util.lua75
61 files changed, 1976 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..6a2e7a8
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,2 @@
+layout python
+python -c 'import pyparsing' 2> /dev/null || pip install pyparsing==3.0.9 black isort mypy
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..de8b9ee
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,112 @@
+name: Bug Report
+description: File a bug/issue
+title: "bug: "
+labels: [bug]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Before reporting a bug, make sure to search [existing issues](https://github.com/stevearc/conform.nvim/issues)
+ - type: input
+ attributes:
+ label: "Neovim version (nvim -v)"
+ placeholder: "0.8.0 commit db1b0ee3b30f"
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: "Operating system/version"
+ placeholder: "MacOS 11.5"
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: "Output of :checkhealth conform"
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Describe the bug
+ description: A clear and concise description of what the bug is.
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Steps To Reproduce
+ description: Steps to reproduce the behavior.
+ placeholder: |
+ 1. nvim -u repro.lua
+ 2.
+ 3.
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Expected Behavior
+ description: A concise description of what you expected to happen.
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Minimal example file
+ description: A small example file you are editing that produces the issue
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Minimal init.lua
+ description:
+ Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua`
+ This uses lazy.nvim (a plugin manager).
+ value: |
+ -- DO NOT change the paths and don't remove the colorscheme
+ local root = vim.fn.fnamemodify("./.repro", ":p")
+
+ -- set stdpaths to use .repro
+ for _, name in ipairs({ "config", "data", "state", "cache" }) do
+ vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
+ end
+
+ -- bootstrap lazy
+ local lazypath = root .. "/plugins/lazy.nvim"
+ if not vim.loop.fs_stat(lazypath) then
+ vim.fn.system({
+ "git",
+ "clone",
+ "--filter=blob:none",
+ "--single-branch",
+ "https://github.com/folke/lazy.nvim.git",
+ lazypath,
+ })
+ end
+ vim.opt.runtimepath:prepend(lazypath)
+
+ -- install plugins
+ local plugins = {
+ "folke/tokyonight.nvim",
+ {
+ "stevearc/conform.nvim",
+ config = function()
+ require("conform").setup({
+ log_level = vim.log.levels.DEBUG,
+ -- add your config here
+ })
+ end,
+ },
+ -- add any other plugins here
+ }
+ require("lazy").setup(plugins, {
+ root = root .. "/plugins",
+ })
+
+ vim.cmd.colorscheme("tokyonight")
+ -- add anything else here
+ render: Lua
+ validations:
+ required: false
+ - type: textarea
+ attributes:
+ label: Additional context
+ description: Any additional information or screenshots you would like to provide
+ validations:
+ required: false
diff --git a/.github/generate.py b/.github/generate.py
new file mode 100755
index 0000000..3f4fa27
--- /dev/null
+++ b/.github/generate.py
@@ -0,0 +1,91 @@
+import os
+import os.path
+import re
+from typing import List
+
+from nvim_doc_tools import (
+ Vimdoc,
+ VimdocSection,
+ generate_md_toc,
+ parse_functions,
+ read_nvim_json,
+ render_md_api,
+ render_vimdoc_api,
+ replace_section,
+)
+
+HERE = os.path.dirname(__file__)
+ROOT = os.path.abspath(os.path.join(HERE, os.path.pardir))
+README = os.path.join(ROOT, "README.md")
+DOC = os.path.join(ROOT, "doc")
+VIMDOC = os.path.join(DOC, "conform.txt")
+
+
+def update_formatter_list():
+ formatters = sorted(
+ [
+ os.path.splitext(file)[0]
+ for file in os.listdir(os.path.join(ROOT, "lua", "conform", "formatters"))
+ ]
+ )
+ formatter_lines = ["\n"]
+ for formatter in formatters:
+ meta = read_nvim_json(f'require("conform.formatters.{formatter}").meta')
+ formatter_lines.append(
+ f"- [{formatter}]({meta['url']}) - {meta['description']}\n"
+ )
+ replace_section(
+ README,
+ r"^<!-- FORMATTERS -->$",
+ r"^<!-- /FORMATTERS -->$",
+ formatter_lines,
+ )
+
+
+def add_md_link_path(path: str, lines: List[str]) -> List[str]:
+ ret = []
+ for line in lines:
+ ret.append(re.sub(r"(\(#)", "(" + path + "#", line))
+ return ret
+
+
+def update_md_api():
+ funcs = parse_functions(os.path.join(ROOT, "lua", "conform", "init.lua"))
+ lines = ["\n"] + render_md_api(funcs, 3)[:-1] # trim last newline
+ replace_section(
+ README,
+ r"^<!-- API -->$",
+ r"^<!-- /API -->$",
+ lines,
+ )
+
+
+def update_readme_toc():
+ toc = ["\n"] + generate_md_toc(README) + ["\n"]
+ replace_section(
+ README,
+ r"^<!-- TOC -->$",
+ r"^<!-- /TOC -->$",
+ toc,
+ )
+
+
+def generate_vimdoc():
+ doc = Vimdoc("conform.txt", "conform")
+ funcs = parse_functions(os.path.join(ROOT, "lua", "conform", "init.lua"))
+ doc.sections.extend(
+ [
+ VimdocSection("API", "conform-api", render_vimdoc_api("conform", funcs)),
+ ]
+ )
+
+ with open(VIMDOC, "w", encoding="utf-8") as ofile:
+ ofile.writelines(doc.render())
+
+
+def main() -> None:
+ """Update the README"""
+ update_formatter_list()
+ update_md_api()
+ update_readme_toc()
+ generate_vimdoc()
diff --git a/.github/main.py b/.github/main.py
new file mode 100755
index 0000000..4dffddf
--- /dev/null
+++ b/.github/main.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+import argparse
+import os
+import sys
+
+HERE = os.path.dirname(__file__)
+ROOT = os.path.abspath(os.path.join(HERE, os.path.pardir))
+DOC = os.path.join(ROOT, "doc")
+
+
+def main() -> None:
+ """Generate docs"""
+ sys.path.append(HERE)
+ parser = argparse.ArgumentParser(description=main.__doc__)
+ parser.add_argument("command", choices=["generate", "lint"])
+ args = parser.parse_args()
+ if args.command == "generate":
+ import generate
+
+ generate.main()
+ elif args.command == "lint":
+ from nvim_doc_tools import lint_md_links
+
+ files = [os.path.join(ROOT, "README.md")] + [
+ os.path.join(DOC, file) for file in os.listdir(DOC) if file.endswith(".md")
+ ]
+ lint_md_links.main(ROOT, files)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/nvim_doc_tools b/.github/nvim_doc_tools
new file mode 160000
+Subproject 4260b374395d963b8ae74134908e70650f591d2
diff --git a/.github/pre-commit b/.github/pre-commit
new file mode 100755
index 0000000..49ee249
--- /dev/null
+++ b/.github/pre-commit
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e
+luacheck lua tests
+
+stylua --check .
diff --git a/.github/pre-push b/.github/pre-push
new file mode 100755
index 0000000..d117589
--- /dev/null
+++ b/.github/pre-push
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -e
+luacheck lua tests
+
+stylua --check .
+
+lua-typecheck lua
diff --git a/.github/workflows/install_nvim.sh b/.github/workflows/install_nvim.sh
new file mode 100644
index 0000000..c5119dc
--- /dev/null
+++ b/.github/workflows/install_nvim.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+set -e
+PLUGINS="$HOME/.local/share/nvim/site/pack/plugins/start"
+mkdir -p "$PLUGINS"
+
+wget "https://github.com/neovim/neovim/releases/download/${NVIM_TAG}/nvim.appimage"
+chmod +x nvim.appimage
+./nvim.appimage --appimage-extract >/dev/null
+rm -f nvim.appimage
+mkdir -p ~/.local/share/nvim
+mv squashfs-root ~/.local/share/nvim/appimage
+sudo ln -s "$HOME/.local/share/nvim/appimage/AppRun" /usr/bin/nvim
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..ca71ef2
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,65 @@
+name: Run tests
+
+on: [push, pull_request]
+
+jobs:
+ luacheck:
+ name: Luacheck
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Prepare
+ run: |
+ sudo apt-get update
+ sudo add-apt-repository universe
+ sudo apt install luarocks -y
+ sudo luarocks install luacheck
+
+ - name: Run Luacheck
+ run: luacheck .
+
+ typecheck:
+ name: typecheck
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v3
+ - uses: stevearc/nvim-typecheck-action@v1
+ with:
+ path: lua
+
+ stylua:
+ name: StyLua
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v3
+ - name: Stylua
+ uses: JohnnyMorganz/stylua-action@v3
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ version: v0.15.2
+ args: --check .
+
+ release:
+ name: release
+
+ if: ${{ github.ref == 'refs/heads/master' }}
+ needs:
+ - luacheck
+ - stylua
+ - typecheck
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: google-github-actions/release-please-action@v3
+ id: release
+ with:
+ release-type: simple
+ package-name: conform.nvim
+ - uses: actions/checkout@v3
+ - uses: rickstaa/action-create-tag@v1
+ if: ${{ steps.release.outputs.release_created }}
+ with:
+ tag: stable
+ message: "Current stable release: ${{ steps.release.outputs.tag_name }}"
+ tag_exists_error: false
+ force_push_tag: true
diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml
new file mode 100644
index 0000000..813e69c
--- /dev/null
+++ b/.github/workflows/update-docs.yml
@@ -0,0 +1,35 @@
+name: Update docs
+
+on: push
+
+jobs:
+ update-readme:
+ name: Update docs
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ submodules: true
+
+ - name: Install Neovim and dependencies
+ env:
+ NVIM_TAG: v0.8.3
+ run: |
+ bash ./.github/workflows/install_nvim.sh
+
+ - name: Update docs
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COMMIT_MSG: |
+ [docgen] Update docs
+ skip-checks: true
+ run: |
+ git config user.email "actions@github"
+ git config user.name "Github Actions"
+ git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
+ python -m pip install pyparsing==3.0.9
+ python .github/main.py generate
+ python .github/main.py lint
+ git add README.md doc
+ # Only commit and push if we have changes
+ git diff --quiet && git diff --staged --quiet || (git commit -m "${COMMIT_MSG}"; git push origin HEAD:${GITHUB_REF})
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d818abb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,44 @@
+# Compiled Lua sources
+luac.out
+
+# luarocks build files
+*.src.rock
+*.zip
+*.tar.gz
+
+# Object files
+*.o
+*.os
+*.ko
+*.obj
+*.elf
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+*.def
+*.exp
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
+.direnv/
+.testenv/
+doc/tags
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..bec7b87
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule ".github/nvim_doc_tools"]
+ path = .github/nvim_doc_tools
+ url = ../../stevearc/nvim_doc_tools
diff --git a/.luacheckrc b/.luacheckrc
new file mode 100644
index 0000000..7efefde
--- /dev/null
+++ b/.luacheckrc
@@ -0,0 +1,19 @@
+max_comment_line_length = false
+codes = true
+
+exclude_files = {
+ "tests/treesitter",
+}
+
+ignore = {
+ "212", -- Unused argument
+ "631", -- Line is too long
+ "122", -- Setting a readonly global
+ "542", -- Empty if branch
+}
+
+read_globals = {
+ "vim",
+ "a",
+ "assert",
+}
diff --git a/.stylua.toml b/.stylua.toml
new file mode 100644
index 0000000..3cfeffd
--- /dev/null
+++ b/.stylua.toml
@@ -0,0 +1,3 @@
+column_width = 100
+indent_type = "Spaces"
+indent_width = 2
diff --git a/README.md b/README.md
index 7d80e70..7fa3fe6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,292 @@
# conform.nvim
+
Formatter plugin for Neovim
+
+<!-- TOC -->
+
+- [Requirements](#requirements)
+- [Installation](#installation)
+- [Setup](#setup)
+- [Formatters](#formatters)
+- [Custom formatters](#custom-formatters)
+- [API](#api)
+ - [format(opts)](#formatopts)
+ - [list_formatters(bufnr)](#list_formattersbufnr)
+ - [list_all_formatters()](#list_all_formatters)
+- [Acknowledgements](#acknowledgements)
+
+<!-- /TOC -->
+
+## Requirements
+
+- Neovim 0.8+
+
+## Installation
+
+conform.nvim supports all the usual plugin managers
+
+<details>
+ <summary>lazy.nvim</summary>
+
+```lua
+{
+ 'stevearc/conform.nvim',
+ opts = {},
+}
+```
+
+</details>
+
+<details>
+ <summary>Packer</summary>
+
+```lua
+require('packer').startup(function()
+ use {
+ 'stevearc/conform.nvim',
+ config = function() require('conform').setup() end
+ }
+end)
+```
+
+</details>
+
+<details>
+ <summary>Paq</summary>
+
+```lua
+require "paq" {
+ {'stevearc/conform.nvim'};
+}
+```
+
+</details>
+
+<details>
+ <summary>vim-plug</summary>
+
+```vim
+Plug 'stevearc/conform.nvim'
+```
+
+</details>
+
+<details>
+ <summary>dein</summary>
+
+```vim
+call dein#add('stevearc/conform.nvim')
+```
+
+</details>
+
+<details>
+ <summary>Pathogen</summary>
+
+```sh
+git clone --depth=1 https://github.com/stevearc/conform.nvim.git ~/.vim/bundle/
+```
+
+</details>
+
+<details>
+ <summary>Neovim native package</summary>
+
+```sh
+git clone --depth=1 https://github.com/stevearc/conform.nvim.git \
+ "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/pack/conform/start/conform.nvim
+```
+
+</details>
+
+## Setup
+
+At a minimum, you will need to set up some formatters by filetype
+
+```lua
+require("conform").setup({
+ formatters_by_ft = {
+ lua = { "stylua" },
+ -- Conform will use the first available formatter in the list
+ javascript = { "prettier_d", "prettier" },
+ -- Formatters can also be specified with additional options
+ python = {
+ formatters = { "isort", "black" },
+ -- Run formatters one after another instead of stopping at the first success
+ run_all_formatters = true,
+ -- Don't run these formatters as part of the format_on_save autocmd (see below)
+ format_on_save = false
+ },
+ },
+})
+```
+
+You can also modify `formatters_by_ft` directly
+
+```lua
+require("conform").formatters_by_ft.lua = { "stylua" }
+```
+
+Then you can use `conform.format()` just like you would `vim.lsp.buf.format()`. For example, to format on save:
+
+```lua
+vim.api.nvim_create_autocmd("BufWritePre", {
+ pattern = "*",
+ callback = function(args)
+ require("conform").format({ buf = args.buf })
+ end,
+})
+```
+
+As a shortcut, conform will optionally set up this format-on-save autocmd for you
+
+```lua
+require("conform").setup({
+ format_on_save = true,
+})
+```
+
+You can also use an option table and it will get passed to `conform.format()`
+
+```lua
+require("conform").setup({
+ format_on_save = {
+ timeout_ms = 500,
+ lsp_fallback = true,
+ },
+})
+```
+
+See [conform.format()](#formatopts) for more details about capabilities and parameters.
+
+To view configured and available formatters, as well as to see the path to the log file, run `:checkhealth conform`
+
+## Formatters
+
+<!-- FORMATTERS -->
+
+- [autoflake](https://github.com/PyCQA/autoflake) - Removes unused imports and unused variables as reported by pyflakes.
+- [autopep8](https://github.com/hhatto/autopep8) - A tool that automatically formats Python code to conform to the PEP 8 style guide.
+- [black](https://github.com/psf/black) - The uncompromising Python code formatter.
+- [clang_format](https://www.kernel.org/doc/html/latest/process/clang-format.html) - Tool to format C/C++/… code according to a set of rules and heuristics.
+- [cljstyle](https://github.com/greglook/cljstyle) - Formatter for Clojure code.
+- [cmake_format](https://github.com/cheshirekow/cmake_format) - Parse cmake listfiles and format them nicely.
+- [dart_format](https://dart.dev/tools/dart-format) - Replace the whitespace in your program with formatting that follows Dart guidelines.
+- [dfmt](https://github.com/dlang-community/dfmt) - Formatter for D source code.
+- [elm_format](https://github.com/avh4/elm-format) - 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).
+- [erb_format](https://github.com/nebulab/erb-formatter) - Format ERB files with speed and precision.
+- [eslint_d](https://github.com/mantoni/eslint_d.js/) - Like ESLint, but faster.
+- [gdformat](https://github.com/Scony/godot-gdscript-toolkit) - A formatter for Godot's gdscript.
+- [gofmt](https://pkg.go.dev/cmd/gofmt) - Formats go programs.
+- [gofumpt](https://github.com/mvdan/gofumpt) - 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.
+- [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) - Updates your Go import lines, adding missing ones and removing unreferenced ones.
+- [htmlbeautifier](https://github.com/threedaymonk/htmlbeautifier) - A normaliser/beautifier for HTML that also understands embedded Ruby. Ideal for tidying up Rails templates.
+- [isort](https://github.com/PyCQA/isort) - Python utility / library to sort imports alphabetically and automatically separate them into sections and by type.
+- [jq](https://github.com/stedolan/jq) - Command-line JSON processor.
+- [nixfmt](https://github.com/serokell/nixfmt) - nixfmt is a formatter for Nix code, intended to apply a uniform style.
+- [nixpkgs_fmt](https://github.com/nix-community/nixpkgs-fmt) - nixpkgs-fmt is a Nix code formatter for nixpkgs.
+- [ocamlformat](https://github.com/ocaml-ppx/ocamlformat) - Auto-formatter for OCaml code.
+- [pg_format](https://github.com/darold/pgFormatter) - PostgreSQL SQL syntax beautifier.
+- [prettier](https://github.com/prettier/prettier) - 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.
+- [prettierd](https://github.com/fsouza/prettierd) - prettier, as a daemon, for ludicrous formatting speed.
+- [rubocop](https://github.com/rubocop/rubocop) - Ruby static code analyzer and formatter, based on the community Ruby style guide.
+- [rustfmt](https://github.com/rust-lang/rustfmt) - A tool for formatting rust code according to style guidelines.
+- [scalafmt](https://github.com/scalameta/scalafmt) - Code formatter for Scala.
+- [shfmt](https://github.com/mvdan/sh) - A shell parser, formatter, and interpreter with `bash` support.
+- [sql_formatter](https://github.com/sql-formatter-org/sql-formatter) - A whitespace formatter for different query languages.
+- [stylua](https://github.com/JohnnyMorganz/StyLua) - An opinionated code formatter for Lua.
+- [swift_format](https://github.com/apple/swift-format) - Swift formatter from apple. Requires building from source with `swift build`.
+- [swiftformat](https://github.com/nicklockwood/SwiftFormat) - SwiftFormat is a code library and command-line tool for reformatting `swift` code on macOS or Linux.
+- [terraform_fmt](https://www.terraform.io/docs/cli/commands/fmt.html) - The terraform-fmt command rewrites `terraform` configuration files to a canonical format and style.
+- [uncrustify](https://github.com/uncrustify/uncrustify) - A source code beautifier for C, C++, C#, ObjectiveC, D, Java, Pawn and Vala.
+- [xmlformat](https://github.com/pamoller/xmlformatter) - xmlformatter is an Open Source Python package, which provides formatting of XML documents.
+- [yamlfix](https://github.com/lyz-code/yamlfix) - A configurable YAML formatter that keeps comments.
+- [yamlfmt](https://github.com/google/yamlfmt) - yamlfmt is an extensible command line tool or library to format yaml files.
+- [yapf](https://github.com/google/yapf) - Yet Another Python Formatter.
+- [zigfmt](https://github.com/ziglang/zig) - Reformat Zig source into canonical form.
+<!-- /FORMATTERS -->
+
+## Custom formatters
+
+You can create your own custom formatters
+
+```lua
+require("conform").setup({
+ formatters = {
+ my_formatter = {
+ -- This can be a string or a function that returns a string
+ command = 'my_cmd',
+ -- OPTIONAL - all fields below this are optional
+ -- A list of strings, or a function that returns a list of strings
+ args = { "--stdin-from-filename", "$FILENAME" },
+ -- Send file contents to stdin, read new contents from stdout (default true)
+ -- When false, will create a temp file (will appear in "$FILENAME" args). The temp
+ -- file is assumed to be modified in-place by the format command.
+ stdin = true,
+ -- A function the calculates the directory to run the command in
+ cwd = require("conform.util").root_file({ ".editorconfig", "package.json" }),
+ -- When cwd is not found, don't run the formatter (default false)
+ require_cwd = true
+ -- When returns false, the formatter will not be used
+ condition = function(bufnr)
+ local basename = vim.api.nvim_buf_get_name(bufnr)
+ return vim.fs.basename(bufname) ~= "README.md"
+ end,
+ -- Exit codes that indicate success (default {0})
+ exit_codes = { 0, 1 },
+ }
+ }
+})
+```
+
+Again, you can also set these directly
+
+```lua
+require("conform").formatters.my_formatter = { ... }
+```
+
+## API
+
+<!-- API -->
+
+### format(opts)
+
+`format(opts): boolean` \
+Format a buffer
+
+| Param | Type | Desc | |
+| ----- | ------------ | --------------- | ------------------------------------------------------------------------------------------ |
+| opts | `nil\|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. |
+
+Returns:
+| Type | Desc |
+| ------- | ------------------------------------- |
+| boolean | True if any formatters were attempted |
+
+### list_formatters(bufnr)
+
+`list_formatters(bufnr): conform.FormatterInfo[]` \
+Retried the available formatters for a buffer
+
+| Param | Type | Desc |
+| ----- | -------------- | ---- |
+| bufnr | `nil\|integer` | |
+
+### list_all_formatters()
+
+`list_all_formatters(): conform.FormatterInfo[]` \
+List information about all filetype-configured formatters
+
+<!-- /API -->
+
+## Acknowledgements
+
+Thanks to
+
+- [nvim-lint](https://github.com/mfussenegger/nvim-lint) for providing inspiration for the config and API. It's an excellent plugin that balances power and simplicity.
+- [null-ls](https://github.com/jose-elias-alvarez/null-ls.nvim) for formatter configurations and being my formatter/linter of choice for a long time.
diff --git a/doc/conform.txt b/doc/conform.txt
new file mode 100644
index 0000000..859c780
--- /dev/null
+++ b/doc/conform.txt
@@ -0,0 +1,39 @@
+*conform.txt*
+*Conform* *conform* *conform.nvim*
+--------------------------------------------------------------------------------
+CONTENTS *conform-contents*
+
+ 1. Api |conform-api|
+
+--------------------------------------------------------------------------------
+API *conform-api*
+
+format({opts}): boolean *conform.format*
+ Format a buffer
+
+ Parameters:
+ {opts} `nil|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.
+ Returns:
+ `boolean` True if any formatters were attempted
+
+list_formatters({bufnr}): conform.FormatterInfo[] *conform.list_formatters*
+ Retried the available formatters for a buffer
+
+ Parameters:
+ {bufnr} `nil|integer`
+
+list_all_formatters(): conform.FormatterInfo[] *conform.list_all_formatters*
+ List information about all filetype-configured formatters
+
+================================================================================
+vim:tw=80:ts=2:ft=help:norl:syntax=help:
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<string, string[]|conform.FormatterList>
+M.formatters_by_ft = {}
+
+---@type table<string, conform.FormatterConfig|fun(): conform.FormatterConfig>
+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