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, ...}:
with lib;
(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);
with lib; rec {
keymaps = import ./keymap-helpers.nix {inherit lib;};
# 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 "";
# vim dictionaries are, in theory, compatible with JSON
toVimDict = args:
toJSON
(lib.filterAttrs (n: v: v != null) args);
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;
submodule {
# Adds flexibility for other keys
freeformType = types.attrs;
emptyTable = {"__empty" = null;};
# :help nvim_set_hl()
options = {
fg = mkNullOrOption str "Color for the foreground (color name or '#RRGGBB').";
bg = mkNullOrOption str "Color for the background (color name or '#RRGGBB').";
sp = mkNullOrOption str "Special color (color name or '#RRGGBB').";
blend = mkNullOrOption (numbers.between 0 100) "Integer between 0 and 100.";
bold = mkNullOrOption bool "";
standout = mkNullOrOption bool "";
underline = mkNullOrOption bool "";
undercurl = mkNullOrOption bool "";
underdouble = mkNullOrOption bool "";
underdotted = mkNullOrOption bool "";
underdashed = mkNullOrOption bool "";
strikethrough = mkNullOrOption bool "";
italic = mkNullOrOption bool "";
reverse = mkNullOrOption bool "";
nocombine = mkNullOrOption bool "";
link = mkNullOrOption str "Name of another highlight group to link to.";
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.
'';
};
highlightType = with lib.types;
submodule {
# Adds flexibility for other keys
freeformType = types.attrs;
# :help nvim_set_hl()
options = {
fg = mkNullOrOption str "Color for the foreground (color name or '#RRGGBB').";
bg = mkNullOrOption str "Color for the background (color name or '#RRGGBB').";
sp = mkNullOrOption str "Special color (color name or '#RRGGBB').";
blend = mkNullOrOption (numbers.between 0 100) "Integer between 0 and 100.";
bold = mkNullOrOption bool "";
standout = mkNullOrOption bool "";
underline = mkNullOrOption bool "";
undercurl = mkNullOrOption bool "";
underdouble = mkNullOrOption bool "";
underdotted = mkNullOrOption bool "";
underdashed = mkNullOrOption bool "";
strikethrough = mkNullOrOption bool "";
italic = mkNullOrOption bool "";
reverse = mkNullOrOption bool "";
nocombine = mkNullOrOption bool "";
link = mkNullOrOption str "Name of another highlight group to link to.";
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.
mkNullOrOption = type: desc:
lib.mkOption {
type = lib.types.nullOr type;
default = null;
description = desc;
};
# Creates an option with a nullable type that defaults to null.
mkNullOrOption = type: desc:
lib.mkOption {
type = lib.types.nullOr type;
default = null;
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:
if (x == null)
then null
else y;
ifNonNull' = x: y:
if (x == null)
then null
else y;
mkCompositeOption = desc: options:
mkNullOrOption (types.submodule {inherit options;}) desc;
mkCompositeOption = desc: options:
mkNullOrOption (types.submodule {inherit options;}) desc;
defaultNullOpts = rec {
mkNullable = type: default: desc:
mkNullOrOption type (
let
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.
'';
defaultNullOpts = rec {
mkNullable = type: default: desc:
mkNullOrOption type (
let
defaultDesc = "default: `${default}`";
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;
};
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)
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
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: ''
do
${string}
end
'';
rawType = mkOptionType {
name = "rawType";
description = "raw lua code";
descriptionClass = "noun";
merge = mergeEqualOption;
check = isRawType;
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)
'';
};
};
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, ...}:
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.
mergeKeymap = defaults: keymap: let
# First, merge the `options` attrs of both options.

View file

@ -5,142 +5,6 @@
}:
with lib; let
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 {
options = {
maps =
@ -157,21 +21,26 @@ in {
either
str
(
mkMapOptionSubmodule
helpers.keymaps.mkMapOptionSubmodule
{
defaultMode = modeProps.short;
withKeyOpt = false;
flatConfig = true;
actionIsOptional = config.plugins.which-key.enable;
}
)
);
default = {};
}
)
modes;
helpers.keymaps.modes;
keymaps = mkOption {
type = types.listOf (mkMapOptionSubmodule {});
type =
types.listOf
(helpers.keymaps.mkMapOptionSubmodule {
actionIsOptional = config.plugins.which-key.enable;
});
default = [];
example = [
{
@ -246,7 +115,7 @@ in {
// {
options =
getAttrs
(attrNames mapConfigOptions)
(attrNames helpers.keymaps.mapConfigOptions)
action;
}
)
@ -254,7 +123,7 @@ in {
)
config.maps.${modeOptionName}
)
modes
helpers.keymaps.modes
);
mappings = let

View file

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

View file

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

View file

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