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 ## Key mappings
It is fully possible to define key mappings from within NixVim. This is done It is fully possible to define key mappings from within NixVim. This is done
using the `maps` attribute: using the `keymaps` attribute:
```nix ```nix
{ {
programs.nixvim = { programs.nixvim = {
maps = { keymaps = [
normalVisualOp.";" = ":"; {
normal."<leader>m" = { key = ";";
silent = true; action = ":";
action = "<cmd>make<CR>"; }
}; {
mode = "n";
key = "<leader>m";
options.silent = true;
action = "<cmd>!make<CR>";
}; };
];
}; };
} }
``` ```
@ -344,34 +349,35 @@ noremap ; :
nnoremap <leader>m <silent> <cmd>make<CR> 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 | | Short | Description |
|----------------|--------------------------------------------------| |-------|--------------------------------------------------|
| normal | Normal mode | | `"n"` | Normal mode |
| insert | Insert mode | | `"i"` | Insert mode |
| visual | Visual and Select mode | | `"v"` | Visual and Select mode |
| select | Select mode | | `"s"` | Select mode |
| terminal | Terminal mode | | `"t"` | Terminal mode |
| normalVisualOp | Normal, visual, select and operator-pending mode | | `"" ` | Normal, visual, select and operator-pending mode |
| visualOnly | Visual mode only, without select | | `"x"` | Visual mode only, without select |
| operator | Operator-pending mode | | `"o"` | Operator-pending mode |
| insertCommand | Insert and command-line mode | | `"!"` | Insert and command-line mode |
| lang | Insert, command-line and lang-arg mode | | `"l"` | Insert, command-line and lang-arg mode |
| command | Command-line mode | | `"c"` | Command-line mode |
The map options can be set to either a string, containing just the action, Each keymap can specify the following settings in the `options` attrs.
or to a set describing additional options:
| NixVim | Default | VimScript | | NixVim | Default | VimScript |
|---------|---------|------------------------------------------| |---------|---------|---------------------------------------------------|
| silent | false | `<silent>` | | silent | false | `<silent>` |
| nowait | false | `<silent>` | | nowait | false | `<silent>` |
| script | false | `<script>` | | script | false | `<script>` |
| expr | false | `<expr>` | | expr | false | `<expr>` |
| unique | false | `<unique>` | | unique | false | `<unique>` |
| noremap | true | Use the 'noremap' variant of the mapping | | 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 ## Globals
Sometimes you might want to define a global variable, for example to set the 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. # when compared to just installing NeoVim.
enable = true; enable = true;
maps.normal = { keymaps = [
# Equivalent to nnoremap ; : # Equivalent to nnoremap ; :
";" = ":"; {
key = ";";
action = ":";
}
# Equivalent to nmap <silent> <buffer> <leader>gg <cmd>Man<CR> # Equivalent to nmap <silent> <buffer> <leader>gg <cmd>Man<CR>
"<leader>gg" = { {
key = "<leader>gg";
action = "<cmd>Man<CR>";
options = {
silent = true; silent = true;
remap = false; remap = false;
action = "<cmd>Man<CR>";
# Etc...
}; };
}
# Etc...
];
# We can set the leader key: # We can set the leader key:
leader = ","; leader = ",";
@ -74,5 +82,4 @@
# Use extraPlugins: # Use extraPlugins:
extraPlugins = with pkgs.vimPlugins; [vim-toml]; extraPlugins = with pkgs.vimPlugins; [vim-toml];
}; };
};
} }

View file

