modules/keymaps: refactor + new syntax

This commit is contained in:
Gaetan Lepage 2023-09-10 09:59:22 +02:00 committed by Gaétan Lepage
parent 382973b627
commit 574fb73258
6 changed files with 716 additions and 521 deletions

View file

@ -321,18 +321,23 @@ If you are using `makeNixvimWithModule`, then options is treated as options for
## Key mappings
It is fully possible to define key mappings from within NixVim. This is done
using the `maps` attribute:
using the `keymaps` attribute:
```nix
{
programs.nixvim = {
maps = {
normalVisualOp.";" = ":";
normal."<leader>m" = {
silent = true;
action = "<cmd>make<CR>";
};
keymaps = [
{
key = ";";
action = ":";
}
{
mode = "n";
key = "<leader>m";
options.silent = true;
action = "<cmd>!make<CR>";
};
];
};
}
```
@ -344,34 +349,35 @@ noremap ; :
nnoremap <leader>m <silent> <cmd>make<CR>
```
This table describes all modes for the `maps` option:
This table describes all modes for the `keymaps` option.
You can provide several mode to a single mapping by using a list of strings.
| NixVim | NeoVim |
|----------------|--------------------------------------------------|
| normal | Normal mode |
| insert | Insert mode |
| visual | Visual and Select mode |
| select | Select mode |
| terminal | Terminal mode |
| normalVisualOp | Normal, visual, select and operator-pending mode |
| visualOnly | Visual mode only, without select |
| operator | Operator-pending mode |
| insertCommand | Insert and command-line mode |
| lang | Insert, command-line and lang-arg mode |
| command | Command-line mode |
| Short | Description |
|-------|--------------------------------------------------|
| `"n"` | Normal mode |
| `"i"` | Insert mode |
| `"v"` | Visual and Select mode |
| `"s"` | Select mode |
| `"t"` | Terminal mode |
| `"" ` | Normal, visual, select and operator-pending mode |
| `"x"` | Visual mode only, without select |
| `"o"` | Operator-pending mode |
| `"!"` | Insert and command-line mode |
| `"l"` | Insert, command-line and lang-arg mode |
| `"c"` | Command-line mode |
The map options can be set to either a string, containing just the action,
or to a set describing additional options:
Each keymap can specify the following settings in the `options` attrs.
| NixVim | Default | VimScript |
|---------|---------|------------------------------------------|
|---------|---------|---------------------------------------------------|
| silent | false | `<silent>` |
| nowait | false | `<silent>` |
| script | false | `<script>` |
| expr | false | `<expr>` |
| unique | false | `<unique>` |
| noremap | true | Use the 'noremap' variant of the mapping |
| action | N/A | Action to execute |
| remap | false | Make the mapping recursive (inverses `noremap`) |
| desc | "" | A description of this keymap |
## Globals
Sometimes you might want to define a global variable, for example to set the

View file

@ -5,16 +5,24 @@
# when compared to just installing NeoVim.
enable = true;
maps.normal = {
keymaps = [
# Equivalent to nnoremap ; :
";" = ":";
{
key = ";";
action = ":";
}
# Equivalent to nmap <silent> <buffer> <leader>gg <cmd>Man<CR>
"<leader>gg" = {
{
key = "<leader>gg";
action = "<cmd>Man<CR>";
options = {
silent = true;
remap = false;
action = "<cmd>Man<CR>";
# Etc...
};
}
# Etc...
];
# We can set the leader key:
leader = ",";
@ -74,5 +82,4 @@
# Use extraPlugins:
extraPlugins = with pkgs.vimPlugins; [vim-toml];
};
};
}

View file

@ -1,5 +1,7 @@
{lib, ...}:
with lib; rec {
with lib;
(import ./keymap-helpers.nix {inherit lib;})
// rec {
# vim dictionaries are, in theory, compatible with JSON
toVimDict = args:
toJSON
@ -84,52 +86,6 @@ with lib; rec {
};
};
# Given an attrs of key mappings (for a single mode), applies the defaults to each one of them.
#
# Example:
# mkModeMaps { silent = true; } {
# Y = "y$";
# "<C-c>" = { action = ":b#<CR>"; silent = false; };
# };
#
# would give:
# {
# Y = {
# action = "y$";
# silent = true;
# };
# "<C-c>" = {
# action = ":b#<CR>";
# silent = false;
# };
# };
mkModeMaps = defaults:
mapAttrs
(
shortcut: action: let
actionAttrs =
if isString action
then {inherit action;}
else action;
in
defaults // actionAttrs
);
# Applies some default mapping options to a set of mappings
#
# Example:
# maps = mkMaps { silent = true; expr = true; } {
# normal = {
# ...
# };
# visual = {
# ...
# };
# }
mkMaps = defaults:
mapAttrs
(name: modeMaps: (mkModeMaps defaults modeMaps));
# Creates an option with a nullable type that defaults to null.
mkNullOrOption = type: desc:
lib.mkOption {

64
lib/keymap-helpers.nix Normal file
View file

@ -0,0 +1,64 @@
{lib, ...}:
with lib; rec {
# Correctly merge two attrs (partially) representing a mapping.
mergeKeymap = defaults: keymap: let
# First, merge the `options` attrs of both options.
mergedOpts = (defaults.options or {}) // (keymap.options or {});
in
# Then, merge the root attrs together and add the previously merged `options` attrs.
(defaults // keymap) // {options = mergedOpts;};
# Given an attrs of key mappings (for a single mode), applies the defaults to each one of them.
#
# Example:
# mkModeMaps { silent = true; } {
# Y = "y$";
# "<C-c>" = { action = ":b#<CR>"; silent = false; };
# };
#
# would give:
# {
# Y = {
# action = "y$";
# silent = true;
# };
# "<C-c>" = {
# action = ":b#<CR>";
# silent = false;
# };
# };
mkModeMaps = defaults:
mapAttrs
(
key: action: let
actionAttrs =
if isString action
then {inherit action;}
else action;
in
defaults // actionAttrs
);
# Applies some default mapping options to a set of mappings
#
# Example:
# maps = mkMaps { silent = true; expr = true; } {
# normal = {
# ...
# };
# visual = {
# ...
# };
# }
mkMaps = defaults:
mapAttrs
(
name: modeMaps:
mkModeMaps defaults modeMaps
);
# TODO deprecate `mkMaps` and `mkModeMaps` and leave only this one
mkKeymaps = defaults:
map
(mergeKeymap defaults);
}

View file

@ -41,69 +41,80 @@ with lib; let
"A textual description of this keybind, to be shown in which-key, if you have it.";
};
# Generates maps for a lua config
genMaps = mode: maps: let
/*
Take a user-defined action (string or attrs) and return the following attribute set:
{
action = (string) the actual action to map to this key
config = (attrs) the configuration options for this mapping (noremap, silent...)
}
- If the action is a string:
{
action = action;
config = {};
}
- If the action is an attrs:
{
action = action;
config = {
inherit (action) <values of the config options that have been explicitly set by the user>
modes = {
normal.short = "n";
insert.short = "i";
visual = {
desc = "visual and select";
short = "v";
};
}
*/
normalizeAction = action:
if isString action
# Case 1: action is a string
then {
inherit action;
config = helpers.emptyTable;
}
else
# Case 2: action is an attrs
let
# Extract the values of the config options that have been explicitly set by the user
config =
filterAttrs (n: v: v != null)
(getAttrs (attrNames mapConfigOptions) action);
in {
config =
if config == {}
then helpers.emptyTable
else config;
action =
if action.lua
then helpers.mkRaw action.action
else action.action;
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";
};
in
builtins.attrValues (builtins.mapAttrs
(key: action: let
normalizedAction = normalizeAction action;
in {
inherit (normalizedAction) action config;
inherit key mode;
})
maps);
mapOption = types.oneOf [
types.str
mkMapOptionSubmodule = {
defaultMode ? "",
withKeyOpt ? true,
flatConfig ? false,
}:
with types;
either
str
(types.submodule {
options =
mapConfigOptions
(
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"
@ -121,69 +132,153 @@ with lib; let
'';
default = false;
};
};
})
];
mapOptions = mode:
}
// (
if flatConfig
then mapConfigOptions
else {
options = mapConfigOptions;
}
);
});
in {
options = {
maps =
mapAttrs
(
modeName: modeProps: let
desc = modeProps.desc or modeName;
in
mkOption {
description = "Mappings for ${mode} mode";
type = types.attrsOf mapOption;
description = "Mappings for ${desc} mode";
type = with types;
attrsOf
(
either
str
(
mkMapOptionSubmodule
{
defaultMode = modeProps.short;
withKeyOpt = false;
flatConfig = true;
}
)
);
default = {};
};
in {
options = {
maps = mkOption {
type = types.submodule {
options = {
normal = mapOptions "normal";
insert = mapOptions "insert";
select = mapOptions "select";
visual = mapOptions "visual and select";
terminal = mapOptions "terminal";
normalVisualOp = mapOptions "normal, visual, select and operator-pending (same as plain 'map')";
}
)
modes;
visualOnly = mapOptions "visual only";
operator = mapOptions "operator-pending";
insertCommand = mapOptions "insert and command-line";
lang = mapOptions "insert, command-line and lang-arg";
command = mapOptions "command-line";
};
};
default = {};
description = ''
Custom keybindings for any mode.
For plain maps (e.g. just 'map' or 'remap') use maps.normalVisualOp.
'';
example = ''
maps = {
normalVisualOp.";" = ":"; # Same as noremap ; :
normal."<leader>m" = {
silent = true;
keymaps = mkOption {
type = types.listOf (mkMapOptionSubmodule {});
default = [];
example = [
{
key = "<C-m>";
action = "<cmd>make<CR>";
}; # Same as nnoremap <leader>m <silent> <cmd>make<CR>
};
'';
options.silent = true;
}
];
};
};
config = let
mappings =
(genMaps "" config.maps.normalVisualOp)
++ (genMaps "n" config.maps.normal)
++ (genMaps "i" config.maps.insert)
++ (genMaps "v" config.maps.visual)
++ (genMaps "x" config.maps.visualOnly)
++ (genMaps "s" config.maps.select)
++ (genMaps "t" config.maps.terminal)
++ (genMaps "o" config.maps.operator)
++ (genMaps "l" config.maps.lang)
++ (genMaps "!" config.maps.insertCommand)
++ (genMaps "c" config.maps.command);
in {
extraConfigLua =
config = {
warnings =
optional
(
any
(modeMaps: modeMaps != {})
(attrValues config.maps)
)
''
The `maps` option will be deprecated in the near future.
Please, use the new `keymaps` option which works as follows:
keymaps = [
{
# Default mode is "" which means normal-visual-op
key = "<C-m>";
action = ":!make<CR>";
}
{
# Mode can be a string or a list of strings
mode = "n";
key = "<leader>p";
action = "require('my-plugin').do_stuff";
lua = true;
# Note that all of the mapping options are now under the `options` attrs
options = {
silent = true;
desc = "My plugin does stuff";
};
}
];
'';
extraConfigLua = let
modeMapsAsList =
flatten
(
mapAttrsToList
(
modeOptionName: modeProps:
mapAttrsToList
(
key: action:
(
if isString action
then {
mode = modeProps.short;
inherit action;
lua = false;
options = {};
}
else
{
inherit
(action)
action
lua
mode
;
}
// {
options =
getAttrs
(attrNames mapConfigOptions)
action;
}
)
// {inherit key;}
)
config.maps.${modeOptionName}
)
modes
);
mappings = let
normalizeMapping = keyMapping: {
inherit
(keyMapping)
mode
key
;
action =
if keyMapping.lua
then helpers.mkRaw keyMapping.action
else keyMapping.action;
options =
if keyMapping.options == {}
then helpers.emptyTable
else keyMapping.options;
};
in
map normalizeMapping
(config.keymaps ++ modeMapsAsList);
in
optionalString (mappings != [])
(
if config.plugins.which-key.enable
@ -193,9 +288,9 @@ in {
local __nixvim_binds = ${helpers.toLuaObject mappings}
for i, map in ipairs(__nixvim_binds) do
if not map.action then
require("which-key").register({[map.key] = {name = map.config.desc }})
require("which-key").register({[map.key] = {name = map.options.desc }})
else
vim.keymap.set(map.mode, map.key, map.action, map.config)
vim.keymap.set(map.mode, map.key, map.action, map.options)
end
end
end
@ -206,7 +301,7 @@ in {
do
local __nixvim_binds = ${helpers.toLuaObject mappings}
for i, map in ipairs(__nixvim_binds) do
vim.keymap.set(map.mode, map.key, map.action, map.config)
vim.keymap.set(map.mode, map.key, map.action, map.options)
end
end
-- }}}

View file

@ -1,5 +1,72 @@
{
example = {
{helpers, ...}: {
legacy = {
maps.normal."," = "<cmd>echo \"test\"<cr>";
};
legacy-mkMaps = {
maps = helpers.mkMaps {silent = true;} {
normal."," = "<cmd>echo \"test\"<cr>";
visual = {
"<C-a>" = {
action = "function() print('toto') end";
lua = true;
silent = false;
};
"<C-z>" = {
action = "bar";
};
};
};
};
legacy-mkModeMaps = {
maps.normal = helpers.mkModeMaps {silent = true;} {
"," = "<cmd>echo \"test\"<cr>";
"<C-a>" = {
action = "function() print('toto') end";
lua = true;
silent = false;
};
"<leader>b" = {
action = "bar";
};
};
};
example = {
keymaps = [
{
key = ",";
action = "<cmd>echo \"test\"<cr>";
mode = ["n" "s"];
};
};
};
mkMaps = {
keymaps =
helpers.mkKeymaps
{
mode = "x";
options.silent = true;
}
[
{
mode = "n";
key = ",";
action = "<cmd>echo \"test\"<cr>";
}
{
key = "<C-a>";
action = "function() print('toto') end";
lua = true;
options.silent = false;
}
{
mode = ["n" "v"];
key = "<C-z>";
action = "bar";
}
];
};
}