modules/keymaps: factor out helper functions for use in plugins

This commit is contained in:
Gaetan Lepage 2023-10-02 15:44:06 +02:00 committed by Gaétan Lepage
parent 7eb1a85ccb
commit 418bf5da17
6 changed files with 416 additions and 407 deletions

View file

@ -1,286 +1,286 @@
{lib, ...}: {lib, ...}:
with lib; with lib; rec {
(import ./keymap-helpers.nix {inherit lib;}) keymaps = import ./keymap-helpers.nix {inherit lib;};
// rec {
# vim dictionaries are, in theory, compatible with JSON
toVimDict = args:
toJSON
(lib.filterAttrs (n: v: v != null) args);
# Black functional magic that converts a bunch of different Nix types to their # vim dictionaries are, in theory, compatible with JSON
# lua equivalents! toVimDict = args:
toLuaObject = args: toJSON
if builtins.isAttrs args (lib.filterAttrs (n: v: v != null) args);
then
if hasAttr "__raw" args
then args.__raw
else if hasAttr "__empty" args
then "{ }"
else
"{"
+ (concatStringsSep ","
(mapAttrsToList
(n: v:
if head (stringToCharacters n) == "@"
then toLuaObject v
else if n == "__emptyString"
then "[''] = " + (toLuaObject v)
else "[${toLuaObject n}] = " + (toLuaObject v))
(filterAttrs
(
n: v:
v != null && (toLuaObject v != "{}")
)
args)))
+ "}"
else if builtins.isList args
then "{" + concatMapStringsSep "," toLuaObject args + "}"
else if builtins.isString args
then
# This should be enough!
builtins.toJSON args
else if builtins.isPath args
then builtins.toJSON (toString args)
else if builtins.isBool args
then "${boolToString args}"
else if builtins.isFloat args
then "${toString args}"
else if builtins.isInt args
then "${toString args}"
else if (args == null)
then "nil"
else "";
emptyTable = {"__empty" = null;}; # Black functional magic that converts a bunch of different Nix types to their
# lua equivalents!
toLuaObject = args:
if builtins.isAttrs args
then
if hasAttr "__raw" args
then args.__raw
else if hasAttr "__empty" args
then "{ }"
else
"{"
+ (concatStringsSep ","
(mapAttrsToList
(n: v:
if head (stringToCharacters n) == "@"
then toLuaObject v
else if n == "__emptyString"
then "[''] = " + (toLuaObject v)
else "[${toLuaObject n}] = " + (toLuaObject v))
(filterAttrs
(
n: v:
v != null && (toLuaObject v != "{}")
)
args)))
+ "}"
else if builtins.isList args
then "{" + concatMapStringsSep "," toLuaObject args + "}"
else if builtins.isString args
then
# This should be enough!
builtins.toJSON args
else if builtins.isPath args
then builtins.toJSON (toString args)
else if builtins.isBool args
then "${boolToString args}"
else if builtins.isFloat args
then "${toString args}"
else if builtins.isInt args
then "${toString args}"
else if (args == null)
then "nil"
else "";
highlightType = with lib.types; emptyTable = {"__empty" = null;};
submodule {
# Adds flexibility for other keys
freeformType = types.attrs;
# :help nvim_set_hl() highlightType = with lib.types;
options = { submodule {
fg = mkNullOrOption str "Color for the foreground (color name or '#RRGGBB')."; # Adds flexibility for other keys
bg = mkNullOrOption str "Color for the background (color name or '#RRGGBB')."; freeformType = types.attrs;
sp = mkNullOrOption str "Special color (color name or '#RRGGBB').";
blend = mkNullOrOption (numbers.between 0 100) "Integer between 0 and 100."; # :help nvim_set_hl()
bold = mkNullOrOption bool ""; options = {
standout = mkNullOrOption bool ""; fg = mkNullOrOption str "Color for the foreground (color name or '#RRGGBB').";
underline = mkNullOrOption bool ""; bg = mkNullOrOption str "Color for the background (color name or '#RRGGBB').";
undercurl = mkNullOrOption bool ""; sp = mkNullOrOption str "Special color (color name or '#RRGGBB').";
underdouble = mkNullOrOption bool ""; blend = mkNullOrOption (numbers.between 0 100) "Integer between 0 and 100.";
underdotted = mkNullOrOption bool ""; bold = mkNullOrOption bool "";
underdashed = mkNullOrOption bool ""; standout = mkNullOrOption bool "";
strikethrough = mkNullOrOption bool ""; underline = mkNullOrOption bool "";
italic = mkNullOrOption bool ""; undercurl = mkNullOrOption bool "";
reverse = mkNullOrOption bool ""; underdouble = mkNullOrOption bool "";
nocombine = mkNullOrOption bool ""; underdotted = mkNullOrOption bool "";
link = mkNullOrOption str "Name of another highlight group to link to."; underdashed = mkNullOrOption bool "";
default = mkNullOrOption bool "Don't override existing definition."; strikethrough = mkNullOrOption bool "";
ctermfg = mkNullOrOption str "Sets foreground of cterm color."; italic = mkNullOrOption bool "";
ctermbg = mkNullOrOption str "Sets background of cterm color."; reverse = mkNullOrOption bool "";
cterm = mkNullOrOption attrs '' nocombine = mkNullOrOption bool "";
cterm attribute map, like |highlight-args|. link = mkNullOrOption str "Name of another highlight group to link to.";
If not set, cterm attributes will match those from the attribute map documented above. default = mkNullOrOption bool "Don't override existing definition.";
''; ctermfg = mkNullOrOption str "Sets foreground of cterm color.";
}; ctermbg = mkNullOrOption str "Sets background of cterm color.";
cterm = mkNullOrOption attrs ''
cterm attribute map, like |highlight-args|.
If not set, cterm attributes will match those from the attribute map documented above.
'';
}; };
};
# Creates an option with a nullable type that defaults to null. # Creates an option with a nullable type that defaults to null.
mkNullOrOption = type: desc: mkNullOrOption = type: desc:
lib.mkOption { lib.mkOption {
type = lib.types.nullOr type; type = lib.types.nullOr type;
default = null; default = null;
description = desc; description = desc;
}; };
mkIfNonNull' = x: y: (mkIf (x != null) y); mkIfNonNull' = x: y: (mkIf (x != null) y);
mkIfNonNull = x: (mkIfNonNull' x x); mkIfNonNull = x: (mkIfNonNull' x x);
ifNonNull' = x: y: ifNonNull' = x: y:
if (x == null) if (x == null)
then null then null
else y; else y;
mkCompositeOption = desc: options: mkCompositeOption = desc: options:
mkNullOrOption (types.submodule {inherit options;}) desc; mkNullOrOption (types.submodule {inherit options;}) desc;
defaultNullOpts = rec { defaultNullOpts = rec {
mkNullable = type: default: desc: mkNullable = type: default: desc:
mkNullOrOption type ( mkNullOrOption type (
let let
defaultDesc = "default: `${default}`"; defaultDesc = "default: `${default}`";
in
if desc == ""
then defaultDesc
else ''
${desc}
${defaultDesc}
''
);
mkNum = default: mkNullable lib.types.number (toString default);
mkInt = default: mkNullable lib.types.int (toString default);
# Positive: >0
mkPositiveInt = default: mkNullable lib.types.ints.positive (toString default);
# Unsigned: >=0
mkUnsignedInt = default: mkNullable lib.types.ints.unsigned (toString default);
mkBool = default:
mkNullable lib.types.bool (
if default
then "true"
else "false"
);
mkStr = default: mkNullable lib.types.str ''${builtins.toString default}'';
mkAttributeSet = default: mkNullable lib.types.attrs ''${default}'';
mkEnum = enum: default: mkNullable (lib.types.enum enum) ''"${default}"'';
mkEnumFirstDefault = enum: mkEnum enum (head enum);
mkBorder = default: name: desc:
mkNullable
(
with lib.types;
oneOf [
str
(listOf str)
(listOf (listOf str))
]
)
default
(let
defaultDesc = ''
Defines the border to use for ${name}.
Accepts same border values as `nvim_open_win()`. See `:help nvim_open_win()` for more info.
'';
in in
if desc == "" if desc == ""
then defaultDesc then defaultDesc
else '' else ''
${desc} ${desc}
${defaultDesc} ${defaultDesc}
''); ''
);
mkHighlight = default: name: desc: mkNum = default: mkNullable lib.types.number (toString default);
mkNullable mkInt = default: mkNullable lib.types.int (toString default);
highlightType # Positive: >0
default mkPositiveInt = default: mkNullable lib.types.ints.positive (toString default);
( # Unsigned: >=0
if desc == "" mkUnsignedInt = default: mkNullable lib.types.ints.unsigned (toString default);
then "Highlight settings." mkBool = default:
else desc mkNullable lib.types.bool (
); if default
}; then "true"
else "false"
mkPackageOption = name: default: );
mkOption { mkStr = default: mkNullable lib.types.str ''${builtins.toString default}'';
type = types.package; mkAttributeSet = default: mkNullable lib.types.attrs ''${default}'';
inherit default; mkEnum = enum: default: mkNullable (lib.types.enum enum) ''"${default}"'';
description = "Plugin to use for ${name}"; mkEnumFirstDefault = enum: mkEnum enum (head enum);
}; mkBorder = default: name: desc:
mkNullable
mkPlugin = { (
config, with lib.types;
lib, oneOf [
... str
}: { (listOf str)
name, (listOf (listOf str))
description, ]
package ? null, )
extraPlugins ? [], default
extraPackages ? [], (let
options ? {}, defaultDesc = ''
globalPrefix ? "", Defines the border to use for ${name}.
... Accepts same border values as `nvim_open_win()`. See `:help nvim_open_win()` for more info.
}: let
cfg = config.plugins.${name};
# TODO support nested options!
pluginOptions = mapAttrs (k: v: v.option) options;
globals =
mapAttrs'
(name: opt: {
name = globalPrefix + opt.global;
value =
if cfg.${name} != null
then opt.value cfg.${name}
else null;
})
options;
# does this evaluate package?
packageOption =
if package == null
then {}
else {
package = mkPackageOption name package;
};
in {
options.plugins.${name} =
{
enable = mkEnableOption description;
}
// packageOption
// pluginOptions;
config = mkIf cfg.enable {
inherit extraPackages globals;
# does this evaluate package? it would not be desired to evaluate pacakge if we use another package.
extraPlugins = extraPlugins ++ optional (package != null) cfg.package;
};
};
globalVal = val:
if builtins.isBool val
then
(
if !val
then 0
else 1
)
else val;
mkDefaultOpt = {
type,
global,
description ? null,
example ? null,
default ? null,
value ? v: (globalVal v),
...
}: {
option = mkOption {
type = types.nullOr type;
inherit default description example;
};
inherit value global;
};
extraOptionsOptions = {
extraOptions = mkOption {
default = {};
type = types.attrs;
description = ''
These attributes will be added to the table parameter for the setup function.
(Can override other attributes set by nixvim)
''; '';
in
if desc == ""
then defaultDesc
else ''
${desc}
${defaultDesc}
'');
mkHighlight = default: name: desc:
mkNullable
highlightType
default
(
if desc == ""
then "Highlight settings."
else desc
);
};
mkPackageOption = name: default:
mkOption {
type = types.package;
inherit default;
description = "Plugin to use for ${name}";
};
mkPlugin = {
config,
lib,
...
}: {
name,
description,
package ? null,
extraPlugins ? [],
extraPackages ? [],
options ? {},
globalPrefix ? "",
...
}: let
cfg = config.plugins.${name};
# TODO support nested options!
pluginOptions = mapAttrs (k: v: v.option) options;
globals =
mapAttrs'
(name: opt: {
name = globalPrefix + opt.global;
value =
if cfg.${name} != null
then opt.value cfg.${name}
else null;
})
options;
# does this evaluate package?
packageOption =
if package == null
then {}
else {
package = mkPackageOption name package;
}; };
in {
options.plugins.${name} =
{
enable = mkEnableOption description;
}
// packageOption
// pluginOptions;
config = mkIf cfg.enable {
inherit extraPackages globals;
# does this evaluate package? it would not be desired to evaluate pacakge if we use another package.
extraPlugins = extraPlugins ++ optional (package != null) cfg.package;
};
};
globalVal = val:
if builtins.isBool val
then
(
if !val
then 0
else 1
)
else val;
mkDefaultOpt = {
type,
global,
description ? null,
example ? null,
default ? null,
value ? v: (globalVal v),
...
}: {
option = mkOption {
type = types.nullOr type;
inherit default description example;
}; };
mkRaw = r: {__raw = r;}; inherit value global;
};
wrapDo = string: '' extraOptionsOptions = {
do extraOptions = mkOption {
${string} default = {};
end type = types.attrs;
''; description = ''
These attributes will be added to the table parameter for the setup function.
rawType = mkOptionType { (Can override other attributes set by nixvim)
name = "rawType"; '';
description = "raw lua code";
descriptionClass = "noun";
merge = mergeEqualOption;
check = isRawType;
}; };
};
isRawType = v: lib.isAttrs v && lib.hasAttr "__raw" v && lib.isString v.__raw; mkRaw = r: {__raw = r;};
}
wrapDo = string: ''
do
${string}
end
'';
rawType = mkOptionType {
name = "rawType";
description = "raw lua code";
descriptionClass = "noun";
merge = mergeEqualOption;
check = isRawType;
};
isRawType = v: lib.isAttrs v && lib.hasAttr "__raw" v && lib.isString v.__raw;
}

