From 02a67920f2e64c33c661e7d20771d0b0ab124e34 Mon Sep 17 00:00:00 2001 From: aftix Date: Sat, 14 Dec 2024 16:15:42 -0600 Subject: [PATCH] nix_completer: Add action for completing attributes on flakes Fixes: https://github.com/carapace-sh/carapace-bin/issues/2374 This will only complete attributes for flakes that are in the user's registry or are a local path. This means that the flake eval cache should be very useful for these completions, and that carapace won't go out and fetch random flakes when the user is trying to tab complete. --- completers/nix_completer/cmd/action/flake.go | 105 +++++++++++++++++++ completers/nix_completer/cmd/build.go | 3 +- completers/nix_completer/cmd/develop.go | 3 +- pkg/actions/tools/nix/flake.go | 2 +- 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 completers/nix_completer/cmd/action/flake.go diff --git a/completers/nix_completer/cmd/action/flake.go b/completers/nix_completer/cmd/action/flake.go new file mode 100644 index 0000000000..88e827f59a --- /dev/null +++ b/completers/nix_completer/cmd/action/flake.go @@ -0,0 +1,105 @@ +package action + +import ( + "bytes" + "fmt" + "os" + "slices" + "strings" + + "github.com/carapace-sh/carapace" + "github.com/carapace-sh/carapace-bin/pkg/actions/tools/nix" +) + +// ActionFlakeRefs completes a full flake reference +// It will only complete attributes for local flakes or flakes +// in the flake registry. +// It takes in the subcommand being completed +// +// nixpkgs#hello +// .#foo +func ActionFlakeRefs(fullCmd []string) carapace.Action { + return carapace.ActionMultiPartsN("#", 2, func(c carapace.Context) carapace.Action { + switch len(c.Parts) { + case 0: + return nix.ActionFlakes().Suffix("#") + default: + return ActionFlakeAttributes(fullCmd, c.Parts[0], c.Value) + } + }) +} + +// ActionFlakeAttributes completes attributes on a flake +// Completions are only supplied for local flakes or flakes +// in the registry. +// +// hello +// packages.x86_64-linux.hello +func ActionFlakeAttributes(fullCmd []string, flake, flakePath string) carapace.Action { + return carapace.ActionCallback(func(c carapace.Context) carapace.Action { + // Get completions from the flake registry + c.Setenv("NIX_GET_COMPLETIONS", fmt.Sprint(len(fullCmd))) + completionArgs := append(fullCmd[1:], flake) + + var stdout, stderr bytes.Buffer + cmd := c.Command("nix", completionArgs...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return carapace.ActionValues() + } + + // Check if the given flake is in the registry or is a local path + // For some reason, nix adds \b to completion items + rawLines := strings.ReplaceAll(stdout.String(), "\r\b", "") + lines := strings.Split(rawLines, "\n") + + flakeInRegistry := slices.ContainsFunc(lines[1:], func(s string) bool { + return strings.Trim(s, " \t") == flake + }) + flakeIsLocal := directoryExists(flake) + + if !flakeInRegistry && !flakeIsLocal { + return carapace.ActionValues() + } + + // Get completions using the nix cli + stdout.Reset() + stderr.Reset() + + completionArgs = append(fullCmd[1:], flake+"#"+flakePath) + cmd = c.Command("nix", completionArgs...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return carapace.ActionValues() + } + + // Remove \r for consistency with line endings + // For some reason, nix adds \b to completion items + rawLines = strings.ReplaceAll(stdout.String(), "\r\b", "") + lines = strings.Split(rawLines, "\n") + + // The first line is always "attrs" + completionResults := lines[1:] + + // Remove the flake# prefix + for i := range completionResults { + completionResults[i] = strings.Trim(completionResults[i], " \t") + completionResults[i] = strings.TrimPrefix(completionResults[i], flake+"#") + } + + return carapace.ActionValues(completionResults...) + }) +} + +func directoryExists(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + + return info.IsDir() +} diff --git a/completers/nix_completer/cmd/build.go b/completers/nix_completer/cmd/build.go index b395efc680..b10fdb4f36 100644 --- a/completers/nix_completer/cmd/build.go +++ b/completers/nix_completer/cmd/build.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/carapace-sh/carapace" + "github.com/carapace-sh/carapace-bin/completers/nix_completer/cmd/action" "github.com/carapace-sh/carapace-bin/pkg/actions/tools/nix" "github.com/spf13/cobra" ) @@ -39,5 +40,5 @@ func init() { "profile": carapace.ActionFiles(), "reference-lock-file": carapace.ActionFiles("lock"), }) - carapace.Gen(buildCmd).PositionalCompletion(nix.ActionFlakes()) + carapace.Gen(buildCmd).PositionalCompletion(action.ActionFlakeRefs([]string{"nix", "build"})) } diff --git a/completers/nix_completer/cmd/develop.go b/completers/nix_completer/cmd/develop.go index 1724a7a0a0..0173096b4f 100644 --- a/completers/nix_completer/cmd/develop.go +++ b/completers/nix_completer/cmd/develop.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/carapace-sh/carapace" + "github.com/carapace-sh/carapace-bin/completers/nix_completer/cmd/action" "github.com/carapace-sh/carapace-bin/pkg/actions/os" "github.com/carapace-sh/carapace-bin/pkg/actions/tools/nix" "github.com/spf13/cobra" @@ -50,5 +51,5 @@ func init() { "reference-lock-file": carapace.ActionFiles("lock"), "unset": os.ActionEnvironmentVariables(), }) - carapace.Gen(developCmd).PositionalCompletion(nix.ActionDevShells()) + carapace.Gen(developCmd).PositionalCompletion(action.ActionFlakeRefs([]string{"nix", "develop"})) } diff --git a/pkg/actions/tools/nix/flake.go b/pkg/actions/tools/nix/flake.go index 2b5f360383..7e48d6e696 100644 --- a/pkg/actions/tools/nix/flake.go +++ b/pkg/actions/tools/nix/flake.go @@ -30,7 +30,7 @@ func ActionFlakes() carapace.Action { carapace.ActionDirectories(), carapace.ActionStyledValuesDescribed(vals...), ).ToA() - }).Cache(time.Minute).Suffix("#") + }).Cache(time.Minute) } func styleForRegistry(s string) string {