Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 105 additions & 47 deletions lua/null-ls/builtins/formatting/nix_flake_fmt.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,43 @@ 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

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",
Expand All @@ -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 = {
Expand All @@ -74,7 +74,7 @@ local find_nix_fmt = function(opts, done)
"flake",
"metadata",
"--json",
root,
_root,
},
})

Expand All @@ -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 .. [[";
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
Expand Down