diff --git a/lua/guard/format.lua b/lua/guard/format.lua index b6713a2..6f47a3f 100644 --- a/lua/guard/format.lua +++ b/lua/guard/format.lua @@ -1,9 +1,7 @@ local async = require('guard._async') local util = require('guard.util') local filetype = require('guard.filetype') -local api = vim.api - -local M = {} +local api, iter, filter = vim.api, vim.iter, vim.tbl_filter local function save_views(bufnr) local views = {} @@ -21,107 +19,42 @@ local function restore_views(views) end end -local function update_buffer(bufnr, prev_lines, new_lines, srow, erow, old_indent) - if not new_lines or #new_lines == 0 then +local function update_buffer(bufnr, prev_content, new_content, srow, erow, old_indent) + if not new_content or #new_content == 0 then return end - local views = save_views(bufnr) - new_lines = vim.split(new_lines, '\r?\n') - if new_lines[#new_lines] == '' then - new_lines[#new_lines] = nil - end + -- Always update if content changed (compare strings directly) + if prev_content ~= new_content then + local views = save_views(bufnr) + + local new_lines = vim.split(new_content, '\r?\n') + if new_lines[#new_lines] == '' then + new_lines[#new_lines] = nil + end - if not vim.deep_equal(new_lines, prev_lines) then api.nvim_buf_set_lines(bufnr, srow, erow, false, new_lines) + if util.getopt('save_on_fmt') then api.nvim_command('silent! noautocmd write!') end + if old_indent then vim.cmd(('silent %d,%dleft'):format(srow + 1, erow)) end + restore_views(views) end end -local function emit_event(status, data) - util.doau('GuardFmt', vim.tbl_extend('force', { status = status }, data or {})) -end - local function fail(msg) - emit_event('failed', { msg = msg }) + util.doau('GuardFmt', { + status = 'failed', + msg = msg, + }) vim.notify('[Guard]: ' .. msg, vim.log.levels.WARN) end ----Apply a single pure formatter ----@async ----@param buf number ----@param range table? ----@param config table ----@param fname string ----@param cwd string ----@param input string ----@return string? output ----@return string? error_msg -local function apply_pure_formatter(buf, range, config, fname, cwd, input) - -- Eval dynamic args - local cfg = vim.tbl_extend('force', {}, config) - if type(cfg.args) == 'function' then - cfg.args = cfg.args(buf) - end - - if cfg.fn then - return cfg.fn(buf, range, input), nil - end - - local result = async.await(1, function(callback) - local handle = vim.system(util.get_cmd(cfg, fname, buf), { - stdin = true, - cwd = cwd, - env = cfg.env, - timeout = cfg.timeout, - }, callback) - handle:write(input) - handle:write(nil) - end) - - if result.code ~= 0 and #result.stderr > 0 then - return nil, ('%s exited with code %d\n%s'):format(cfg.cmd, result.code, result.stderr) - end - - return result.stdout, nil -end - ----Apply a single impure formatter ----@async ----@param buf number ----@param config table ----@param fname string ----@param cwd string ----@return string? error_msg -local function apply_impure_formatter(buf, config, fname, cwd) - -- Eval dynamic args - local cfg = vim.tbl_extend('force', {}, config) - if type(cfg.args) == 'function' then - cfg.args = cfg.args(buf) - end - - local result = async.await(1, function(callback) - vim.system(util.get_cmd(cfg, fname, buf), { - text = true, - cwd = cwd, - env = cfg.env or {}, - timeout = cfg.timeout, - }, callback) - end) - - if result.code ~= 0 and #result.stderr > 0 then - return ('%s exited with code %d\n%s'):format(cfg.cmd, result.code, result.stderr) - end - - return nil -end - local function do_fmt(buf) buf = buf or api.nvim_get_current_buf() local ft_conf = filetype[vim.bo[buf].filetype] @@ -131,7 +64,6 @@ local function do_fmt(buf) return end - -- Get format range local srow, erow = 0, -1 local range = nil local mode = api.nvim_get_mode().mode @@ -141,70 +73,116 @@ local function do_fmt(buf) erow = range['end'][1] end - local old_indent = (mode == 'V') and vim.fn.indent(srow + 1) or nil + local old_indent + if mode == 'V' then + old_indent = vim.fn.indent(srow + 1) + end - -- Get and filter configs local fmt_configs = util.eval(ft_conf.formatter) local fname, cwd = util.buf_get_info(buf) - fmt_configs = vim.tbl_filter(function(config) + fmt_configs = filter(function(config) return util.should_run(config, buf) end, fmt_configs) - -- Check executability - for _, config in ipairs(fmt_configs) do + local all_executable = not iter(fmt_configs):any(function(config) if config.cmd and vim.fn.executable(config.cmd) ~= 1 then util.report_error(config.cmd .. ' not executable') - return + return true end + return false + end) + + if not all_executable then + return end - -- Classify formatters - local pure = vim.tbl_filter(function(config) + local pure = filter(function(config) return config.fn or (config.cmd and config.stdin) end, fmt_configs) - local impure = vim.tbl_filter(function(config) + local impure = filter(function(config) return config.cmd and not config.stdin end, fmt_configs) - -- Check range formatting compatibility if range and #impure > 0 then - local impure_cmds = vim.tbl_map(function(c) - return c.cmd - end, impure) util.report_error('Cannot apply range formatting for filetype ' .. vim.bo[buf].filetype) + local impure_cmds = {} + for _, config in ipairs(impure) do + table.insert(impure_cmds, config.cmd) + end util.report_error(table.concat(impure_cmds, ', ') .. ' does not support reading from stdin') return end - emit_event('pending', { using = fmt_configs }) + util.doau('GuardFmt', { + status = 'pending', + using = fmt_configs, + }) + + local prev_lines = api.nvim_buf_get_lines(buf, srow, erow, false) + local prev_content = table.concat(prev_lines, '\n') + local new_lines = prev_content + local errno = nil async.run(function() - -- Initialize changedtick BEFORE any formatting (explicitly wait) + -- Explicitly wait for changedtick initialization local changedtick = async.await(1, function(callback) vim.schedule(function() callback(api.nvim_buf_get_changedtick(buf)) end) end) - local prev_lines = api.nvim_buf_get_lines(buf, srow, erow, false) - local new_lines = table.concat(prev_lines, '\n') - -- Apply pure formatters sequentially for _, config in ipairs(pure) do - local output, err = apply_pure_formatter(buf, range, config, fname, cwd, new_lines) - if err then - fail(err) - return + if errno then + break + end + + -- Eval dynamic args + local cfg = vim.tbl_extend('force', {}, config) + if type(cfg.args) == 'function' then + cfg.args = cfg.args(buf) + end + + if cfg.fn then + new_lines = cfg.fn(buf, range, new_lines) + else + local result = async.await(1, function(callback) + local handle = vim.system(util.get_cmd(cfg, fname, buf), { + stdin = true, + cwd = cwd, + env = cfg.env, + timeout = cfg.timeout, + }, callback) + handle:write(new_lines) + handle:write(nil) + end) + + if result.code ~= 0 and #result.stderr > 0 then + errno = { + cmd = cfg.cmd, + code = result.code, + stderr = result.stderr, + reason = cfg.cmd .. ' exited with errors', + } + else + new_lines = result.stdout + end end - new_lines = output end + -- Wait for schedule and update buffer async.await(1, function(callback) vim.schedule(function() - if not api.nvim_buf_is_valid(buf) then - fail('buffer no longer valid') + if errno then + if errno.reason:match('exited with errors$') then + fail(('%s exited with code %d\n%s'):format(errno.cmd, errno.code, errno.stderr)) + elseif errno.reason == 'buffer changed' then + fail('buffer changed during formatting') + else + fail(errno.reason) + end callback() return end @@ -215,35 +193,73 @@ local function do_fmt(buf) return end - update_buffer(buf, prev_lines, new_lines, srow, erow, old_indent) + if not api.nvim_buf_is_valid(buf) then + fail('buffer no longer valid') + callback() + return + end + + update_buffer(buf, prev_content, new_lines, srow, erow, old_indent) callback() end) end) + -- Stop if there was an error + if errno then + return + end + -- Apply impure formatters sequentially + local impure_error = nil for _, config in ipairs(impure) do - local err = apply_impure_formatter(buf, config, fname, cwd) - if err then - fail(err) - return + -- Eval dynamic args + local cfg = vim.tbl_extend('force', {}, config) + if type(cfg.args) == 'function' then + cfg.args = cfg.args(buf) + end + + local result = async.await(1, function(callback) + vim.system(util.get_cmd(cfg, fname, buf), { + text = true, + cwd = cwd, + env = cfg.env or {}, + }, callback) + end) + + if result.code ~= 0 and #result.stderr > 0 then + impure_error = { + cmd = cfg.cmd, + code = result.code, + stderr = result.stderr, + } + break end end - -- Refresh buffer if impure formatters were used + if impure_error then + fail( + ('%s exited with code %d\n%s'):format( + impure_error.cmd, + impure_error.code, + impure_error.stderr + ) + ) + return + end + if #impure > 0 then - async.await(1, function(callback) - vim.schedule(function() - api.nvim_buf_call(buf, function() - local views = save_views(buf) - api.nvim_command('silent! edit!') - restore_views(views) - end) - callback() + vim.schedule(function() + api.nvim_buf_call(buf, function() + local views = save_views(buf) + api.nvim_command('silent! edit!') + restore_views(views) end) end) end - emit_event('done') + util.doau('GuardFmt', { + status = 'done', + }) if util.getopt('refresh_diagnostic') then vim.diagnostic.show() @@ -251,6 +267,6 @@ local function do_fmt(buf) end) end -M.do_fmt = do_fmt - -return M +return { + do_fmt = do_fmt, +}