diff options
author | Steven Arcangeli <stevearc@stevearc.com> | 2023-10-08 13:10:24 -0700 |
---|---|---|
committer | Steven Arcangeli <stevearc@stevearc.com> | 2023-10-08 16:58:44 -0700 |
commit | a94f686986631d5b97bd75b3877813c39de55c47 (patch) | |
tree | a0c975e5799bb2b9c05ace69b7dbe8025a431837 /lua/conform | |
parent | e75819642c36810a55a7235b6b5e16a5ce896ed3 (diff) |
feat: errors do not stop formatting early
Diffstat (limited to 'lua/conform')
-rw-r--r-- | lua/conform/errors.lua | 63 | ||||
-rw-r--r-- | lua/conform/formatters/injected.lua | 27 | ||||
-rw-r--r-- | lua/conform/init.lua | 8 | ||||
-rw-r--r-- | lua/conform/runner.lua | 140 |
4 files changed, 129 insertions, 109 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 |