aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md44
-rw-r--r--doc/conform.txt20
-rw-r--r--lua/conform/init.lua79
-rw-r--r--lua/conform/log.lua12
-rw-r--r--lua/conform/lsp_format.lua112
-rw-r--r--lua/conform/runner.lua178
-rw-r--r--lua/conform/util.lua7
-rw-r--r--scripts/autoformat_doc.lua (renamed from tests/autoformat_doc.lua)15
-rwxr-xr-xscripts/generate.py4
-rw-r--r--scripts/options_doc.lua (renamed from tests/options_doc.lua)0
10 files changed, 332 insertions, 139 deletions
diff --git a/README.md b/README.md
index 1a94de2..a5b5797 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Lightweight yet powerful formatter plugin for Neovim
- [Options](#options)
- [Autoformat on save](#autoformat-on-save)
- [API](#api)
- - [format(opts)](#formatopts)
+ - [format(opts, callback)](#formatopts-callback)
- [list_formatters(bufnr)](#list_formattersbufnr)
- [list_all_formatters()](#list_all_formatters)
- [Acknowledgements](#acknowledgements)
@@ -143,7 +143,7 @@ require("conform").setup({
})
```
-See [conform.format()](#formatopts) for more details about the parameters.
+See [conform.format()](#formatopts-callback) for more details about the parameters.
To view configured and available formatters, as well as to see the path to the log file, run `:ConformInfo`
@@ -287,6 +287,7 @@ using your own autocmd. For example:
<!-- AUTOFORMAT -->
```lua
+-- Format synchronously on save
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*",
callback = function(args)
@@ -307,6 +308,20 @@ vim.api.nvim_create_autocmd("BufWritePre", {
require("conform").format({ timeout_ms = 500, lsp_fallback = true, buf = args.buf })
end,
})
+
+-- Format asynchronously on save
+vim.api.nvim_create_autocmd("BufWritePost", {
+ pattern = "*",
+ callback = function(args)
+ require("conform").format({ async = true, lsp_fallback = true, buf = args.buf }, function(err)
+ if not err then
+ vim.api.nvim_buf_call(args.buf, function()
+ vim.cmd.update()
+ end)
+ end
+ end)
+ end,
+})
```
<!-- /AUTOFORMAT -->
@@ -315,21 +330,22 @@ vim.api.nvim_create_autocmd("BufWritePre", {
<!-- API -->
-### format(opts)
+### format(opts, callback)
-`format(opts): boolean` \
+`format(opts, callback): boolean` \
Format a buffer
-| Param | Type | Desc | |
-| ----- | ------------ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
-| opts | `nil\|table` | | |
-| | timeout_ms | `nil\|integer` | Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. |
-| | bufnr | `nil\|integer` | Format this buffer (default 0) |
-| | async | `nil\|boolean` | If true the method won't block. Defaults to false. |
-| | formatters | `nil\|string[]` | List of formatters to run. Defaults to all formatters for the buffer filetype. |
-| | lsp_fallback | `nil\|boolean` | Attempt LSP formatting if no formatters are available. Defaults to false. |
-| | quiet | `nil\|boolean` | Don't show any notifications for warnings or failures. Defaults to false. |
-| | range | `nil\|table` | Range to format. Table must contain `start` and `end` keys with {row, col} tuples using (1,0) indexing. Defaults to current selection in visual mode |
+| Param | Type | Desc | |
+| -------- | ---------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
+| opts | `nil\|table` | | |
+| | timeout_ms | `nil\|integer` | Time in milliseconds to block for formatting. Defaults to 1000. No effect if async = true. |
+| | bufnr | `nil\|integer` | Format this buffer (default 0) |
+| | async | `nil\|boolean` | If true the method won't block. Defaults to false. |
+| | formatters | `nil\|string[]` | List of formatters to run. Defaults to all formatters for the buffer filetype. |
+| | lsp_fallback | `nil\|boolean` | Attempt LSP formatting if no formatters are available. Defaults to false. |
+| | quiet | `nil\|boolean` | Don't show any notifications for warnings or failures. Defaults to false. |
+| | range | `nil\|table` | Range to format. Table must contain `start` and `end` keys with {row, col} tuples using (1,0) indexing. Defaults to current selection in visual mode |
+| callback | `nil\|fun(err: nil\|string)` | Called once formatting has completed | |
Returns:
diff --git a/doc/conform.txt b/doc/conform.txt
index 16faaf4..1482ea2 100644
--- a/doc/conform.txt
+++ b/doc/conform.txt
@@ -88,11 +88,11 @@ OPTIONS *conform-option
--------------------------------------------------------------------------------
API *conform-api*
-format({opts}): boolean *conform.format*
+format({opts}, {callback}): boolean *conform.format*
Format a buffer
Parameters:
- {opts} `nil|table`
+ {opts} `nil|table`
{timeout_ms} `nil|integer` Time in milliseconds to block for
formatting. Defaults to 1000. No effect if async =
true.
@@ -108,6 +108,7 @@ format({opts}): boolean *conform.forma
{range} `nil|table` Range to format. Table must contain `start`
and `end` keys with {row, col} tuples using (1,0)
indexing. Defaults to current selection in visual mode
+ {callback} `nil|fun(err: nil|string)` Called once formatting has completed
Returns:
`boolean` True if any formatters were attempted
@@ -196,6 +197,7 @@ AUTOFORMAT *conform-autoforma
If you want more complex logic than the `format_on_save` option allows, you can
write it yourself using your own autocmd. For example:
>lua
+ -- Format synchronously on save
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*",
callback = function(args)
@@ -216,6 +218,20 @@ write it yourself using your own autocmd. For example:
require("conform").format({ timeout_ms = 500, lsp_fallback = true, buf = args.buf })
end,
})
+
+ -- Format asynchronously on save
+ vim.api.nvim_create_autocmd("BufWritePost", {
+ pattern = "*",
+ callback = function(args)
+ require("conform").format({ async = true, lsp_fallback = true, buf = args.buf }, function(err)
+ if not err then
+ vim.api.nvim_buf_call(args.buf, function()
+ vim.cmd.update()
+ end)
+ end
+ end)
+ end,
+ })
<
================================================================================
diff --git a/lua/conform/init.lua b/lua/conform/init.lua
index 4a6b226..4aac7f2 100644
--- a/lua/conform/init.lua
+++ b/lua/conform/init.lua
@@ -52,25 +52,6 @@ M.formatters = {}
M.notify_on_error = true
----@private
-M.original_apply_text_edits = vim.lsp.util.apply_text_edits
-
-local function apply_text_edits(text_edits, bufnr, offset_encoding)
- if
- #text_edits == 1
- and text_edits[1].range.start.line == 0
- and text_edits[1].range.start.character == 0
- and text_edits[1].range["end"].line == vim.api.nvim_buf_line_count(bufnr) + 1
- and text_edits[1].range["end"].character == 0
- then
- local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
- local new_lines = vim.split(text_edits[1].newText, "\n", { plain = true })
- require("conform.runner").apply_format(bufnr, original_lines, new_lines, nil, false)
- else
- M.original_apply_text_edits(text_edits, bufnr, offset_encoding)
- end
-end
-
M.setup = function(opts)
opts = opts or {}
@@ -118,22 +99,6 @@ M.setup = function(opts)
vim.api.nvim_create_user_command("ConformInfo", function()
require("conform.health").show_window()
end, { desc = "Show information about Conform formatters" })
-
- -- Monkey patch lsp.util.apply_text_edits to handle LSP clients that replace the entire buffer
- -- during formatting. This is unfortunately the best place to shim that logic in.
- vim.lsp.util.apply_text_edits = apply_text_edits
-end
-
----@param bufnr integer
----@return boolean
-local function supports_lsp_format(bufnr)
- ---@diagnostic disable-next-line: deprecated
- for _, client in ipairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do
- if client.supports_method("textDocument/formatting", { bufnr = bufnr }) then
- return true
- end
- end
- return false
end
---@private
@@ -238,8 +203,9 @@ end
--- lsp_fallback nil|boolean Attempt LSP formatting if no formatters are available. Defaults to false.
--- quiet nil|boolean Don't show any notifications for warnings or failures. Defaults to false.
--- range nil|table Range to format. Table must contain `start` and `end` keys with {row, col} tuples using (1,0) indexing. Defaults to current selection in visual mode
+---@param callback? fun(err: nil|string) Called once formatting has completed
---@return boolean True if any formatters were attempted
-M.format = function(opts)
+M.format = function(opts, callback)
---@type {timeout_ms: integer, bufnr: integer, async: boolean, lsp_fallback: boolean, quiet: boolean, formatters?: string[], range?: conform.Range}
opts = vim.tbl_extend("keep", opts or {}, {
timeout_ms = 1000,
@@ -248,7 +214,10 @@ M.format = function(opts)
lsp_fallback = false,
quiet = false,
})
+ callback = callback or function(_err) end
local log = require("conform.log")
+ local lsp_format = require("conform.lsp_format")
+ local runner = require("conform.runner")
local formatters = {}
local any_formatters_configured
@@ -287,20 +256,38 @@ M.format = function(opts)
opts.range = range_from_selection(opts.bufnr, mode)
end
+ ---@param err? conform.Error
+ local function handle_err(err)
+ if err then
+ local level = runner.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
+ should_notify = should_notify and M.notify_on_error and not err.debounce_message
+ notify_msg = "Formatter failed. See :ConformInfo for details"
+ end
+ if should_notify then
+ vim.notify(notify_msg, level)
+ end
+ end
+ local err_message = err and err.message
+ if not err_message and not vim.api.nvim_buf_is_valid(opts.bufnr) then
+ err_message = "buffer was deleted"
+ end
+ callback(err_message)
+ end
+
if opts.async then
- require("conform.runner").format_async(opts.bufnr, formatters, opts.quiet, opts.range)
+ runner.format_async(opts.bufnr, formatters, opts.range, handle_err)
else
- require("conform.runner").format_sync(
- opts.bufnr,
- formatters,
- opts.timeout_ms,
- opts.quiet,
- opts.range
- )
+ local err = runner.format_sync(opts.bufnr, formatters, opts.timeout_ms, opts.range)
+ handle_err(err)
end
- elseif opts.lsp_fallback and supports_lsp_format(opts.bufnr) then
+ elseif opts.lsp_fallback and not vim.tbl_isempty(lsp_format.get_format_clients(opts)) then
log.debug("Running LSP formatter on %s", vim.api.nvim_buf_get_name(opts.bufnr))
- vim.lsp.buf.format(opts)
+ lsp_format.format(opts, callback)
elseif any_formatters_configured and not opts.quiet then
vim.notify("No formatters found for buffer. See :ConformInfo", vim.log.levels.WARN)
else
diff --git a/lua/conform/log.lua b/lua/conform/log.lua
index 753a3c6..3e31fd2 100644
--- a/lua/conform/log.lua
+++ b/lua/conform/log.lua
@@ -5,7 +5,7 @@ vim.tbl_add_reverse_lookup(levels)
local Log = {}
---@type integer
-Log.level = vim.log.levels.ERROR
+Log.level = vim.log.levels.WARN
---@return string
Log.get_logfile = function()
@@ -33,11 +33,17 @@ local function format(level, msg, ...)
end
end
local ok, text = pcall(string.format, msg, vim.F.unpack_len(args))
+ local timestr = vim.fn.strftime("%H:%M:%S")
if ok then
local str_level = levels[level]
- return string.format("%s[%s] %s", vim.fn.strftime("%H:%M:%S"), str_level, text)
+ return string.format("%s[%s] %s", timestr, str_level, text)
else
- return string.format("[ERROR] error formatting log line: '%s' args %s", msg, vim.inspect(args))
+ return string.format(
+ "%s[ERROR] error formatting log line: '%s' args %s",
+ timestr,
+ vim.inspect(msg),
+ vim.inspect(args)
+ )
end
end
diff --git a/lua/conform/lsp_format.lua b/lua/conform/lsp_format.lua
new file mode 100644
index 0000000..66be47d
--- /dev/null
+++ b/lua/conform/lsp_format.lua
@@ -0,0 +1,112 @@
+---This module replaces the default vim.lsp.buf.format() so that we can inject our own logic
+local util = require("vim.lsp.util")
+
+local M = {}
+
+local function apply_text_edits(text_edits, bufnr, offset_encoding)
+ if
+ #text_edits == 1
+ and text_edits[1].range.start.line == 0
+ and text_edits[1].range.start.character == 0
+ and text_edits[1].range["end"].line == vim.api.nvim_buf_line_count(bufnr) + 1
+ and text_edits[1].range["end"].character == 0
+ then
+ local original_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
+ local new_lines = vim.split(text_edits[1].newText, "\n", { plain = true })
+ require("conform.runner").apply_format(bufnr, original_lines, new_lines, nil, false)
+ else
+ vim.lsp.util.apply_text_edits(text_edits, bufnr, offset_encoding)
+ end
+end
+
+---@param options table
+---@return table[] clients
+function M.get_format_clients(options)
+ local method = options.range and "textDocument/rangeFormatting" or "textDocument/formatting"
+
+ local clients = vim.lsp.get_active_clients({
+ id = options.id,
+ bufnr = options.bufnr,
+ name = options.name,
+ })
+ if options.filter then
+ clients = vim.tbl_filter(options.filter, clients)
+ end
+ return vim.tbl_filter(function(client)
+ return client.supports_method(method, { bufnr = options.bufnr })
+ end, clients)
+end
+
+---@param options table
+---@param callback fun(err?: string)
+function M.format(options, callback)
+ options = options or {}
+ if not options.bufnr or options.bufnr == 0 then
+ options.bufnr = vim.api.nvim_get_current_buf()
+ end
+ local bufnr = options.bufnr
+ local range = options.range
+ local method = range and "textDocument/rangeFormatting" or "textDocument/formatting"
+
+ local clients = M.get_format_clients(options)
+
+ if #clients == 0 then
+ return callback("[LSP] Format request failed, no matching language servers.")
+ end
+
+ local function set_range(client, params)
+ if range then
+ local range_params =
+ util.make_given_range_params(range.start, range["end"], bufnr, client.offset_encoding)
+ params.range = range_params.range
+ end
+ return params
+ end
+
+ if options.async then
+ local changedtick = vim.b[bufnr].changedtick
+ local do_format
+ do_format = function(idx, client)
+ if not client then
+ return callback()
+ end
+ local params = set_range(client, util.make_formatting_params(options.formatting_options))
+ client.request(method, params, function(err, result, ctx, _)
+ if not result then
+ return callback(err or "No result returned from LSP formatter")
+ elseif not vim.api.nvim_buf_is_valid(bufnr) then
+ return callback("buffer was deleted")
+ elseif vim.b[bufnr].changedtick ~= changedtick then
+ return
+ callback(
+ string.format(
+ "Async LSP formatter discarding changes for %s: concurrent modification",
+ vim.api.nvim_buf_get_name(bufnr)
+ )
+ )
+ else
+ apply_text_edits(result, ctx.bufnr, client.offset_encoding)
+ changedtick = vim.b[bufnr].changedtick
+
+ do_format(next(clients, idx))
+ end
+ end, bufnr)
+ end
+ do_format(next(clients))
+ else
+ local timeout_ms = options.timeout_ms or 1000
+ for _, client in pairs(clients) do
+ local params = set_range(client, util.make_formatting_params(options.formatting_options))
+ local result, err = client.request_sync(method, params, timeout_ms, bufnr)
+ if result and result.result then
+ apply_text_edits(result.result, bufnr, client.offset_encoding)
+ elseif err then
+ vim.notify(string.format("[LSP][%s] %s", client.name, err), vim.log.levels.WARN)
+ return callback(string.format("[LSP][%s] %s", client.name, err))
+ end
+ end
+ callback()
+ end
+end
+
+return M
diff --git a/lua/conform/runner.lua b/lua/conform/runner.lua
index 1020a8a..a438718 100644
--- a/lua/conform/runner.lua
+++ b/lua/conform/runner.lua
@@ -4,6 +4,48 @@ local util = require("conform.util")
local uv = vim.uv or vim.loop
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 ocurred 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.FormatterConfig
M.build_cmd = function(ctx, config)
@@ -134,6 +176,9 @@ end
---@param range? conform.Range
---@param only_apply_range boolean
M.apply_format = function(bufnr, original_lines, new_lines, range, only_apply_range)
+ if not vim.api.nvim_buf_is_valid(bufnr) then
+ return
+ end
local bufname = vim.api.nvim_buf_get_name(bufnr)
-- If the formatter output didn't have a trailing newline, add one
if new_lines[#new_lines] ~= "" then
@@ -188,21 +233,22 @@ M.apply_format = function(bufnr, original_lines, new_lines, range, only_apply_ra
end
log.trace("Applying text edits for %s", bufname)
- require("conform").original_apply_text_edits(text_edits, bufnr, "utf-8")
+ vim.lsp.util.apply_text_edits(text_edits, bufnr, "utf-8")
log.trace("Done formatting %s", bufname)
end
+---Map of formatter name to if the last run of that formatter produced an error
+---@type table<string, boolean>
local last_run_errored = {}
---@param bufnr integer
---@param formatter conform.FormatterInfo
---@param config conform.FormatterConfig
---@param ctx conform.Context
----@param quiet boolean
---@param input_lines string[]
----@param callback fun(err?: string, output?: string[])
+---@param callback fun(err?: conform.Error, output?: string[])
---@return integer job_id
-local function run_formatter(bufnr, formatter, config, ctx, quiet, input_lines, callback)
+local function run_formatter(bufnr, formatter, config, ctx, input_lines, callback)
local cmd = M.build_cmd(ctx, config)
local cwd = nil
if config.cwd then
@@ -214,15 +260,8 @@ local function run_formatter(bufnr, formatter, config, ctx, quiet, input_lines,
end
callback = util.wrap_callback(callback, function(err)
if err then
- if
- not last_run_errored[formatter.name]
- and not quiet
- and require("conform").notify_on_error
- then
- vim.notify(
- string.format("Formatter '%s' failed. See :ConformInfo for details", formatter.name),
- vim.log.levels.ERROR
- )
+ if last_run_errored[formatter.name] then
+ err.debounce_message = true
end
last_run_errored[formatter.name] = true
else
@@ -262,7 +301,8 @@ local function run_formatter(bufnr, formatter, config, ctx, quiet, input_lines,
local stdout
local stderr
local exit_codes = config.exit_codes or { 0 }
- local jid = vim.fn.jobstart(cmd, {
+ local jid
+ jid = vim.fn.jobstart(cmd, {
cwd = cwd,
env = env,
stdout_buffered = true,
@@ -300,14 +340,30 @@ local function run_formatter(bufnr, formatter, config, ctx, quiet, input_lines,
elseif stdout and not vim.tbl_isempty(stdout) then
err_str = table.concat(stdout, "\n")
end
- callback(string.format("Formatter '%s' error: %s", formatter.name, err_str))
+ if vim.api.nvim_buf_is_valid(bufnr) and jid ~= vim.b[bufnr].conform_jid then
+ callback({
+ code = M.ERROR_CODE.INTERRUPTED,
+ message = string.format("Formatter '%s' was interrupted", formatter.name),
+ })
+ else
+ callback({
+ code = M.ERROR_CODE.RUNTIME,
+ message = string.format("Formatter '%s' error: %s", formatter.name, err_str),
+ })
+ end
end
end,
})
if jid == 0 then
- callback(string.format("Formatter '%s' invalid arguments", formatter.name))
+ callback({
+ code = M.ERROR_CODE.INVALID_ARGS,
+ message = string.format("Formatter '%s' invalid arguments", formatter.name),
+ })
elseif jid == -1 then
- callback(string.format("Formatter '%s' command is not executable", formatter.name))
+ callback({
+ code = M.ERROR_CODE.NOT_EXECUTABLE,
+ message = string.format("Formatter '%s' command is not executable", formatter.name),
+ })
elseif config.stdin then
vim.api.nvim_chan_send(jid, buffer_text)
vim.fn.chanclose(jid, "stdin")
@@ -359,10 +415,9 @@ end
---@param bufnr integer
---@param formatters conform.FormatterInfo[]
----@param quiet boolean
---@param range? conform.Range
----@param callback? fun(err?: string)
-M.format_async = function(bufnr, formatters, quiet, range, callback)
+---@param callback fun(err?: conform.Error)
+M.format_async = function(bufnr, formatters, range, callback)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
@@ -384,15 +439,16 @@ M.format_async = function(bufnr, formatters, quiet, range, callback)
local formatter = formatters[idx]
if not formatter then
-- discard formatting if buffer has changed
- if vim.b[bufnr].changedtick == changedtick then
- M.apply_format(bufnr, original_lines, input_lines, range, not all_support_range_formatting)
+ if not vim.api.nvim_buf_is_valid(bufnr) or vim.b[bufnr].changedtick ~= changedtick then
+ callback({
+ code = M.ERROR_CODE.CONCURRENT_MODIFICATION,
+ message = string.format(
+ "Async formatter discarding changes for %s: concurrent modification",
+ vim.api.nvim_buf_get_name(bufnr)
+ ),
+ })
else
- log.info(
- "Async formatter discarding changes for %s: concurrent modification",
- vim.api.nvim_buf_get_name(bufnr)
- )
- end
- if callback then
+ M.apply_format(bufnr, original_lines, input_lines, range, not all_support_range_formatting)
callback()
end
return
@@ -401,17 +457,9 @@ M.format_async = function(bufnr, formatters, quiet, range, callback)
local config = assert(require("conform").get_formatter_config(formatter.name, bufnr))
local ctx = M.build_context(bufnr, config, range)
- local jid
- jid = run_formatter(bufnr, formatter, config, ctx, quiet, input_lines, function(err, output)
+ run_formatter(bufnr, formatter, config, ctx, input_lines, function(err, output)
if err then
- -- Only log the error if the job wasn't canceled
- if vim.api.nvim_buf_is_valid(bufnr) and jid == vim.b[bufnr].conform_jid then
- log.error(err)
- end
- if callback then
- callback(err)
- end
- return
+ return callback(err)
end
input_lines = output
run_next_formatter()
@@ -424,9 +472,9 @@ end
---@param bufnr integer
---@param formatters conform.FormatterInfo[]
---@param timeout_ms integer
----@param quiet boolean
---@param range? conform.Range
-M.format_sync = function(bufnr, formatters, timeout_ms, quiet, range)
+---@return conform.Error? error
+M.format_sync = function(bufnr, formatters, timeout_ms, range)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
end
@@ -446,32 +494,21 @@ M.format_sync = function(bufnr, formatters, timeout_ms, quiet, range)
for _, formatter in ipairs(formatters) do
local remaining = timeout_ms - (uv.hrtime() / 1e6 - start)
if remaining <= 0 then
- if quiet then
- log.warn("Formatter '%s' timed out", formatter.name)
- else
- vim.notify(string.format("Formatter '%s' timed out", formatter.name), vim.log.levels.WARN)
- end
- return
+ return {
+ code = M.ERROR_CODE.TIMEOUT,
+ message = string.format("Formatter '%s' timed out", formatter.name),
+ }
end
local done = false
local result = nil
+ local run_err = nil
local config = assert(require("conform").get_formatter_config(formatter.name, bufnr))
local ctx = M.build_context(bufnr, config, range)
- local jid = run_formatter(
- bufnr,
- formatter,
- config,
- ctx,
- quiet,
- input_lines,
- function(err, output)
- if err then
- log.error(err)
- end
- done = true
- result = output
- end
- )
+ local jid = run_formatter(bufnr, formatter, config, ctx, input_lines, function(err, output)
+ run_err = err
+ done = true
+ result = output
+ end)
all_support_range_formatting = all_support_range_formatting and config.range_args ~= nil
local wait_result, wait_reason = vim.wait(remaining, function()
@@ -479,19 +516,22 @@ M.format_sync = function(bufnr, formatters, timeout_ms, quiet, range)
end, 5)
if not wait_result then
+ vim.fn.jobstop(jid)
if wait_reason == -1 then
- if quiet then
- log.warn("Formatter '%s' timed out", formatter.name)
- else
- vim.notify(string.format("Formatter '%s' timed out", formatter.name), vim.log.levels.WARN)
- end
+ return {
+ code = M.ERROR_CODE.TIMEOUT,
+ message = string.format("Formatter '%s' timed out", formatter.name),
+ }
+ else
+ return {
+ code = M.ERROR_CODE.INTERRUPTED,
+ message = string.format("Formatter '%s' was interrupted", formatter.name),
+ }
end
- vim.fn.jobstop(jid)
- return
end
if not result then
- return
+ return run_err
end
input_lines = result
diff --git a/lua/conform/util.lua b/lua/conform/util.lua
index a408675..ea13e2d 100644
--- a/lua/conform/util.lua
+++ b/lua/conform/util.lua
@@ -61,9 +61,10 @@ M.tbl_slice = function(tbl, start_idx, end_idx)
return ret
end
----@param cb fun(...)
----@param wrapper fun(...)
----@return fun(...)
+---@generic T : fun()
+---@param cb T
+---@param wrapper T
+---@return T
M.wrap_callback = function(cb, wrapper)
return function(...)
wrapper(...)
diff --git a/tests/autoformat_doc.lua b/scripts/autoformat_doc.lua
index 8c0761e..1ea8ac4 100644
--- a/tests/autoformat_doc.lua
+++ b/scripts/autoformat_doc.lua
@@ -1,3 +1,4 @@
+-- Format synchronously on save
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*",
callback = function(args)
@@ -18,3 +19,17 @@ vim.api.nvim_create_autocmd("BufWritePre", {
require("conform").format({ timeout_ms = 500, lsp_fallback = true, buf = args.buf })
end,
})
+
+-- Format asynchronously on save
+vim.api.nvim_create_autocmd("BufWritePost", {
+ pattern = "*",
+ callback = function(args)
+ require("conform").format({ async = true, lsp_fallback = true, buf = args.buf }, function(err)
+ if not err then
+ vim.api.nvim_buf_call(args.buf, function()
+ vim.cmd.update()
+ end)
+ end
+ end)
+ end,
+})
diff --git a/scripts/generate.py b/scripts/generate.py
index e8b0dad..06fbdb0 100755
--- a/scripts/generate.py
+++ b/scripts/generate.py
@@ -23,8 +23,8 @@ ROOT = os.path.abspath(os.path.join(HERE, os.path.pardir))
README = os.path.join(ROOT, "README.md")
DOC = os.path.join(ROOT, "doc")
VIMDOC = os.path.join(DOC, "conform.txt")
-OPTIONS = os.path.join(ROOT, "tests", "options_doc.lua")
-AUTOFORMAT = os.path.join(ROOT, "tests", "autoformat_doc.lua")
+OPTIONS = os.path.join(ROOT, "scripts", "options_doc.lua")
+AUTOFORMAT = os.path.join(ROOT, "scripts", "autoformat_doc.lua")
@dataclass
diff --git a/tests/options_doc.lua b/scripts/options_doc.lua
index 82447fb..82447fb 100644
--- a/tests/options_doc.lua
+++ b/scripts/options_doc.lua