aboutsummaryrefslogtreecommitdiffstats
path: root/lua/conform/init.lua
diff options
context:
space:
mode:
authorSteven Arcangeli <stevearc@stevearc.com>2023-08-25 11:15:12 -0700
committerSteven Arcangeli <stevearc@stevearc.com>2023-08-25 11:43:47 -0700
commiteb5987e9dd40ce1e27c9c07e41d09571f1bd876e (patch)
treeb4cffe35e0893272cedc0ecf0229d08be343d70e /lua/conform/init.lua
parent100fd00d40423af85c4c7efcf875f8e4ee329f50 (diff)
feat: first working version
Diffstat (limited to 'lua/conform/init.lua')
-rw-r--r--lua/conform/init.lua291
1 files changed, 291 insertions, 0 deletions
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<string, string[]|conform.FormatterList>
+M.formatters_by_ft = {}
+
+---@type table<string, conform.FormatterConfig|fun(): conform.FormatterConfig>
+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