Skip to content

Commit

Permalink
feat: syntax for using first available formatter
Browse files Browse the repository at this point in the history
Alternations are now supported. You can specify a sub-list in place of a
formatter name and conform will use the first formatter in that list
that is available. For example, this will use either prettierd or
prettier (whichever is available), and then always trim whitespace
afterwards:

conform.format(formatters = { { "prettierd", "prettier" }, "trim_whitespace" })

This syntax is available both in the formatters_by_ft config option and
in the `formatters` argument of the `format` method.
  • Loading branch information
stevearc committed Sep 8, 2023
1 parent bd1aa02 commit 2568d74
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 89 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,10 @@ At a minimum, you will need to set up some formatters by filetype
require("conform").setup({
formatters_by_ft = {
lua = { "stylua" },
-- Conform will run multiple formatters sequentially
-- Conform will run multiple formatters sequentially
python = { "isort", "black" },
-- Use a sub-list to run only the first available formatter
javascript = { { "prettierd", "prettier" } },
},
})
```
Expand Down Expand Up @@ -210,6 +212,8 @@ require("conform").setup({
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" } },
},
-- If this is set, Conform will run the formatter on save.
-- It will pass the table to conform.format().
Expand Down Expand Up @@ -384,10 +388,10 @@ List information about all filetype-configured formatters
`get_formatter_info(formatter, bufnr): conform.FormatterInfo` \
Get information about a formatter (including availability)

| Param | Type | Desc |
| --------- | -------------- | ---- |
| formatter | `string` | |
| bufnr | `nil\|integer` | |
| Param | Type | Desc |
| --------- | -------------- | ------------------------- |
| formatter | `string` | The name of the formatter |
| bufnr | `nil\|integer` | |
<!-- /API -->

## Acknowledgements
Expand Down
4 changes: 3 additions & 1 deletion doc/conform.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ OPTIONS *conform-option
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" } },
},
-- If this is set, Conform will run the formatter on save.
-- It will pass the table to conform.format().
Expand Down Expand Up @@ -118,7 +120,7 @@ get_formatter_info({formatter}, {bufnr}): conform.FormatterInfo *conform.get_for
Get information about a formatter (including availability)

Parameters:
{formatter} `string`
{formatter} `string` The name of the formatter
{bufnr} `nil|integer`

--------------------------------------------------------------------------------
Expand Down
92 changes: 65 additions & 27 deletions lua/conform/health.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,24 @@ local function get_formatter_filetypes(name)
local conform = require("conform")
local filetypes = {}
for filetype, formatters in pairs(conform.formatters_by_ft) do
-- support the old structure where formatters could be a subkey
if not vim.tbl_islist(formatters) then
---@diagnostic disable-next-line: undefined-field
formatters = formatters.formatters
end
if vim.tbl_contains(formatters, name) then
table.insert(filetypes, filetype)

for _, ft_name in ipairs(formatters) do
if type(ft_name) == "string" then
if ft_name == name then
table.insert(filetypes, filetype)
break
end
else
if vim.tbl_contains(ft_name, name) then
table.insert(filetypes, filetype)
break
end
end
end
end
return filetypes
Expand Down Expand Up @@ -38,6 +51,22 @@ M.check = function()
end
end

---@param formatters conform.FormatterUnit[]
---@return string[]
local function flatten_formatters(formatters)
local flat = {}
for _, name in ipairs(formatters) do
if type(name) == "string" then
table.insert(flat, name)
else
for _, f in ipairs(flatten_formatters(name)) do
table.insert(flat, f)
end
end
end
return flat
end

M.show_window = function()
local conform = require("conform")
local lines = {}
Expand All @@ -60,35 +89,43 @@ M.show_window = function()
end
table.insert(lines, "")

---@param formatters conform.FormatterInfo[]
---@param formatter conform.FormatterInfo
local function append_formatter_info(formatter)
if not formatter.available then
local line = string.format("%s unavailable: %s", formatter.name, formatter.available_msg)
table.insert(lines, line)
table.insert(
highlights,
{ "DiagnosticWarn", #lines, formatter.name:len(), formatter.name:len() + 12 }
)
else
local filetypes = get_formatter_filetypes(formatter.name)
local line = string.format("%s ready (%s)", formatter.name, table.concat(filetypes, ", "))
table.insert(lines, line)
table.insert(
highlights,
{ "DiagnosticInfo", #lines, formatter.name:len(), formatter.name:len() + 6 }
)
end
end

local seen = {}
---@param formatters string[]
local function append_formatters(formatters)
for _, formatter in ipairs(formatters) do
if not formatter.available then
local line = string.format("%s unavailable: %s", formatter.name, formatter.available_msg)
table.insert(lines, line)
table.insert(
highlights,
{ "DiagnosticWarn", #lines, formatter.name:len(), formatter.name:len() + 12 }
)
for _, name in ipairs(formatters) do
if type(name) == "table" then
append_formatters(name)
else
local filetypes = get_formatter_filetypes(formatter.name)
local line = string.format("%s ready (%s)", formatter.name, table.concat(filetypes, ", "))
table.insert(lines, line)
table.insert(
highlights,
{ "DiagnosticInfo", #lines, formatter.name:len(), formatter.name:len() + 6 }
)
seen[name] = true
local formatter = conform.get_formatter_info(name)
append_formatter_info(formatter)
end
end
end

table.insert(lines, "Formatters for this buffer:")
table.insert(highlights, { "Title", #lines, 0, -1 })
local seen = {}
local buf_formatters = conform.list_formatters_for_buffer()
for _, formatter in ipairs(buf_formatters) do
seen[formatter.name] = true
end
local buf_formatters = flatten_formatters(conform.list_formatters_for_buffer())
append_formatters(buf_formatters)
if vim.tbl_isempty(buf_formatters) then
table.insert(lines, "<none>")
Expand All @@ -97,10 +134,11 @@ M.show_window = function()
table.insert(lines, "")
table.insert(lines, "Other formatters:")
table.insert(highlights, { "Title", #lines, 0, -1 })
local all_formatters = vim.tbl_filter(function(f)
return not seen[f.name]
end, conform.list_all_formatters())
append_formatters(all_formatters)
for _, formatter in ipairs(conform.list_all_formatters()) do
if not seen[formatter.name] then
append_formatter_info(formatter)
end
end

local bufnr = vim.api.nvim_create_buf(false, true)
local winid = vim.api.nvim_open_win(bufnr, true, {
Expand Down
130 changes: 74 additions & 56 deletions lua/conform/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,33 @@ end

---@private
---@param bufnr? integer
---@return conform.FormatterInfo[]
---@return conform.FormatterUnit[]
M.list_formatters_for_buffer = function(bufnr)
if not bufnr or bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
local formatters = {}
local seen = {}
local filetypes = vim.split(vim.bo[bufnr].filetype, ".", { plain = true })

local function dedupe_formatters(names, collect)
for _, name in ipairs(names) do
if type(name) == "table" then
local alternation = {}
dedupe_formatters(name, alternation)
if not vim.tbl_isempty(alternation) then
table.insert(collect, alternation)
end
elseif not seen[name] then
table.insert(collect, name)
seen[name] = true
end
end
end

table.insert(filetypes, "*")
for _, filetype in ipairs(filetypes) do
---@type conform.FormatterUnit[]
local ft_formatters = M.formatters_by_ft[filetype]
if ft_formatters then
-- support the old structure where formatters could be a subkey
Expand All @@ -117,35 +134,11 @@ M.list_formatters_for_buffer = function(bufnr)
ft_formatters = ft_formatters.formatters
end

for _, formatter in ipairs(ft_formatters) do
if not seen[formatter] then
table.insert(formatters, formatter)
seen[formatter] = true
end
end
dedupe_formatters(ft_formatters, formatters)
end
end

---@type conform.FormatterInfo[]
local all_info = vim.tbl_map(function(f)
return M.get_formatter_info(f, bufnr)
end, formatters)

return all_info
end

---@param formatters conform.FormatterInfo[]
---@return conform.FormatterInfo[]
local function filter_formatters(formatters)
---@type conform.FormatterInfo[]
local all_info = {}
for _, info in ipairs(formatters) do
if info.available then
table.insert(all_info, info)
end
end

return all_info
return formatters
end

---@param bufnr integer
Expand Down Expand Up @@ -179,6 +172,41 @@ local function range_from_selection(bufnr, mode)
}
end

---@param names conform.FormatterUnit[]
---@param bufnr integer
---@param warn_on_missing boolean
---@return conform.FormatterInfo[]
local function resolve_formatters(names, bufnr, warn_on_missing)
local all_info = {}
local function add_info(info, warn)
if info.available then
table.insert(all_info, info)
elseif warn then
vim.notify(
string.format("Formatter '%s' unavailable: %s", info.name, info.available_msg),
vim.log.levels.WARN
)
end
return info.available
end

for _, name in ipairs(names) do
if type(name) == "string" then
local info = M.get_formatter_info(name, bufnr)
add_info(info, warn_on_missing)
else
-- If this is an alternation, take the first one that's available
for i, v in ipairs(name) do
local info = M.get_formatter_info(v, bufnr)
if add_info(info, i == #name) then
break
end
end
end
end
return all_info
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.
Expand All @@ -204,34 +232,15 @@ M.format = function(opts, callback)
local lsp_format = require("conform.lsp_format")
local runner = require("conform.runner")

local formatters = {}
local any_formatters_configured
if opts.formatters then
any_formatters_configured = true
for _, formatter in ipairs(opts.formatters) do
local info = M.get_formatter_info(formatter)
if info.available then
table.insert(formatters, info)
else
if opts.quiet then
log.warn("Formatter '%s' unavailable: %s", info.name, info.available_msg)
else
vim.notify(
string.format("Formatter '%s' unavailable: %s", info.name, info.available_msg),
vim.log.levels.WARN
)
end
end
end
else
formatters = M.list_formatters_for_buffer(opts.bufnr)
any_formatters_configured = not vim.tbl_isempty(formatters)
formatters = filter_formatters(formatters)
end
local formatter_names = vim.tbl_map(function(f)
local formatter_names = opts.formatters or M.list_formatters_for_buffer(opts.bufnr)
local any_formatters_configured = formatter_names ~= nil and not vim.tbl_isempty(formatter_names)
local formatters =
resolve_formatters(formatter_names, opts.bufnr, not opts.quiet and opts.formatters ~= nil)

local resolved_names = vim.tbl_map(function(f)
return f.name
end, formatters)
log.debug("Running formatters on %s: %s", vim.api.nvim_buf_get_name(opts.bufnr), formatter_names)
log.debug("Running formatters on %s: %s", vim.api.nvim_buf_get_name(opts.bufnr), resolved_names)

local any_formatters = not vim.tbl_isempty(formatters)
if any_formatters then
Expand Down Expand Up @@ -287,8 +296,11 @@ end
---@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 = M.list_formatters_for_buffer(bufnr)
return filter_formatters(formatters)
return resolve_formatters(formatters, bufnr, false)
end

---List information about all filetype-configured formatters
Expand All @@ -303,7 +315,13 @@ M.list_all_formatters = function()
end

for _, formatter in ipairs(ft_formatters) do
formatters[formatter] = true
if type(formatter) == "table" then
for _, v in ipairs(formatter) do
formatters[v] = true
end
else
formatters[formatter] = true
end
end
end

Expand Down Expand Up @@ -348,7 +366,7 @@ M.get_formatter_config = function(formatter, bufnr)
end

---Get information about a formatter (including availability)
---@param formatter string
---@param formatter string The name of the formatter
---@param bufnr? integer
---@return conform.FormatterInfo
M.get_formatter_info = function(formatter, bufnr)
Expand Down
2 changes: 2 additions & 0 deletions scripts/options_doc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ require("conform").setup({
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" } },
},
-- If this is set, Conform will run the formatter on save.
-- It will pass the table to conform.format().
Expand Down

0 comments on commit 2568d74

Please sign in to comment.