From 56cf39cdf81db712c242f081306de35bb6fc34ec Mon Sep 17 00:00:00 2001 From: Ben Lubas <56943754+benlubas@users.noreply.github.com> Date: Thu, 11 Jan 2024 14:16:34 -0500 Subject: [PATCH] feat: custom status line hints (#21) Co-authored-by: benlubas --- .editorconfig | 4 ++ README.md | 18 ++++--- doc/hydra.txt | 20 ++++---- lua/hydra/hint/init.lua | 9 +++- lua/hydra/hint/parser.lua | 55 ++++++++++++++++++++ lua/hydra/hint/statusline.lua | 97 +++++++++++++++++++++++++++++++---- lua/hydra/hint/window.lua | 53 +++++-------------- lua/hydra/lib/types.lua | 2 +- lua/hydra/statusline.lua | 7 +-- 9 files changed, 192 insertions(+), 73 deletions(-) create mode 100644 .editorconfig create mode 100644 lua/hydra/hint/parser.lua diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bb32eab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ + +[*] +indent_style = space +indent_size = 3 diff --git a/README.md b/README.md index faa111f..631306b 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ Hydra({ By default, a one line hint is generated and displayed in the cmdline. Heads and their descriptions are placed in the order they were passed into the `heads` table. Heads with -`{opts = {desc = false}}` don't appear in auto-generated hints. +`{ opts = { desc = false }}` don't appear in auto-generated hints. Values in the hint string are parsed with the following rules: @@ -208,12 +208,12 @@ Values in the hint string are parsed with the following rules: inserted into the hint - Updated each time a head is called - Pass these functions to `config.hint.funcs` (discussed below) - - There are built-in functions located here: [this - file](https://github.com/nvimtools/hydra.nvim/blob/main/lua/hydra/hint/vim-options.lua) + - There are built-in functions located + [here](https://github.com/nvimtools/hydra.nvim/blob/main/lua/hydra/hint/vim-options.lua) **Heads not in the manually created hint, will be automatically added to the bottom of the hint window, following the same rules as auto-generated hint. You can avoid this with -`{desc = false}`** +`{ desc = false }`** ### Hint Configuration @@ -225,10 +225,12 @@ Hydra({ config = { -- either a table like below, or `false` to disable the hint hint = { - -- "window" | "cmdline" | "statusline" - -- "window" : show hint in a floating window - -- "cmdline" : show hint in the echo area + -- "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() type = "window", -- defaults to "window" if `hint` is passed to the hydra -- otherwise defaults to "cmdline" @@ -482,7 +484,7 @@ you to integrate Hydra in your statusline: - `get_name()` — get the name of an active hydra if it has it; - `get_color()` — get the color of an active hydra; - `get_hint()` — get an active hydra's statusline hint. Return not `nil` only when - `config.hint` is set to `false.` + `config.hint` is set to `false` or when `config.hint.type == "statuslinemanual"` ## Limitations diff --git a/doc/hydra.txt b/doc/hydra.txt index 5a08e9f..11feab7 100644 --- a/doc/hydra.txt +++ b/doc/hydra.txt @@ -1,4 +1,4 @@ -*hydra.txt* For NVIM v0.9.4 Last change: 2024 January 06 +*hydra.txt* For NVIM v0.9.4 Last change: 2024 January 10 ============================================================================== Table of Contents *hydra-table-of-contents* @@ -205,7 +205,7 @@ The string for the hint is passed directly to the hydra: By default, a one line hint is generated and displayed in the cmdline. Heads and their descriptions are placed in the order they were passed into the -`heads` table. Heads with `{opts = {desc = false}}` don’t appear in +`heads` table. Heads with `{ opts = { desc = false }}` don’t appear in auto-generated hints. Values in the hint string are parsed with the following rules: @@ -218,12 +218,12 @@ Values in the hint string are parsed with the following rules: inserted into the hint - Updated each time a head is called - Pass these functions to `config.hint.funcs` (discussed below) - - There are built-in functions located here: this - file + - There are built-in functions located + here **Heads not in the manually created hint, will be automatically added to the bottom of the hint window, following the same rules as auto-generated hint. You -can avoid this with {desc = false}** +can avoid this with { desc = false }** HINT CONFIGURATION *hydra-hint-hint-configuration* @@ -236,10 +236,12 @@ disable the hint by setting this value to `false`. config = { -- either a table like below, or `false` to disable the hint hint = { - -- "window" | "cmdline" | "statusline" - -- "window" : show hint in a floating window - -- "cmdline" : show hint in the echo area + -- "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() type = "window", -- defaults to "window" if `hint` is passed to the hydra -- otherwise defaults to "cmdline" @@ -506,7 +508,7 @@ can help you to integrate Hydra in your statusline: - `get_name()` — get the name of an active hydra if it has it; - `get_color()` — get the color of an active hydra; - `get_hint()` — get an active hydra’s statusline hint. Return not `nil` only when - `config.hint` is set to `false.` + `config.hint` is set to `false` or when `config.hint.type == "statuslinemanual"` ============================================================================== diff --git a/lua/hydra/hint/init.lua b/lua/hydra/hint/init.lua index 7a7d6ba..ce0c562 100644 --- a/lua/hydra/hint/init.lua +++ b/lua/hydra/hint/init.lua @@ -8,7 +8,8 @@ local HintManualCmdline = cmdline.HintManualCmdline local HintAutoWindow = window.HintAutoWindow local HintManualWindow = window.HintManualWindow -local HintStatusLine = statusline.HintStatusLine +local HintAutoStatusLine = statusline.HintAutoStatusLine +local HintManualStatusLine = statusline.HintManualStatusLine local HintStatusLineMute = statusline.HintStatusLineMute ---@return hydra.Hint @@ -19,12 +20,16 @@ local function make_hint(input) return HintStatusLineMute(input) elseif hint and config.type == 'window' then return HintManualWindow(input) + elseif hint and config.type == 'statusline' then + return HintManualStatusLine(input) + elseif hint and config.type == 'statuslinemanual' then + return HintStatusLineMute(input) elseif hint then return HintManualCmdline(input) elseif config.type == 'cmdline' then return HintAutoCmdline(input) elseif config.type == 'statusline' then - return HintStatusLine(input) + return HintAutoStatusLine(input) elseif config.type == 'window' then return HintAutoWindow(input) end diff --git a/lua/hydra/hint/parser.lua b/lua/hydra/hint/parser.lua new file mode 100644 index 0000000..494e59d --- /dev/null +++ b/lua/hydra/hint/parser.lua @@ -0,0 +1,55 @@ +local Parser = {} + +---Evaluate function values +---@param line string +---@param funcs table +---@return string +---@return boolean +function Parser.eval_funcs(line, funcs) + local start, stop, fname = 0, nil, nil + local need_to_update = false + while start do + start, stop, fname = line:find("%%{(.-)}", 1) + if start then + need_to_update = true + + local fun = funcs[fname] + if not fun then + error(string.format('[Hydra] "%s" not present in "config.hint.functions" table', fname)) + end + + line = table.concat({ + line:sub(1, start - 1), + fun(), + line:sub(stop + 1), + }) + end + end + + return line, need_to_update +end + +---Parse the heads in a hint string, adding highlights with the given function +--- modifies the heads table +---@param line string +---@param heads table +---@param highlight function +Parser.parse_heads = function(line, heads, highlight) + local start, stop, head = 0, 0, nil + while start do + start, stop, head = line:find('_(.-)_', stop + 1) + if head and vim.startswith(head, [[\]]) then head = head:sub(2) end + if start then + if not heads[head] then + error(string.format('[Hydra] docsting error, head "%s" does not exist', head)) + end + local color = heads[head].color + -- TODO: create this function. and pass it. + -- buffer:add_highlight(namespace, 'Hydra' .. color, n - 1, start, stop - 1) + highlight(color, start, stop - 1) + heads[head] = nil + end + end +end + +return Parser diff --git a/lua/hydra/hint/statusline.lua b/lua/hydra/hint/statusline.lua index f4cde1b..85b2d9b 100644 --- a/lua/hydra/hint/statusline.lua +++ b/lua/hydra/hint/statusline.lua @@ -2,15 +2,16 @@ local class = require('hydra.lib.class') local BaseHint = require('hydra.hint.basehint') local M = {} +---Auto generated status line ---@class hydra.hint.StatusLine : hydra.Hint ---@field update nil -local HintStatusLine = class(BaseHint) +local HintAutoStatusLine = class(BaseHint) -function HintStatusLine:initialize(input) +function HintAutoStatusLine:initialize(input) BaseHint.initialize(self, input) end -function HintStatusLine:_make_statusline() +function HintAutoStatusLine:_make_statusline() if self.statusline then return end require('hydra.lib.highlight') @@ -32,7 +33,7 @@ function HintStatusLine:_make_statusline() self.statusline = statusline:gsub(', $', '') end -function HintStatusLine:show() +function HintAutoStatusLine:show() if not self.statusline then self:_make_statusline() end local statusline = { ' ', self.statusline } ---@type string[] @@ -45,7 +46,7 @@ function HintStatusLine:show() vim.wo.statusline = statusline end -function HintStatusLine:close() +function HintAutoStatusLine:close() if self.original_statusline then vim.wo.statusline = self.original_statusline self.original_statusline = nil @@ -54,13 +55,90 @@ end -------------------------------------------------------------------------------- +---Statusline with custom hint string +---@class hydra.hint.ManualStatusLine : hydra.hint.StatusLine +---@field need_to_update boolean +local HintManualStatusLine = class(HintAutoStatusLine) + +local vim_options = require('hydra.hint.vim-options') +function HintManualStatusLine:initialize(input) + HintAutoStatusLine.initialize(self, input) + self.need_to_update = false + + if type(self.config) == "table" then + self.config.funcs = setmetatable(self.config.funcs or {}, { + __index = vim_options + }) + end +end + +function HintManualStatusLine:_make_statusline() + if self.statusline then return end + if not self.hint then return HintAutoStatusLine._make_statusline(self) end + + require('hydra.lib.highlight') + local parser = require("hydra.hint.parser") + + ---@type string + local hint = self.hint + hint = hint:gsub("%^", "") + + ---@type table + local heads = vim.deepcopy(self.heads) + + ---@type string[] + local statusline = {} + + -- eval funcs + local parsed_line, need_to_update = parser.eval_funcs(hint, self.config.funcs) + self.need_to_update = self.need_to_update or need_to_update + hint = parsed_line + + local last_end = nil + parser.parse_heads(hint, heads, function(color, start, end_) + if last_end ~= nil then + vim.list_extend(statusline, { + hint:sub(last_end + 2, start - 1) + }) + end + local head_key = hint:sub(start + 1, end_) + vim.list_extend(statusline, { + string.format('%%#HydraStatusLine%s#', color), + head_key, + '%#StatusLine#', + }) + last_end = end_ + end) + + vim.list_extend(statusline, { + hint:sub(last_end + 2) + }) + + statusline = table.concat(statusline) ---@diagnostic disable-line + self.statusline = statusline +end + +function HintManualStatusLine:update() + print("hi") + if not self.need_to_update then return end + print("need to update") + + local saved_statusline = self.original_statusline + self.statusline = nil + self:_make_statusline() + self:show() + self.original_statusline = saved_statusline +end + +-------------------------------------------------------------------------------- + ---Statusline hint that won't be shown. It is used in "hydra.statusline" module. ----@class hydra.hint.StatusLineMute : hydra.hint.StatusLine +---@class hydra.hint.StatusLineMute : hydra.hint.ManualStatusLine ---@field config nil -local HintStatusLineMute = class(HintStatusLine) +local HintStatusLineMute = class(HintManualStatusLine) function HintStatusLineMute:initialize(input) - HintStatusLine.initialize(self, input) + HintManualStatusLine.initialize(self, input) end ---@param do_return? boolean Do return statusline hint string? @@ -74,6 +152,7 @@ end -------------------------------------------------------------------------------- -M.HintStatusLine = HintStatusLine +M.HintAutoStatusLine = HintAutoStatusLine +M.HintManualStatusLine = HintManualStatusLine M.HintStatusLineMute = HintStatusLineMute return M diff --git a/lua/hydra/hint/window.lua b/lua/hydra/hint/window.lua index bec1240..dbfad4f 100644 --- a/lua/hydra/hint/window.lua +++ b/lua/hydra/hint/window.lua @@ -168,6 +168,7 @@ function HintManualWindow:initialize(input) end function HintManualWindow:_make_buffer() + local parser = require("hydra.hint.parser") local bufnr = api.nvim_create_buf(false, true) ---@type hydra.api.Buffer @@ -182,27 +183,11 @@ function HintManualWindow:_make_buffer() self.win_width = 0 -- The width of the window for n, line in ipairs(hint) do - local start, stop, fname = 0, nil, nil - while start do - start, stop, fname = line:find('%%{(.-)}', 1) - if start then - self.need_to_update = true - - local fun = self.config.funcs[fname] - if not fun then - error(string.format('[Hydra] "%s" not present in "config.hint.functions" table', fname)) - end - - line = table.concat({ - line:sub(1, start - 1), - fun(), - line:sub(stop + 1) - }) - hint[n] = line - end - end + local parsed_line, need_to_update = parser.eval_funcs(line, self.config.funcs) + self.need_to_update = self.need_to_update or need_to_update + hint[n] = parsed_line - local visible_line_len = strdisplaywidth(line:gsub('[_^]', '')) + local visible_line_len = strdisplaywidth(parsed_line:gsub('[_^]', '')) if visible_line_len > self.win_width then self.win_width = visible_line_len end @@ -214,19 +199,10 @@ function HintManualWindow:_make_buffer() buffer:set_lines(0, line_count, hint) for n, line in ipairs(hint) do - local start, stop, head = 0, 0, nil - while start do - start, stop, head = line:find('_(.-)_', stop + 1) - if head and vim.startswith(head, [[\]]) then head = head:sub(2) end - if start then - if not heads[head] then - error(string.format('[Hydra] docsting error, head "%s" does not exist', head)) - end - local color = heads[head].color - buffer:add_highlight(namespace, 'Hydra'..color, n-1, start, stop-1) - heads[head] = nil - end + local hl = function(color, start, end_) + buffer:add_highlight(namespace, 'Hydra' .. color, n - 1, start, end_) end + parser.parse_heads(line, heads, hl) end -- Remove heads with `desc = false`. @@ -236,7 +212,7 @@ function HintManualWindow:_make_buffer() end end - -- If there are remain hydra heads, that not present in manually created hint. + -- If there are remaining hydra heads, we should add them to the end of the hint if not vim.tbl_isempty(heads) then ---@type string[] local heads_lhs = vim.tbl_keys(heads) @@ -264,14 +240,9 @@ function HintManualWindow:_make_buffer() buffer:set_lines(-1, -1, { '', line }) self.win_height = self.win_height + 2 - local start, stop, head = 0, 0, nil - while start do - start, stop, head = line:find('_(.-)_', stop+1) - if start then - local color = self.heads[head].color - buffer:add_highlight(namespace, 'Hydra'..color, self.win_height-1, start, stop-1) - end - end + parser.parse_heads(line, heads, function(color, start, end_) + buffer:add_highlight(namespace, 'Hydra'..color, self.win_height-1, start, end_) + end) end buffer.bo.buftype = 'nofile' diff --git a/lua/hydra/lib/types.lua b/lua/hydra/lib/types.lua index 19b5327..a7b6f9c 100644 --- a/lua/hydra/lib/types.lua +++ b/lua/hydra/lib/types.lua @@ -31,7 +31,7 @@ ---@field hint? hydra.hint.OptionalConfig | false ---@class hydra.hint.Config ----@field type 'statusline' | 'cmdline' | 'window' | nil +---@field type 'statusline' | 'cmdline' | 'window' | 'manualstatusline' | nil ---@field position hydra.hint.Config.position ---@field offset integer ---@field border? string | table -- deprecated, use `float_opts.border` diff --git a/lua/hydra/statusline.lua b/lua/hydra/statusline.lua index e713421..73d8535 100644 --- a/lua/hydra/statusline.lua +++ b/lua/hydra/statusline.lua @@ -15,13 +15,14 @@ end ---Get an active Hydra's statusline hint if it provides it ---@return string? function statusline.get_hint() - if _G.Hydra and _G.Hydra.config.hint == false then + local hint_type = _G.Hydra.config.hint and _G.Hydra.config.hint.type + if _G.Hydra and _G.Hydra.config.hint == false or hint_type == "statuslinemanual" or hint_type == "statusline" then return _G.Hydra.hint:show(true) end end ----Get the color of an active Hydra ----@return string +---Get the color of an active Hydra, or nil if there is no active hydra +---@return string | nil function statusline.get_color() return _G.Hydra and string.lower(_G.Hydra.config.color) end