aboutsummaryrefslogtreecommitdiffstats
path: root/lua/conform/runner.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/conform/runner.lua')
-rw-r--r--lua/conform/runner.lua283
1 files changed, 283 insertions, 0 deletions
diff --git a/lua/conform/runner.lua b/lua/conform/runner.lua
new file mode 100644
index 0000000..fd806ce
--- /dev/null
+++ b/lua/conform/runner.lua
@@ -0,0 +1,283 @@
+local fs = require("conform.fs")
+local log = require("conform.log")
+local util = require("conform.util")
+local uv = vim.uv or vim.loop
+local M = {}
+
+---@param ctx conform.Context
+---@param config conform.FormatterConfig
+local function build_cmd(ctx, config)
+ local command = config.command
+ if type(command) == "function" then
+ command = command(ctx)
+ end
+ local cmd = { command }
+ if config.args then
+ local args = config.args
+ if type(config.args) == "function" then
+ args = config.args(ctx)
+ end
+ ---@cast args string[]
+ for _, v in ipairs(args) do
+ if v == "$FILENAME" then
+ v = ctx.filename
+ elseif v == "$DIRNAME" then
+ v = vim.fs.dirname(ctx.filename)
+ end
+ table.insert(cmd, v)
+ end
+ end
+ return cmd
+end
+
+---@param bufnr integer
+---@param original_lines string[]
+---@param new_lines string[]
+local function apply_format(bufnr, original_lines, new_lines)
+ local restore = util.save_win_positions(bufnr)
+
+ local original_text = table.concat(original_lines, "\n")
+ local new_text = table.concat(new_lines, "\n")
+ local indices = vim.diff(original_text, new_text, {
+ result_type = "indices",
+ algorithm = "minimal",
+ })
+ assert(indices)
+ for i = #indices, 1, -1 do
+ local start_a, count_a, start_b, count_b = unpack(indices[i])
+ -- When count_a is 0, the diff is an insert after the line
+ if count_a == 0 then
+ start_a = start_a + 1
+ end
+ local replacement = util.tbl_slice(new_lines, start_b, start_b + count_b - 1)
+ vim.api.nvim_buf_set_lines(bufnr, start_a - 1, start_a - 1 + count_a, true, replacement)
+ end
+
+ restore()
+end
+
+---@param bufnr integer
+---@param formatter conform.FormatterInfo
+---@param input_lines string[]
+---@param callback fun(err?: string, output?: string[])
+---@return integer
+local function run_formatter(bufnr, formatter, input_lines, callback)
+ local config = assert(require("conform").get_formatter_config(formatter.name))
+ local ctx = M.build_context(bufnr, config)
+ local cmd = build_cmd(ctx, config)
+ local cwd = nil
+ if config.cwd then
+ cwd = config.cwd(ctx)
+ end
+ log.info("Running formatter %s on buffer %d", formatter.name, bufnr)
+ if not config.stdin then
+ log.debug("Creating temp file %s", ctx.filename)
+ local fd = assert(uv.fs_open(ctx.filename, "w", 448)) -- 0700
+ uv.fs_write(fd, table.concat(input_lines, "\n"))
+ uv.fs_close(fd)
+ local final_cb = callback
+ callback = function(...)
+ log.debug("Cleaning up temp file %s", ctx.filename)
+ uv.fs_unlink(ctx.filename)
+ final_cb(...)
+ end
+ end
+ log.debug("Running command: %s", cmd)
+ if cwd then
+ log.debug("Running in CWD: %s", cwd)
+ end
+ local stdout
+ local stderr
+ local exit_codes = config.exit_codes or { 0 }
+ local jid = vim.fn.jobstart(cmd, {
+ cwd = cwd,
+ stdout_buffered = true,
+ stderr_buffered = true,
+ stdin = config.stdin and "pipe" or "null",
+ on_stdout = function(_, data)
+ if config.stdin then
+ stdout = data
+ end
+ end,
+ on_stderr = function(_, data)
+ stderr = data
+ end,
+ on_exit = function(_, code)
+ local output
+ if not config.stdin then
+ local fd = assert(uv.fs_open(ctx.filename, "r", 448)) -- 0700
+ local stat = assert(uv.fs_fstat(fd))
+ local content = assert(uv.fs_read(fd, stat.size))
+ uv.fs_close(fd)
+ output = vim.split(content, "\n", { plain = true })
+ else
+ output = stdout
+ end
+ if vim.tbl_contains(exit_codes, code) then
+ log.debug("Formatter %s exited with code %d", formatter.name, code)
+ callback(nil, output)
+ else
+ log.error("Formatter %s exited with code %d", formatter.name, code)
+ log.warn("Formatter %s stdout:", formatter.name, stdout)
+ log.warn("Formatter %s stderr:", formatter.name, stderr)
+ callback(
+ string.format("Formatter '%s' error: %s", formatter.name, table.concat(stderr, "\n"))
+ )
+ end
+ end,
+ })
+ if jid == 0 then
+ callback(string.format("Formatter '%s' invalid arguments", formatter.name))
+ elseif jid == -1 then
+ callback(string.format("Formatter '%s' command is not executable", formatter.name))
+ elseif config.stdin then
+ local text = table.concat(input_lines, "\n")
+ vim.api.nvim_chan_send(jid, text)
+ vim.fn.chanclose(jid, "stdin")
+ end
+ vim.b[bufnr].conform_jid = jid
+
+ return jid
+end
+
+---@param bufnr integer
+---@param config conform.FormatterConfig
+---@return conform.Context
+M.build_context = function(bufnr, config)
+ if bufnr == 0 then
+ bufnr = vim.api.nvim_get_current_buf()
+ end
+ local filename = vim.api.nvim_buf_get_name(bufnr)
+
+ -- Hack around checkhealth. For buffers that are not files, we need to fabricate a filename
+ if vim.bo[bufnr].buftype ~= "" then
+ filename = ""
+ end
+ local dirname
+ if filename == "" then
+ dirname = vim.fn.getcwd()
+ filename = fs.join(dirname, "unnamed_temp")
+ local ft = vim.bo[bufnr].filetype
+ if ft and ft ~= "" then
+ filename = filename .. "." .. ft
+ end
+ else
+ dirname = vim.fs.dirname(filename)
+ end
+
+ if not config.stdin then
+ local basename = vim.fs.basename(filename)
+ local tmpname = string.format(".conform.%d.%s", math.random(1000000, 9999999), basename)
+ local parent = vim.fs.dirname(filename)
+ filename = fs.join(parent, tmpname)
+ end
+ return {
+ buf = bufnr,
+ filename = filename,
+ dirname = dirname,
+ }
+end
+
+---@param bufnr integer
+---@param formatters conform.FormatterInfo[]
+---@param callback? fun(err?: string)
+M.format_async = function(bufnr, formatters, callback)
+ local idx = 1
+ local changedtick = vim.b[bufnr].changedtick
+ local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
+ local input_lines = original_lines
+
+ -- kill previous jobs for buffer
+ local prev_jid = vim.b[bufnr].conform_jid
+ if prev_jid then
+ if vim.fn.jobstop(prev_jid) == 1 then
+ log.info("Canceled previous format job for buffer %d", bufnr)
+ end
+ end
+
+ local function run_next_formatter()
+ local formatter = formatters[idx]
+ if not formatter then
+ -- discard formatting if buffer has changed
+ if vim.b[bufnr].changedtick == changedtick then
+ apply_format(bufnr, original_lines, input_lines)
+ else
+ log.warn("Async formatter discarding changes for buffer %d: concurrent modification", bufnr)
+ end
+ if callback then
+ callback()
+ end
+ return
+ end
+ idx = idx + 1
+
+ local jid
+ jid = run_formatter(bufnr, formatter, input_lines, function(err, output)
+ if err then
+ -- Only display the error if the job wasn't canceled
+ if jid == vim.b[bufnr].conform_jid then
+ vim.notify(err, vim.log.levels.ERROR)
+ end
+ if callback then
+ callback(err)
+ end
+ return
+ end
+ input_lines = output
+ run_next_formatter()
+ end)
+ end
+ run_next_formatter()
+end
+
+---@param bufnr integer
+---@param formatters conform.FormatterInfo[]
+---@param timeout_ms integer
+M.format_sync = function(bufnr, formatters, timeout_ms)
+ local start = uv.hrtime() / 1e6
+ local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
+ local input_lines = original_lines
+
+ -- kill previous jobs for buffer
+ local prev_jid = vim.b[bufnr].conform_jid
+ if prev_jid then
+ if vim.fn.jobstop(prev_jid) == 1 then
+ log.info("Canceled previous format job for buffer %d", bufnr)
+ end
+ end
+
+ for _, formatter in ipairs(formatters) do
+ local remaining = timeout_ms - (uv.hrtime() / 1e6 - start)
+ local done = false
+ local result = nil
+ run_formatter(bufnr, formatter, input_lines, function(err, output)
+ if err then
+ vim.notify(err, vim.log.levels.ERROR)
+ end
+ done = true
+ result = output
+ end)
+
+ local wait_result, wait_reason = vim.wait(remaining, function()
+ return done
+ end, 5)
+
+ if not wait_result then
+ if wait_reason == -1 then
+ vim.notify(string.format("Formatter '%s' timed out", formatter.name), vim.log.levels.WARN)
+ end
+ return
+ end
+
+ if not result then
+ return
+ end
+
+ input_lines = result
+ end
+
+ local final_result = input_lines
+ apply_format(bufnr, original_lines, final_result)
+end
+
+return M