aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBronson Jordan <80419011+bpjordan@users.noreply.github.com>2024-01-15 21:48:26 -0600
committerGitHub <noreply@github.com>2024-01-15 19:48:26 -0800
commite0276bb32e9b33ece11fef2a5cfc8fb2108df0df (patch)
treea8b9bb0a2c16ab85f2ce74c198de3b6965424451
parent75e7c5c7eb5fbd53f8b12dc420b31ec70770b231 (diff)
feat: Add dry_run option and report if buffer was/would be changed by formatters (#273)
* feat: add dry_run option and pass return values for if buffer would be modified * fix: implement dry_run for blocking calls to lsp formatter * refactor: change `changed` variable to `did_edit` * docs: Update README * fix: address PR comments * fix: small cleanups --------- Co-authored-by: Steven Arcangeli <stevearc@stevearc.com>
-rw-r--r--README.md29
-rw-r--r--doc/conform.txt5
-rw-r--r--lua/conform/init.lua28
-rw-r--r--lua/conform/lsp_format.lua41
-rw-r--r--lua/conform/runner.lua44
-rw-r--r--tests/runner_spec.lua5
6 files changed, 107 insertions, 45 deletions
diff --git a/README.md b/README.md
index 6e16696..893a8ec 100644
--- a/README.md
+++ b/README.md
@@ -502,20 +502,21 @@ require("conform").formatters.my_formatter = {
`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. If the buffer is modified before the formatter completes, the formatting will be discarded. |
-| | formatters | `nil\|string[]` | List of formatters to run. Defaults to all formatters for the buffer filetype. |
-| | lsp_fallback | `nil\|boolean\|"always"` | Attempt LSP formatting if no formatters are available. Defaults to false. If "always", will attempt LSP formatting even if formatters are available. |
-| | 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 |
-| | id | `nil\|integer` | Passed to vim.lsp.buf.format when lsp_fallback = true |
-| | name | `nil\|string` | Passed to vim.lsp.buf.format when lsp_fallback = true |
-| | filter | `nil\|fun(client: table): boolean` | Passed to vim.lsp.buf.format when lsp_fallback = true |
-| callback | `nil\|fun(err: nil\|string)` | Called once formatting has completed | |
+| 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. If the buffer is modified before the formatter completes, the formatting will be discarded. |
+| | dry_run | `nil\|boolean` | If true don't apply formatting changes to the buffer |
+| | formatters | `nil\|string[]` | List of formatters to run. Defaults to all formatters for the buffer filetype. |
+| | lsp_fallback | `nil\|boolean\|"always"` | Attempt LSP formatting if no formatters are available. Defaults to false. If "always", will attempt LSP formatting even if formatters are available. |
+| | 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 |
+| | id | `nil\|integer` | Passed to vim.lsp.buf.format when lsp_fallback = true |
+| | name | `nil\|string` | Passed to vim.lsp.buf.format when lsp_fallback = true |
+| | filter | `nil\|fun(client: table): boolean` | Passed to vim.lsp.buf.format when lsp_fallback = true |
+| callback | `nil\|fun(err: nil\|string, did_edit: nil\|boolean)` | Called once formatting has completed | |
Returns:
diff --git a/doc/conform.txt b/doc/conform.txt
index ad2c736..ed1241f 100644
--- a/doc/conform.txt
+++ b/doc/conform.txt
@@ -120,6 +120,8 @@ format({opts}, {callback}): boolean *conform.forma
{async} `nil|boolean` If true the method won't block. Defaults
to false. If the buffer is modified before the
formatter completes, the formatting will be discarded.
+ {dry_run} `nil|boolean` If true don't apply formatting changes to
+ the buffer
{formatters} `nil|string[]` List of formatters to run. Defaults to
all formatters for the buffer filetype.
{lsp_fallback} `nil|boolean|"always"` Attempt LSP formatting if no
@@ -137,7 +139,8 @@ format({opts}, {callback}): boolean *conform.forma
lsp_fallback = true
{filter} `nil|fun(client: table): boolean` Passed to
|vim.lsp.buf.format| when lsp_fallback = true
- {callback} `nil|fun(err: nil|string)` Called once formatting has completed
+ {callback} `nil|fun(err: nil|string, did_edit: nil|boolean)` Called once
+ formatting has completed
Returns:
`boolean` True if any formatters were attempted
diff --git a/lua/conform/init.lua b/lua/conform/init.lua
index 35ffb6e..db0e939 100644
--- a/lua/conform/init.lua
+++ b/lua/conform/init.lua
@@ -354,6 +354,7 @@ end
--- 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. If the buffer is modified before the formatter completes, the formatting will be discarded.
+--- dry_run nil|boolean If true don't apply formatting changes to the buffer
--- formatters nil|string[] List of formatters to run. Defaults to all formatters for the buffer filetype.
--- lsp_fallback nil|boolean|"always" Attempt LSP formatting if no formatters are available. Defaults to false. If "always", will attempt LSP formatting even if formatters are available.
--- quiet nil|boolean Don't show any notifications for warnings or failures. Defaults to false.
@@ -361,14 +362,15 @@ end
--- id nil|integer Passed to |vim.lsp.buf.format| when lsp_fallback = true
--- name nil|string Passed to |vim.lsp.buf.format| when lsp_fallback = true
--- filter nil|fun(client: table): boolean Passed to |vim.lsp.buf.format| when lsp_fallback = true
----@param callback? fun(err: nil|string) Called once formatting has completed
+---@param callback? fun(err: nil|string, did_edit: nil|boolean) Called once formatting has completed
---@return boolean True if any formatters were attempted
M.format = function(opts, callback)
- ---@type {timeout_ms: integer, bufnr: integer, async: boolean, lsp_fallback: boolean|"always", quiet: boolean, formatters?: string[], range?: conform.Range}
+ ---@type {timeout_ms: integer, bufnr: integer, async: boolean, dry_run: boolean, lsp_fallback: boolean|"always", quiet: boolean, formatters?: string[], range?: conform.Range}
opts = vim.tbl_extend("keep", opts or {}, {
timeout_ms = 1000,
bufnr = 0,
async = false,
+ dry_run = false,
lsp_fallback = false,
quiet = false,
})
@@ -379,7 +381,7 @@ M.format = function(opts, callback)
if not opts.range and mode == "v" or mode == "V" then
opts.range = range_from_selection(opts.bufnr, mode)
end
- callback = callback or function(_err) end
+ callback = callback or function(_err, _did_edit) end
local errors = require("conform.errors")
local log = require("conform.log")
local lsp_format = require("conform.lsp_format")
@@ -403,7 +405,8 @@ M.format = function(opts, callback)
if any_formatters then
---@param err? conform.Error
- local function handle_err(err)
+ ---@param did_edit? boolean
+ local function handle_result(err, did_edit)
if err then
local level = errors.level_for_code(err.code)
log.log(level, err.message)
@@ -426,22 +429,25 @@ M.format = function(opts, callback)
return callback(err_message)
end
- if
+ if opts.dry_run and did_edit then
+ callback(nil, true)
+ elseif
opts.lsp_fallback == "always" 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))
lsp_format.format(opts, callback)
else
- callback()
+ callback(nil, did_edit)
end
end
- local run_opts = { exclusive = true }
+ local run_opts = { exclusive = true, dry_run = opts.dry_run }
if opts.async then
- runner.format_async(opts.bufnr, formatters, opts.range, run_opts, handle_err)
+ runner.format_async(opts.bufnr, formatters, opts.range, run_opts, handle_result)
else
- local err = runner.format_sync(opts.bufnr, formatters, opts.timeout_ms, opts.range, run_opts)
- handle_err(err)
+ local err, did_edit =
+ runner.format_sync(opts.bufnr, formatters, opts.timeout_ms, opts.range, run_opts)
+ handle_result(err, did_edit)
end
return true
elseif opts.lsp_fallback and not vim.tbl_isempty(lsp_format.get_format_clients(opts)) then
@@ -496,7 +502,7 @@ M.format_lines = function(formatter_names, lines, opts, callback)
callback(err, new_lines)
end
- local run_opts = { exclusive = false }
+ local run_opts = { exclusive = false, dry_run = false }
if opts.async then
runner.format_lines_async(opts.bufnr, formatters, nil, lines, run_opts, handle_err)
else
diff --git a/lua/conform/lsp_format.lua b/lua/conform/lsp_format.lua
index cd6e429..f49e15f 100644
--- a/lua/conform/lsp_format.lua
+++ b/lua/conform/lsp_format.lua
@@ -4,7 +4,7 @@ local util = require("vim.lsp.util")
local M = {}
-local function apply_text_edits(text_edits, bufnr, offset_encoding)
+local function apply_text_edits(text_edits, bufnr, offset_encoding, dry_run)
if
#text_edits == 1
and text_edits[1].range.start.line == 0
@@ -19,9 +19,19 @@ local function apply_text_edits(text_edits, bufnr, offset_encoding)
table.remove(new_lines)
end
log.debug("Converting full-file LSP format to piecewise format")
- require("conform.runner").apply_format(bufnr, original_lines, new_lines, nil, false)
+ return require("conform.runner").apply_format(
+ bufnr,
+ original_lines,
+ new_lines,
+ nil,
+ false,
+ dry_run
+ )
+ elseif dry_run then
+ return #text_edits > 0
else
vim.lsp.util.apply_text_edits(text_edits, bufnr, offset_encoding)
+ return #text_edits > 0
end
end
@@ -56,7 +66,7 @@ function M.get_format_clients(options)
end
---@param options table
----@param callback fun(err?: string)
+---@param callback fun(err?: string, did_edit?: boolean)
function M.format(options, callback)
options = options or {}
if not options.bufnr or options.bufnr == 0 then
@@ -84,9 +94,10 @@ function M.format(options, callback)
if options.async then
local changedtick = vim.b[bufnr].changedtick
local do_format
+ local did_edit = false
do_format = function(idx, client)
if not client then
- return callback()
+ return callback(nil, did_edit)
end
local params = set_range(client, util.make_formatting_params(options.formatting_options))
local auto_id = vim.api.nvim_create_autocmd("LspDetach", {
@@ -112,21 +123,35 @@ function M.format(options, callback)
)
)
else
- apply_text_edits(result, ctx.bufnr, client.offset_encoding)
+ local this_did_edit =
+ apply_text_edits(result, ctx.bufnr, client.offset_encoding, options.dry_run)
changedtick = vim.b[bufnr].changedtick
- do_format(next(clients, idx))
+ if options.dry_run and this_did_edit then
+ callback(nil, true)
+ else
+ did_edit = did_edit or this_did_edit
+ do_format(next(clients, idx))
+ end
end
end, bufnr)
end
do_format(next(clients))
else
local timeout_ms = options.timeout_ms or 1000
+ local did_edit = false
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)
+ local this_did_edit =
+ apply_text_edits(result.result, bufnr, client.offset_encoding, options.dry_run)
+ did_edit = did_edit or this_did_edit
+
+ if options.dry_run and did_edit then
+ callback(nil, true)
+ return true
+ end
elseif err then
if not options.quiet then
vim.notify(string.format("[LSP][%s] %s", client.name, err), vim.log.levels.WARN)
@@ -134,7 +159,7 @@ function M.format(options, callback)
return callback(string.format("[LSP][%s] %s", client.name, err))
end
end
- callback()
+ callback(nil, did_edit)
end
end
diff --git a/lua/conform/runner.lua b/lua/conform/runner.lua
index 0f4604d..3d9df8b 100644
--- a/lua/conform/runner.lua
+++ b/lua/conform/runner.lua
@@ -7,6 +7,7 @@ local M = {}
---@class (exact) conform.RunOpts
---@field exclusive boolean If true, ensure only a single formatter is running per buffer
+---@field dry_run boolean If true, do not apply changes and stop after the first formatter attempts to do so
---@param formatter_name string
---@param ctx conform.Context
@@ -152,9 +153,10 @@ end
---@param new_lines string[]
---@param range? conform.Range
---@param only_apply_range boolean
-M.apply_format = function(bufnr, original_lines, new_lines, range, only_apply_range)
+---@return boolean any_changes
+M.apply_format = function(bufnr, original_lines, new_lines, range, only_apply_range, dry_run)
if not vim.api.nvim_buf_is_valid(bufnr) then
- return
+ return false
end
local bufname = vim.api.nvim_buf_get_name(bufnr)
log.trace("Applying formatting to %s", bufname)
@@ -173,7 +175,7 @@ M.apply_format = function(bufnr, original_lines, new_lines, range, only_apply_ra
-- This is to hack around oddly behaving formatters (e.g black outputs nothing for excluded files).
if new_text:match("^%s*$") and not original_text:match("^%s*$") then
log.warn("Aborting because a formatter returned empty output for buffer %s", bufname)
- return
+ return false
end
log.trace("Comparing lines %s and %s", original_lines, new_lines)
@@ -228,9 +230,13 @@ M.apply_format = function(bufnr, original_lines, new_lines, range, only_apply_ra
end
end
- log.trace("Applying text edits: %s", text_edits)
- vim.lsp.util.apply_text_edits(text_edits, bufnr, "utf-8")
- log.trace("Done formatting %s", bufname)
+ if not dry_run then
+ log.trace("Applying text edits: %s", text_edits)
+ vim.lsp.util.apply_text_edits(text_edits, bufnr, "utf-8")
+ log.trace("Done formatting %s", bufname)
+ end
+
+ return not vim.tbl_isempty(text_edits)
end
---Map of formatter name to if the last run of that formatter produced an error
@@ -452,7 +458,7 @@ end
---@param formatters conform.FormatterInfo[]
---@param range? conform.Range
---@param opts conform.RunOpts
----@param callback fun(err?: conform.Error)
+---@param callback fun(err?: conform.Error, did_edit?: boolean)
M.format_async = function(bufnr, formatters, range, opts, callback)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
@@ -475,6 +481,7 @@ M.format_async = function(bufnr, formatters, range, opts, callback)
original_lines,
opts,
function(err, output_lines, all_support_range_formatting)
+ local did_edit = nil
-- discard formatting if buffer has changed
if not vim.api.nvim_buf_is_valid(bufnr) or changedtick ~= util.buf_get_changedtick(bufnr) then
err = {
@@ -485,9 +492,16 @@ M.format_async = function(bufnr, formatters, range, opts, callback)
),
}
else
- M.apply_format(bufnr, original_lines, output_lines, range, not all_support_range_formatting)
+ did_edit = M.apply_format(
+ bufnr,
+ original_lines,
+ output_lines,
+ range,
+ not all_support_range_formatting,
+ opts.dry_run
+ )
end
- callback(err)
+ callback(err, did_edit)
end
)
end
@@ -534,6 +548,7 @@ end
---@param range? conform.Range
---@param opts conform.RunOpts
---@return conform.Error? error
+---@return boolean did_edit
M.format_sync = function(bufnr, formatters, timeout_ms, range, opts)
if bufnr == 0 then
bufnr = vim.api.nvim_get_current_buf()
@@ -551,8 +566,15 @@ M.format_sync = function(bufnr, formatters, timeout_ms, range, opts)
local err, final_result, all_support_range_formatting =
M.format_lines_sync(bufnr, formatters, timeout_ms, range, original_lines, opts)
- M.apply_format(bufnr, original_lines, final_result, range, not all_support_range_formatting)
- return err
+ local did_edit = M.apply_format(
+ bufnr,
+ original_lines,
+ final_result,
+ range,
+ not all_support_range_formatting,
+ opts.dry_run
+ )
+ return err, did_edit
end
---@param bufnr integer
diff --git a/tests/runner_spec.lua b/tests/runner_spec.lua
index 0c45117..8c0dadf 100644
--- a/tests/runner_spec.lua
+++ b/tests/runner_spec.lua
@@ -343,6 +343,11 @@ print("a")
assert.are.same(lines, vim.api.nvim_buf_get_lines(0, 0, -1, false))
end)
+ it("does not change output if dry_run is true", function()
+ run_formatter("hello", "foo", { dry_run = true })
+ assert.are.same({ "hello" }, vim.api.nvim_buf_get_lines(0, 0, -1, false))
+ end)
+
describe("range formatting", function()
it("applies edits that overlap the range start", function()
run_formatter(