View file

@ -1,5 +1,145 @@
{lib, ...}: {lib, ...}:
with lib; rec { with lib; let
helpers = import ../lib/helpers.nix {inherit lib;};
in rec {
# These are the configuration options that change the behavior of each mapping.
mapConfigOptions = {
silent =
helpers.defaultNullOpts.mkBool false
"Whether this mapping should be silent. Equivalent to adding <silent> to a map.";
nowait =
helpers.defaultNullOpts.mkBool false
"Whether to wait for extra input on ambiguous mappings. Equivalent to adding <nowait> to a map.";
script =
helpers.defaultNullOpts.mkBool false
"Equivalent to adding <script> to a map.";
expr =
helpers.defaultNullOpts.mkBool false
"Means that the action is actually an expression. Equivalent to adding <expr> to a map.";
unique =
helpers.defaultNullOpts.mkBool false
"Whether to fail if the map is already defined. Equivalent to adding <unique> to a map.";
noremap =
helpers.defaultNullOpts.mkBool true
"Whether to use the 'noremap' variant of the command, ignoring any custom mappings on the defined action. It is highly advised to keep this on, which is the default.";
remap =
helpers.defaultNullOpts.mkBool false
"Make the mapping recursive. Inverses \"noremap\"";
desc =
helpers.mkNullOrOption types.str
"A textual description of this keybind, to be shown in which-key, if you have it.";
};
modes = {
normal.short = "n";
insert.short = "i";
visual = {
desc = "visual and select";
short = "v";
};
visualOnly = {
desc = "visual only";
short = "x";
};
select.short = "s";
terminal.short = "t";
normalVisualOp = {
desc = "normal, visual, select and operator-pending (same as plain 'map')";
short = "";
};
operator.short = "o";
lang = {
desc = "normal, visual, select and operator-pending (same as plain 'map')";
short = "l";
};
insertCommand = {
desc = "insert and command-line";
short = "!";
};
command.short = "c";
};
modeEnum =
types.enum
# ["" "n" "v" ...]
(
map
(
{short, ...}: short
)
(attrValues modes)
);
# TODO: When `maps` will have to be deprecated (early December 2023), change this.
# mapOptionSubmodule = {...} (no need for options... except for 'optionalAction')
mkMapOptionSubmodule = {
defaultMode ? "",
withKeyOpt ? true,
flatConfig ? false,
actionIsOptional ? false,
}:
with types;
either
str
(submodule {
options =
(
if withKeyOpt
then {
key = mkOption {
type = str;
description = "The key to map.";
example = "<C-m>";
};
}
else {}
)
// {
mode = mkOption {
type = either modeEnum (listOf modeEnum);
description = ''
One or several modes.
Use the short-names (`"n"`, `"v"`, ...).
See `:h map-modes` to learn more.
'';
default = defaultMode;
example = ["n" "v"];
};
action =
if actionIsOptional
then helpers.mkNullOrOption str "The action to execute"
else
mkOption {
type = str;
description = "The action to execute.";
};
lua = mkOption {
type = bool;
description = ''
If true, `action` is considered to be lua code.
Thus, it will not be wrapped in `""`.
'';
default = false;
};
}
// (
if flatConfig
then mapConfigOptions
else {
options = mapConfigOptions;
}
);
});
# Correctly merge two attrs (partially) representing a mapping. # Correctly merge two attrs (partially) representing a mapping.
mergeKeymap = defaults: keymap: let mergeKeymap = defaults: keymap: let
# First, merge the `options` attrs of both options. # First, merge the `options` attrs of both options.

