With the introduction of vim.system in neovim 0.10, it is now easy to write custom formatting logic. Let's work through an example of how you could do so.
prettierd does not work well with guard because of it's error mechanism. Guard follows a reasonable unix standard when it comes to determining exit status, that is, assuming the program would exit with non-zero exit code and print some reasonable error message in stderr:
if exit_code ~= 0 and num_stderr_chunks ~= 0 then
-- failed
else
-- success
endHowever, prettierd prints error messages to stdout, so guard will fail to detect an error and proceed to replace your code with its error message 😢
But fear not! You can create your custom logic by passing a function in the config table, let's do this step by step:
local function prettierd_fmt(buf, range, acc)
local co = assert(coroutine.running())
endGuard runs the format function in a coroutine so as not to block the UI, to achieve what we want we have to interact with the current coroutine.
We can now go on to mimic how we would call prettierd on the cmdline:
cat test.js | prettierd test.js
-- previous code omitted
local handle = vim.system({ "prettierd", vim.api.nvim_buf_get_name(buf) }, {
stdin = true,
}, function(result)
if result.code ~= 0 then
-- "returns" the error
coroutine.resume(co, result)
else
-- "returns" the result
coroutine.resume(co, result.stdout)
end
end)We get the unformatted code, then call vim.system with 3 arguments
- the cmd, which is of the form
prettierd <file> - the option table, here we only specified that we wish to write to its stdin, but you can refer to
:h vim.systemfor more options - the
at_exitfunction, which takes in a result table (again, check out:h vim.systemfor more details)
Now we can do our custom error handling, here we simply return if prettierd failed. But if it succeeded we replace the range with the formatted code and save the file.
Finally we write the unformatted code to stdin
-- previous code omitted
handle:write(acc)
handle:write(nil) -- closes stdin
return coroutine.yield() -- this returns either the error or the formatted code we returned earlierWhoa la! Now we can tell guard to register our formatting function
ft("javascript"):fmt({
fn = prettierd_fmt
})Screencast_20240613_111632.mp4
You can always refer to spawn.lua.
clippy-driver is a linter for rust, because it prints diagnostics to stderr, you cannot just specify it the usual way. But guard allows you to pass a custom function, which would make it work :)
Let's start by doing some imports:
local ft = require("guard.filetype")
local lint = require("guard.lint")The lint function is a simple modification of the one in spawn.lua.
local function clippy_driver_lint(acc)
local co = assert(coroutine.running())
local handle = vim.system({ "clippy-driver", "-", "--error-format=json", "--edition=2021" }, {
stdin = true,
}, function(result)
-- wake coroutine on exit, omit error checking
coroutine.resume(co, result.stderr)
end)
-- write contents to stdin and close it
handle:write(acc)
handle:write(nil)
-- sleep until awakened after process finished
return coroutine.yield()
endWe register it via guard:
ft("rust"):lint({
fn = clippy_driver_lint,
stdin = true,
parse = clippy_diagnostic_parse, -- TODO!
})To write the lint function, we inspect the output of clippy-driver when called with the arguments above:
full output
❯ cat test.rs
fn main() {
let _ = 'a' .. 'z';
if 42 > i32::MAX {}
}
❯ cat test.rs | clippy-driver - --error-format=json --edition=2021
{"$message_type":"diagnostic","message":"almost complete ascii range","code":{"code":"clippy::almost_complete_range","explanation":null},"level":"warning","spans":[{"file_name":"<anon>","byte_start":24,"byte_end":34,"line_start":2,"line_end":2,"column_start":13,"column_end":23,"is_primary":true,"text":[{"text":" let _ = 'a' .. 'z';","highlight_start":13,"highlight_end":23}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#almost_complete_range","code":null,"level":"help","spans":[],"children":[],"rendered":null},{"message":"`#[warn(clippy::almost_complete_range)]` on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null},{"message":"use an inclusive range","code":null,"level":"help","spans":[{"file_name":"<anon>","byte_start":28,"byte_end":30,"line_start":2,"line_end":2,"column_start":17,"column_end":19,"is_primary":true,"text":[{"text":" let _ = 'a' .. 'z';","highlight_start":17,"highlight_end":19}],"label":null,"suggested_replacement":"..=","suggestion_applicability":"MaybeIncorrect","expansion":null}],"children":[],"rendered":null}],"rendered":"warning: almost complete ascii range\n --> <anon>:2:13\n |\n2 | let _ = 'a' .. 'z';\n | ^^^^--^^^^\n | |\n | help: use an inclusive range: `..=`\n |\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#almost_complete_range\n = note: `#[warn(clippy::almost_complete_range)]` on by default\n\n"}
{"$message_type":"diagnostic","message":"this comparison involving the minimum or maximum element for this type contains a case that is always true or always false","code":{"code":"clippy::absurd_extreme_comparisons","explanation":null},"level":"error","spans":[{"file_name":"<anon>","byte_start":43,"byte_end":56,"line_start":3,"line_end":3,"column_start":8,"column_end":21,"is_primary":true,"text":[{"text":" if 42 > i32::MAX {}","highlight_start":8,"highlight_end":21}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[{"message":"because `i32::MAX` is the maximum value for this type, this comparison is always false","code":null,"level":"help","spans":[],"children":[],"rendered":null},{"message":"for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#absurd_extreme_comparisons","code":null,"level":"help","spans":[],"children":[],"rendered":null},{"message":"`#[deny(clippy::absurd_extreme_comparisons)]` on by default","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"error: this comparison involving the minimum or maximum element for this type contains a case that is always true or always false\n --> <anon>:3:8\n |\n3 | if 42 > i32::MAX {}\n | ^^^^^^^^^^^^^\n |\n = help: because `i32::MAX` is the maximum value for this type, this comparison is always false\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#absurd_extreme_comparisons\n = note: `#[deny(clippy::absurd_extreme_comparisons)]` on by default\n\n"}
{"$message_type":"diagnostic","message":"aborting due to 1 previous error; 1 warning emitted","code":null,"level":"error","spans":[],"children":[],"rendered":"error: aborting due to 1 previous error; 1 warning emitted\n\n"}
That's a lot of output! But we can see three main blocks: the first two diagnostics and the last one an overview. We only need the first two:
local clippy_diagnostic_parse = lint.from_json({
get_diagnostics = function(line)
local json = vim.json.decode(line)
-- ignore overview json which does not have position info
if not vim.tbl_isempty(json.spans) then
return json
end
end,
lines = true,
attributes = { ... } -- TODO
})Now our diagnostics are transformed into a list of json, we just need to get the attributes we need: the positions, the message and the error level. That's what the attributes field does, it extracts them from the json table:
attributes = {
-- it is json turned into a lua table
lnum = function(it)
return math.ceil(tonumber(it.spans[1].line_start))
end,
lnum_end = function(it)
return math.ceil(tonumber(it.spans[1].line_end))
end,
code = function(it)
return it.code.code
end,
col = function(it)
return it.spans[1].column_start
end,
col_end = function(it)
return it.spans[1].column_end
end,
severity = "level", -- "it.level"
message = "message", -- "it.message"
},Et voilà!
When writing custom lint logic, fn and parse receive buffer context:
lint.fn(prev_lines, fname, cwd)
lint.parse(result, bufnr, fname, cwd)Parameters:
prev_lines— buffer content (string)result— linter output (string)bufnr— target buffer numberfname— absolute file pathcwd— working directory
Use fname and cwd when linters output relative paths (e.g., terraform validate on a directory returns diagnostics for all files).
Example — filtering terraform validate by current file:
parse = function(result, bufnr, fname, cwd)
local current = fname:sub(#cwd + 2) -- +2 to include '/'
local decoded = vim.json.decode(result)
for _, d in ipairs(decoded.diagnostics or {}) do
-- terraform returns relative filenames; match against current
if d.range and d.range.filename == current then
-- add to diagnostics...
end
end
endGuard exposes a GuardFmt user event that you can use. It is called both before formatting starts and after it is completely done. To differentiate between pre-format and post-format calls, a data table is passed.
-- for pre-format calls
data = {
status = "pending", -- type: string, called whenever a format is requested
using = {...} -- type: table, formatters that are going to run
}
-- for post-format calls
data = {
status = "done", -- type: string, only called on success
}
-- or
data = {
status = "failed" -- type: string, buffer remain unchanged
msg = "..." -- type: string, reason for failure
}A handy use case for it is to retrieve formatting status, here's a bare-bones example:
local is_formatting = false
_G.guard_status = function()
-- display icon if auto-format is enabled for current buffer
local au = vim.api.nvim_get_autocmds({
group = "Guard",
buffer = 0,
})
if filetype[vim.bo.ft] and #au ~= 0 then
return is_formatting and "" or ""
end
return ""
end
-- sets a super simple statusline when entering a buffer
vim.cmd("au BufEnter * lua vim.opt.stl = [[%f %m ]] .. guard_status()")
-- update statusline on GuardFmt event
vim.api.nvim_create_autocmd("User", {
pattern = "GuardFmt",
callback = function(opt)
-- receive data from opt.data
is_formatting = opt.data.status == "pending"
vim.opt.stl = [[%f %m ]] .. guard_status()
end,
})Screencast_20240613_111857.mp4
You can do the similar for your statusline plugin of choice as long as you "refresh" it on GuardFmt.
A formatter can be a function that returns a config, using this you can implement some pretty interesting behaviour, in #168, one of the users asked for a particular formatter to respect editor space/tab settings. It's achievable by using fn, but using function configs are easier and more straight forward, let's see how we can cook up the behaviour we want:
local ft = require("guard.filetype")
ft("c"):fmt(function()
if vim.uv.fs_stat(".clang-format") then
return {
cmd = "clang-format",
stdin = true,
}
else
return {
cmd = "clang-format",
args = {
("--style={BasedOnStyle: llvm, IndentWidth: %d, TabWidth: %d, UseTab: %s}"):format(
vim.bo.shiftwidth,
vim.bo.tabstop,
vim.bo.expandtab and "Never" or "Always"
),
},
stdin = true,
}
end
end)What we are doing here is try look for a .clang-format configuration file, if found, great, just use that. Otherwise, we want clang-format to respect the editor's settings, here I am using space/tab settings as an example, but you can easily tell that this can extend to anything.
By using this config, we get a formatting behaviour that respects our live input, you can run set sw=16 and the next time clang-format will give you a file that uses 16 spaces as indent (please do not try this at home 😄).
