Skip to content

Commit 0e206c8

Browse files
committed
fix: improve async format module
1 parent 03fe793 commit 0e206c8

File tree

1 file changed

+145
-129
lines changed

1 file changed

+145
-129
lines changed

lua/guard/format.lua

Lines changed: 145 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
local async = require('guard._async')
22
local util = require('guard.util')
33
local filetype = require('guard.filetype')
4-
local api = vim.api
5-
6-
local M = {}
4+
local api, iter, filter = vim.api, vim.iter, vim.tbl_filter
75

86
local function save_views(bufnr)
97
local views = {}
@@ -21,107 +19,42 @@ local function restore_views(views)
2119
end
2220
end
2321

24-
local function update_buffer(bufnr, prev_lines, new_lines, srow, erow, old_indent)
25-
if not new_lines or #new_lines == 0 then
22+
local function update_buffer(bufnr, prev_content, new_content, srow, erow, old_indent)
23+
if not new_content or #new_content == 0 then
2624
return
2725
end
2826

29-
local views = save_views(bufnr)
30-
new_lines = vim.split(new_lines, '\r?\n')
31-
if new_lines[#new_lines] == '' then
32-
new_lines[#new_lines] = nil
33-
end
27+
-- Always update if content changed (compare strings directly)
28+
if prev_content ~= new_content then
29+
local views = save_views(bufnr)
30+
31+
local new_lines = vim.split(new_content, '\r?\n')
32+
if new_lines[#new_lines] == '' then
33+
new_lines[#new_lines] = nil
34+
end
3435

35-
if not vim.deep_equal(new_lines, prev_lines) then
3636
api.nvim_buf_set_lines(bufnr, srow, erow, false, new_lines)
37+
3738
if util.getopt('save_on_fmt') then
3839
api.nvim_command('silent! noautocmd write!')
3940
end
41+
4042
if old_indent then
4143
vim.cmd(('silent %d,%dleft'):format(srow + 1, erow))
4244
end
45+
4346
restore_views(views)
4447
end
4548
end
4649

47-
local function emit_event(status, data)
48-
util.doau('GuardFmt', vim.tbl_extend('force', { status = status }, data or {}))
49-
end
50-
5150
local function fail(msg)
52-
emit_event('failed', { msg = msg })
51+
util.doau('GuardFmt', {
52+
status = 'failed',
53+
msg = msg,
54+
})
5355
vim.notify('[Guard]: ' .. msg, vim.log.levels.WARN)
5456
end
5557

56-
---Apply a single pure formatter
57-
---@async
58-
---@param buf number
59-
---@param range table?
60-
---@param config table
61-
---@param fname string
62-
---@param cwd string
63-
---@param input string
64-
---@return string? output
65-
---@return string? error_msg
66-
local function apply_pure_formatter(buf, range, config, fname, cwd, input)
67-
-- Eval dynamic args
68-
local cfg = vim.tbl_extend('force', {}, config)
69-
if type(cfg.args) == 'function' then
70-
cfg.args = cfg.args(buf)
71-
end
72-
73-
if cfg.fn then
74-
return cfg.fn(buf, range, input), nil
75-
end
76-
77-
local result = async.await(1, function(callback)
78-
local handle = vim.system(util.get_cmd(cfg, fname, buf), {
79-
stdin = true,
80-
cwd = cwd,
81-
env = cfg.env,
82-
timeout = cfg.timeout,
83-
}, callback)
84-
handle:write(input)
85-
handle:write(nil)
86-
end)
87-
88-
if result.code ~= 0 and #result.stderr > 0 then
89-
return nil, ('%s exited with code %d\n%s'):format(cfg.cmd, result.code, result.stderr)
90-
end
91-
92-
return result.stdout, nil
93-
end
94-
95-
---Apply a single impure formatter
96-
---@async
97-
---@param buf number
98-
---@param config table
99-
---@param fname string
100-
---@param cwd string
101-
---@return string? error_msg
102-
local function apply_impure_formatter(buf, config, fname, cwd)
103-
-- Eval dynamic args
104-
local cfg = vim.tbl_extend('force', {}, config)
105-
if type(cfg.args) == 'function' then
106-
cfg.args = cfg.args(buf)
107-
end
108-
109-
local result = async.await(1, function(callback)
110-
vim.system(util.get_cmd(cfg, fname, buf), {
111-
text = true,
112-
cwd = cwd,
113-
env = cfg.env or {},
114-
timeout = cfg.timeout,
115-
}, callback)
116-
end)
117-
118-
if result.code ~= 0 and #result.stderr > 0 then
119-
return ('%s exited with code %d\n%s'):format(cfg.cmd, result.code, result.stderr)
120-
end
121-
122-
return nil
123-
end
124-
12558
local function do_fmt(buf)
12659
buf = buf or api.nvim_get_current_buf()
12760
local ft_conf = filetype[vim.bo[buf].filetype]
@@ -131,7 +64,6 @@ local function do_fmt(buf)
13164
return
13265
end
13366

134-
-- Get format range
13567
local srow, erow = 0, -1
13668
local range = nil
13769
local mode = api.nvim_get_mode().mode
@@ -141,70 +73,116 @@ local function do_fmt(buf)
14173
erow = range['end'][1]
14274
end
14375

144-
local old_indent = (mode == 'V') and vim.fn.indent(srow + 1) or nil
76+
local old_indent
77+
if mode == 'V' then
78+
old_indent = vim.fn.indent(srow + 1)
79+
end
14580

146-
-- Get and filter configs
14781
local fmt_configs = util.eval(ft_conf.formatter)
14882
local fname, cwd = util.buf_get_info(buf)
14983

150-
fmt_configs = vim.tbl_filter(function(config)
84+
fmt_configs = filter(function(config)
15185
return util.should_run(config, buf)
15286
end, fmt_configs)
15387

154-
-- Check executability
155-
for _, config in ipairs(fmt_configs) do
88+
local all_executable = not iter(fmt_configs):any(function(config)
15689
if config.cmd and vim.fn.executable(config.cmd) ~= 1 then
15790
util.report_error(config.cmd .. ' not executable')
158-
return
91+
return true
15992
end
93+
return false
94+
end)
95+
96+
if not all_executable then
97+
return
16098
end
16199

162-
-- Classify formatters
163-
local pure = vim.tbl_filter(function(config)
100+
local pure = filter(function(config)
164101
return config.fn or (config.cmd and config.stdin)
165102
end, fmt_configs)
166103

167-
local impure = vim.tbl_filter(function(config)
104+
local impure = filter(function(config)
168105
return config.cmd and not config.stdin
169106
end, fmt_configs)
170107

171-
-- Check range formatting compatibility
172108
if range and #impure > 0 then
173-
local impure_cmds = vim.tbl_map(function(c)
174-
return c.cmd
175-
end, impure)
176109
util.report_error('Cannot apply range formatting for filetype ' .. vim.bo[buf].filetype)
110+
local impure_cmds = {}
111+
for _, config in ipairs(impure) do
112+
table.insert(impure_cmds, config.cmd)
113+
end
177114
util.report_error(table.concat(impure_cmds, ', ') .. ' does not support reading from stdin')
178115
return
179116
end
180117

181-
emit_event('pending', { using = fmt_configs })
118+
util.doau('GuardFmt', {
119+
status = 'pending',
120+
using = fmt_configs,
121+
})
122+
123+
local prev_lines = api.nvim_buf_get_lines(buf, srow, erow, false)
124+
local prev_content = table.concat(prev_lines, '\n')
125+
local new_lines = prev_content
126+
local errno = nil
182127

183128
async.run(function()
184-
-- Initialize changedtick BEFORE any formatting (explicitly wait)
129+
-- Explicitly wait for changedtick initialization
185130
local changedtick = async.await(1, function(callback)
186131
vim.schedule(function()
187132
callback(api.nvim_buf_get_changedtick(buf))
188133
end)
189134
end)
190135

191-
local prev_lines = api.nvim_buf_get_lines(buf, srow, erow, false)
192-
local new_lines = table.concat(prev_lines, '\n')
193-
194136
-- Apply pure formatters sequentially
195137
for _, config in ipairs(pure) do
196-
local output, err = apply_pure_formatter(buf, range, config, fname, cwd, new_lines)
197-
if err then
198-
fail(err)
199-
return
138+
if errno then
139+
break
140+
end
141+
142+
-- Eval dynamic args
143+
local cfg = vim.tbl_extend('force', {}, config)
144+
if type(cfg.args) == 'function' then
145+
cfg.args = cfg.args(buf)
146+
end
147+
148+
if cfg.fn then
149+
new_lines = cfg.fn(buf, range, new_lines)
150+
else
151+
local result = async.await(1, function(callback)
152+
local handle = vim.system(util.get_cmd(cfg, fname, buf), {
153+
stdin = true,
154+
cwd = cwd,
155+
env = cfg.env,
156+
timeout = cfg.timeout,
157+
}, callback)
158+
handle:write(new_lines)
159+
handle:write(nil)
160+
end)
161+
162+
if result.code ~= 0 and #result.stderr > 0 then
163+
errno = {
164+
cmd = cfg.cmd,
165+
code = result.code,
166+
stderr = result.stderr,
167+
reason = cfg.cmd .. ' exited with errors',
168+
}
169+
else
170+
new_lines = result.stdout
171+
end
200172
end
201-
new_lines = output
202173
end
203174

175+
-- Wait for schedule and update buffer
204176
async.await(1, function(callback)
205177
vim.schedule(function()
206-
if not api.nvim_buf_is_valid(buf) then
207-
fail('buffer no longer valid')
178+
if errno then
179+
if errno.reason:match('exited with errors$') then
180+
fail(('%s exited with code %d\n%s'):format(errno.cmd, errno.code, errno.stderr))
181+
elseif errno.reason == 'buffer changed' then
182+
fail('buffer changed during formatting')
183+
else
184+
fail(errno.reason)
185+
end
208186
callback()
209187
return
210188
end
@@ -215,42 +193,80 @@ local function do_fmt(buf)
215193
return
216194
end
217195

218-
update_buffer(buf, prev_lines, new_lines, srow, erow, old_indent)
196+
if not api.nvim_buf_is_valid(buf) then
197+
fail('buffer no longer valid')
198+
callback()
199+
return
200+
end
201+
202+
update_buffer(buf, prev_content, new_lines, srow, erow, old_indent)
219203
callback()
220204
end)
221205
end)
222206

207+
-- Stop if there was an error
208+
if errno then
209+
return
210+
end
211+
223212
-- Apply impure formatters sequentially
213+
local impure_error = nil
224214
for _, config in ipairs(impure) do
225-
local err = apply_impure_formatter(buf, config, fname, cwd)
226-
if err then
227-
fail(err)
228-
return
215+
-- Eval dynamic args
216+
local cfg = vim.tbl_extend('force', {}, config)
217+
if type(cfg.args) == 'function' then
218+
cfg.args = cfg.args(buf)
219+
end
220+
221+
local result = async.await(1, function(callback)
222+
vim.system(util.get_cmd(cfg, fname, buf), {
223+
text = true,
224+
cwd = cwd,
225+
env = cfg.env or {},
226+
}, callback)
227+
end)
228+
229+
if result.code ~= 0 and #result.stderr > 0 then
230+
impure_error = {
231+
cmd = cfg.cmd,
232+
code = result.code,
233+
stderr = result.stderr,
234+
}
235+
break
229236
end
230237
end
231238

232-
-- Refresh buffer if impure formatters were used
239+
if impure_error then
240+
fail(
241+
('%s exited with code %d\n%s'):format(
242+
impure_error.cmd,
243+
impure_error.code,
244+
impure_error.stderr
245+
)
246+
)
247+
return
248+
end
249+
233250
if #impure > 0 then
234-
async.await(1, function(callback)
235-
vim.schedule(function()
236-
api.nvim_buf_call(buf, function()
237-
local views = save_views(buf)
238-
api.nvim_command('silent! edit!')
239-
restore_views(views)
240-
end)
241-
callback()
251+
vim.schedule(function()
252+
api.nvim_buf_call(buf, function()
253+
local views = save_views(buf)
254+
api.nvim_command('silent! edit!')
255+
restore_views(views)
242256
end)
243257
end)
244258
end
245259

246-
emit_event('done')
260+
util.doau('GuardFmt', {
261+
status = 'done',
262+
})
247263

248264
if util.getopt('refresh_diagnostic') then
249265
vim.diagnostic.show()
250266
end
251267
end)
252268
end
253269

254-
M.do_fmt = do_fmt
255-
256-
return M
270+
return {
271+
do_fmt = do_fmt,
272+
}

0 commit comments

Comments
 (0)