View file

@ -5,142 +5,6 @@
}: }:
with lib; let with lib; let
helpers = import ../lib/helpers.nix {inherit lib;}; helpers = import ../lib/helpers.nix {inherit lib;};
# These are the configuration options that change the behavior of each mapping.
mapConfigOptions = {
silent =
helpers.defaultNullOpts.mkBool false
"Whether this mapping should be silent. Equivalent to adding <silent> to a map.";
nowait =
helpers.defaultNullOpts.mkBool false
"Whether to wait for extra input on ambiguous mappings. Equivalent to adding <nowait> to a map.";
script =
helpers.defaultNullOpts.mkBool false
"Equivalent to adding <script> to a map.";
expr =
helpers.defaultNullOpts.mkBool false
"Means that the action is actually an expression. Equivalent to adding <expr> to a map.";
unique =
helpers.defaultNullOpts.mkBool false
"Whether to fail if the map is already defined. Equivalent to adding <unique> to a map.";
noremap =
helpers.defaultNullOpts.mkBool true
"Whether to use the 'noremap' variant of the command, ignoring any custom mappings on the defined action. It is highly advised to keep this on, which is the default.";
remap =
helpers.defaultNullOpts.mkBool false
"Make the mapping recursive. Inverses \"noremap\"";
desc =
helpers.mkNullOrOption types.str
"A textual description of this keybind, to be shown in which-key, if you have it.";
};
modes = {
normal.short = "n";
insert.short = "i";
visual = {
desc = "visual and select";
short = "v";
};
visualOnly = {
desc = "visual only";
short = "x";
};
select.short = "s";
terminal.short = "t";
normalVisualOp = {
desc = "normal, visual, select and operator-pending (same as plain 'map')";
short = "";
};
operator.short = "o";
lang = {
desc = "normal, visual, select and operator-pending (same as plain 'map')";
short = "l";
};
insertCommand = {
desc = "insert and command-line";
short = "!";
};
command.short = "c";
};
mkMapOptionSubmodule = {
defaultMode ? "",
withKeyOpt ? true,
flatConfig ? false,
}:
with types;
either
str
(types.submodule {
options =
(
if withKeyOpt
then {
key = mkOption {
type = types.str;
description = "The key to map.";
example = "<C-m>";
};
}
else {}
)
// {
mode = mkOption {
type = let
modeEnum =
enum
# ["" "n" "v" ...]
(
map
(
{short, ...}: short
)
(attrValues modes)
);
in
either modeEnum (listOf modeEnum);
description = ''
One or several modes.
Use the short-names (`"n"`, `"v"`, ...).
See `:h map-modes` to learn more.
'';
default = defaultMode;
example = ["n" "v"];
};
action =
if config.plugins.which-key.enable
then helpers.mkNullOrOption types.str "The action to execute"
else
mkOption {
type = types.str;
description = "The action to execute.";
};
lua = mkOption {
type = types.bool;
description = ''
If true, `action` is considered to be lua code.
Thus, it will not be wrapped in `""`.
'';
default = false;
};
}
// (
if flatConfig
then mapConfigOptions
else {
options = mapConfigOptions;
}
);
});
in { in {
options = { options = {
maps = maps =
@ -157,21 +21,26 @@ in {
either either
str str
( (
mkMapOptionSubmodule helpers.keymaps.mkMapOptionSubmodule
{ {
defaultMode = modeProps.short; defaultMode = modeProps.short;
withKeyOpt = false; withKeyOpt = false;
flatConfig = true; flatConfig = true;
actionIsOptional = config.plugins.which-key.enable;
} }
) )
); );
default = {}; default = {};
} }
) )
modes; helpers.keymaps.modes;
keymaps = mkOption { keymaps = mkOption {
type = types.listOf (mkMapOptionSubmodule {}); type =
types.listOf
(helpers.keymaps.mkMapOptionSubmodule {
actionIsOptional = config.plugins.which-key.enable;
});
default = []; default = [];
example = [ example = [
{ {
@ -246,7 +115,7 @@ in {
// { // {
options = options =
getAttrs getAttrs
(attrNames mapConfigOptions) (attrNames helpers.keymaps.mapConfigOptions)
action; action;
} }
) )
@ -254,7 +123,7 @@ in {
) )
config.maps.${modeOptionName} config.maps.${modeOptionName}
) )
modes helpers.keymaps.modes
); );
mappings = let mappings = let

