aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteven Arcangeli <506791+stevearc@users.noreply.github.com>2023-10-15 16:18:08 -0700
committerGitHub <noreply@github.com>2023-10-15 16:18:08 -0700
commit7027ebbd772e2d3593f7dd566dea06d2d20622ee (patch)
treeb78bbe24dda9cd7e1272bce6c699b5e576d76a66
parent9b5fbddfca5080c6961dabafb3f0a6ef7e2fc18a (diff)
feat!: merge configs in conform.formatters with defaults (#140)
This breaking change should make it significantly easier to modify formatters. While I expect 99% of configs to be backwards-compatible, this can still potentially cause problems. If you: * define a formatter in the `formatters` option * that has the same name as a built-in formatter * and omits a property from the original formatter (e.g. leaves out `range_args` or `cwd`) Then you may encounter breaking behavior from this commit, because now your config definition will be merged with the built-in definition, and so will inherit those omitted properties. This config merging behavior can be opted-out of by adding `inherit = false` to your formatter config.
-rw-r--r--README.md189
-rw-r--r--doc/conform.txt17
-rw-r--r--doc/recipes.md78
-rw-r--r--lua/conform/formatters/injected.lua11
-rw-r--r--lua/conform/init.lua46
-rw-r--r--lua/conform/util.lua11
-rw-r--r--scripts/options_doc.lua17
7 files changed, 213 insertions, 156 deletions
diff --git a/README.md b/README.md
index 7a8c60c..f3283ee 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,10 @@ Lightweight yet powerful formatter plugin for Neovim
- [Installation](#installation)
- [Setup](#setup)
- [Formatters](#formatters)
-- [Options](#options)
+- [Customizing formatters](#customizing-formatters)
- [Recipes](#recipes)
- [Advanced topics](#advanced-topics)
+- [Options](#options)
- [API](#api)
- [format(opts, callback)](#formatopts-callback)
- [list_formatters(bufnr)](#list_formattersbufnr)
@@ -30,9 +31,9 @@ Lightweight yet powerful formatter plugin for Neovim
- **Preserves extmarks and folds** - Most formatters replace the entire buffer, which clobbers extmarks and folds, and can cause the viewport and cursor to jump unexpectedly. Conform calculates minimal diffs and applies them using the built-in LSP format utilities.
- **Fixes bad-behaving LSP formatters** - Some LSP servers are lazy and simply replace the entire buffer, leading to the problems mentioned above. Conform hooks into the LSP handler and turns these responses into proper piecewise changes.
-- **Enables range formatting for all formatters** - Since conform calculates minimal diffs, it can perform range formatting even if the underlying formatter doesn't support it.
+- **Enables range formatting for all formatters** - Since conform calculates minimal diffs, it can perform range formatting [even if the underlying formatter doesn't support it.](doc/advanced_topics.md#range-formatting)
- **Simple API** - Conform exposes a simple, imperative API modeled after `vim.lsp.buf.format()`.
-- **Formats embedded code blocks** - Use the `injected` formatter to format code blocks e.g. in markdown files.
+- **Formats embedded code blocks** - Can format code blocks inside markdown files or similar (see [injected language formatting](doc/advanced_topics.md#injected-language-formatting-code-blocks))
## Installation
@@ -48,17 +49,21 @@ conform.nvim supports all the usual plugin managers
}
```
+For a more thorough configuration involving lazy-loading, see [Lazy loading with lazy.nvim](doc/recipes.md#lazy-loading-with-lazynvim).
+
</details>
<details>
<summary>Packer</summary>
```lua
-require('packer').startup(function()
- use {
- 'stevearc/conform.nvim',
- config = function() require('conform').setup() end
- }
+require("packer").startup(function()
+ use({
+ "stevearc/conform.nvim",
+ config = function()
+ require("conform").setup()
+ end,
+ })
end)
```
@@ -68,9 +73,9 @@ end)
<summary>Paq</summary>
```lua
-require "paq" {
- {'stevearc/conform.nvim'};
-}
+require("paq")({
+ { "stevearc/conform.nvim" },
+})
```
</details>
@@ -118,13 +123,13 @@ At a minimum, you will need to set up some formatters by filetype
```lua
require("conform").setup({
- formatters_by_ft = {
- lua = { "stylua" },
- -- Conform will run multiple formatters sequentially
- python = { "isort", "black" },
- -- Use a sub-list to run only the first available formatter
- javascript = { { "prettierd", "prettier" } },
- },
+ formatters_by_ft = {
+ lua = { "stylua" },
+ -- Conform will run multiple formatters sequentially
+ python = { "isort", "black" },
+ -- Use a sub-list to run only the first available formatter
+ javascript = { { "prettierd", "prettier" } },
+ },
})
```
@@ -132,10 +137,10 @@ Then you can use `conform.format()` just like you would `vim.lsp.buf.format()`.
```lua
vim.api.nvim_create_autocmd("BufWritePre", {
- pattern = "*",
- callback = function(args)
- require("conform").format({ bufnr = args.buf })
- end,
+ pattern = "*",
+ callback = function(args)
+ require("conform").format({ bufnr = args.buf })
+ end,
})
```
@@ -143,11 +148,11 @@ As a shortcut, conform will optionally set up this format-on-save autocmd for yo
```lua
require("conform").setup({
- format_on_save = {
- -- These options will be passed to conform.format()
- timeout_ms = 500,
- lsp_fallback = true,
- },
+ format_on_save = {
+ -- These options will be passed to conform.format()
+ timeout_ms = 500,
+ lsp_fallback = true,
+ },
})
```
@@ -165,6 +170,9 @@ To view configured and available formatters, as well as to see the log file, run
You can view this list in vim with `:help conform-formatters`
+<details>
+ <summary>Expand to see all formatters</summary>
+
<!-- FORMATTERS -->
- [alejandra](https://kamadorueda.com/alejandra/) - The Uncompromising Nix Code Formatter.
@@ -255,6 +263,92 @@ You can view this list in vim with `:help conform-formatters`
- [zigfmt](https://github.com/ziglang/zig) - Reformat Zig source into canonical form.
<!-- /FORMATTERS -->
+</details>
+
+## Customizing formatters
+
+You can override/add to the default values of formatters
+
+```lua
+require("conform").setup({
+ formatters = {
+ yamlfix = {
+ -- Change where to find the command
+ command = "local/path/yamlfix",
+ -- Adds environment args to the yamlfix formatter
+ env = {
+ YAMLFIX_SEQUENCE_STYLE = "block_style",
+ },
+ },
+ },
+})
+
+-- These can also be set directly
+require("conform").formatters.yamlfix = {
+ env = {
+ YAMLFIX_SEQUENCE_STYLE = "block_style",
+ },
+}
+
+-- This can also be a function that returns the config,
+-- which can be useful if you're doing lazy loading
+require("conform").formatters.yamlfix = function(bufnr)
+ return {
+ command = require("conform.util").find_executable({
+ "local/path/yamlfix",
+ }, "yamlfix"),
+ }
+end
+```
+
+In addition to being able to override any of the original properties on the formatter, there is another property for easily adding additional arguments to the format command
+
+```lua
+require("conform").formatters.shfmt = {
+ prepend_args = { "-i", "2" },
+ -- The base args are { "-filename", "$FILENAME" } so the final args will be
+ -- { "-i", "2", "-filename", "$FILENAME" }
+}
+-- prepend_args can be a function, just like args
+require("conform").formatters.shfmt = {
+ prepend_args = function(ctx)
+ return { "-i", "2" }
+ end,
+}
+```
+
+If you want to overwrite the entire formatter definition and _not_ merge with the default values, pass `inherit = false`. This is also the default behavior if there is no built-in formatter with the given name, which can be used to add your own custom formatters.
+
+```lua
+require("conform").formatters.shfmt = {
+ inherit = false,
+ command = "shfmt",
+ args = { "-i", "2", "-filename", "$FILENAME" },
+}
+```
+
+## Recipes
+
+<!-- RECIPES -->
+
+- [Format command](doc/recipes.md#format-command)
+- [Autoformat with extra features](doc/recipes.md#autoformat-with-extra-features)
+- [Command to toggle format-on-save](doc/recipes.md#command-to-toggle-format-on-save)
+- [Automatically run slow formatters async](doc/recipes.md#automatically-run-slow-formatters-async)
+- [Lazy loading with lazy.nvim](doc/recipes.md#lazy-loading-with-lazynvim)
+
+<!-- /RECIPES -->
+
+## Advanced topics
+
+<!-- ADVANCED -->
+
+- [Minimal format diffs](doc/advanced_topics.md#minimal-format-diffs)
+- [Range formatting](doc/advanced_topics.md#range-formatting)
+- [Injected language formatting (code blocks)](doc/advanced_topics.md#injected-language-formatting-code-blocks)
+
+<!-- /ADVANCED -->
+
## Options
A complete list of all configuration options
@@ -294,14 +388,14 @@ require("conform").setup({
log_level = vim.log.levels.ERROR,
-- Conform will notify you when a formatter errors
notify_on_error = true,
- -- Define custom formatters here
+ -- Custom formatters and changes to built-in formatters
formatters = {
my_formatter = {
- -- This can be a string or a function that returns a string
+ -- This can be a string or a function that returns a string.
+ -- When defining a new formatter, this is the only field that is *required*
command = "my_cmd",
- -- OPTIONAL - all fields below this are optional
-- A list of strings, or a function that returns a list of strings
- -- Return a single string instead to run the command in a shell
+ -- Return a single string instead of a list to run the command in a shell
args = { "--stdin-from-filename", "$FILENAME" },
-- If the formatter supports range formatting, create the range arguments here
range_args = function(ctx)
@@ -319,15 +413,20 @@ require("conform").setup({
condition = function(ctx)
return vim.fs.basename(ctx.filename) ~= "README.md"
end,
- -- Exit codes that indicate success (default {0})
+ -- Exit codes that indicate success (default { 0 })
exit_codes = { 0, 1 },
-- Environment variables. This can also be a function that returns a table.
env = {
VAR = "value",
},
+ -- Set to false to disable merging the config with the base definition
+ inherit = true,
+ -- When inherit = true, add these additional arguments to the command.
+ -- This can also be a function, like args
+ prepend_args = { "--use-tabs" },
},
-- These can also be a function that returns the formatter
- other_formatter = function()
+ other_formatter = function(bufnr)
return {
command = "my_cmd",
}
@@ -344,30 +443,6 @@ require("conform").formatters.my_formatter = {
<!-- /OPTIONS -->
-## Recipes
-
-<!-- RECIPES -->
-
-- [Format command](doc/recipes.md#format-command)
-- [Customizing formatters](doc/recipes.md#customizing-formatters)
-- [Autoformat with extra features](doc/recipes.md#autoformat-with-extra-features)
-- [Command to toggle format-on-save](doc/recipes.md#command-to-toggle-format-on-save)
-- [Automatically run slow formatters async](doc/recipes.md#automatically-run-slow-formatters-async)
-- [Add extra arguments to a formatter command](doc/recipes.md#add-extra-arguments-to-a-formatter-command)
-- [Lazy loading with lazy.nvim](doc/recipes.md#lazy-loading-with-lazynvim)
-
-<!-- /RECIPES -->
-
-## Advanced topics
-
-<!-- ADVANCED -->
-
-- [Minimal format diffs](doc/advanced_topics.md#minimal-format-diffs)
-- [Range formatting](doc/advanced_topics.md#range-formatting)
-- [Injected language formatting (code blocks)](doc/advanced_topics.md#injected-language-formatting-code-blocks)
-
-<!-- /ADVANCED -->
-
## API
<!-- API -->
diff --git a/doc/conform.txt b/doc/conform.txt
index c2fe9f0..c123d11 100644
--- a/doc/conform.txt
+++ b/doc/conform.txt
@@ -44,14 +44,14 @@ OPTIONS *conform-option
log_level = vim.log.levels.ERROR,
-- Conform will notify you when a formatter errors
notify_on_error = true,
- -- Define custom formatters here
+ -- Custom formatters and changes to built-in formatters
formatters = {
my_formatter = {
- -- This can be a string or a function that returns a string
+ -- This can be a string or a function that returns a string.
+ -- When defining a new formatter, this is the only field that is *required*
command = "my_cmd",
- -- OPTIONAL - all fields below this are optional
-- A list of strings, or a function that returns a list of strings
- -- Return a single string instead to run the command in a shell
+ -- Return a single string instead of a list to run the command in a shell
args = { "--stdin-from-filename", "$FILENAME" },
-- If the formatter supports range formatting, create the range arguments here
range_args = function(ctx)
@@ -69,15 +69,20 @@ OPTIONS *conform-option
condition = function(ctx)
return vim.fs.basename(ctx.filename) ~= "README.md"
end,
- -- Exit codes that indicate success (default {0})
+ -- Exit codes that indicate success (default { 0 })
exit_codes = { 0, 1 },
-- Environment variables. This can also be a function that returns a table.
env = {
VAR = "value",
},
+ -- Set to false to disable merging the config with the base definition
+ inherit = true,
+ -- When inherit = true, add these additional arguments to the command.
+ -- This can also be a function, like args
+ prepend_args = { "--use-tabs" },
},
-- These can also be a function that returns the formatter
- other_formatter = function()
+ other_formatter = function(bufnr)
return {
command = "my_cmd",
}
diff --git a/doc/recipes.md b/doc/recipes.md
index 2d33ca2..97eaa16 100644
--- a/doc/recipes.md
+++ b/doc/recipes.md
@@ -3,11 +3,9 @@
<!-- TOC -->
- [Format command](#format-command)
-- [Customizing formatters](#customizing-formatters)
- [Autoformat with extra features](#autoformat-with-extra-features)
- [Command to toggle format-on-save](#command-to-toggle-format-on-save)
- [Automatically run slow formatters async](#automatically-run-slow-formatters-async)
-- [Add extra arguments to a formatter command](#add-extra-arguments-to-a-formatter-command)
- [Lazy loading with lazy.nvim](#lazy-loading-with-lazynvim)
<!-- /TOC -->
@@ -30,37 +28,6 @@ vim.api.nvim_create_user_command("Format", function(args)
end, { range = true })
```
-## Customizing formatters
-
-If you want to customize how a formatter runs (for example, to pass in environment variables or
-change the command arguments), you can either edit the formatter directly or create one yourself.
-
-```lua
--- Directly change the values on the built-in configuration
-require("conform.formatters.yamlfix").env = {
- YAMLFIX_SEQUENCE_STYLE = "block_style",
-}
-
--- Or create your own formatter that overrides certain values
-require("conform").formatters.yamlfix =
- vim.tbl_deep_extend("force", require("conform.formatters.yamlfix"), {
- env = {
- YAMLFIX_SEQUENCE_STYLE = "block_style",
- },
- })
-
--- Here is an example that modifies the command arguments for prettier to add
--- a custom config file, if it is present
-require("conform.formatters.prettier").args = function(ctx)
- local args = { "--stdin-filepath", "$FILENAME" }
- local found = vim.fs.find(".custom-config.json", { upward = true, path = ctx.dirname })[1]
- if found then
- vim.list_extend(args, { "--config", found })
- end
- return args
-end
-```
-
## Autoformat with extra features
If you want more complex logic than the basic `format_on_save` option allows, you can use a function instead.
@@ -168,36 +135,6 @@ require("conform").setup({
})
```
-## Add extra arguments to a formatter command
-
-The official recommended way to change the arguments of a formatter is to just copy the default
-values and mutate them however you like. For example:
-
-```lua
-require("conform.formatters.shfmt").args = { "-i", "4", "-filename", "$FILENAME" }
-```
-
-But if you really want to _add_ arguments instead of replacing them, there is a utility function to
-make this easier:
-
-```lua
-local util = require("conform.util")
-local prettier = require("conform.formatters.prettier")
-require("conform").formatters.prettier = vim.tbl_deep_extend("force", prettier, {
- args = util.extend_args(prettier.args, { "--tab", "--indent", "2" }),
- range_args = util.extend_args(prettier.range_args, { "--tab", "--indent", "2" }),
-})
-
--- Pass append=true to append the extra arguments to the end
-local deno_fmt = require("conform.formatters.deno_fmt")
-require("conform").formatters.deno_fmt = vim.tbl_deep_extend("force", deno_fmt, {
- args = util.extend_args(deno_fmt.args, { "--use-tabs" }, { append = true }),
-})
-
--- There is also a utility to modify a formatter in-place
-util.add_formatter_args(require("conform.formatters.prettier"), { "--tab", "--indent", "2" })
-```
-
## Lazy loading with lazy.nvim
Here is the recommended config for lazy-loading using lazy.nvim
@@ -220,23 +157,24 @@ return {
},
-- Everything in opts will be passed to setup()
opts = {
+ -- Define your formatters
formatters_by_ft = {
lua = { "stylua" },
python = { "isort", "black" },
javascript = { { "prettierd", "prettier" } },
},
+ -- Set up format-on-save
format_on_save = { timeout_ms = 500, lsp_fallback = true },
+ -- Customize formatters
+ formatters = {
+ shfmt = {
+ prepend_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,
-
- -- This function is optional, but if you want to customize formatters do it here
- config = function(_, opts)
- local util = require("conform.util")
- util.add_formatter_args(require("conform.formatters.shfmt"), { "-i", "2" })
- require("conform").setup(opts)
- end,
}
```
diff --git a/lua/conform/formatters/injected.lua b/lua/conform/formatters/injected.lua
index 71005bc..21a9830 100644
--- a/lua/conform/formatters/injected.lua
+++ b/lua/conform/formatters/injected.lua
@@ -60,15 +60,10 @@ local function apply_indent(lines, indentation)
end
end
----@class conform.InjectedFormatterConfig : conform.FileLuaFormatterConfig
----@field format fun(self: conform.InjectedFormatterConfig, ctx: conform.Context, lines: string[], callback: fun(err: nil|string, new_lines: nil|string[]))
----@field condition? fun(self: conform.InjectedFormatterConfig, ctx: conform.Context): boolean
----@field options conform.InjectedFormatterOptions
-
---@class (exact) conform.InjectedFormatterOptions
---@field ignore_errors boolean
----@type conform.InjectedFormatterConfig
+---@type conform.FileLuaFormatterConfig
return {
meta = {
url = "doc/advanced_topics.md#injected-language-formatting-code-blocks",
@@ -94,6 +89,8 @@ return {
callback("No treesitter parser for buffer")
return
end
+ ---@type conform.InjectedFormatterOptions
+ local options = self.options
--- Disable diagnostic to pass the typecheck github action
--- This is available on nightly, but not on stable
--- Stable doesn't have any parameters, so it's safe to always pass `true`
@@ -134,7 +131,7 @@ return {
i = i + 1
end
end
- if self.options.ignore_errors then
+ if options.ignore_errors then
format_error = nil
end
end
diff --git a/lua/conform/init.lua b/lua/conform/init.lua
index 7a3cae6..724aad6 100644
--- a/lua/conform/init.lua
+++ b/lua/conform/init.lua
@@ -21,6 +21,7 @@ local M = {}
---@class (exact) conform.LuaFormatterConfig
---@field format fun(self: conform.LuaFormatterConfig, ctx: conform.Context, lines: string[], callback: fun(err: nil|string, new_lines: nil|string[]))
---@field condition? fun(self: conform.LuaFormatterConfig, ctx: conform.Context): boolean
+---@field options? table
---@class (exact) conform.FileLuaFormatterConfig : conform.LuaFormatterConfig
---@field meta conform.FormatterMeta
@@ -30,6 +31,12 @@ local M = {}
---@alias conform.FormatterConfig conform.JobFormatterConfig|conform.LuaFormatterConfig
+---@class (exact) conform.FormatterConfigOverride : conform.JobFormatterConfig
+---@field inherit? boolean
+---@field command? string|fun(ctx: conform.Context): string
+---@field prepend_args? string|string[]|fun(ctx: conform.Context): string|string[]
+---@field options? table
+
---@class (exact) conform.FormatterMeta
---@field url string
---@field description string
@@ -53,7 +60,7 @@ local M = {}
---@type table<string, conform.FormatterUnit[]>
M.formatters_by_ft = {}
----@type table<string, conform.FormatterConfig|fun(bufnr: integer): nil|conform.FormatterConfig>
+---@type table<string, conform.FormatterConfigOverride|fun(bufnr: integer): nil|conform.FormatterConfigOverride>
M.formatters = {}
M.notify_on_error = true
@@ -538,20 +545,39 @@ M.get_formatter_config = function(formatter, bufnr)
if not bufnr or bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
- ---@type nil|conform.FormatterConfig|fun(bufnr: integer): nil|conform.FormatterConfig
- local config = M.formatters[formatter]
- if type(config) == "function" then
- config = config(bufnr)
+ ---@type nil|conform.FormatterConfigOverride|fun(bufnr: integer): nil|conform.FormatterConfigOverride
+ local override = M.formatters[formatter]
+ if type(override) == "function" then
+ override = override(bufnr)
end
- if not config then
- local ok
- ok, config = pcall(require, "conform.formatters." .. formatter)
- if not ok then
+
+ ---@type nil|conform.FormatterConfig
+ local config = override
+ if not override or override.inherit ~= false then
+ local ok, mod_config = pcall(require, "conform.formatters." .. formatter)
+ if ok then
+ if override then
+ config = require("conform.util").merge_formatter_configs(mod_config, override)
+ else
+ config = mod_config
+ end
+ elseif override then
+ if override.command then
+ config = override
+ else
+ local msg = string.format(
+ "Formatter '%s' missing built-in definition\nSet `command` to get rid of this error.",
+ formatter
+ )
+ vim.notify_once(msg, vim.log.levels.ERROR)
+ return nil
+ end
+ else
return nil
end
end
- if config.stdin == nil then
+ if config and config.stdin == nil then
config.stdin = true
end
return config
diff --git a/lua/conform/util.lua b/lua/conform/util.lua
index 8a0073a..bbb711a 100644
--- a/lua/conform/util.lua
+++ b/lua/conform/util.lua
@@ -156,6 +156,17 @@ M.add_formatter_args = function(formatter, extra_args, opts)
end
end
+---@param config conform.FormatterConfig
+---@param override conform.FormatterConfigOverride
+---@return conform.FormatterConfig
+M.merge_formatter_configs = function(config, override)
+ local ret = vim.tbl_deep_extend("force", config, override)
+ if override.prepend_args then
+ M.add_formatter_args(ret, override.prepend_args, { append = false })
+ end
+ return ret
+end
+
---@param bufnr integer
---@return integer
M.buf_get_changedtick = function(bufnr)
diff --git a/scripts/options_doc.lua b/scripts/options_doc.lua
index b68614e..6f850ef 100644
--- a/scripts/options_doc.lua
+++ b/scripts/options_doc.lua
@@ -30,14 +30,14 @@ require("conform").setup({
log_level = vim.log.levels.ERROR,
-- Conform will notify you when a formatter errors
notify_on_error = true,
- -- Define custom formatters here
+ -- Custom formatters and changes to built-in formatters
formatters = {
my_formatter = {
- -- This can be a string or a function that returns a string
+ -- This can be a string or a function that returns a string.
+ -- When defining a new formatter, this is the only field that is *required*
command = "my_cmd",
- -- OPTIONAL - all fields below this are optional
-- A list of strings, or a function that returns a list of strings
- -- Return a single string instead to run the command in a shell
+ -- Return a single string instead of a list to run the command in a shell
args = { "--stdin-from-filename", "$FILENAME" },
-- If the formatter supports range formatting, create the range arguments here
range_args = function(ctx)
@@ -55,15 +55,20 @@ require("conform").setup({
condition = function(ctx)
return vim.fs.basename(ctx.filename) ~= "README.md"
end,
- -- Exit codes that indicate success (default {0})
+ -- Exit codes that indicate success (default { 0 })
exit_codes = { 0, 1 },
-- Environment variables. This can also be a function that returns a table.
env = {
VAR = "value",
},
+ -- Set to false to disable merging the config with the base definition
+ inherit = true,
+ -- When inherit = true, add these additional arguments to the command.
+ -- This can also be a function, like args
+ prepend_args = { "--use-tabs" },
},
-- These can also be a function that returns the formatter
- other_formatter = function()
+ other_formatter = function(bufnr)
return {
command = "my_cmd",
}