DEV Community

Artem Vasilyev
Artem Vasilyev

Posted on

conform.nvim: store formatters settings in a project config

.conform.json
I like the conform.nvim plugin - it helps me automatically format different file types.

But I had two common cases where I wanted a bit more flexibility:

  • Sometimes I need to use different formatters per project.
  • Sometimes I want to pass extra arguments to a formatter, especially if the project doesn’t already include a formatter-specific config file.

At first, I hardcoded all of this directly in my conform.lua setup.
But then I thought - what if I could store the configuration inside each project repository?

This is where .conform.json comes in.

Example .conform.json file

{
  "formatters_by_ft": {
    "python": ["isort", "yapf"],
    "json": ["prettier"],
    "yaml": ["prettier"]
  },
  "args": {
    "yapf": ["--style", "{based_on_style: pep8, indent_width: 4}"],
    "isort": ["--profile", "open_stack", "--line-length", "79"]
  }
}
Enter fullscreen mode Exit fullscreen mode

If the project contains .conform.json, conform.nvim will read it and use whatever formatters and arguments you define there.
If not, it will just fall back to the defaults.

  • formatters_by_ft — which formatter(s) to use per filetype.
  • args — optional arguments to pass to each formatter.

Tip: If you don’t want to commit .conform.json to your repository, you can add it to your global gitignore file:

echo ".conform.json" >> ~/.gitignore_global
Enter fullscreen mode Exit fullscreen mode

Plugin config

Here’s the conform.nvim configuration that automatically reads .conform.json from the git root if it exists:

return {
    "stevearc/conform.nvim",
    config = function()
        local conform = require("conform")

        -- Constant path to the config file (relative to git root)
        local config_filename = ".conform.json"

        -- Default formatters if no config file present
        local defaults = {
            formatters = {
                lua = { "stylua" },
                python = { "isort", "yapf" },
                json = { "prettier" },
                yaml = { "prettier" },
                markdown = { "prettier" },
            },
            args = {},
        }

        --- Load configuration from `.conform.json` in the git repository root
        --- Example `.conform.json`:
        -- {
        --   "formatters_by_ft": {
        --     "python": ["isort", "yapf"],
        --     "json": ["prettier"],
        --     "yaml": ["prettier"]
        --   },
        --   "args": {
        --     "yapf": ["--style", "{based_on_style: pep8, indent_width: 4}"],
        --     "isort": ["--profile", "open_stack", "--line-length", "79"]
        --   }
        -- }

        local load_config = function(bufnr)
            local path = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ":h")
            local git_root = vim.fn.systemlist(
                "git -C " .. vim.fn.fnameescape(path) .. " rev-parse --show-toplevel"
            )[1]

            if vim.v.shell_error ~= 0 or not git_root then
                vim.notify("No git root found, using defaults", vim.log.levels.DEBUG)
                return nil
            end

            local cfg_path = git_root .. "/" .. config_filename
            if vim.fn.filereadable(cfg_path) == 0 then
                vim.notify("No " .. config_filename .. " found, using defaults", vim.log.levels.DEBUG)
                return nil
            end

            local ok_read, content = pcall(vim.fn.readfile, cfg_path)
            if not ok_read or not content then
                vim.notify("Failed to read " .. cfg_path, vim.log.levels.ERROR)
                return nil
            end

            local ok_parse, parsed = pcall(vim.fn.json_decode, table.concat(content, "\n"))
            if not ok_parse then
                vim.notify("Invalid JSON in " .. cfg_path, vim.log.levels.ERROR)
                return nil
            end

            return {
                formatters = parsed.formatters_by_ft or {},
                args = parsed.args or {},
            }
        end

        local orig_format = conform.format

        conform.format = function(opts)
            local bufnr = opts.buf or vim.api.nvim_get_current_buf()
            local cfg = load_config(bufnr) or defaults
            vim.notify("Using conform config: " .. vim.inspect(cfg), vim.log.levels.DEBUG)

            conform.formatters_by_ft = cfg.formatters

            for name, arg_list in pairs(cfg.args) do
                conform.formatters[name] = conform.formatters[name] or {}
                conform.formatters[name].prepend_args = function()
                    return arg_list
                end
            end

            orig_format(opts)
        end

        vim.keymap.set({ "n", "v" }, "<leader>cf", function()
            conform.format({
                lsp_fallback = true,
                async = false,
                timeout_ms = 5000,
            })
        end, { desc = "Format current file with conform" })
    end,
}
Enter fullscreen mode Exit fullscreen mode

Why this is useful

This small setup makes conform.nvim much more flexible: each project can define its own formatter set and options, without touching your main neovim config.

Link to my config

You can check out my dotfiles here: https://github.com/art-vasilyev/.dotfiles

Top comments (0)