View file

@ -35,7 +35,7 @@ in {
extraPackages = with pkgs; [typst]; extraPackages = with pkgs; [typst];
keymaps = with cfg.keymaps; keymaps = with cfg.keymaps;
helpers.mkKeymaps helpers.keymaps.mkKeymaps
{ {
mode = "n"; mode = "n";
options.silent = silent; options.silent = silent;

View file

@ -34,7 +34,7 @@ in {
extraPlugins = [cfg.package]; extraPlugins = [cfg.package];
keymaps = with cfg.keymaps; keymaps = with cfg.keymaps;
helpers.mkKeymaps helpers.keymaps.mkKeymaps
{ {
mode = "n"; mode = "n";
options.silent = cfg.keymapsSilent; options.silent = cfg.keymapsSilent;

View file

@ -4,7 +4,7 @@
}; };
legacy-mkMaps = { legacy-mkMaps = {
maps = helpers.mkMaps {silent = true;} { maps = helpers.keymaps.mkMaps {silent = true;} {
normal."," = "<cmd>echo \"test\"<cr>"; normal."," = "<cmd>echo \"test\"<cr>";
visual = { visual = {
"<C-a>" = { "<C-a>" = {
@ -20,7 +20,7 @@
}; };
legacy-mkModeMaps = { legacy-mkModeMaps = {
maps.normal = helpers.mkModeMaps {silent = true;} { maps.normal = helpers.keymaps.mkModeMaps {silent = true;} {
"," = "<cmd>echo \"test\"<cr>"; "," = "<cmd>echo \"test\"<cr>";
"<C-a>" = { "<C-a>" = {
action = "function() print('toto') end"; action = "function() print('toto') end";
@ -49,7 +49,7 @@
mkMaps = { mkMaps = {
keymaps = keymaps =
helpers.mkKeymaps helpers.keymaps.mkKeymaps
{ {
mode = "x"; mode = "x";
options.silent = true; options.silent = true;