Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "nix flake fmt" builtin formatter #192

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft

Conversation

jfly
Copy link
Collaborator

@jfly jfly commented Oct 11, 2024

This implements a builtin formatter that (essentially) invokes nix fmt on the given buffer. I called it "nix flake fmt", even though the underlying command is nix fmt, because I didn't want people to confuse it with none-ls's pre-existing nixfmt builtin formatter. Naming is hard.

This was pretty tricky to build. Issues I ran into:

  • nix fmt is slow enough that it's unbearable to run every time you save a buffer. The slowness is the time spent evaluating all the relevant nix code each time, so I opted to cache the result.
    • I wanted to cache this per project rather than per buffer, so I added a new by_bufroot cache helper (inspired by the existing by_bufnr cache helper). That's the first commit in this PR, and could be merged up independently, but it might be silly to do so without any builtins using it.
    • The underlying nix eval, nix build, etc implementation makes me cry 😭. I'm hoping a Nix expert swoops in and shows me a better way. Or, perhaps we can use this as motivation to add a feature to the nix cli to make this easier. I have a few ideas about things that could make this simpler.
  • The current implementation assumes that the nix fmt entrypoint takes a --walk=filesystem parameter. That's a bogus assumption. I've left a comment in the code explaining how I think we should fix this (it'll require a change to treefmt itself). I'd like to address this before we merge this PR up, so I've marked this as a draft.

Comment on lines 124 to 142
-- `--walk` is specific to treefmt, and this formatter is supposed
-- to be generic.
-- Note: this could get converted to the new `TREEFMT_WALK`
-- environment variable once
-- <https://github.com/numtide/treefmt/commit/bac4a0d102e1142406d3a7d15106e5ba108bfcf8>
-- is fixed, which would at least play nicely with other types of
-- `nix fmt` entrypoints.
--
-- However, IMO, the real fix is to change treefmt itself to be
-- willing to format files passed explicitly, even if they're
-- gitignored:
-- https://github.com/numtide/treefmt/issues/435
"--walk=filesystem",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, this is the thing I'd like to figure out before we merge this PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized I said "gitignored" in this comment. What I really meant was "even if they're not tracked by git".

Copy link
Collaborator Author

@jfly jfly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, @Mic92!

lua/null-ls/builtins/formatting/nix_flake_fmt.lua Outdated Show resolved Hide resolved
lua/null-ls/builtins/formatting/nix_flake_fmt.lua Outdated Show resolved Hide resolved
lua/null-ls/builtins/formatting/nix_flake_fmt.lua Outdated Show resolved Hide resolved
lua/null-ls/helpers/cache.lua Show resolved Hide resolved
@jfly jfly force-pushed the add-nix-fmt branch 2 times, most recently from 3ba73f7 to ec16dd4 Compare October 11, 2024 21:31
@Mic92
Copy link

Mic92 commented Oct 12, 2024

I actually just learned that neovim seems to have a tempdir with automatic cleanup already builtin. So all you have to do is doing this once and save it in temporary variable:

tmp_dir = vim.fn.tempname()

And pass this as an argument for the gcroot.
When vim terminates this directory is removed again.

This logic seems much easier than having to check store paths with nix-store and is also more sound.

I'm building a [`nix fmt`](https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-fmt) formatter.

The tricky thing about `nix fmt` is that for nontrivial projects,
the entrypoint (`nix fmt`) can be slow enough to be a bad experience to
run on every save, and can even trigger [neovim's lsp
timeout](https://github.com/nvimtools/none-ls.nvim?tab=readme-ov-file#i-am-seeing-a-formatting-timeout-error-message).
It's not the that underlying formatter it invokes are slow, it's that it
can take 1-2 seconds for nix to evaluate the `flake.nix` and determine
what command `nix fmt` should run under the hood.

Since the underlying command shouldn't change very often for a given
project, I feel like it would be reasonable to cache this result per
project, hence this new `by_bufroot` callback.
This uses `nix fmt` under the hood. I introduced some caching make it
performant, as `nix fmt` can take a little while to evaluate all your
nix code to figure out the formatter entrypoint.
This protects us against garbage collection so long as vim is running.
Comment on lines +77 to +93
-- Dirty hack to check if the flake actually defines a formatter.
--
-- I cannot for the *life* of me figure out a less hacky way of
-- checking if a flake defines a formatter. Things I've tried:
--
-- - `nix eval . --apply '...'`: This doesn't not give me the flake
-- itself, it gives me the default package.
-- - `builtins.getFlake`: Every incantation I've tried requires
-- `--impure`, which has the performance downside described above.
-- - `nix flake show --json .`: This works, but it can be quite slow:
-- we end up evaluating all outputs, which can take a while for
-- `nixosConfigurations`.
if cp.stderr:find("error: flake .+ does not provide attribute .+ or 'formatter'") then
log:warn("this flake does not define a `nix fmt` entrypoint")
else
log:error(string.format("unable discover 'nix fmt' command. stderr: %s", cp.stderr))
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know of any better way to check if a flake exposes an attr. but can't we just forward the error as warning in any case this fails and hope that nix provides a good enough warning so we don't need to handle it explicitly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could, but how would I choose the appropriate log level (warn vs error)?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably warn everytime?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants