conform.nvim

repository·master·Indexed 26 days ago

https://github.com/stevearc/conform.nvim

A lightweight Neovim plugin for code formatting that preserves extmarks and folds. It supports sequential formatter execution, LSP fallback, range formatting, and format-on-save. It utilizes minimal diffs via Neovim's xdiff bindings to apply changes and includes an 'injected' formatter for embedded code blocks using Treesitter.

Tokens
8.8K
Snippets
23
Records
34
Agent score
89%

What's inside conform.nvim

  1. Understand minimal format diffs

    master

    Unlike many formatting plugins that replace the entire buffer with the new content, conform.nvim uses Neovim's xdiff bindings (:help vim.text.diff) to calculate minimal chunks of changes.

    These chunks are converted into LSP TextEdit objects and applied via vim.lsp.util.apply_text_edits(). This approach provides several benefits:

    • Preservation of extmarks.
    • Better preservation of cursor position, folds, and viewport position.
    • More seamless integration with the LSP formatting experience.
  2. Enable format on save

    master

    You can enable automatic formatting on save by adding the format_on_save option to your setup() call. This is a shortcut that sets up a BufWritePre autocmd for you. You can pass options like timeout_ms and lsp_format which will be passed directly to the format() function.

    require("conform").setup({
      format_on_save = {
        -- These options will be passed to conform.format()
        timeout_ms = 500,
        lsp_format = "fallback",
      },
    })
  3. Lazy load conform.nvim with lazy.nvim

    master

    The recommended configuration for using conform.nvim with the lazy.nvim plugin manager. It sets up event-based loading on BufWritePre, defines keymaps, and provides a structured opts table for formatters and default options.

    return {
      "stevearc/conform.nvim",
      event = { "BufWritePre" },
      cmd = { "ConformInfo" },
      keys = {
        {
          -- Customize or remove this keymap to your liking
          "<leader>f",
          function()
            require("conform").format({ async = true })
          end,
          mode = "",
          desc = "Format buffer",
        },
      },
      -- This will provide type hinting with LuaLS
      ---@module "conform"
      ---@type conform.setupOpts
      opts = {
        -- Define your formatters
        formatters_by_ft = {
          lua = { "stylua" },
          python = { "isort", "black" },
          javascript = { "prettierd", "prettier", stop_after_first = true },
        },
        -- Set default options
        default_format_opts = {
          lsp_format = "fallback",
        },
        -- Set up format-on-save
        format_on_save = { timeout_ms = 500 },
        -- Customize formatters
        formatters = {
          shfmt = {
            append_args = { "-i", "2" },
          },
        },
      },
      init = function()
        -- If you want the formatexpr, here is the place to set it
        vim.o.formatexpr = "v:lua.require'conform'.formatexpr()"
      end,
    }
  4. Configure complex autoformat logic

    master

    Instead of a simple boolean, you can pass a function to format_on_save or format_after_save. This function is called during BufWritePre (for format_on_save) or BufWritePost (for format_after_save). The function can return a table of options or return nothing to disable formatting based on filetype, file path, or custom variables.

    -- if format_on_save is a function, it will be called during BufWritePre
    require("conform").setup({
      format_on_save = function(bufnr)
        -- Disable autoformat on certain filetypes
        local ignore_filetypes = { "sql", "java" }
        if vim.tbl_contains(ignore_filetypes, vim.bo[bufnr].filetype) then
          return
        end
        -- Disable with a global or buffer-local variable
        if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then
          return
        end
        -- Disable autoformat for files in a certain path
        local bufname = vim.api.nvim_buf_get_name(bufnr)
        if bufname:match("/node_modules/") then
          return
        end
        -- ...additional logic...
        return { timeout_ms = 500, lsp_format = "fallback" }
      end,
    })
    
    -- There is a similar affordance for format_after_save, which uses BufWritePost.
    -- This is good for formatters that are too slow to run synchronously.
    require("conform").setup({
      format_after_save = function(bufnr)
        if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then
          return
        end
        -- ...additional logic...
        return { lsp_format = "fallback" }
      end,
    })
  5. Use :ConformInfo to diagnose formatter issues

    master

    Run the :ConformInfo command to open a diagnostic window. This window provides critical information for troubleshooting:

    • Log file location: A snippet of the current log and a path to the full log file (use gf on the path to open it).
    • Formatter status: Shows if a formatter is ready, its error messages, the filetypes it applies to, and the resolved path to its executable.
    • Configuration: Lists all configured formatters and available formatters for the current buffer.

    Use this to verify that your formatter's executable path is correct and that it is assigned to the expected filetype.

  6. Run LSP commands before formatting

    master

    To perform LSP actions (like organizing imports) before running conform, use an autocmd on BufWritePre. This example demonstrates using ts_ls to execute the _typescript.organizeImports command before calling conform.format.

    vim.api.nvim_create_autocmd("BufWritePre", {
      desc = "Format before save",
      pattern = "*",
      group = vim.api.nvim_create_augroup("FormatConfig", { clear = true }),
      callback = function(ev)
        local conform_opts = { bufnr = ev.buf, lsp_format = "fallback", timeout_ms = 2000 }
        local client = vim.lsp.get_clients({ name = "ts_ls", bufnr = ev.buf })[1]
    
        if not client then
          require("conform").format(conform_opts)
          return
        end
    
        local request_result = client:request_sync("workspace/executeCommand", {
          command = "_typescript.organizeImports",
          arguments = { vim.api.nvim_buf_get_name(ev.buf) },
        })
    
        if request_result and request_result.err then
          vim.notify(request_result.err.message, vim.log.levels.ERROR)
          return
        end
    
        require("conform").format(conform_opts)
      end,
    })
  7. Format injected language code blocks

    master

    To format embedded code chunks (e.g., code blocks within Markdown or Neorg files), use the injected formatter.

    When the injected formatter is run, it uses Treesitter to identify blocks with different languages and executes the corresponding formatters configured in formatters_by_ft for each block. These formatters run in parallel, with one job per language block.

    Note: This feature is experimental and its configuration options are subject to change.

  8. Format specific ranges of text

    master

    If a formatter does not natively support range formatting, conform.nvim can attempt to 'fake it' as a best-effort operation. When a range is requested, conform.nvim formats the entire buffer but discards diffs that fall outside the selected range.

    Warning: This is not guaranteed to be correct or error-free. Formatting might exceed the range if the diff covering the range is large, or results may be semantically incorrect if the formatter requires changes in multiple locations to maintain correctness.

  9. Test formatters manually in the shell

    master

    To isolate whether a problem lies with the formatter or conform.nvim, extract the command from your debug logs and run it directly in your terminal.

    1. Identify the command and CWD: Look for Run command: { ... } and Run default CWD: ... in your logs.
    2. For stdin/out formatters: Use cat to pipe the file content into the command. Example: cat path/to/file.py | black --stdin-filename path/to/file.py --quiet -
    3. For non-stdin formatters: Run the command directly on the file path. Example: black --quiet path/to/file.py

    Important: You must cd into the directory specified in Run default CWD before running the command to ensure the environment matches what conform uses.

  10. Toggle format-on-save with commands

    master

    You can implement a toggle mechanism for autoformatting by using global (vim.g) or buffer-local (vim.b) variables within a format_on_save function, and then creating user commands to flip those variables.

    require("conform").setup({
      format_on_save = function(bufnr)
        -- Disable with a global or buffer-local variable
        if vim.g.disable_autoformat or vim.b[bufnr].disable_autoformat then
          return
        end
        return { timeout_ms = 500, lsp_format = "fallback" }
      end,
    })
    
    vim.api.nvim_create_user_command("FormatDisable", function(args)
      if args.bang then
        -- FormatDisable! will disable formatting just for this buffer
        vim.b.disable_autoformat = true
      else
        vim.g.disable_autoformat = true
      end
    end, {
      desc = "Disable autoformat-on-save",
      bang = true,
    })
    vim.api.nvim_create_user_command("FormatEnable", function()
      vim.b.disable_autoformat = false
      vim.g.disable_autoformat = false
    end, {
      desc = "Re-enable autoformat-on-save",
    })
  11. Set formatexpr for conform.nvim

    master

    To allow conform.nvim to work with tools that rely on the formatexpr (like certain LSP-based formatting workflows), set the Neovim option formatexpr to use conform's implementation.

    vim.o.formatexpr = "v:lua.require'conform'.formatexpr()"