Skip to content

Commit 8ec6d56

Browse files
committed
chore: rework nix_flake_fmt to use the new nix formatter subcommand
Core `nix` accepted [my PR to add a new `nix formatter build` command](NixOS/nix#13063)! That means we can (eventually) get rid of this huge mess of code. I'm excited to try out the new feature, so I (unfortunately) made things even more complicated by adding a "new" and and "old" codepath. I've structured things so somebody with no knowledge of this code could remove the legacy codepath in the future.
1 parent a49f5a7 commit 8ec6d56

1 file changed

Lines changed: 99 additions & 42 deletions

File tree

lua/null-ls/builtins/formatting/nix_flake_fmt.lua

Lines changed: 99 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,8 @@ local log = require("null-ls.logger")
44
local client = require("null-ls.client")
55

66
local FORMATTING = methods.internal.FORMATTING
7-
local NOTIFICATION_TITLE = "discovering `nix fmt` entrypoint"
8-
local NOTIFICATION_TOKEN = "nix-flake-fmt-discovery"
9-
10-
--- Asynchronously computes the command that `nix fmt` would run, or nil if
11-
--- we're not in a flake with a formatter, or if we fail to discover the
12-
--- formatter somehow. When finished, it invokes the `done` callback with a
13-
--- single string|nil parameter identifier the `nix fmt` entrypoint if found.
14-
---
15-
--- The formatter must follow treefmt's [formatter
16-
--- spec](https://github.com/numtide/treefmt/blob/main/docs/formatter-spec.md).
17-
---
18-
--- This basically re-implements the "entrypoint discovery" that `nix fmt` does.
19-
--- So why are we doing this ourselves rather than just invoking `nix fmt`?
20-
--- Unfortunately, it can take a few moments to evaluate all your nix code to
21-
--- figure out the formatter entrypoint. It can even be slow enough to exceed
22-
--- Neovim's default LSP timeout.
23-
--- By doing this ourselves, we can cache the result.
24-
local find_nix_fmt = function(opts, done)
25-
done = vim.schedule_wrap(done)
267

8+
local run_job = function(opts)
279
local async = require("plenary.async")
2810
local Job = require("plenary.job")
2911

@@ -35,12 +17,30 @@ local find_nix_fmt = function(opts, done)
3517
Job:new(_opts):start()
3618
end, 2)
3719