@ -1,5 +1,7 @@
{lib, ...}: {lib, ...}:
with lib; rec { with lib;
(import ./keymap-helpers.nix {inherit lib;})
// rec {
# vim dictionaries are, in theory, compatible with JSON # vim dictionaries are, in theory, compatible with JSON
toVimDict = args: toVimDict = args:
toJSON 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. # Creates an option with a nullable type that defaults to null.
mkNullOrOption = type: desc: mkNullOrOption = type: desc:
lib.mkOption { 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."; "A textual description of this keybind, to be shown in which-key, if you have it.";
}; };
# Generates maps for a lua config modes = {
genMaps = mode: maps: let normal.short = "n";
/* insert.short = "i";
Take a user-defined action (string or attrs) and return the following attribute set: visual = {
{ desc = "visual and select";
action = (string) the actual action to map to this key short = "v";
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>
}; };
} visualOnly = {
*/ desc = "visual only";
normalizeAction = action: short = "x";
if isString action };
# Case 1: action is a string select.short = "s";
then { terminal.short = "t";
inherit action; normalVisualOp = {
config = helpers.emptyTable; desc = "normal, visual, select and operator-pending (same as plain 'map')";
} short = "";
else };
# Case 2: action is an attrs operator.short = "o";
let lang = {
# Extract the values of the config options that have been explicitly set by the user desc = "normal, visual, select and operator-pending (same as plain 'map')";
config = short = "l";
filterAttrs (n: v: v != null) };
(getAttrs (attrNames mapConfigOptions) action); insertCommand = {
in { desc = "insert and command-line";
config = short = "!";
if config == {} };
then helpers.emptyTable command.short = "c";
else config;
action =
if action.lua
then helpers.mkRaw action.action
else action.action;
}; };
in
builtins.attrValues (builtins.mapAttrs
(key: action: let
normalizedAction = normalizeAction action;
in {
inherit (normalizedAction) action config;
inherit key mode;
})
maps);
mapOption = types.oneOf [ mkMapOptionSubmodule = {
types.str defaultMode ? "",
withKeyOpt ? true,
flatConfig ? false,
}:
with types;
either
str
(types.submodule { (types.submodule {
options = 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 = action =
if config.plugins.which-key.enable if config.plugins.which-key.enable
then helpers.mkNullOrOption types.str "The action to execute" then helpers.mkNullOrOption types.str "The action to execute"
@ -121,69 +132,153 @@ with lib; let
''; '';
default = false; default = false;
}; };
}; }
}) // (
]; if flatConfig
then mapConfigOptions
mapOptions = mode: else {
options = mapConfigOptions;
}
);
});
in {
options = {
maps =
mapAttrs
(
modeName: modeProps: let
desc = modeProps.desc or modeName;
in
mkOption { mkOption {
description = "Mappings for ${mode} mode"; description = "Mappings for ${desc} mode";
type = types.attrsOf mapOption; type = with types;
attrsOf
(
either
str
(
mkMapOptionSubmodule
{
defaultMode = modeProps.short;
withKeyOpt = false;
flatConfig = true;
}
)
);
default = {}; default = {};
}; }
in { )
options = { modes;
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')";
visualOnly = mapOptions "visual only"; keymaps = mkOption {
operator = mapOptions "operator-pending"; type = types.listOf (mkMapOptionSubmodule {});
insertCommand = mapOptions "insert and command-line"; default = [];
lang = mapOptions "insert, command-line and lang-arg"; example = [
command = mapOptions "command-line"; {
}; key = "<C-m>";
};
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;
action = "<cmd>make<CR>"; action = "<cmd>make<CR>";
}; # Same as nnoremap <leader>m <silent> <cmd>make<CR> options.silent = true;
}; }
''; ];
}; };
}; };
config = let config = {
mappings = warnings =
(genMaps "" config.maps.normalVisualOp) optional
++ (genMaps "n" config.maps.normal) (
++ (genMaps "i" config.maps.insert) any
++ (genMaps "v" config.maps.visual) (modeMaps: modeMaps != {})
++ (genMaps "x" config.maps.visualOnly) (attrValues config.maps)
++ (genMaps "s" config.maps.select) )
++ (genMaps "t" config.maps.terminal) ''
++ (genMaps "o" config.maps.operator) The `maps` option will be deprecated in the near future.
++ (genMaps "l" config.maps.lang) Please, use the new `keymaps` option which works as follows:
++ (genMaps "!" config.maps.insertCommand)
++ (genMaps "c" config.maps.command); keymaps = [
in { {
extraConfigLua = # 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 != []) optionalString (mappings != [])
( (
if config.plugins.which-key.enable if config.plugins.which-key.enable
@ -193,9 +288,9 @@ in {
local __nixvim_binds = ${helpers.toLuaObject mappings} local __nixvim_binds = ${helpers.toLuaObject mappings}
for i, map in ipairs(__nixvim_binds) do for i, map in ipairs(__nixvim_binds) do
if not map.action then 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 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 end
end end
@ -206,7 +301,7 @@ in {
do do
local __nixvim_binds = ${helpers.toLuaObject mappings} local __nixvim_binds = ${helpers.toLuaObject mappings}
for i, map in ipairs(__nixvim_binds) do 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
end end
-- }}} -- }}}

View file

@ -1,5 +1,72 @@
{ {helpers, ...}: {
example = { legacy = {
maps.normal."," = "<cmd>echo \"test\"<cr>"; 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";
}
];
};
} }