-
-
Notifications
You must be signed in to change notification settings - Fork 25
fix: improve async format module #230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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)) | ||||||||||||||||||||||||||||
|
Comment on lines
+179
to
+180
|
||||||||||||||||||||||||||||
| elseif errno.reason == 'buffer changed' then | ||||||||||||||||||||||||||||
| fail('buffer changed during formatting') | ||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||
| fail(errno.reason) | ||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||
| callback() | ||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||
|
|
@@ -215,42 +193,80 @@ 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 | ||||||||||||||||||||||||||||
|
Comment on lines
+208
to
+210
|
||||||||||||||||||||||||||||
| if errno then | |
| return | |
| end |
Copilot
AI
Nov 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The impure formatter execution is missing the timeout parameter that was present in the original code. This could lead to formatters hanging indefinitely if they don't complete. The timeout should be added to the vim.system call options.
| env = cfg.env or {}, | |
| env = cfg.env or {}, | |
| timeout = cfg.timeout or 5000, |
Copilot
AI
Nov 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The final vim.schedule call for impure formatters is missing the async.await wrapper. This means the code won't wait for the buffer refresh to complete before emitting the 'done' event. This could cause a race condition where the 'done' event fires before the buffer is actually refreshed. The original code wrapped this in async.await(1, function(callback) ... callback() end) to ensure proper sequencing.
| vim.schedule(function() | |
| api.nvim_buf_call(buf, function() | |
| local views = save_views(buf) | |
| api.nvim_command('silent! edit!') | |
| restore_views(views) | |
| 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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential issue with capturing
prev_linesandprev_contentat lines 123-124. These are captured outside the async.run block, before the changedtick is initialized inside async.run at line 130-134. If the buffer changes between line 124 (capturing content) and line 134 (capturing changedtick), the changedtick validation at line 189 would pass butprev_contentwould be stale. Consider moving lines 123-124 inside the async.run block, within the same scheduled callback as the changedtick capture (lines 130-134) to ensure atomic capture of both values.