38-
local tmpname = async.wrap(function(_done)
20+
return run_job(opts)
21+
end
22+
23+
local tmpname = function()
24+
local async = require("plenary.async")
25+
26+
local mktemp = async.wrap(function(_done)
3927
vim.defer_fn(function()
4028
_done(vim.fn.tempname())
4129
end, 0)
4230
end, 1)
31+
return mktemp()
32+
end
4333

34+
--- Asynchronously build and return the formatter for the flake located at {root},
35+
--- If {root} is not a flake, or does not have a formatter, or we cannot build the formatter, return `nil`.
36+
--- This legacy codepath is quite complicated, and unnecessary now that `nix` has core support for
37+
--- returning the fromatter command.
38+
--- TODO: remove after the `nix formatter` subcommand has been released for a while.
39+
--- The command was introduced in https://github.com/NixOS/nix/commit/d155bb901241441149c701b9efc92f5785c2e1c3
40+
---
41+
--- @param root string
42+
--- @return string|nil
43+
local legacy_find_nix_fmt = function(root)
4444
local get_current_system = function()
4545
local status, stdout_lines, stderr_lines = run_job({
4646
command = "nix",
@@ -141,12 +141,6 @@ local find_nix_fmt = function(opts, done)
141141
builtins.toJSON result
142142
]]
143143

144-
client.send_progress_notification(NOTIFICATION_TOKEN, {
145-
kind = "report",
146-
title = NOTIFICATION_TITLE,
147-
message = "evaluating",
148-
})
149-
150144
local status, stdout_lines, stderr_lines = run_job({
151145
command = "nix",
152146
args = {
@@ -218,34 +212,97 @@ local find_nix_fmt = function(opts, done)
218212
return true
219213
end
220214

215+
local drv_path, nix_fmt_path = evaluate_flake_formatter(root)
216+
if drv_path == nil then
217+
return nil
218+
end
219+
220+
-- Build the derivation. This ensures that `nix_fmt_path` exists.
221+
if not build_derivation({ drv = drv_path, out_link = tmpname() }) then
222+
return nil
223+
end
224+
225+
return nix_fmt_path
226+
end
227+
228+
local nix_has_formatter_subcommand = function()
229+
local status, _, _ = run_job({
230+
command = "nix",
231+
args = {
232+
"--extra-experimental-features",
233+
"nix-command flakes",
234+
"formatter",
235+
"--help",
236+
},
237+
})
238+
239+
return status == 0
240+
end
241+
242+
--- Asynchronously computes the command that `nix fmt` would run, or nil if
243+
--- we're not in a flake with a formatter, or if we fail to discover the
244+
--- formatter somehow. When finished, it invokes the `done` callback with a
245+
--- single string|nil parameter identifier the `nix fmt` entrypoint if found.
246+
---
247+
--- The formatter must follow treefmt's [formatter
248+
--- spec](https://github.com/numtide/treefmt/blob/main/docs/formatter-spec.md).
249+
---
250+
--- This basically re-implements the "entrypoint discovery" that `nix fmt` does.
251+
--- So why are we doing this ourselves rather than just invoking `nix fmt`?
252+
--- Unfortunately, it can take a few moments to evaluate all your nix code to
253+
--- figure out the formatter entrypoint. It can even be slow enough to exceed
254+
--- Neovim's default LSP timeout.
255+
--- By doing this ourselves, we can cache the result.
256+
local find_nix_fmt = function(opts, done)
257+
done = vim.schedule_wrap(done)
258+
259+
local async = require("plenary.async")
260+
261+
local notification_title = "discovering `nix fmt` entrypoint"
262+
local notification_token = "nix-flake-fmt-discovery"
263+
221264
async.run(function()
222-
client.send_progress_notification(NOTIFICATION_TOKEN, {
265+
client.send_progress_notification(notification_token, {
223266
kind = "begin",
224-
title = NOTIFICATION_TITLE,
267+
title = notification_title,
225268
})
226269

227270
local _done = function(result)
228271
done(result)
229-
client.send_progress_notification(NOTIFICATION_TOKEN, {
272+
client.send_progress_notification(notification_token, {
230273
kind = "end",
231-
title = NOTIFICATION_TITLE,
274+
title = notification_title,
232275
message = "done",
233276
})
234277
end
235278

236-
local drv_path, nix_fmt_path = evaluate_flake_formatter(opts.root)
237-
if drv_path == nil then
238-
return _done(nil)
239-
end
279+
local nix_fmt_path ---@type string|nil
280+
local is_legacy = not nix_has_formatter_subcommand()
281+
if is_legacy then
282+
nix_fmt_path = legacy_find_nix_fmt()
283+
else
284+
local status, stdout_lines, stderr_lines = run_job({
285+
command = "nix",
286+
args = {
287+
"--extra-experimental-features",
288+
"nix-command",
289+
"formatter",
290+
"build",
291+
"--out-link",
292+
tmpname(),
293+
},
294+
})
240295

241-
-- Build the derivation. This ensures that `nix_fmt_path` exists.
242-
client.send_progress_notification(NOTIFICATION_TOKEN, {
243-
kind = "report",
244-
title = NOTIFICATION_TITLE,
245-
message = "building",
246-
})
247-
if not build_derivation({ drv = drv_path, out_link = tmpname() }) then
248-
return _done(nil)
296+
if status ~= 0 then
297+
local stderr = table.concat(stderr_lines, "\n")
298+
vim.defer_fn(function()
299+
log:warn(string.format("unable to build 'nix fmt' entrypoint. stderr: %s", stderr))
300+
end, 0)
301+
return false
302+
end
303+
304+
local stdout = table.concat(stdout_lines, "\n")
305+
nix_fmt_path = stdout
249306
end
250307

251308
return _done(nix_fmt_path)

0 commit comments

Comments
 (0)