From 62cf01df74cb68853448de80f3e772d096295392 Mon Sep 17 00:00:00 2001 From: Gaetan Lepage Date: Mon, 22 Apr 2024 13:29:58 +0200 Subject: [PATCH] plugins/hydra: init --- plugins/default.nix | 1 + plugins/utils/hydra/default.nix | 52 +++++ plugins/utils/hydra/hydra-config-opts.nix | 189 +++++++++++++++++ plugins/utils/hydra/hydras-option.nix | 229 +++++++++++++++++++++ tests/test-sources/plugins/utils/hydra.nix | 158 ++++++++++++++ 5 files changed, 629 insertions(+) create mode 100644 plugins/utils/hydra/default.nix create mode 100644 plugins/utils/hydra/hydra-config-opts.nix create mode 100644 plugins/utils/hydra/hydras-option.nix create mode 100644 tests/test-sources/plugins/utils/hydra.nix diff --git a/plugins/default.nix b/plugins/default.nix index f8f62d1e..bc74499a 100644 --- a/plugins/default.nix +++ b/plugins/default.nix @@ -152,6 +152,7 @@ ./utils/hardtime.nix ./utils/harpoon.nix ./utils/hop.nix + ./utils/hydra ./utils/illuminate.nix ./utils/improved-search.nix ./utils/indent-blankline.nix diff --git a/plugins/utils/hydra/default.nix b/plugins/utils/hydra/default.nix new file mode 100644 index 00000000..f5b9db8b --- /dev/null +++ b/plugins/utils/hydra/default.nix @@ -0,0 +1,52 @@ +{ + lib, + helpers, + config, + pkgs, + ... +}: +with lib; + helpers.neovim-plugin.mkNeovimPlugin config { + name = "hydra"; + originalName = "hydra.nvim"; + defaultPackage = pkgs.vimPlugins.hydra-nvim; + + maintainers = [maintainers.GaetanLepage]; + + extraOptions = { + # A list of `Hydra` definitions + hydras = import ./hydras-option.nix {inherit lib helpers;}; + }; + + settingsOptions = import ./hydra-config-opts.nix {inherit lib helpers;}; + + settingsExample = { + exit = false; + foreign_keys = "run"; + color = "red"; + buffer = true; + invoke_on_body = false; + desc = null; + on_enter = '' + function() + print('hello') + end + ''; + timeout = 5000; + hint = false; + }; + + callSetup = false; + extraConfig = cfg: { + extraConfigLua = '' + hydra = require('hydra') + + hydra.setup(${helpers.toLuaObject cfg.settings}) + + __hydra_defs = ${helpers.toLuaObject cfg.hydras} + for _, hydra_config in ipairs(__hydra_defs) do + hydra(hydra_config) + end + ''; + }; + } diff --git a/plugins/utils/hydra/hydra-config-opts.nix b/plugins/utils/hydra/hydra-config-opts.nix new file mode 100644 index 00000000..9feb9662 --- /dev/null +++ b/plugins/utils/hydra/hydra-config-opts.nix @@ -0,0 +1,189 @@ +# Those are the configuration options for both the plugin's `setup` function (defaults for all +# hydras) and each Hydra. +# So this attrs of options is used: +# - as the `plugins.hydra.settings` option definition +# - for `plugins.hydra.hydras.[].config` +# +# -> https://github.com/nvimtools/hydra.nvim?tab=readme-ov-file#config +{ + helpers, + lib, + ... +}: +with lib; { + debug = helpers.defaultNullOpts.mkBool false '' + Whether to enable debug mode. + ''; + + exit = helpers.defaultNullOpts.mkBool false '' + Set the default exit value for each head in the hydra. + ''; + + foreign_keys = helpers.defaultNullOpts.mkEnum ["warn" "run"] "null" '' + Decides what to do when a key which doesn't belong to any head is pressed + - `null`: hydra exits and foreign key behaves normally, as if the hydra wasn't active + - `"warn"`: hydra stays active, issues a warning and doesn't run the foreign key + - `"run"`: hydra stays active, runs the foreign key + ''; + + color = helpers.defaultNullOpts.mkStr "red" '' + See `:h hydra-colors`. + `"red" | "amaranth" | "teal" | "pink"` + ''; + + buffer = + helpers.defaultNullOpts.mkNullable + ( + with types; + either + (enum [true]) + ints.unsigned + ) + "null" + "Define a hydra for the given buffer, pass `true` for current buf."; + + invoke_on_body = helpers.defaultNullOpts.mkBool false '' + When true, summon the hydra after pressing only the `body` keys. + Normally a head is required. + ''; + + desc = helpers.defaultNullOpts.mkStr "null" '' + Description used for the body keymap when `invoke_on_body` is true. + When nil, "[Hydra] .. name" is used. + ''; + + on_enter = helpers.mkNullOrLuaFn '' + Called when the hydra is activated. + ''; + + on_exit = helpers.mkNullOrLuaFn '' + Called before the hydra is deactivated. + ''; + + on_key = helpers.mkNullOrLuaFn '' + Called after every hydra head. + ''; + + timeout = helpers.defaultNullOpts.mkNullable (with types; either bool ints.unsigned) "false" '' + Timeout after which the hydra is automatically disabled. + Calling any head will refresh the timeout + - `true`: timeout set to value of `timeoutlen` (`:h timeoutlen`) + - `5000`: set to desired number of milliseconds + + By default hydras wait forever (`false`). + ''; + + hint = let + hintConfigType = types.submodule { + freeformType = with types; attrsOf anything; + options = { + type = + helpers.mkNullOrOption + (types.enum + [ + "window" + "cmdline" + "statusline" + "statuslinemanual" + ]) + '' + - "window": show hint in a floating window + - "cmdline": show hint in the echo area + - "statusline": show auto-generated hint in the status line + - "statuslinemanual": Do not show a hint, but return a custom status line hint from + `require("hydra.statusline").get_hint()` + + Defaults to "window" if `hint` is passed to the hydra otherwise defaults to "cmdline". + ''; + + position = + helpers.defaultNullOpts.mkEnum + [ + "top-left" + "top" + "top-right" + "middle-left" + "middle" + "middle-right" + "bottom-left" + "bottom" + "bottom-right" + ] + "bottom" + "Set the position of the hint window."; + + offset = helpers.defaultNullOpts.mkInt 0 '' + Offset of the floating window from the nearest editor border. + ''; + + float_opts = helpers.mkNullOrOption (with types; attrsOf anything) '' + Options passed to `nvim_open_win()`. See `:h nvim_open_win()`. + Lets you set `border`, `header`, `footer`, etc. + + Note: `row`, `col`, `height`, `width`, `relative`, and `anchor` should not be overridden. + ''; + + show_name = helpers.defaultNullOpts.mkBool true '' + Show the hydras name (or "HYDRA:" if not given a name), at the beginning of an + auto-generated hint. + ''; + + hide_on_load = helpers.defaultNullOpts.mkBool false '' + If set to true, this will prevent the hydra's hint window from displaying immediately. + + Note: you can still show the window manually by calling `Hydra.hint:show()` and manually + close it with `Hydra.hint:close()`. + ''; + + funcs = mkOption { + type = with helpers.nixvimTypes; attrsOf strLuaFn; + description = '' + Table from function names to function. + Functions should return a string. + These functions can be used in hints with `%{func_name}` more in `:h hydra-hint`. + ''; + default = {}; + example = { + number = '' + function() + if vim.o.number then + return '[x]' + else + return '[ ]' + end + end + ''; + relativenumber = '' + function() + if vim.o.relativenumber then + return '[x]' + else + return '[ ]' + end + end + ''; + }; + apply = mapAttrs (_: helpers.mkRaw); + }; + }; + }; + in + helpers.defaultNullOpts.mkNullable + ( + with types; + either + (enum [false]) + hintConfigType + ) + '' + { + show_name = true; + position = "bottom"; + offset = 0; + } + '' + '' + Configure the hint. + Set to `false` to disable. + ''; +} diff --git a/plugins/utils/hydra/hydras-option.nix b/plugins/utils/hydra/hydras-option.nix new file mode 100644 index 00000000..4a172fb8 --- /dev/null +++ b/plugins/utils/hydra/hydras-option.nix @@ -0,0 +1,229 @@ +{ + lib, + helpers, +}: +with lib; let + hydraType = types.submodule { + freeformType = with types; attrsOf anything; + options = { + name = helpers.mkNullOrStr '' + Hydra's name. + Only used in auto-generated hint. + ''; + + mode = + helpers.defaultNullOpts.mkNullable + ( + with helpers.nixvimTypes; + either + helpers.keymaps.modeEnum + (listOf helpers.keymaps.modeEnum) + ) + "n" + "Modes where the hydra exists, same as `vim.keymap.set()` accepts."; + + body = helpers.mkNullOrStr '' + Key required to activate the hydra, when excluded, you can use `Hydra:activate()`. + ''; + + hint = helpers.mkNullOrStr '' + The hint for a hydra can let you know that it's active, and remind you of the + hydra's heads. + The string for the hint is passed directly to the hydra. + + See [the README](https://github.com/nvimtools/hydra.nvim?tab=readme-ov-file#hint) + for more information. + ''; + + config = import ./hydra-config-opts.nix {inherit lib helpers;}; + + heads = let + headsOptType = types.submodule { + freeformType = with types; attrsOf anything; + options = { + private = helpers.defaultNullOpts.mkBool false '' + "When the hydra hides, this head does not stick out". + Private heads are unreachable outside of the hydra state. + ''; + + exit = helpers.defaultNullOpts.mkBool false '' + When true, stops the hydra after executing this head. + NOTE: + - All exit heads are private + - If no exit head is specified, `esc` is set by default + ''; + + exit_before = helpers.defaultNullOpts.mkBool false '' + Like `exit`, but stops the hydra BEFORE executing the command. + ''; + + ok_key = helpers.defaultNullOpts.mkBool true '' + When set to `false`, `config.on_key` isn't run after this head. + ''; + + desc = helpers.mkNullOrStr '' + Value shown in auto-generated hint. + When false, this key doesn't show up in the auto-generated hint. + ''; + + expr = helpers.defaultNullOpts.mkBool false '' + Same as the builtin `expr` map option. + See `:h :map-expression`. + ''; + + silent = helpers.defaultNullOpts.mkBool false '' + Same as the builtin `silent` map option. + See `:h :map-silent`. + ''; + + nowait = helpers.defaultNullOpts.mkBool false '' + For Pink Hydras only. + Allows binding a key which will immediately perform its action and not wait + `timeoutlen` for a possible continuation. + ''; + + mode = + helpers.mkNullOrOption + ( + with helpers.nixvimTypes; + either + helpers.keymaps.modeEnum + (listOf helpers.keymaps.modeEnum) + ) + "Override `mode` for this head."; + }; + }; + headType = with helpers.nixvimTypes; + # More precisely, a tuple: [head action opts] + listOf ( + nullOr ( # action can be `null` + oneOf [ + str # for `head` and `action` + rawLua # for `action` + headsOptType # for opts + ] + ) + ); + in + helpers.mkNullOrOption + (types.listOf headType) + '' + Each Hydra's head has the form: + `[head rhs opts] + + Similar to the `vim.keymap.set()` function. + + - The `head` is the "lhs" of the mapping (given as a string). + These are the keys you press to perform the action. + - The `rhs` is the action that gets performed. + It can be a string, function (use `__raw`) or `null`. + When `null`, the action is a no-op. + - The `opts` attrs is empty by default. + ''; + }; + }; +in + mkOption { + type = types.listOf hydraType; + default = []; + example = [ + { + name = "git"; + hint.__raw = '' + [[ + _J_: next hunk _s_: stage hunk _d_: show deleted _b_: blame line + _K_: prev hunk _u_: undo stage hunk _p_: preview hunk _B_: blame show full + ^ ^ _S_: stage buffer ^ ^ _/_: show base file + ^ + ^ ^ __: Neogit _q_: exit + ]] + ''; + config = { + color = "pink"; + invoke_on_body = true; + hint = { + position = "bottom"; + }; + on_enter = '' + function() + vim.bo.modifiable = false + gitsigns.toggle_signs(true) + gitsigns.toggle_linehl(true) + end + ''; + on_exit = '' + function() + gitsigns.toggle_signs(false) + gitsigns.toggle_linehl(false) + gitsigns.toggle_deleted(false) + vim.cmd("echo") -- clear the echo area + end + ''; + }; + mode = ["n" "x"]; + body = "g"; + heads = [ + [ + "J" + { + __raw = '' + function() + if vim.wo.diff then + return "]c" + end + vim.schedule(function() + gitsigns.next_hunk() + end) + return "" + end + ''; + } + {expr = true;} + ] + [ + "K" + { + __raw = '' + function() + if vim.wo.diff then + return "[c" + end + vim.schedule(function() + gitsigns.prev_hunk() + end) + return "" + end + ''; + } + {expr = true;} + ] + ["s" ":Gitsigns stage_hunk" {silent = true;}] + ["u" {__raw = "require('gitsigns').undo_stage_hunk";}] + ["S" {__raw = "require('gitsigns').stage_buffer";}] + ["p" {__raw = "require('gitsigns').preview_hunk";}] + ["d" {__raw = "require('gitsigns').toggle_deleted";} {nowait = true;}] + ["b" {__raw = "require('gitsigns').blame_line";}] + [ + "B" + { + __raw = '' + function() + gitsigns.blame_line({ full = true }) + end, + ''; + } + ] + ["/" {__raw = "require('gitsigns').show";} {exit = true;}] + ["" "Neogit" {exit = true;}] + [ + "q" + null + { + exit = true; + nowait = true; + } + ] + ]; + } + ]; + } diff --git a/tests/test-sources/plugins/utils/hydra.nix b/tests/test-sources/plugins/utils/hydra.nix new file mode 100644 index 00000000..1fc099cb --- /dev/null +++ b/tests/test-sources/plugins/utils/hydra.nix @@ -0,0 +1,158 @@ +{ + empty = { + plugins.hydra.enable = true; + }; + + defaults = { + plugins.hydra = { + enable = false; + + settings = { + debug = false; + exit = false; + foreign_keys = null; + color = "red"; + buffer = null; + invoke_on_body = false; + desc = null; + on_enter = null; + on_exit = null; + on_key = null; + timeout = false; + hint = { + show_name = true; + position = "bottom"; + offset = 0; + }; + }; + }; + }; + + example = { + plugins = { + # This example turns out to use gitsigns + gitsigns.enable = true; + + hydra = { + enable = false; + + settings = { + exit = false; + foreign_keys = "run"; + color = "red"; + buffer = true; + invoke_on_body = false; + desc = null; + on_enter = '' + function() + print('hello') + end + ''; + timeout = 5000; + hint = false; + }; + + hydras = [ + { + name = "git"; + hint.__raw = '' + [[ + _J_: next hunk _s_: stage hunk _d_: show deleted _b_: blame line + _K_: prev hunk _u_: undo stage hunk _p_: preview hunk _B_: blame show full + ^ ^ _S_: stage buffer ^ ^ _/_: show base file + ^ + ^ ^ __: Neogit _q_: exit + ]] + ''; + config = { + color = "pink"; + invoke_on_body = true; + hint = { + position = "bottom"; + }; + on_enter = '' + function() + vim.bo.modifiable = false + gitsigns.toggle_signs(true) + gitsigns.toggle_linehl(true) + end + ''; + on_exit = '' + function() + gitsigns.toggle_signs(false) + gitsigns.toggle_linehl(false) + gitsigns.toggle_deleted(false) + vim.cmd("echo") -- clear the echo area + end + ''; + }; + mode = ["n" "x"]; + body = "g"; + heads = [ + [ + "J" + { + __raw = '' + function() + if vim.wo.diff then + return "]c" + end + vim.schedule(function() + gitsigns.next_hunk() + end) + return "" + end + ''; + } + {expr = true;} + ] + [ + "K" + { + __raw = '' + function() + if vim.wo.diff then + return "[c" + end + vim.schedule(function() + gitsigns.prev_hunk() + end) + return "" + end + ''; + } + {expr = true;} + ] + ["s" ":Gitsigns stage_hunk" {silent = true;}] + ["u" {__raw = "require('gitsigns').undo_stage_hunk";}] + ["S" {__raw = "require('gitsigns').stage_buffer";}] + ["p" {__raw = "require('gitsigns').preview_hunk";}] + ["d" {__raw = "require('gitsigns').toggle_deleted";} {nowait = true;}] + ["b" {__raw = "require('gitsigns').blame_line";}] + [ + "B" + { + __raw = '' + function() + gitsigns.blame_line({ full = true }) + end, + ''; + } + ] + ["/" {__raw = "require('gitsigns').show";} {exit = true;}] + ["" "Neogit" {exit = true;}] + [ + "q" + null + { + exit = true; + nowait = true; + } + ] + ]; + } + ]; + }; + }; + }; +}