From 3f34f2de48e393b2ee289f2c8fa613c7eabae6d8 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli <506791+stevearc@users.noreply.github.com> Date: Thu, 31 Aug 2023 08:54:11 -0700 Subject: feat: format() takes an optional callback (#21) * refactor: replicate lsp.buf.format call * feat: format() takes an optional callback * fix: improper logging * fix: callback returns error if buffer is no longer valid * fix: provide more detailed error message to callback * fix: properly detect task interruption * cleanup: remove unnecessary error code translation * fix: lsp formatting for Neovim 0.9 * doc: add example of async formatting on save * fix: async LSP formatter discards changes if buffer was modified * fix: error code comparison * fix: use the same LSP client filtering logic everywhere * fix: add buffer validity guard checks * fix: add buffer validity guard to LSP formatter * refactor: change the default log level to WARN --- README.md | 44 +++++++---- doc/conform.txt | 20 ++++- lua/conform/init.lua | 79 +++++++++----------- lua/conform/log.lua | 12 ++- lua/conform/lsp_format.lua | 112 ++++++++++++++++++++++++++++ lua/conform/runner.lua | 178 +++++++++++++++++++++++++++------------------ lua/conform/util.lua | 7 +- scripts/autoformat_doc.lua | 35 +++++++++ scripts/generate.py | 4 +- scripts/options_doc.lua | 71 ++++++++++++++++++ tests/autoformat_doc.lua | 20 ----- tests/options_doc.lua | 71 ------------------ 12 files changed, 423 insertions(+), 230 deletions(-) create mode 100644 lua/conform/lsp_format.lua create mode 100644 scripts/autoformat_doc.lua create mode 100644 scripts/options_doc.lua delete mode 100644 tests/autoformat_doc.lua delete mode 100644 tests/options_doc.lua 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: ```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, +}) ``` @@ -315,21 +330,22 @@ vim.api.nvim_create_autocmd("BufWritePre", { -### 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 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/scripts/autoformat_doc.lua b/scripts/autoformat_doc.lua new file mode 100644 index 0000000..1ea8ac4 --- /dev/null +++ b/scripts/autoformat_doc.lua @@ -0,0 +1,35 @@ +-- Format synchronously on save +vim.api.nvim_create_autocmd("BufWritePre", { + pattern = "*", + callback = function(args) + -- Disable autoformat on certain filetypes + local ignore_filetypes = { "sql", "java" } + if vim.tbl_contains(ignore_filetypes, vim.bo[args.buf].filetype) then + return + end + -- Disable with a global or buffer-local variable + if vim.g.disable_autoformat or vim.b[args.buf].disable_autoformat then + return + end + -- Disable autoformat for files in a certain path + local bufname = vim.api.nvim_buf_get_name(args.buf) + if bufname:match("/node_modules/") then + return + end + 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/scripts/options_doc.lua b/scripts/options_doc.lua new file mode 100644 index 0000000..82447fb --- /dev/null +++ b/scripts/options_doc.lua @@ -0,0 +1,71 @@ +require("conform").setup({ + -- Map of filetype to formatters + formatters_by_ft = { + lua = { "stylua" }, + -- Conform will use the first available formatter in the list + javascript = { "prettier_d", "prettier" }, + -- Formatters can also be specified with additional options + python = { + formatters = { "isort", "black" }, + -- Run formatters one after another instead of stopping at the first success + run_all_formatters = true, + -- Don't run these formatters as part of the format_on_save autocmd (see below) + format_on_save = false, + }, + }, + -- If this is set, Conform will run the formatter on save. + -- It will pass the table to conform.format(). + format_on_save = { + -- I recommend these options. See :help conform.format for details. + lsp_fallback = true, + timeout_ms = 500, + }, + -- Set the log level. Use `:ConformInfo` to see the location of the log file. + log_level = vim.log.levels.ERROR, + -- Conform will notify you when a formatter errors + notify_on_error = true, + -- Define custom formatters here + formatters = { + my_formatter = { + -- This can be a string or a function that returns a string + command = "my_cmd", + -- OPTIONAL - all fields below this are optional + -- A list of strings, or a function that returns a list of strings + args = { "--stdin-from-filename", "$FILENAME" }, + -- If the formatter supports range formatting, create the range arguments here + range_args = function(ctx) + return { "--line-start", ctx.range.start[1], "--line-end", ctx.range["end"][1] } + end, + -- Send file contents to stdin, read new contents from stdout (default true) + -- When false, will create a temp file (will appear in "$FILENAME" args). The temp + -- file is assumed to be modified in-place by the format command. + stdin = true, + -- A function the calculates the directory to run the command in + cwd = require("conform.util").root_file({ ".editorconfig", "package.json" }), + -- When cwd is not found, don't run the formatter (default false) + require_cwd = true, + -- When returns false, the formatter will not be used + condition = function(ctx) + return vim.fs.basename(ctx.filename) ~= "README.md" + end, + -- Exit codes that indicate success (default {0}) + exit_codes = { 0, 1 }, + -- Environment variables. This can also be a function that returns a table. + env = { + VAR = "value", + }, + }, + -- These can also be a function that returns the formatter + other_formatter = function() + return { + command = "my_cmd", + } + end, + }, +}) + +-- You can set formatters_by_ft and formatters directly +require("conform").formatters_by_ft.lua = { "stylua" } +require("conform").formatters.my_formatter = { + command = "my_cmd", +} diff --git a/tests/autoformat_doc.lua b/tests/autoformat_doc.lua deleted file mode 100644 index 8c0761e..0000000 --- a/tests/autoformat_doc.lua +++ /dev/null @@ -1,20 +0,0 @@ -vim.api.nvim_create_autocmd("BufWritePre", { - pattern = "*", - callback = function(args) - -- Disable autoformat on certain filetypes - local ignore_filetypes = { "sql", "java" } - if vim.tbl_contains(ignore_filetypes, vim.bo[args.buf].filetype) then - return - end - -- Disable with a global or buffer-local variable - if vim.g.disable_autoformat or vim.b[args.buf].disable_autoformat then - return - end - -- Disable autoformat for files in a certain path - local bufname = vim.api.nvim_buf_get_name(args.buf) - if bufname:match("/node_modules/") then - return - end - require("conform").format({ timeout_ms = 500, lsp_fallback = true, buf = args.buf }) - end, -}) diff --git a/tests/options_doc.lua b/tests/options_doc.lua deleted file mode 100644 index 82447fb..0000000 --- a/tests/options_doc.lua +++ /dev/null @@ -1,71 +0,0 @@ -require("conform").setup({ - -- Map of filetype to formatters - formatters_by_ft = { - lua = { "stylua" }, - -- Conform will use the first available formatter in the list - javascript = { "prettier_d", "prettier" }, - -- Formatters can also be specified with additional options - python = { - formatters = { "isort", "black" }, - -- Run formatters one after another instead of stopping at the first success - run_all_formatters = true, - -- Don't run these formatters as part of the format_on_save autocmd (see below) - format_on_save = false, - }, - }, - -- If this is set, Conform will run the formatter on save. - -- It will pass the table to conform.format(). - format_on_save = { - -- I recommend these options. See :help conform.format for details. - lsp_fallback = true, - timeout_ms = 500, - }, - -- Set the log level. Use `:ConformInfo` to see the location of the log file. - log_level = vim.log.levels.ERROR, - -- Conform will notify you when a formatter errors - notify_on_error = true, - -- Define custom formatters here - formatters = { - my_formatter = { - -- This can be a string or a function that returns a string - command = "my_cmd", - -- OPTIONAL - all fields below this are optional - -- A list of strings, or a function that returns a list of strings - args = { "--stdin-from-filename", "$FILENAME" }, - -- If the formatter supports range formatting, create the range arguments here - range_args = function(ctx) - return { "--line-start", ctx.range.start[1], "--line-end", ctx.range["end"][1] } - end, - -- Send file contents to stdin, read new contents from stdout (default true) - -- When false, will create a temp file (will appear in "$FILENAME" args). The temp - -- file is assumed to be modified in-place by the format command. - stdin = true, - -- A function the calculates the directory to run the command in - cwd = require("conform.util").root_file({ ".editorconfig", "package.json" }), - -- When cwd is not found, don't run the formatter (default false) - require_cwd = true, - -- When returns false, the formatter will not be used - condition = function(ctx) - return vim.fs.basename(ctx.filename) ~= "README.md" - end, - -- Exit codes that indicate success (default {0}) - exit_codes = { 0, 1 }, - -- Environment variables. This can also be a function that returns a table. - env = { - VAR = "value", - }, - }, - -- These can also be a function that returns the formatter - other_formatter = function() - return { - command = "my_cmd", - } - end, - }, -}) - --- You can set formatters_by_ft and formatters directly -require("conform").formatters_by_ft.lua = { "stylua" } -require("conform").formatters.my_formatter = { - command = "my_cmd", -} -- cgit v1.2.3-70-g09d2