Skip to content

lib/modules: init mkBlinkPluginModule #2981

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion flake/dev/list-plugins/list-plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"plugins/by-name/rustaceanvim/renamed-options.nix",
"plugins/by-name/telescope/extensions/_mk-extension.nix",
"plugins/by-name/telescope/extensions/default.nix",
"plugins/cmp/auto-enable.nix",
"plugins/cmp/deprecated-auto-enable.nix",
"plugins/cmp/options/",
"plugins/cmp/sources/cmp-fish.nix",
"plugins/cmp/sources/default.nix",
Expand Down
277 changes: 277 additions & 0 deletions lib/modules.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let
applyExtraConfig = "It has been moved to `lib.plugins.utils`";
mkConfigAt = "It has been moved to `lib.plugins.utils`";
};
internal = lib.mkOption { internal = true; };
in
{
# Evaluate nixvim modules, checking warnings and assertions
Expand Down Expand Up @@ -52,6 +53,282 @@ in
helpers = self;
} // extraSpecialArgs;
};

# Create a module configuring a plugin's integration with blink.cmp
mkBlinkPluginModule =
{
# The plugin's option location-path
loc ? [
"plugins"
pluginName
],
# Name of the plugin, used in documentation
pluginName,
# Name of the module blink should import
# i.e. `sources.providers.<name>.module`
module ? pluginName,
# The default for `blink.settings.name`
# i.e. `sources.providers.<name>.name`
# TODO: consider doing some pre-processing to the default source name,
# e.g. removing `-cmp` or `blink-` prefix/suffix?
sourceName,
# The default for `blink.key`
# i.e. the attr name for `sources.providers.<name>`
key ? lib.strings.toLower sourceName,
# Whether to enable the blink completion provider by default
enableProvider ? true,
# Defaults for the corresponding source options
enableDefault ? true,
enableCmdline ? false,
enabledFiletypes ? { },
# Whether the plugin's settings should be used as the provider's `opts`
usePluginSettings ? true,
settingsExample ? {
score_offset = -7;
fallbacks = [ ];
},
}:
{ config, options, ... }:
let
pluginCfg = lib.getAttrFromPath loc config;
cfg = pluginCfg.blink;
pluginOpts = lib.getAttrFromPath loc options;
opt = pluginOpts.blink;
in
{
options = lib.setAttrByPath loc {
blink = {
enable = lib.mkOption {
type = lib.types.bool;
default = enableProvider;
example = !enableProvider;
description = ''
Whether to integrate this plugin with blink.cmp.
'';
};
key = lib.mkOption {
type = lib.types.str;
default = key;
description = ''
The key to use for ${pluginName}'s blink.cmp provider.
This is the id you should use when including this provider in completion source lists.
Must be unique.
'';
};
default = lib.mkOption {
type = lib.types.bool;
default = enableDefault;
example = !enableDefault;
description = ''
Whether to include this plugin in the `default` completion source list.
'';
};
cmdline = lib.mkOption {
type = lib.types.bool;
default = enableCmdline;
example = !enableCmdline;
description = ''
Whether to include this plugin in the `cmdline` completion source list.
'';
};
filetypes = lib.mkOption {
type = lib.types.attrsOf lib.types.bool;
# Only include `true` attrs in the final value
apply = lib.filterAttrs (_: lib.id);
default = enabledFiletypes;
# TODO: example
description = ''
Whether to include this plugin in the specific `per_filetype` completion source lists.
'';
};
settings = lib.mkOption {
default = { };
description = ''
Settings for the blink.cmp completion provider.
'';
example = settingsExample;
type = lib.types.submodule [
{
options.enabled = internal;
options.module = internal;
}
../plugins/by-name/blink-cmp/provider-config.nix
];
};
};
};
config = lib.mkMerge [
(lib.setAttrByPath loc {
# NOTE: this could be defined within the `blink.settings` submodule,
# but that would not populate the option's `definitions` list.
# Meaning we wouldn't be able to propagate the definitions further using `mkAliasDefinitions`.
blink.settings = {
name = lib.mkDefault sourceName;
inherit module;
opts = lib.mkIf usePluginSettings (lib.modules.mkAliasDefinitions pluginOpts.settings);
Copy link
Contributor

@khaneliman khaneliman Feb 7, 2025

Choose a reason for hiding this comment

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

This doesn't work the way we need it to to support blink's configuration.

      blink-copilot = {
        enable = true;

        # Need these outside of  the provider's opts
        # But don't exist in the module
        # async = true;
        # score_offset = 100;

        # Freeform accepts, but goes into `opts` instead of `providers.${cfg.key}` top level
        settings = {
          async = true;
          score_offset = 100;
        };
      };

Generates an invalid config:

copilot = {
    module = "blink-copilot",
    name = "copilot",
    opts = { async = true, score_offset = 100 },
}

I think we'd want to change it to support settings going to the top level and needing to pass opts in the settings attribute set.
https://cmp.saghen.dev/configuration/sources.html#provider-options

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah... so this looks like we should inherit pluginOpts.settings for the known top level options and then opts inherits from pluginOpts.settings.opts?

Copy link
Member Author

@MattSturgeon MattSturgeon Feb 7, 2025

Choose a reason for hiding this comment

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

plugins.*.settings has always represented the setup function's opts, so I've attempted to be consistent with that here.

The only difference is that blink invokes the setup function for us. But that's an implementation detail that is encapsulated from users in both normal and blink plugins...

Provider settings can still be configured directly, via plugins.*.blink.settings.

It was intentional to keep these separate, but I can see how it could be confusing if you take a different perspective to me.

🤔

Copy link
Contributor

@khaneliman khaneliman Feb 7, 2025

Choose a reason for hiding this comment

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

plugins.*.settings has always represented the setup function's opts, so I've attempted to be consistent with that here.

The only difference is that blink invokes the setup function for us. But that's an implementation detail that is encapsulated from users in both normal and blink plugins...

Provider settings can still be configured directly, via plugins.*.blink.settings.

Doesn't make this feel like a gain UX wise if they need to split their provider configuration between 2 modules, though. Personally, I'd just keep my configuration through blink-cmp so I wouldn't have to jump back and forth between 2 attribute sets to see what i'm doing. It is more niche to actually pass opts to the provider than to declare the top level provider options.

It was intentional to keep these separate, but I can see how it could be confusing if you take a different perspective to me.

🤔

I'd perfer providing the top level options to each blink plugin if we want to keep settings forwarded specifically to opts. So someone can choose to configure their provider from each source plulgin module itself.

};
})
(lib.mkIf (pluginCfg.enable && cfg.enable) {
plugins.blink-cmp.settings.sources = {
# Use mkAliasDefinitions to preserve override priorities
providers.${cfg.key} = lib.modules.mkAliasDefinitions opt.settings;
default = lib.mkIf cfg.default [ cfg.key ];
# FIXME: the reference shows `cmdline` should/could be defined as a function
# https://cmp.saghen.dev/configuration/reference.html#sources
cmdline = lib.mkIf cfg.cmdline [ cfg.key ];
per_filetype = lib.mkIf (cfg.filetypes != { }) (
builtins.mapAttrs (_: _: [ cfg.key ]) cfg.filetypes
);
};
warnings = lib.nixvim.mkWarnings (lib.showOption loc) {
when = !config.plugins.blink-cmp.enable && options.plugins.blink-cmp.enable.highestPrio == 1500;
message = ''
You have enabled the blink.cmp provider, but `plugins.blink-cmp` is not enabled.
You can suppress this warning by explicitly setting `plugins.blink-cmp.enable = false`.
'';
};
})
];
};

