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 --- .envrc | 2 + .github/ISSUE_TEMPLATE/bug_report.yml | 112 ++++++++++++ .github/generate.py | 91 ++++++++++ .github/main.py | 31 ++++ .github/nvim_doc_tools | 1 + .github/pre-commit | 5 + .github/pre-push | 7 + .github/workflows/install_nvim.sh | 12 ++ .github/workflows/tests.yml | 65 +++++++ .github/workflows/update-docs.yml | 35 ++++ .gitignore | 44 +++++ .gitmodules | 3 + .luacheckrc | 19 ++ .stylua.toml | 3 + README.md | 290 +++++++++++++++++++++++++++++ doc/conform.txt | 39 ++++ 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 ++++++++ 61 files changed, 1977 insertions(+) create mode 100644 .envrc create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100755 .github/generate.py create mode 100755 .github/main.py create mode 160000 .github/nvim_doc_tools create mode 100755 .github/pre-commit create mode 100755 .github/pre-push create mode 100644 .github/workflows/install_nvim.sh create mode 100644 .github/workflows/tests.yml create mode 100644 .github/workflows/update-docs.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .luacheckrc create mode 100644 .stylua.toml create mode 100644 doc/conform.txt 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 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"^$", + r"^$", + 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"^$", + r"^$", + lines, + ) + + +def update_readme_toc(): + toc = ["\n"] + generate_md_toc(README) + ["\n"] + replace_section( + README, + r"^$", + r"^$", + 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 index 0000000..4260b37 --- /dev/null +++ b/.github/nvim_doc_tools @@ -0,0 +1 @@ +Subproject commit 4260b374395d963b8ae74134908e70650f591d2d 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 + + + +- [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) + + + +## Requirements + +- Neovim 0.8+ + +## Installation + +conform.nvim supports all the usual plugin managers + +
+ lazy.nvim + +```lua +{ + 'stevearc/conform.nvim', + opts = {}, +} +``` + +
+ +
+ Packer + +```lua +require('packer').startup(function() + use { + 'stevearc/conform.nvim', + config = function() require('conform').setup() end + } +end) +``` + +
+ +
+ Paq + +```lua +require "paq" { + {'stevearc/conform.nvim'}; +} +``` + +
+ +
+ vim-plug + +```vim +Plug 'stevearc/conform.nvim' +``` + +
+ +
+ dein + +```vim +call dein#add('stevearc/conform.nvim') +``` + +
+ +
+ Pathogen + +```sh +git clone --depth=1 https://github.com/stevearc/conform.nvim.git ~/.vim/bundle/ +``` + +
+ +
+ Neovim native package + +```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 +``` + +
+ +## 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 + + + +- [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. + + +## 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 + + + +### 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 + + + +## 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 +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