aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lua/conform/errors.lua63
-rw-r--r--lua/conform/formatters/injected.lua27
-rw-r--r--lua/conform/init.lua8
-rw-r--r--lua/conform/runner.lua140
-rw-r--r--tests/runner_spec.lua12
5 files changed, 139 insertions, 111 deletions
diff --git a/lua/conform/errors.lua b/lua/conform/errors.lua
new file mode 100644
index 0000000..3a56b80
--- /dev/null
+++ b/lua/conform/errors.lua
@@ -0,0 +1,63 @@
+local M = {}
+
+---@class conform.Error
+---@field code conform.ERROR_CODE
+---@field message string
+---@field debounce_message? boolean
+
+---@enum conform.ERROR_CODE
+M.ERROR_CODE = {
+ -- Command was passed invalid arguments
+ INVALID_ARGS = 1,
+ -- Command was not executable
+ NOT_EXECUTABLE = 2,
+ -- Command timed out during execution
+ TIMEOUT = 3,
+ -- Command was pre-empted by another call to format
+ INTERRUPTED = 4,
+ -- Command produced an error during execution
+ RUNTIME = 5,
+ -- Asynchronous formatter results were discarded due to a concurrent modification
+ CONCURRENT_MODIFICATION = 6,
+}
+
+---@param code conform.ERROR_CODE
+---@return integer
+M.level_for_code = function(code)
+ if code == M.ERROR_CODE.CONCURRENT_MODIFICATION then
+ return vim.log.levels.INFO
+ elseif code == M.ERROR_CODE.TIMEOUT or code == M.ERROR_CODE.INTERRUPTED then
+ return vim.log.levels.WARN
+ else
+ return vim.log.levels.ERROR
+ end
+end
+
+---Returns true if the error occurred while attempting to run the formatter
+---@param code conform.ERROR_CODE
+---@return boolean
+M.is_execution_error = function(code)
+ return code == M.ERROR_CODE.RUNTIME
+ or code == M.ERROR_CODE.NOT_EXECUTABLE
+ or code == M.ERROR_CODE.INVALID_ARGS
+end
+
+---@param err1? conform.Error
+---@param err2? conform.Error
+---@return nil|conform.Error
+M.coalesce = function(err1, err2)
+ if not err1 then
+ return err2
+ elseif not err2 then
+ return err1
+ end
+ local level1 = M.level_for_code(err1.code)
+ local level2 = M.level_for_code(err2.code)
+ if level2 > level1 then
+ return err2
+ else
+ return err1
+ end
+end
+
+return M
diff --git a/lua/conform/formatters/injected.lua b/lua/conform/formatters/injected.lua
index b9d3acd..8140550 100644
--- a/lua/conform/formatters/injected.lua
+++ b/lua/conform/formatters/injected.lua
@@ -84,6 +84,7 @@ return {
end,
format = function(self, ctx, lines, callback)
local conform = require("conform")
+ local errors = require("conform.errors")
local log = require("conform.log")
local util = require("conform.util")
local text = table.concat(lines, "\n")
@@ -124,19 +125,17 @@ return {
local function apply_format_results()
if format_error then
- if self.options.ignore_errors then
- -- Find all of the conform errors in the replacements table and remove them
- local i = 1
- while i <= #replacements do
- if replacements[i].code then
- table.remove(replacements, i)
- else
- i = i + 1
- end
+ -- Find all of the conform errors in the replacements table and remove them
+ local i = 1
+ while i <= #replacements do
+ if replacements[i].code then
+ table.remove(replacements, i)
+ else
+ i = i + 1
end
- else
- callback(format_error)
- return
+ end
+ if self.options.ignore_errors then
+ format_error = nil
end
end
@@ -150,13 +149,13 @@ return {
table.insert(formatted_lines, start_lnum, new_lines[i])
end
end
- callback(nil, formatted_lines)
+ callback(format_error, formatted_lines)
end
local num_format = 0
local formatter_cb = function(err, idx, start_lnum, end_lnum, new_lines)
if err then
- format_error = err
+ format_error = errors.coalesce(format_error, err)
replacements[idx] = err
else
replacements[idx] = { start_lnum, end_lnum, new_lines }
diff --git a/lua/conform/init.lua b/lua/conform/init.lua
index fe6f934..18fc83c 100644
--- a/lua/conform/init.lua
+++ b/lua/conform/init.lua
@@ -353,6 +353,7 @@ M.format = function(opts, callback)
opts.range = range_from_selection(opts.bufnr, mode)
end
callback = callback or function(_err) end
+ local errors = require("conform.errors")
local log = require("conform.log")
local lsp_format = require("conform.lsp_format")
local runner = require("conform.runner")
@@ -377,12 +378,12 @@ M.format = function(opts, callback)
---@param err? conform.Error
local function handle_err(err)
if err then
- local level = runner.level_for_code(err.code)
+ local level = errors.level_for_code(err.code)
log.log(level, err.message)
local should_notify = not opts.quiet and level >= vim.log.levels.WARN
-- Execution errors have special handling. Maybe should reconsider this.
local notify_msg = err.message
- if runner.is_execution_error(err.code) then
+ if errors.is_execution_error(err.code) then
should_notify = should_notify and M.notify_on_error and not err.debounce_message
notify_msg = "Formatter failed. See :ConformInfo for details"
end
@@ -449,6 +450,7 @@ M.format_lines = function(formatter_names, lines, opts, callback)
quiet = false,
})
callback = callback or function(_err, _lines) end
+ local errors = require("conform.errors")
local log = require("conform.log")
local runner = require("conform.runner")
local formatters = resolve_formatters(formatter_names, opts.bufnr, not opts.quiet)
@@ -461,7 +463,7 @@ M.format_lines = function(formatter_names, lines, opts, callback)
---@param new_lines? string[]
local function handle_err(err, new_lines)
if err then
- local level = runner.level_for_code(err.code)
+ local level = errors.level_for_code(err.code)
log.log(level, err.message)
end
callback(err, new_lines)
diff --git a/lua/conform/runner.lua b/lua/conform/runner.lua
index ef96e42..5c98159 100644
--- a/lua/conform/runner.lua
+++ b/lua/conform/runner.lua
@@ -1,3 +1,4 @@
+local errors = require("conform.errors")
local fs = require("conform.fs")
local log = require("conform.log")
local util = require("conform.util")
@@ -7,48 +8,6 @@ local M = {}
---@class (exact) conform.RunOpts
---@field exclusive boolean If true, ensure only a single formatter is running per buffer
----@class conform.Error
----@field code conform.ERROR_CODE
----@field message string
----@field debounce_message? boolean
-
----@enum conform.ERROR_CODE
-M.ERROR_CODE = {
- -- Command was passed invalid arguments
- INVALID_ARGS = 1,
- -- Command was not executable
- NOT_EXECUTABLE = 2,
- -- Command timed out during execution
- TIMEOUT = 3,
- -- Command was pre-empted by another call to format
- INTERRUPTED = 4,
- -- Command produced an error during execution
- RUNTIME = 5,
- -- Asynchronous formatter results were discarded due to a concurrent modification
- CONCURRENT_MODIFICATION = 6,
-}
-
----@param code conform.ERROR_CODE
----@return integer
-M.level_for_code = function(code)
- if code == M.ERROR_CODE.CONCURRENT_MODIFICATION then
- return vim.log.levels.INFO
- elseif code == M.ERROR_CODE.TIMEOUT or code == M.ERROR_CODE.INTERRUPTED then
- return vim.log.levels.WARN
- else
- return vim.log.levels.ERROR
- end
-end
-
----Returns true if the error occurred while attempting to run the formatter
----@param code conform.ERROR_CODE
----@return boolean
-M.is_execution_error = function(code)
- return code == M.ERROR_CODE.RUNTIME
- or code == M.ERROR_CODE.NOT_EXECUTABLE
- or code == M.ERROR_CODE.INVALID_ARGS
-end
-
---@param ctx conform.Context
---@param config conform.JobFormatterConfig
---@return string|string[]
@@ -263,12 +222,22 @@ local last_run_errored = {}
local function run_formatter(bufnr, formatter, config, ctx, input_lines, opts, callback)
log.info("Run %s on %s", formatter.name, vim.api.nvim_buf_get_name(bufnr))
log.trace("Input lines: %s", input_lines)
+ callback = util.wrap_callback(callback, function(err)
+ if err then
+ if last_run_errored[formatter.name] then
+ err.debounce_message = true
+ end
+ last_run_errored[formatter.name] = true
+ else
+ last_run_errored[formatter.name] = false
+ end
+ end)
if config.format then
---@cast config conform.LuaFormatterConfig
local ok, err = pcall(config.format, config, ctx, input_lines, callback)
if not ok then
callback({
- code = M.ERROR_CODE.RUNTIME,
+ code = errors.ERROR_CODE.RUNTIME,
message = string.format("Formatter '%s' error: %s", formatter.name, err),
})
end
@@ -284,16 +253,6 @@ local function run_formatter(bufnr, formatter, config, ctx, input_lines, opts, c
if type(env) == "function" then
env = env(ctx)
end
- callback = util.wrap_callback(callback, function(err)
- if err then
- if last_run_errored[formatter.name] then
- err.debounce_message = true
- end
- last_run_errored[formatter.name] = true
- else
- last_run_errored[formatter.name] = false
- end
- end)
local buffer_text
-- If the buffer has a newline at the end, make sure we include that in the input to the formatter
@@ -381,12 +340,12 @@ local function run_formatter(bufnr, formatter, config, ctx, input_lines, opts, c
and opts.exclusive
then
callback({
- code = M.ERROR_CODE.INTERRUPTED,
+ code = errors.ERROR_CODE.INTERRUPTED,
message = string.format("Formatter '%s' was interrupted", formatter.name),
})
else
callback({
- code = M.ERROR_CODE.RUNTIME,
+ code = errors.ERROR_CODE.RUNTIME,
message = string.format("Formatter '%s' error: %s", formatter.name, err_str),
})
end
@@ -395,12 +354,12 @@ local function run_formatter(bufnr, formatter, config, ctx, input_lines, opts, c
})
if jid == 0 then
callback({
- code = M.ERROR_CODE.INVALID_ARGS,
+ code = errors.ERROR_CODE.INVALID_ARGS,
message = string.format("Formatter '%s' invalid arguments", formatter.name),
})
elseif jid == -1 then
callback({
- code = M.ERROR_CODE.NOT_EXECUTABLE,
+ code = errors.ERROR_CODE.NOT_EXECUTABLE,
message = string.format("Formatter '%s' command is not executable", formatter.name),
})
elseif config.stdin then
@@ -481,23 +440,19 @@ M.format_async = function(bufnr, formatters, range, opts, callback)
original_lines,
opts,
function(err, output_lines, all_support_range_formatting)
- if err then
- return callback(err)
- end
- assert(output_lines)
-- discard formatting if buffer has changed
if not vim.api.nvim_buf_is_valid(bufnr) or changedtick ~= util.buf_get_changedtick(bufnr) then
- callback({
- code = M.ERROR_CODE.CONCURRENT_MODIFICATION,
+ err = {
+ code = errors.ERROR_CODE.CONCURRENT_MODIFICATION,
message = string.format(
"Async formatter discarding changes for %s: concurrent modification",
vim.api.nvim_buf_get_name(bufnr)
),
- })
+ }
else
M.apply_format(bufnr, original_lines, output_lines, range, not all_support_range_formatting)
- callback()
end
+ callback(err)
end
)
end
@@ -507,18 +462,19 @@ end
---@param range? conform.Range
---@param input_lines string[]
---@param opts conform.RunOpts
----@param callback fun(err?: conform.Error, output_lines?: string[], all_support_range_formatting?: boolean)
+---@param callback fun(err?: conform.Error, output_lines: string[], all_support_range_formatting: boolean)
M.format_lines_async = function(bufnr, formatters, range, input_lines, opts, callback)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
local idx = 1
local all_support_range_formatting = true
+ local final_err = nil
local function run_next_formatter()
local formatter = formatters[idx]
if not formatter then
- callback(nil, input_lines, all_support_range_formatting)
+ callback(final_err, input_lines, all_support_range_formatting)
return
end
idx = idx + 1
@@ -527,9 +483,9 @@ M.format_lines_async = function(bufnr, formatters, range, input_lines, opts, cal
local ctx = M.build_context(bufnr, config, range)
run_formatter(bufnr, formatter, config, ctx, input_lines, opts, function(err, output)
if err then
- return callback(err)
+ final_err = errors.coalesce(final_err, err)
end
- input_lines = output
+ input_lines = output or input_lines
run_next_formatter()
end)
all_support_range_formatting = all_support_range_formatting and config.range_args ~= nil
@@ -559,12 +515,9 @@ M.format_sync = function(bufnr, formatters, timeout_ms, range, opts)
local err, final_result, all_support_range_formatting =
M.format_lines_sync(bufnr, formatters, timeout_ms, range, original_lines, opts)
- if err then
- return err
- end
- assert(final_result)
M.apply_format(bufnr, original_lines, final_result, range, not all_support_range_formatting)
+ return err
end
---@param bufnr integer
@@ -573,8 +526,8 @@ end
---@param range? conform.Range
---@param opts conform.RunOpts
---@return conform.Error? error
----@return string[]? output_lines
----@return boolean? all_support_range_formatting
+---@return string[] output_lines
+---@return boolean all_support_range_formatting
M.format_lines_sync = function(bufnr, formatters, timeout_ms, range, input_lines, opts)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
@@ -582,17 +535,20 @@ M.format_lines_sync = function(bufnr, formatters, timeout_ms, range, input_lines
local start = uv.hrtime() / 1e6
local all_support_range_formatting = true
+ local final_err = nil
for _, formatter in ipairs(formatters) do
local remaining = timeout_ms - (uv.hrtime() / 1e6 - start)
if remaining <= 0 then
- return {
- code = M.ERROR_CODE.TIMEOUT,
+ return errors.coalesce(final_err, {
+ code = errors.ERROR_CODE.TIMEOUT,
message = string.format("Formatter '%s' timeout", formatter.name),
- }
+ }),
+ input_lines,
+ all_support_range_formatting
end
local done = false
local result = nil
- local run_err = nil
+ ---@type conform.FormatterConfig
local config = assert(require("conform").get_formatter_config(formatter.name, bufnr))
local ctx = M.build_context(bufnr, config, range)
local jid = run_formatter(
@@ -603,7 +559,7 @@ M.format_lines_sync = function(bufnr, formatters, timeout_ms, range, input_lines
input_lines,
opts,
function(err, output)
- run_err = err
+ final_err = errors.coalesce(final_err, err)
done = true
result = output
end
@@ -619,26 +575,26 @@ M.format_lines_sync = function(bufnr, formatters, timeout_ms, range, input_lines
vim.fn.jobstop(jid)
end
if wait_reason == -1 then
- return {
- code = M.ERROR_CODE.TIMEOUT,
+ return errors.coalesce(final_err, {
+ code = errors.ERROR_CODE.TIMEOUT,
message = string.format("Formatter '%s' timeout", formatter.name),
- }
+ }),
+ input_lines,
+ all_support_range_formatting
else
- return {
- code = M.ERROR_CODE.INTERRUPTED,
+ return errors.coalesce(final_err, {
+ code = errors.ERROR_CODE.INTERRUPTED,
message = string.format("Formatter '%s' was interrupted", formatter.name),
- }
+ }),
+ input_lines,
+ all_support_range_formatting
end
end
- if not result then
- return run_err
- end
-
- input_lines = result
+ input_lines = result or input_lines
end
- return nil, input_lines, all_support_range_formatting
+ return final_err, input_lines, all_support_range_formatting
end
return M
diff --git a/tests/runner_spec.lua b/tests/runner_spec.lua
index 56cf8e2..b1885f8 100644
--- a/tests/runner_spec.lua
+++ b/tests/runner_spec.lua
@@ -150,7 +150,6 @@ describe("runner", function()
describe("e2e", function()
before_each(function()
conform.formatters.test = {
- meta = { url = "", description = "" },
command = "tests/fake_formatter.sh",
}
end)
@@ -167,7 +166,7 @@ describe("runner", function()
vim.bo[bufnr].modified = false
local expected_lines = vim.split(expected, "\n", { plain = true })
test_util.set_formatter_output(expected_lines)
- conform.format(vim.tbl_extend("force", opts or {}, { formatters = { "test" }, quiet = true }))
+ conform.format(vim.tbl_extend("keep", opts or {}, { formatters = { "test" }, quiet = true }))
return expected_lines
end
@@ -287,6 +286,15 @@ print("a")
assert.are.same({ "goodbye" }, lines)
end)
+ it("formats file even if one formatter errors", function()
+ conform.formatters.test2 = {
+ command = "tests/fake_formatter.sh",
+ args = { "--fail" },
+ }
+ local lines = run_formatter("hello", "goodbye", { formatters = { "test2", "test" } })
+ assert.are.same(lines, vim.api.nvim_buf_get_lines(0, 0, -1, false))
+ end)
+
describe("range formatting", function()
it("applies edits that overlap the range start", function()
run_formatter(