# Create a module configuring a plugin's integration with nvim-cmp and blink.cmp
mkCmpPluginModule =
{
# The plugin's option location-path
loc ? [
"plugins"
pluginName
],
# Name of the plugin, used in documentation
pluginName,
# The nvim-cmp source name
# TODO: can we compute a sane default for sourceName?
sourceName,
# Defaults for the corresponding cmp options
enableDefault ? true,
enableCmdline ? { },
enabledFiletypes ? { },
# Whether to include a `blink` option at all
offerBlinkCompatibility ? true,
# Defaults for the blink compatibility option
enableBlinkProvider ? false,
enableBlinkDefault ? false,
enableBlinkCmdline ? false,
enabledBlinkFiletypes ? { },
Comment on lines +215 to +217
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just use the corresponding arguments above ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure what you're asking. These are probably badly named, but they're intended to be the default values for the corresponding module options.

Copy link
Contributor

Choose a reason for hiding this comment

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

We have enableDefault, enabldeCmdline, and enableFiletypes already in this argument list. I'm asking why they are special blink versions for the same name

Copy link
Member Author

@MattSturgeon MattSturgeon Feb 6, 2025

Choose a reason for hiding this comment

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

Ah.

Because it felt unlikely that enabling a plugin as a nvim-cmp source by default should also enable it as a blink source by default. I was thinking that a plugin maintainer should manually make that decision.

We could have these defaults to the nvim-cmp equivalent, but I think it should still be possible to set these separately?

# Whether the plugin's settings should be used as the blink provider's `opts`
usePluginSettingsForBlink ? false,
# The key to use with blink,
# i.e. the attr name for `sources.providers.<name>`
# TODO: should this default to pluginName or sourceName?
blinkProviderKey ? lib.strings.toLower pluginName,
}:
{ config, options, ... }:
let
pluginOpt = lib.getAttrFromPath loc options;
pluginCfg = lib.getAttrFromPath loc config;
blinkOpt = pluginOpt.blink;
blinkCfg = pluginCfg.blink;
cfg = pluginCfg.cmp;
toSourceDef = v: lib.optionalAttrs (builtins.isAttrs v) v // { inherit (cfg) name; };
toSources = v: { sources = [ (toSourceDef v) ]; };
in
{
imports = lib.optionals offerBlinkCompatibility [
(lib.nixvim.modules.mkBlinkPluginModule {
inherit loc pluginName sourceName;
key = blinkProviderKey;
module = "blink.compat.source";
enableProvider = enableBlinkProvider;
enableDefault = enableBlinkDefault;
enableCmdline = enableBlinkCmdline;
enabledFiletypes = enabledBlinkFiletypes;
usePluginSettings = usePluginSettingsForBlink;
})
{
options = lib.setAttrByPath loc {
blink.settings.name = internal;
};
config = lib.mkIf (pluginCfg.enable && blinkCfg.enable) {
# Enable blink-compat if the plugin has `blink.enable = true`
plugins.blink-compat.enable = true;
# This warning will show if someone overrides `plugins.blink-compat.enable = mkForce false`
warnings = lib.nixvim.mkWarnings (lib.showOption loc) {
when = !config.plugins.blink-compat.enable;
message = ''
`${blinkOpt.enable}` is enabled, but `${options.plugins.blink-compat.enable}` is not.
This plugin is a nvim-cmp source, so it requires blink.compat when used with blink.cmp.
'';
};
};
}
];
options = lib.setAttrByPath loc {
cmp = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
example = false;
description = ''
Whether to integrate this plugin with nvim-cmp.
'';
};
name = lib.mkOption {
type = lib.types.str;
default = sourceName;
description = "${pluginName}'s nvim-cmp source name.";
internal = true;
};
default = lib.mkOption {
type = with lib.types; either bool (attrsOf anything);
default = enableDefault;
example = !enableDefault;
description = ''
Whether to include this plugin in the `default` completion source list.

Can be defined as attrs to pass additional config to the source.
'';
};
cmdline = lib.mkOption {
type = with lib.types; attrsOf (either bool (attrsOf anything));
# Remove false attrs in the final value
apply = lib.filterAttrs (_: v: v != false);
default = enableCmdline;
# TODO: example
description = ''
Whether to include this plugin in the specific `cmdline` completion source lists.

Elements can be defined as attrs to pass additional config to the source.
'';
};
filetypes = lib.mkOption {
type = with lib.types; attrsOf (either bool (attrsOf anything));
# Remove false attrs in the final value
apply = lib.filterAttrs (_: v: v != false);
default = enabledFiletypes;
# TODO: example
description = ''
Whether to include this plugin in the specific `per_filetype` completion source lists.

Elements can be defined as attrs to pass additional config to the source.
'';
};
};
};
config = lib.mkIf (pluginCfg.enable && cfg.enable) {
plugins.cmp = {
settings = lib.mkIf (cfg.default != false) (toSources cfg.default);
cmdline = lib.mkIf (cfg.cmdline != { }) (builtins.mapAttrs (_: toSources) cfg.cmdline);
filetype = lib.mkIf (cfg.filetypes != { }) (builtins.mapAttrs (_: toSources) cfg.filetypes);
};
warnings = lib.nixvim.mkWarnings (lib.showOption loc) {
when = !config.plugins.cmp.enable && options.plugins.cmp.enable.highestPrio == 1500;
message = ''
You have enabled the nvim-cmp source, but `plugins.cmp` is not enabled.
You can suppress this warning by explicitly setting `plugins.cmp.enable = false`.
'';
};
};
};
}
// lib.mapAttrs (
name: msg:
Expand Down
56 changes: 19 additions & 37 deletions plugins/by-name/blink-cmp-copilot/default.nix
Original file line number Diff line number Diff line change
@@ -1,48 +1,30 @@
{ config, lib, ... }:
lib.nixvim.plugins.mkNeovimPlugin {
let
name = "blink-cmp-copilot";
package = "blink-cmp-copilot";
in
lib.nixvim.plugins.mkNeovimPlugin {
inherit name;

maintainers = [ lib.maintainers.HeitorAugustoLN ];

description = ''
This plugin should be configured through blink-cmp's source settings.

For example:
callSetup = false;
hasLuaConfig = false;

```nix
plugins.blink-cmp = {
enable = true;
settings.sources = {
copilot = {
async = true;
module = "blink-cmp-copilot";
name = "copilot";
score_offset = 100;
};
imports = [
(lib.nixvim.modules.mkBlinkPluginModule {
pluginName = name;
# TODO: compute a sane-default source name
sourceName = "copilot";
settingsExample = {
async = true;
score_offset = 100;
Comment on lines +13 to +20
Copy link
Member Author

Choose a reason for hiding this comment

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

I guess we could streamline this a little, by having a blink argument passed to mkNeovimPlugin.

When present, mkNeovimPlugin could pass the argument to mkBlinkPluginModule, appending some basics like loc.

};
};
```

And then you can add it as a source for blink-cmp:
})
];

```nix
plugins.blink-cmp = {
enable = true;
settings.sources.default = [
"lsp"
"path"
"luasnip"
"buffer"
"copilot"
];
};
```
'';

callSetup = false;
hasLuaConfig = false;
hasSettings = false;
settingsExample = {
max_completions = 3;
};

extraConfig = {
warnings =
Expand Down
Loading