diff --git a/lua/null-ls/builtins/formatting/nix_flake_fmt.lua b/lua/null-ls/builtins/formatting/nix_flake_fmt.lua index 907077d2..0e579ee1 100644 --- a/lua/null-ls/builtins/formatting/nix_flake_fmt.lua +++ b/lua/null-ls/builtins/formatting/nix_flake_fmt.lua @@ -4,30 +4,12 @@ local log = require("null-ls.logger") local client = require("null-ls.client") local FORMATTING = methods.internal.FORMATTING -local NOTIFICATION_TITLE = "discovering `nix fmt` entrypoint" -local NOTIFICATION_TOKEN = "nix-flake-fmt-discovery" - ---- Asynchronously computes the command that `nix fmt` would run, or nil if ---- we're not in a flake with a formatter, or if we fail to discover the ---- formatter somehow. When finished, it invokes the `done` callback with a ---- single string|nil parameter identifier the `nix fmt` entrypoint if found. ---- ---- The formatter must follow treefmt's [formatter ---- spec](https://github.com/numtide/treefmt/blob/main/docs/formatter-spec.md). ---- ---- This basically re-implements the "entrypoint discovery" that `nix fmt` does. ---- So why are we doing this ourselves rather than just invoking `nix fmt`? ---- Unfortunately, it can take a few moments to evaluate all your nix code to ---- figure out the formatter entrypoint. It can even be slow enough to exceed ---- Neovim's default LSP timeout. ---- By doing this ourselves, we can cache the result. -local find_nix_fmt = function(opts, done) - done = vim.schedule_wrap(done) +local run_job = function(opts) local async = require("plenary.async") local Job = require("plenary.job") - local run_job = async.wrap(function(_opts, _done) + local _run_job = async.wrap(function(_opts, _done) _opts.on_exit = function(j, status) _done(status, j:result(), j:stderr_result()) end @@ -35,12 +17,30 @@ local find_nix_fmt = function(opts, done) Job:new(_opts):start() end, 2) - local tmpname = async.wrap(function(_done) + return _run_job(opts) +end + +local tmpname = function() + local async = require("plenary.async") + + local mktemp = async.wrap(function(_done) vim.defer_fn(function() _done(vim.fn.tempname()) end, 0) end, 1) + return mktemp() +end +--- Asynchronously build and return the formatter for the flake located at {root}, +--- If {root} is not a flake, or does not have a formatter, or we cannot build the formatter, return `nil`. +--- This legacy codepath is quite complicated, and unnecessary now that `nix` has core support for +--- returning the fromatter command. +--- TODO: remove after the `nix formatter` subcommand has been released for a while. +--- The command was introduced in https://github.com/NixOS/nix/commit/d155bb901241441149c701b9efc92f5785c2e1c3 +--- +--- @param root string +--- @return string|nil +local legacy_find_nix_fmt = function(root) local get_current_system = function() local status, stdout_lines, stderr_lines = run_job({ command = "nix", @@ -65,7 +65,7 @@ local find_nix_fmt = function(opts, done) return nix_current_system end - local get_flake_ref = function(root) + local get_flake_ref = function(_root) local status, stdout_lines, stderr_lines = run_job({ command = "nix", args = { @@ -74,7 +74,7 @@ local find_nix_fmt = function(opts, done) "flake", "metadata", "--json", - root, + _root, }, }) @@ -101,12 +101,12 @@ local find_nix_fmt = function(opts, done) return flake_ref end - local evaluate_flake_formatter = function(root) + local evaluate_flake_formatter = function(_root) local nix_current_system = get_current_system() if nix_current_system == nil then return end - local flake_ref = get_flake_ref(root) + local flake_ref = get_flake_ref(_root) local eval_nix_formatter = [[ let system = "]] .. nix_current_system .. [["; @@ -141,12 +141,6 @@ local find_nix_fmt = function(opts, done) builtins.toJSON result ]] - client.send_progress_notification(NOTIFICATION_TOKEN, { - kind = "report", - title = NOTIFICATION_TITLE, - message = "evaluating", - }) - local status, stdout_lines, stderr_lines = run_job({ command = "nix", args = { @@ -218,34 +212,98 @@ local find_nix_fmt = function(opts, done) return true end + local drv_path, nix_fmt_path = evaluate_flake_formatter(root) + if drv_path == nil then + return nil + end + + -- Build the derivation. This ensures that `nix_fmt_path` exists. + if not build_derivation({ drv = drv_path, out_link = tmpname() }) then + return nil + end + + return nix_fmt_path +end + +local nix_has_formatter_subcommand = function() + local status, _, _ = run_job({ + command = "nix", + args = { + "--extra-experimental-features", + "nix-command flakes", + "formatter", + "--help", + }, + }) + + return status == 0 +end + +--- Asynchronously computes the command that `nix fmt` would run, or nil if +--- we're not in a flake with a formatter, or if we fail to discover the +--- formatter somehow. When finished, it invokes the `done` callback with a +--- single string|nil parameter identifier the `nix fmt` entrypoint if found. +--- +--- The formatter must follow treefmt's [formatter +--- spec](https://github.com/numtide/treefmt/blob/main/docs/formatter-spec.md). +--- +--- This basically re-implements the "entrypoint discovery" that `nix fmt` does. +--- So why are we doing this ourselves rather than just invoking `nix fmt`? +--- Unfortunately, it can take a few moments to evaluate all your nix code to +--- figure out the formatter entrypoint. It can even be slow enough to exceed +--- Neovim's default LSP timeout. +--- By doing this ourselves, we can cache the result. +local find_nix_fmt = function(opts, done) + done = vim.schedule_wrap(done) + + local async = require("plenary.async") + + local notification_title = "discovering `nix fmt` entrypoint" + local notification_token = "nix-flake-fmt-discovery" + async.run(function() - client.send_progress_notification(NOTIFICATION_TOKEN, { + client.send_progress_notification(notification_token, { kind = "begin", - title = NOTIFICATION_TITLE, + title = notification_title, }) local _done = function(result) done(result) - client.send_progress_notification(NOTIFICATION_TOKEN, { + client.send_progress_notification(notification_token, { kind = "end", - title = NOTIFICATION_TITLE, + title = notification_title, message = "done", }) end - local drv_path, nix_fmt_path = evaluate_flake_formatter(opts.root) - if drv_path == nil then - return _done(nil) - end + local nix_fmt_path ---@type string|nil + local is_legacy = not nix_has_formatter_subcommand() + if is_legacy then + nix_fmt_path = legacy_find_nix_fmt(opts.root) + else + local status, stdout_lines, stderr_lines = run_job({ + command = "nix", + args = { + "--extra-experimental-features", + "nix-command", + "formatter", + "build", + "--out-link", + tmpname(), + }, + cwd = opts.root, + }) - -- Build the derivation. This ensures that `nix_fmt_path` exists. - client.send_progress_notification(NOTIFICATION_TOKEN, { - kind = "report", - title = NOTIFICATION_TITLE, - message = "building", - }) - if not build_derivation({ drv = drv_path, out_link = tmpname() }) then - return _done(nil) + if status ~= 0 then + local stderr = table.concat(stderr_lines, "\n") + vim.defer_fn(function() + log:warn(string.format("unable to build 'nix fmt' entrypoint. stderr: %s", stderr)) + end, 0) + return false + end + + local stdout = table.concat(stdout_lines, "\n") + nix_fmt_path = stdout end return _done(nix_fmt_path)